前言
Go简介
- Google开源
- 编译型语言
- 21世界的C语言
2005年出现多核处理器,其他语言都是单核时代诞生的。Go天生考虑了多核并发。
特点:
- 语法简洁(只有25个关键字,比Python更简洁,自带格式化,互相阅读容易)
- 开发效率高
- 执行性能好(接近java)
发展:
百度自动驾驶,小程序
腾讯蓝鲸,微服务框架
知乎最早用python写,后期承受不了负载,用go重构节约了80%资源。
课程简介
8周基础
3个实战项目
Go项目结构
个人开发者
流行方式
Helloworld
go build
win编译得exe,macos得可执行文件
go install
install相当于build后再移到bin
go run
当脚本运行
支持跨平台交叉编译
// wincmd SET, macos export
export CGO_ENABLED=0 //禁用CGO
export GOOS=linux //设置目标平台linux,windows,darwin
export GOARCH=amd64//目标处理器架构是amd64
go build
export CGO_ENABLED=0 GOOS=linux GOARCH=amd64
go build
变量与常量
函数外不能写语句
标识符:字母数字下划线,不以数字开头
关键字与保留字不建议用于变量名
变量
初始化
数字默认0
,字符串默认空,布尔默认false
,切片、函数、指针默认nil
。
var 变量名 类型 = 表达式
var name string = "Q1mi"
var age int = 18
var name, age = "Q1mi", 20 //会根据值推导类型
var (
a string
b int
c bool
d float32
)
写在函数外为全局变量
函数内声明局部变量简写为
n := 10
m := 200
fmt.Println(m, n)
注意:在Golang里非全局变量声明必须使用,不然编译不通过!
fmt.Print()
fmt.Printf()
fmt.Println() //换行
保存时会自动格式化
命名规则
var studentName string
Golang使用小驼峰命名
匿名变量
用短下划线接收,不占命名空间,不分配内存
x, _ = foo()
_, y = foo()
常量
const pi = 3.14
iota
常量计数器,每新增一行常量声明则计数,注意是一行
const (
n1 = iota //0
n2 //1
n3 //2
n4 //3
)
const (
n1 = iota //0
n2 //1
_ //2 但是被丢弃
n3 //3
)
定义数量级
const (
_ = iota
KB = 1 << (10 * iota)
MB = 1 << (10 * iota)
GB = 1 << (10 * iota)
TB = 1 << (10 * iota)
PB = 1 << (10 * iota)
)
« 左移符号,二进制1左移10位是1024
基本数据类型
整型分为以下两个大类:按长度分为:int8、int16、int32、int64
对应的无符号整型:uint8、uint16、uint32、uint64
uint8就是byte,int16就是short,int64是long
特殊整型
uint int
会根据系统判别是32还是64
uintptr
指针,存放内存地址
进制
Golang无法直接定义二进制数,八、十六均可
// 十进制
var a int = 10
fmt.Printf("%d \n", a) // 10
fmt.Printf("%b \n", a) // 1010 占位符%b表示二进制
// 八进制 以0开头
var b int = 077
fmt.Printf("%o \n", b) // 77
// 十六进制 以0x开头
var c int = 0xff
fmt.Printf("%x \n", c) // ff
fmt.Printf("%X \n", c) // FF
fmt.Printf("%T \n", c) // 输出类型
fmt.Printf("%v \n", c) // 输出变量值,任意类型
浮点数
golang中小数默认float64
math.MaxFloat64 // float64最大值
布尔
默认false,不允许转换
字符串
只能双引号,单引号为字符
转义 | 含义 |
---|---|
\r | 返回行首 |
\n | 换行(下行同列) |
\t | 制表 |
// 在win中路径转义
s := "D:\\Documents\\A"
// 反引号原样输出, 多行字符串
s := `
asda
asd
`
s := "D:\Documents\A"
len(str)
ss := s1 + s2
ret := strings.Split(s3, "\\")
ret = strings.Contains(s3, "abcd")
ret = strings.HasPrefix(s3, "abcd")
ret = strings.HasSufix(s3, "abcd")
ret = strings.Index(s3, "c")
ret = strings.LastIndex(s3, "c")
ret = strings.Join(a, b)
英文字符为byte
,其他语系如中文字符为rune
,实际为int32
,占3位
字符串遍历
for _, char := range str {
fmt.Printf("%c", char)
}
字符串没法直接修改,只能转换为其他类型处理
s3 := []rune(s2) //切片
s3[0] = 'e' //修改
s4 := string(s3)
流程控制
if
if 表达式1 {
分支1
} else if 表达式2 {
分支2
} else{
分支3
}
// 局部变量score只在if中生效,减少内存占用
if score := 65; score >= 90 {
fmt.Println("A")
} else if score > 75 {
fmt.Println("B")
} else {
fmt.Println("C")
}
for
golang只有for
for 初始语句;条件表达式;结束语句{
循环体语句
}
for i := 0; i < 10; i++ {
fmt.Println(i)
}
初始语句和结束语句可省略,相当于while
i := 0
for i < 10 {
fmt.Println(i)
i++
}
无限循环
for {
循环体语句
}
通过break
、goto
、return
、panic
语句强制退出循环
遍历
for range
遍历数组、切片、字符串、map 及通道(channel)
for i,v := range s{
fmt.Println(i, v)
}
- 数组、切片、字符串返回索引和值。
- map返回键和值。
- 通道(channel)只返回通道内的值。
switch
finger := 3
switch finger {
case 1:
fmt.Println("大拇指")
fallthrough
case 2:
fmt.Println("食指")
case 3:
fmt.Println("中指")
case 4:
fmt.Println("无名指")
case 5:
fmt.Println("小拇指")
default:
fmt.Println("无效的输入!")
}
fallthrough
语法可以执行满足条件的case的下一个case,是为了兼容C语言中的case设计的
switch n := 7; n {
case 1, 3, 5, 7, 9:
fmt.Println("奇数")
case 2, 4, 6, 8:
fmt.Println("偶数")
default:
fmt.Println(n)
}
goto
goto
语句通过标签进行代码间的无条件跳转。goto
语句可以在快速跳出循环、避免重复退出上有一定的帮助。Go语言中使用goto
语句能简化一些代码的实现过程。 例如双层嵌套的for循环要退出时
var breakFlag bool
for i := 0; i < 10; i++ {
for j := 0; j < 10; j++ {
if j == 2 {
// 设置退出标签
breakFlag = true
break
}
fmt.Printf("%v-%v\n", i, j)
}
// 外层for循环判断
if breakFlag {
break
}
}
简化为
for i := 0; i < 10; i++ {
for j := 0; j < 10; j++ {
if j == 2 {
// 设置退出标签
goto breakTag
}
fmt.Printf("%v-%v\n", i, j)
}
}
return
// 标签
breakTag:
fmt.Println("结束for循环")
运算符
++
(自增)和--
(自减)在Go语言中是单独的语句,并不是运算符。
// 逻辑运算
&&
||
!
// 位运算
&
|
^
<<
>>
// 赋值
+=
-=
<<=
数组
初始化
数组从声明时就确定,使用时可以修改数组成员,但是数组大小不可变化
var a [3]int
var a [3]int
var b [4]int
a = b //不可以这样做,因为此时a和b是不同的类型
数组可以通过下标进行访问,下标是从0
开始,最后一个元素下标是:len-1
,访问越界(下标在合法范围之外),则触发访问越界,panic
var testArray [3]int //数组会初始化为int类型的零值
var numArray = [3]int{1, 2} //使用指定的初始值完成初始化
var cityArray = [3]string{"北京", "上海", "深圳"} //使用指定的初始值完成初始化
var numArray = [...]int{1, 2} //根据值推断数组长度
var cityArray = [...]string{"北京", "上海", "深圳"}
a := [...]int{1: 1, 3: 5} //指定索引初始化
fmt.Println(a) // [0 1 0 5]
for index, value := range a {
fmt.Println(index, value)
}
多维数组
a := [3][2]string{
{"北京", "上海"},
{"广州", "深圳"},
{"成都", "重庆"},
}
多维数组只有第一层可以使用
...
来让编译器推导数组长度
数组是值类型,赋值和传参会复制整个数组。因此改变副本的值,不会改变本身的值。
- 数组支持 “==“、”!=” 操作符,因为内存总是被初始化过的。
[n]*T
表示指针数组,*[n]T
表示数组指针 。
切片
数组的局限性,长度固定。
切片(Slice)是一个拥有相同类型元素的可变长度的序列。它是基于数组类型做的一层封装。它非常灵活,支持自动扩容。
切片是一个引用类型,它的内部结构包含地址
、长度
和容量
。切片一般用于快速地操作一块数据集合。
初始化
var a = []string //声明一个字符串切片
var b = []int{} //声明一个整型切片并初始化
var c = []bool{false, true} //声明一个布尔切片并初始化
var d = []bool{false, true} //声明一个布尔切片并初始化
切片有指向时值就不为空了。
a1 := [...]int{1, 3, 5, 7, 9, 11, 13}
s3 := a1[0:4] //左包右不包,索引为0-3切片
len(s3) // 4 切片长度
cap(s3) // 7 容量=原数组切片点到末尾的长度
a[2:] // 等同于 a[2:len(a)]
a[:3] // 等同于 a[0:3]
a[:] // 等同于 a[0:len(a)]
原数组元素改了切片也变,引用类型。
a[low : high : max]
a := [5]int{1, 2, 3, 4, 5}
t := a[1:3:5] //t:[2 3] len(t):2 cap(t):4
构造与简单切片表达式a[low: high]
相同类型、相同长度和元素的切片。另外,它会将得到的结果切片的容量设置为max-low
。在完整切片表达式中只有第一个索引值(low)可以省略;它默认为0。
make()
动态创建一个切片
make([]T, size, cap)
a := make([]int, 2, 10) // 初始化值为0
空切片判断
要检查切片是否为空,使用len(s) == 0
来判断,而不应该使用s == nil
来判断。
切片之间是不能比较的,我们不能使用==
操作符来判断两个切片是否含有全部相等元素。 切片唯一合法的比较操作是和nil
比较。 一个nil
值的切片并没有底层数组,一个nil
值的切片的长度和容量都是0。但是我们不能说一个长度和容量都是0的切片一定是nil
赋值
s1 := make([]int, 3) //[0 0 0]
s2 := s1 //将s1直接赋值给s2,s1和s2共用一个底层数组
s2[0] = 100
fmt.Println(s1) //[100 0 0]
fmt.Println(s2) //[100 0 0]
append()
var s []int
s = append(s, 1) // [1]
s = append(s, 2, 3, 4) // [1 2 3 4
s2 := []int{5, 6, 7}
s = append(s, s2...) // [1 2 3 4 5 6 7]
var声明的零值切片可以在
append()
函数直接使用,无需初始化
var s []int
s = append(s, 1, 2, 3)
每个切片会指向一个底层数组,这个数组的容量够用就添加新增元素。当底层数组不能容纳新增的元素时,切片就会自动按照一定的策略进行“扩容”,此时该切片指向的底层数组就会更换。“扩容”操作往往发生在append()
函数调用时,所以我们通常都需要用原变量接收append函数的返回值。
func main() {
//append()添加元素和切片扩容
var numSlice []int
for i := 0; i < 10; i++ {
numSlice = append(numSlice, i)
fmt.Printf("%v len:%d cap:%d ptr:%p\n", numSlice, len(numSlice), cap(numSlice), numSlice)
}
}
输出
[0] len:1 cap:1 ptr:0xc0000a8000
[0 1] len:2 cap:2 ptr:0xc0000a8040
[0 1 2] len:3 cap:4 ptr:0xc0000b2020
[0 1 2 3] len:4 cap:4 ptr:0xc0000b2020
[0 1 2 3 4] len:5 cap:8 ptr:0xc0000b6000
[0 1 2 3 4 5] len:6 cap:8 ptr:0xc0000b6000
[0 1 2 3 4 5 6] len:7 cap:8 ptr:0xc0000b6000
[0 1 2 3 4 5 6 7] len:8 cap:8 ptr:0xc0000b6000
[0 1 2 3 4 5 6 7 8] len:9 cap:16 ptr:0xc0000b8000
[0 1 2 3 4 5 6 7 8 9] len:10 cap:16 ptr:0xc0000b8000
从上面的结果可以看出:
append()
函数将元素追加到切片的最后并返回该切片。- 切片numSlice的容量按照1,2,4,8,16这样的规则自动进行扩容,每次扩容后都是扩容前的2倍。
$GOROOT/src/runtime/slice.go
源码:
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.len < 1024 {
newcap = doublecap
} else {
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = cap
}
}
}
- 首先判断,如果新申请容量(cap)大于2倍的旧容量(old.cap),最终容量(newcap)就是新申请的容量(cap)。
- 否则判断,如果旧切片的长度小于1024,则最终容量(newcap)就是旧容量(old.cap)的两倍,即(newcap=doublecap),
- 否则判断,如果旧切片长度大于等于1024,则最终容量(newcap)从旧容量(old.cap)开始循环增加原来的1/4,即(newcap=old.cap,for {newcap += newcap/4})直到最终容量(newcap)大于等于新申请的容量(cap),即(newcap >= cap)
- 如果最终容量(cap)计算值溢出,则最终容量(cap)就是新申请容量(cap)。
中文字符串是3*2^n
copy()
切片是引用类型,所以a和b其实都指向了同一块内存地址。修改b的同时a的值也会发生变化。
Go语言内建的copy()
函数可以迅速地将一个切片的数据复制到另外一个切片空间中。
a := []int{1, 2, 3, 4, 5}
c := make([]int, 5, 5)
copy(c, a) //使用copy()函数将切片a中的元素复制到切片c
fmt.Println(a) //[1 2 3 4 5]
fmt.Println(c) //[1 2 3 4 5]
c[0] = 1000
fmt.Println(a) //[1 2 3 4 5]
fmt.Println(c) //[1000 2 3 4 5]
删除元素
a = append(a[:index], a[index+1:]...)
a := []int{30, 31, 32, 33, 34, 35, 36, 37}
// 要删除索引为2的元素
a = append(a[:2], a[3:]...)
fmt.Println(a) //[30 31 33 34 35 36 37]
//底层数组长度不变,元素左移,右边的由最右元素补全
排序对切片排
sort.Ints(a[:])
指针
ptr := &v // v的类型为T 输出指针类型*T 如 *string *int
a := 10
b := &a
fmt.Printf("a:%d ptr:%p\n", a, &a) // a:10 ptr:0xc00001a078
fmt.Printf("b:%p type:%T\n", b, b) // b:0xc00001a078 type:*int
fmt.Println(&b) // 0xc00000e018
c := *b // 指针取值(根据指针去内存取值)
fmt.Printf("type of c:%T\n", c)
fmt.Printf("value of c:%v\n", c)
&与*互补
func modify1(x int) {
x = 100
}
func modify2(x *int) {
*x = 100
}
func main() {
a := 10
modify1(a)
fmt.Println(a) // 10
modify2(&a)
fmt.Println(a) // 100
}
new与make
new函数不太常用,使用new函数得到的是一个类型的指针,并且该指针对应的值为该类型的零值
a := new(int)
b := new(bool)
fmt.Printf("%T\n", a) // *int
fmt.Printf("%T\n", b) // *bool
fmt.Println(*a) // 0
fmt.Println(*b) // false
make也是用于内存分配的,区别于new,它只用于slice、map以及chan的内存创建,而且它返回的类型就是这三个类型变量本身,而不是他们的指针类型,因为这三种类型就是引用类型
var b map[string]int
b = make(map[string]int, 10)
b["沙河娜扎"] = 100
fmt.Println(b)
map
Go语言中提供的映射关系容器为map
,其内部使用散列表(hash)
实现,类似python的字典
map是一种无序的基于key-value
的数据结构,Go语言中的map是引用类型,必须初始化才能使用
map类型的变量默认初始值为nil,需要使用make()函数来分配内存
map[KeyType]ValueType
scoreMap := make(map[string]int, 8) // 初始化才能用,避免动态扩容!
scoreMap["张三"] = 90
scoreMap["小明"] = 100
fmt.Println(scoreMap)
fmt.Println(scoreMap["小明"])
fmt.Printf("type of a:%T\n", scoreMap)
userInfo := map[string]string{
"username": "沙河小王子",
"password": "123456",
}
判断键值是否存在
value, ok := map[key] // ok返回key是否存在的bool值
v, ok := scoreMap["张三"]
if ok {
fmt.Println(v)
} else {
fmt.Println("查无此人")
}
map的遍历
for k, v := range scoreMap {
fmt.Println(k, v)
}
for k := range scoreMap {
fmt.Println(k)
}
for _, v := range scoreMap {
fmt.Println(v)
}
注意:遍历map时的元素顺序与添加键值对的顺序无关
删除键值对
delete(map, key)
按照指定顺序遍历
func main() {
rand.Seed(time.Now().UnixNano()) //初始化随机数种子
var scoreMap = make(map[string]int, 200)
for i := 0; i < 100; i++ {
key := fmt.Sprintf("stu%02d", i) //生成stu开头的字符串
value := rand.Intn(100) //生成0~99的随机整数
scoreMap[key] = value
}
//取出map中的所有key存入切片keys
var keys = make([]string, 0, 200)
for key := range scoreMap {
keys = append(keys, key)
}
//对切片进行排序
sort.Strings(keys)
//按照排序后的key遍历map
for _, key := range keys {
fmt.Println(key, scoreMap[key])
}
}
元素为map类型的切片
var mapSlice = make([]map[string]string, 3) // 切片初始化,每个元素都是一个map
for index, value := range mapSlice {
fmt.Printf("index:%d value:%v\n", index, value)
}
fmt.Println("after init")
// 对切片中的map元素进行初始化
mapSlice[0] = make(map[string]string, 10)
mapSlice[0]["name"] = "小王子"
mapSlice[0]["password"] = "123456"
mapSlice[0]["address"] = "沙河"
for index, value := range mapSlice {
fmt.Printf("index:%d value:%v\n", index, value)
}
值为切片类型的map
func main() {
var sliceMap = make(map[string][]string, 3)
fmt.Println(sliceMap)
fmt.Println("after init")
key := "中国"
value, ok := sliceMap[key]
if !ok {
value = make([]string, 0, 2)
}
value = append(value, "北京", "上海")
sliceMap[key] = value
fmt.Println(sliceMap)
}
函数
func 函数名(参数 类型) 返回值类型 {
函数体
}
func intSum(x int, y int) int {
return x + y
}
参数同类型简写
func intSum(x, y int) int {
return x + y
}
可变参数
func intSum2(x ...int) int {
fmt.Println(x) //x是一个切片
sum := 0
for _, v := range x {
sum = sum + v
}
return sum
}
返回值
//有命名的返回
func calc(x, y int) (sum, sub int) {
sum = x + y
sub = x - y
return
}
//切片
func someFunc(x string) []int {
if x == "" {
return nil // 没必要返回[]int{}
}
...
}
如果局部变量和全局变量重名,优先访问局部变量
函数类型与变量
我们可以使用type
关键字来定义一个函数类型,具体格式如下:
type calculation func(int, int) int
上面语句定义了一个calculation
类型,它是一种函数类型,这种函数接收两个int类型的参数并且返回一个int类型的返回值。
func main() {
var c calculation // 声明一个calculation类型的变量c
c = add // 把add赋值给c
fmt.Printf("type of c:%T\n", c) // type of c:main.calculation
fmt.Println(c(1, 2)) // 像调用add一样调用c
f := add // 将函数add赋值给变量f1
fmt.Printf("type of f:%T\n", f) // type of f:func(int, int) int
fmt.Println(f(10, 20)) // 像调用add一样调用f
}
函数作参数与返回值
func add(x, y int) int {
return x + y
}
func calc(x, y int, op func(int, int) int) int {
return op(x, y)
}
func main() {
ret2 := calc(10, 20, add)
fmt.Println(ret2) //30
}
func do(s string) (func(int, int) int, error) {
switch s {
case "+":
return add, nil
case "-":
return sub, nil
default:
err := errors.New("无法识别的操作符")
return nil, err
}
}
匿名函数
函数内部定义函数
func main() {
// 将匿名函数保存到变量
add := func(x, y int) {
fmt.Println(x + y)
}
add(10, 20) // 通过变量调用匿名函数
//自执行函数:匿名函数定义完加()直接执行
func(x, y int) {
fmt.Println(x + y)
}(10, 20)
}
闭包
闭包指的是一个函数和与其相关的引用环境组合而成的实体。简单来说,闭包=函数+引用环境
func adder() func(int) int {
var x int
return func(y int) int {
x += y
return x
}
}
func main() {
var f = adder()
fmt.Println(f(10)) //10
fmt.Println(f(20)) //30
fmt.Println(f(30)) //60
f1 := adder()
fmt.Println(f1(40)) //40
fmt.Println(f1(50)) //90
}
defer
defer
语句会将其后面跟随的语句进行延迟处理。在defer
归属的函数即将返回时,将延迟处理的语句按defer
定义的逆序进行执行,也就是说,先被defer
的语句最后被执行,最后被defer
的语句,最先被执行
func main() {
fmt.Println("start")
defer fmt.Println(1)
defer fmt.Println(2)
defer fmt.Println(3)
fmt.Println("end")
}
/*
start
end
3
2
1
*/
//面试题 defer注册要延迟执行的函数时,该函数所有的参数都需要确定其值
func calc(index string, a, b int) int {
ret := a + b
fmt.Println(index, a, b, ret)
return ret
}
func main() {
x := 1
y := 2
defer calc("AA", x, calc("A", x, y))
x = 10
defer calc("BB", x, calc("B", x, y))
y = 20
}
/*
A 1 2 3 //defer calc("AA", 1, 3)
B 10 2 12 //defer calc("BB", 10, 12)
BB 10 12 22
AA 1 3 4
*/
内置函数
内置函数 | 介绍 |
---|---|
close | 主要用来关闭channel |
len | 用来求长度,比如string、array、slice、map、channel |
new | 用来分配内存,主要用来分配值类型,比如int、struct。返回的是指针 |
make | 用来分配内存,主要用来分配引用类型,比如chan、map、slice |
append | 用来追加元素到数组、slice中 |
panic和recover | 用来做错误处理 |
Go语言中目前(Go1.12)是没有异常机制,但是使用panic/recover
模式来处理错误。 panic
可以在任何地方引发,但recover
只有在defer
调用的函数中有效
func funcA() {
fmt.Println("func A")
}
func funcB() {
defer func() {
err := recover()
//如果程序出出现了panic错误,可以通过recover恢复过来
if err != nil {
fmt.Println("recover in B")
}
}()
panic("panic in B")
}
func funcC() {
fmt.Println("func C")
}
func main() {
funcA()
funcB()
funcC()
}
recover()
必须搭配defer
使用。defer
一定要在可能引发panic
的语句之前定义。
fmt标准库
fmt包实现了类似C语言printf和scanf的格式化I/O。主要分为向外输出内容和获取输入内容两大部分
func main() {
fmt.Print("在终端打印该信息。") //不换行
name := "沙河小王子"
fmt.Printf("我是:%s\n", name)
fmt.Println("在终端打印单独一行显示")
}
FPrint
Fprint
系列函数会将内容输出到一个io.Writer
接口类型的变量w
中,我们通常用这个函数往文件中写入内容
// 向标准输出写入内容
fmt.Fprintln(os.Stdout, "向标准输出写入内容")
fileObj, err := os.OpenFile("./xx.txt", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
fmt.Println("打开文件出错,err:", err)
return
}
name := "沙河小王子"
// 向打开的文件句柄中写入内容
fmt.Fprintf(fileObj, "往文件中写如信息:%s", name)
只要满足
io.Writer
接口的类型都支持写入
Sprint
Sprint
系列函数会把传入的数据生成并返回一个字符串
s3 := fmt.Sprintln("沙河小王子")
Errorf
e := errors.New("原始错误e")
w := fmt.Errorf("Wrap了一个错误%w", e)
Scan
fmt.Scan(&name, &age, &married)
fmt.Scanf("1:%s 2:%d 3:%t", &name, &age, &married)
fmt.Scanln(&name, &age, &married)
另有Fscan,Sscan
bufio.NewReader
func bufioDemo() {
reader := bufio.NewReader(os.Stdin) // 从标准输入生成读对象
fmt.Print("请输入内容:")
text, _ := reader.ReadString('\n') // 读到换行终止 空格也读入
text = strings.TrimSpace(text)
fmt.Printf("%#v\n", text)
}
结构体
Go语言中没有“类”的概念,也不支持“类”的继承等面向对象的概念。Go语言中通过结构体的内嵌再配合接口比面向对象具有更高的扩展性和灵活性。
自定义类型
自定义类型是定义了一个全新的类型。我们可以基于内置的基本类型定义,也可以通过struct定义
//将MyInt定义为int类型
type MyInt int
通过type
关键字的定义,MyInt
就是一种新的类型,它具有int
的特性。
类型别名
类型别名规定:TypeAlias只是Type的别名,本质上TypeAlias与Type是同一个类型
type TypeAlias = Type
我们之前见过的rune
和byte
就是类型别名
type byte = uint8
type rune = int32
结构体定义
使用type
和struct
关键字来定义结构体,具体代码格式如下:
type 类型名 struct {
字段名 字段类型
字段名 字段类型
…
}
type person struct {
name string
city string
age int8
}
type person1 struct {
name, city string
age int8
}
其中:
- 类型名:标识自定义结构体的名称,在同一个包内不能重复。
- 字段名:表示结构体字段名。结构体中的字段名必须唯一。
- 字段类型:表示结构体字段的具体类型。
实例化
只有当结构体实例化时,才会真正地分配内存。必须实例化后才能使用结构体的字段。
结构体本身也是一种类型,我们可以像声明内置类型一样使用var
关键字声明结构体类型。
var 结构体实例 结构体类型
基本实例化
type person struct {
name string
city string
age int8
}
func main() {
var p1 person
p1.name = "沙河娜扎"
p1.city = "北京"
p1.age = 18
fmt.Printf("p1=%v\n", p1) //p1={沙河娜扎 北京 18}
fmt.Printf("p1=%#v\n", p1) //p1=main.person{name:"沙河娜扎", city:"北京", age:18}
}
匿名结构体,用于临时数据结构
func main() {
var user struct{Name string; Age int}
user.Name = "小王子"
user.Age = 18
fmt.Printf("%#v\n", user)
}
指针类型结构体,使用new分配地址
var p2 = new(person)
//使用&对结构体进行取地址操作相当于对该结构体类型进行了一次new实例化操作
p3 := &person{}
fmt.Printf("%T\n", p2) //*main.person
fmt.Printf("p2=%#v\n", p2) //p2=&main.person{name:"", city:"", age:0}
//支持对结构体指针直接使用.来访问结构体的成员
p2.name = "小王子"
p2.age = 28
p2.city = "上海"
fmt.Printf("p2=%#v\n", p2) //p2=&main.person{name:"小王子", city:"上海", age:28}
初始化
没有初始化的结构体,其成员变量都是对应其类型的零值。初始化是赋值的实例化。
使用键值对初始化
p5 := person{
name: "小王子",
city: "北京",
age: 18,
}
对结构体指针初始化
p6 := &person{
name: "小王子",
city: "北京",
age: 18,
}
用列表初始化
p8 := &person{
"沙河娜扎",
"北京",
28,
}
内存布局
结构体占用一块连续的内存,空结构体不占空间
构造函数
实现类似其他语言面向对象的构造函数,Go是面向接口编程。