1、基础
Println 跟 Printf 都是fmt包中的公共方法。
Println:打印字符串、变量;
Printf:打印需要格式化的字符串,可以输出字符串类型的变量;不可以输出整型变量和整型;
println 会根据你输入格式原样输出,printf需要格式化输出并带输出格式;
函数 | 同函数输出多项 | 不同函数输出 |
---|---|---|
Println | 之间存在空格 | 换行 |
不存在空格 | 不换行 | |
Printf | 格式化输出 | 不换行 |
Go 语言的字符串常见转义符包含回车、换行、单双引号、制表符等,如下表所示。
转义符 | 含义 |
---|---|
\r | 回车符(返回行首) |
\n | 换行符(直接跳到下一行的同列位置) |
\t | 制表符 |
\' | 单引号 |
\" | 双引号 |
\\ | 反斜杠 |
1、普通占位符
占位符 | 说明 | 举例 | 输出 |
---|---|---|---|
%v | 只输出所有的值 | Printf(“%v”, people) | {zhangsan} |
%+v | 先输出字段类型,再输出该字段的值 | Printf(“%+v”, people) | {Name:zhangsan} |
%#v | 先输出结构体名字值,再输出结构体(字段类型+字段的值) | Printf(“#v”, people) | main.Human{Name:“zhangsan”} |
%T | 打印出相应值的数据类型 | Printf(“%T”, people) | main.Human |
%% | 字面上的百分号,并非值的占位符 | Printf(“%%”) | % |
2、布尔占位符
占位符 | 说明 | 举例 | 输出 |
---|---|---|---|
%t | true 或 false | Printf(“%t”, true) | true |
3. 整数占位符
占位符 | 说明 | 举例 | 输出 |
---|---|---|---|
%b | 二进制表示 | Printf(“%b”, 5) | 101 |
%c | 相应Unicode码点所表示的字符 | Printf(“%c”, 0x4E2D) | 中 |
%d | 十进制表示 | Printf(“%d”, 0x12) | 18 |
%o | 八进制表示 | Printf(“%d”, 10) | 12 |
%q | 单引号围绕的字符字面值, 由Go语法安全地转义 | Printf(“%q”, 0x4E2D) | ‘中’ |
%x | 十六进制表示,字母形式为小写 a-f | Printf(“%x”, 13) | d |
%X | 十六进制表示,字母形式为大写 A-F | Printf(“%x”, 13) | D |
%U | Unicode格式:U+1234,等同于 “U+%04X” | Printf(“%U”, 0x4E2D) | U+4E2D |
4. 浮点数和复数的组成部分(实部和虚部)
占位符 | 说明 | 举例 | 输出 |
---|---|---|---|
%b | 无小数部分的,指数为二的幂的科学计数法,与 strconv.FormatFloat 的 ‘b’ 转换格式一致。 | -123456p-78 | |
%e | 科学计数法 | Printf(“%e”, 10.2) | 1.020000e+01 |
%E | 科学计数法 | Printf(“%e”, 10.2) | 1.020000E+01 |
%f | 有小数点而无指数 | Printf(“%f”, 10.2) | 10.200000 |
%g | 根据情况选择 %e 或 %f | Printf(“%g”, 10.20) | 10.2 |
%G | 根据情况选择 %E 或 %f | Printf(“%G”, 10.20+2i) | (10.2+2i) |
5. 字符串与字节切片
占位符 | 说明 | 举例 | 输出 |
---|---|---|---|
%s | 输出字符串表示(string类型或[]byte) | Printf(“%s”, []byte(“Go语言”)) | Go语言 |
%q | 双引号围绕的字符串, | Printf(“%q”, “Go语言”) | “Go语言” |
%x | 十六进制, 小写字母, 每字节两个字符 | Printf(“%x”, “golang”) | 676f6c616e67 |
%X | 十六进制, 大写字母, 每字节两个字符 | Printf(“%X”, “golang”) | 676F6C616E67 |
6. 指针
占位符 | 说明 | 举例 | 输出 |
---|---|---|---|
%p | 十六进制表示,前缀 0x | Printf(“%p”, &people) | 0x4f57f0 |
2、循环
1、if循环
func main() { score := 65 if score >= 90 { //注意,{一定要跟if语句在同一行 fmt.Println("A") } else if score > 75 { fmt.Println("B") } else { fmt.Println("C") } }
1.1if条件判断特殊写法,if里的变量只在循环里有效
if条件判断还有一种特殊的写法,可以在 if 表达式之前添加一个执行语句,再根据变量值进行判断,举个例子:
func main() { if score := 65; score >= 90 { fmt.Println("A") } else if score > 75 { fmt.Println("B") } else { fmt.Println("C") } }
2、for循环
条件表达式返回true
时循环体不停地进行循环,直到条件表达式返回false
时自动退出循环
1、基本循环
func main() { for i := 0; i < 10; i++ { fmt.Println(i) } }
for循环的初始语句可以被忽略,但是初始语句后的分号必须要写,例如:
func main() { i := 0 for ; i < 10; i++ { fmt.Println(i) } }
for循环的初始语句和结束语句都可以省略,例如:
func main() { i := 0 for i < 10 { fmt.Println(i) i++ } }
1、 break停止循环
func main() { for i := 0; i < 5; i++ fmt.Println(i) if i == 3{ break } } }
2、continue 跳过某次循环
func main() { for i := 0; i<5; i++{ if i == 3{ continue //跳过这次循环,继续下一次循环 } fmt.Println(i) } }
3、switch case
1、switch使用case进行条件判断
func main() { test :=3 switch test { case 1: fmt.Println("gogo") case 2: fmt.Println("gogogo") case 3: //判断test的值是否为3,然后输出 fmt.Println("gogogogo") case 4: fmt.Println("test") default: //每个switch只能又一个default分支 fmt.Println("fail") } }
2、case一次判断多个值
func main () { n := 7; switch n { case 1, 3, 5, 7, 9: fmt.Println("奇数") case 2, 4, 6, 8: fmt.Println("偶数") default: fmt.Println(n) } }
3、case中使用表达式
func main() { age := 30 switch { case age < 25: fmt.Println("好好学习吧") case age > 25 && age < 35: fmt.Println("好好工作吧") case age > 60: fmt.Println("好好享受吧") default: fmt.Println("活着真好") } }
3、数组
数组是同一种数据类型元素的集合。 在Go语言中,数组从声明时就确定,使用时可以修改数组成员,但是数组大小不可变化。 基本语法:
var 数组变量名 [元素数量]T
// 定义一个长度为3元素类型为int的数组a var a [3]int
1、初始化数组
数组长度是从0开始的,
func main() { var test [3]int //自定义数组元素的值 var num = [...]int{1, 2} // ... 让编辑器根据初始值的个数自行推断数组的长度 var chengshi = [3]string{"北京", "上海", "深圳"} fmt.Println(test) //[0 0 0] fmt.Println(num) //[1 2] fmt.Printf("type of num:%T\n", num) //type of numArray:[2]int fmt.Println(chengshi) //[北京 上海 深圳] fmt.Printf("type类型:%T\n", chengshi) //:[3]string //Printf 打印出数组类型 }
2、遍历数组
指沿着某条搜索路线,依次对树(或图)中每个节点均做一次访问
func main() { var a = [...]string{"北京", "上海", "深圳"} // 方法1:for循环遍历 for i := 0; i < len(a); i++ { fmt.Println(a[i]) } // 方法2:for range遍历 for index, value := range a { fmt.Println(index, value) } }
3、二维数组
func main() { a := [3][2]string{ {"北京", "上海"}, {"广州", "深圳"}, {"成都", "重庆"}, } fmt.Println(a) //[[北京 上海] [广州 深圳] [成都 重庆]] fmt.Println(a[2][1]) //索引取值:重庆,注意,数组是从0开始的
3.1二维数组的遍历
func main() { a := [3][2]string{ //只有最外层可以用...来让编辑器自行推导 {"北京", "上海"}, {"广州", "深圳"}, {"成都", "重庆"}, } for _, v1 := range a { for _, v2 := range v1 { fmt.Printf("%s\t", v2) // } fmt.Println() } }
4、切片
1、简述
简述切片(Slice)是一个拥有相同类型元素的可变长度的序列。它是基于数组类型做的一层封装。它非常灵活,支持自动扩容。
切片是一个引用类型,它的内部结构包含地址
、长度
和容量
,切片一般用于快速地操作一块数据集合。
1.1 基本语法如下
var name []T
其中,
- name:表示变量名
- T:表示切片中的元素类型 (切片要初始化后才能使用)
1.2 切片的长度和容量
切片拥有自己的长度和容量,我们可以通过使用内置的len()
函数求长度,使用内置的cap()
函数求切片的容量。
1.3 切片表达式
切片表达式从字符串、数组、指向数组或切片的指针构造子字符串或切片。它有两种变体:一种指定low和high两个索引界限值的简单的形式,另一种是除了low和high索引界限值外还指定容量的完整的形式。
1.4 简单切片表达式
切片的底层就是一个数组,所以我们可以基于数组通过切片表达式得到切片。 切片表达式中的low
和high
表示一个索引范围(左边包含,右边不包含),也就是下面代码中从数组a中选出1<=索引值<4
的元素组成切片s,得到的切片长度=high-low
,容量等于得到的切片的底层数组的容量。
2、基于数组的切片表达式
func main() { a := [5]int{1, 2, 3, 4, 5} //数组里是从0开始的 s := a[1:3] //在数组里是从0开始,所以s拿a的是1到3,即是拿a的第2,3位(后面的3需要-1) fmt.Println(s) //直接输出内容,得到的是2和3 fmt.Printf("%T\n",s) //printf 按格式%T,打印出相应值的数据类型并换行(\n) }
注意:如果指定切片的容量,那么容量就为指定的值
2.1、切片再切片
func main() { a := [5]int{1, 2, 3, 4, 5} s := a[1:3] fmt.Println(s) //直接输出内容 fmt.Printf("%T\n",s) //printf 按格式%T,打印出相应值的数据类型并换行(\n)得到的是s的数据类型 c :=s[0:len(s)] //以s的结果再做切片,因为定义了len的长度是s,s为的长度为2,所以打印的是数组0:2 fmt.Println(c) //所以输出的内容跟s是相同的,也是2和3 fmt.Printf("c:%v len(c):%v cap(c):%v\n", c, len(c), cap(c)) //len是切片长度,cap是切片容量 }
注意:len是长度,切片的长度就是它所包含的元素个数,上述有5个元素,所以他的切片长度是5
cap是切片的容量,在声明切片时,如果指定切片的容量,那么容量就为指定的值;如果没有说明切片的容量,那么默认容量和切片的长度保持一致
上题中,在切片中再切片的时候,因为s是在a的基础上划分出来的,所以5-1=4(5是a的长度,而1是切片s在a的起始位置)
3、make()函数构造切片
如果需要动态的创建一个切片,我们就需要使用内置的make()
函数,格式如下:
make([]T, size, cap) make([]int,5,10) 长度为5,容量为10的int类型的切片
举例:
func main() { a := make([]int, 2, 10) fmt.Println(a) //[0 0] fmt.Println(len(a)) //2 fmt.Println(cap(a)) //10 }
4、切片之间不能直接比较
切片之间是不能比较的,我们不能使用==
操作符来判断两个切片是否含有全部相等元素。 切片唯一合法的比较操作是和nil
比较。
func main() { var a []int //声明int类型空切片,没定义长度和容量 b := []int{} //声明int类型的数组并初始化 s3 := make([]int, 0) //make定义一个切片 if a == nil { //将空切片跟nil做比较 fmt.Println("a=nil") } if b == nil { //将空切片跟nil做比较 fmt.Println("b=nil") //无法输出,因为在创建的时候,已经创建了一个初始的底层数组 } if c == nil { //将空切片跟nil做比较 fmt.Println("c=nil") //无法输出,已经创建了一个底层函数 } fmt.Println(a,cap(a),len(a)) //输出[] 0 0 fmt.Println(b,cap(b),len(b)) //输出[] 0 0 fmt.Println(c,cap(c),len(c)) //输出[] 0 0 }
5、切片的赋值拷贝
对一个切片的修改会影响另一个切片的内容
func main() { a := make([]int, 3) //[0 0 0] b := a //将a直接赋值给b,a和b共用一个底层数组 b[0] = 100 fmt.Println(s1) //[100 0 0] fmt.Println(s2) //[100 0 0] }
6、切片的遍历
切片的遍历方式和数组是一致的,支持索引遍历和for range
遍历。
func main() { s := []int{1,2,3,4,5} for i := 0; i < len(s); i++ { //i=0, i小于s的切片长度时,i++ fmt.Println(i, s[i]) } for t, v := range s { // fmt.Println(t, v) } }
7、append()方法为切片添加元素⭐
func main(){ 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] fmt.Println(s,s2) //[1 2 3 4 5 6 7] [5 6 7] }
每个切片会指向一个底层数组,这个数组的容量够用就直接添加新增元素。当底层数组不能容纳新增的元素时,切片就会自动按照一定的策略进行“扩容”,此时该切片指向的底层数组就会更换。“扩容”操作往往发生在append()
函数调用时,所以我们通常都需要用原变量接收append函数的返回值。
func main() { //append()添加元素和切片扩容 var a []int for i := 0; i < 10; i++ { a = append(a, i) fmt.Printf("%v len:%d cap:%d ptr:%p\n", a, len(a), cap(a), a) } }
输出:
[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倍。
7.1、一次性追加多个元素
var a []string // 追加一个元素 a = append(a, "北京") // 追加多个元素 a = append(a, "上海", "广州", "深圳") // 追加切片 a := []string{"成都", "重庆"} a = append(a, a...) //3个. 是为了让a把上面的切片里的元素一个个拿出来追加,而不是当成一个元素里追加 fmt.Println(a) //[北京 上海 广州 深圳 成都 重庆]
8、切片的扩容策略(go语言不同版本有差异)
在golang1.18版本更新之前网上大多数的文章都是这样描述slice的扩容策略的:
当原slice容量小于1024的时候,新 slice 容量变成原来的 2 倍;原 slice 容量超过 1024,新slice容量变成原来的1.25倍。
在1.18版本更新之后,slice的扩容策略变为了:
当原slice容量(oldcap)小于256的时候,新slice(newcap)容量为原来的2倍;原slice容量超过256,新slice容量newcap = oldcap+(oldcap+3*256)/4
详细的可以看:https://golang.design/go-questions/slice/grow/
或者是到$GOROOT/src/runtime/slice.go 里查看对应的源码
或者通过go version查看版本再看官方的更新
9、copy函数复制切片
copy()函数的使用格式如下:
copy(dest, srce []T) src: 数据来源切片 dest: 目标切片
func main() { // copy()复制切片 a := []int{1, 2, 3, 4, 5} c := make([]int, 5, 5) //声明一个长度容量为5的int切片 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 //将c里的第一个数据替换为1000 fmt.Println(a) //[1 2 3 4 5] fmt.Println(c) //[1000 2 3 4 5] }
10、切片中删除元素
Go语言中并没有删除切片元素的专用方法,我们可以使用切片本身的特性来删除元素。
func main() { // 从切片中删除元素 a := []int{30, 31, 32, 33, 34, 35, 36, 37} // 要删除索引为2的元素 a = append(a[:2], a[3:]...) //从0位到2位之前的30,31,到第3位之后的33~37,没有追加输出a[2]的32 fmt.Println(a) //[30 31 33 34 35 36 37] }
小结:要从切片a中删除索引为index
的元素,操作方法是a = append(a[:index], a[index+1:]…)
11、切片的排序(sort包)
Go 内置 sort 包中提供了根据一些排序函数可对任何序列进行排序,并提供自定义排序规则的能力。
sort 包实现了四种基本排序算法:插入排序(Shell 排序)、归并排序、堆排序和快速排序。 但是这四种排序方法是不公开的,它们只被用于 sort 包内部使用,sort 包会根据实际数据自动选择高效的排序算法。
Go sort 包主要提供了三种排序能力: (1)基本类型切片排序; (2)自定义比较器; (3)排序任意数据结构。
三者易用程度逐渐降低。
2.基本类型切片排序 对于 int 、 float64 和 string 数组或是分片的排序, go 分别提供了 sort.Ints() 、 sort.Float64s() 和 sort.Strings() 函数, 默认都是从小到大排序。。
11.1、默认升序,从小到大
package main import ( "fmt" "sort" ) func main() { intList := []int {2, 4, 3, 5, 7, 6, 9, 8, 1, 0} //定义一个int切片类型,排序随便 float := []float64 {4.2, 5.9, 12.3, 10.0, 50.4, 99.9, 31.4, 27.81828, 3.14} //浮点数 stringList := []string {"a", "c", "b", "d", "f", "i", "z", "x", "w", "y"} //字符串 sort.Ints(intList) sort.Float64s(float) sort.Strings(stringList) fmt.Printf("%v\n%v\n%v\n", intList, float, stringList) }
11.2、降序
go 的 sort 包可以使用 sort.Reverse(slice) 来调换 slice.Interface.Less ,也就是比较函数,所以, int 、 float64 和 string 的逆序排序函数可以这么写:
package main import ( "fmt" "sort" ) func main() { intList := [] int {2, 4, 3, 5, 7, 6, 9, 8, 1, 0} float8List := [] float64 {4.2, 5.9, 12.3, 10.0, 50.4, 99.9, 31.4, 27.81828, 3.14} stringList := [] string {"a", "c", "b", "d", "f", "i", "z", "x", "w", "y"} sort.Sort(sort.Reverse(sort.IntSlice(intList))) sort.Sort(sort.Reverse(sort.Float64Slice(float8List))) sort.Sort(sort.Reverse(sort.StringSlice(stringList))) fmt.Printf("%v\n%v\n%v\n", intList, float8List, stringList) }
[https://itimetraveler.github.io/2016/09/07/%E3%80%90Go%E8%AF%AD%E8%A8%80%E3%80%91%E5%9F%BA%E6%9C%AC%E7%B1%BB%E5%9E%8B%E6%8E%92%E5%BA%8F%E5%92%8C%20slice%20%E6%8E%92%E5%BA%8F/] 参考资料
11.3、自定义排序升序(复杂,与切片无关)
使用 sort.Slice() 函数可以排序任意类型的切片,但是需要用户提供函数来判断元素大小关系。
函数类型为func( i, j int ) bool,其中参数 i, j 是序列中的索引。
func Slice(x interface{}, less func(i, j int) bool)
这个用来比较大小的函数就是比较器,返回值 true 表示下标 i 的元素小于下表 j 的元素。如果是降序排列的话,可以理解为返回 true 表示下标 i 的元素排在下标 j 的元素前面。
如果想稳定排序的话,使用 sort.SliceStable(),在排序切片时会保留相等元素的原始顺序。按照年龄升序排序的示例:
func main() { slStdnt := []struct { Name string //定义了三列数据的类型 Age int Height int }{ {"Alice", 23, 175}, {"David", 18, 185}, {"Eve", 18, 165}, {"Bob", 25, 170}, } // 用 定义的age进行排序,年龄相等的元素保持原始顺序 sort.SliceStable(slStdnt, func(i, j int) bool { return slStdnt[i].Age < slStdnt[j].Age }) fmt.Println(slStdnt) //输出 [{David 18 185} {Eve 18 165} {Alice 23 175} {Bob 25 170}]
如果想要在年龄相等的情况下再按照身高排序,我们多加一个if循环 即可。
// 优先按照年龄排,年龄相等的话再按照身高排 sort.SliceStable(slStdnt, func(i, j int) bool { //i表示a if slStdnt[i].Age < slStdnt[j].Age { return true } if slStdnt[i].Age > slStdnt[j].Age { return false } return slStdnt[i].Height < slStdnt[j].Height //年龄排序完按身高排 }) fmt.Println(slStdnt) //[{Eve 18 165} {David 18 185} {Alice 23 175} {Bob 25 170}] }
11.4、排序任意数据结构(复杂,与切片无关)
使用 sort.Sort() 或者 sort.Stable() 函数可完成对任意类型元素的排序。
一个内置的排序算法需要知道三个东西:序列的长度,表示两个元素比较的结果,一种交换两个元素的方式;这就是 sort.Interface 的三个方法:
type Interface interface { Len() int Less(i, j int) bool // i, j 是元素的索引 Swap(i, j int)
这种方法相较于前面介绍的两种排序,用起来稍微麻烦了点,因为需要用户自定义的东西更多了,不过带来的好处也是显而易见的,更加普适。
这种方法相较于前面介绍的两种排序,用起来稍微麻烦了点,因为需要用户自定义的东西更多了,不过带来的好处也是显而易见的,更加普适。
还是以学生排序为例,在自定义的结构体上实现 srot.Interface 接口。
type Student struct { Name string Age int Height int } // ByAgeHeight 实现 sort.Interface 接口 type ByAgeHeight []Student func (a ByAgeHeight) Len() int { return len(a) } // Less 先用年龄排序,年龄相等再用身高排 func (a ByAgeHeight) Less(i, j int) bool { if a[i].Age < a[j].Age { return true } if a[i].Age > a[j].Age { return false } return a[i].Height < a[j].Height } func (a ByAgeHeight) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func main() { slStdnt := []Student{ {"Alice", 23, 175}, {"David", 18, 185}, {"Eve", 18, 165}, {"Bob", 25, 170}, } sort.Stable(ByAgeHeight(slStdnt)) fmt.Println(slStdnt) //[{Eve 18 165} {David 18 185} {Alice 23 175} {Bob 25 170}] }
12、总结
- 1、Go切片本质上是一个结构体,保存了其长度、底层数组的容量、底层数组的指针。
- 2、Go切片创建方式比较多样:变量声明、切片字面量、make创建、new创建、从切片/数组截取。
- 3、Go切片使用len()计算长度、cap()计算容量、append()来添加元素。
- 4、Go切片相比数组更灵活,有很多技巧,也正因为灵活,容易发生类似内存泄露的问题,需要注意。
5、map
map是一种无序的基于key-value的数据结构,Go语言中的map是引用类型,必须初始化才能使用。
Go语言中 map
的定义语法如下:
map[KeyType]ValueType
其中,
- KeyType:表示键的类型。
- ValueType:表示键对应的值的类型。
map类型的变量默认初始值为nil,需要使用make()函数来分配内存。语法为:
make(map[KeyType]ValueType, [cap])
其中cap表示map的容量,该参数虽然不是必须的,但是我们应该在初始化map的时候就为其指定一个合适的容量。
1.1、声明一个map
声明变量后一定要完成初始化,否则map的值就是nil
func main() { a := make(map[string]int, 8) //make创建一个map类型为string,值为int,容量为8的类型 a["张三"] = 90 //添加键值对 a["小明"] = 100 fmt.Println(a) fmt.Println(a["小明"]) //输出a变量里string类型名字为小明的数据,得到100 fmt.Printf("type of a:%T\n", a) //type of a:map[string]int }
1.2、声明变量时完成初始化
没有初始化不能给他赋值或者是添加键值对
func main() { b:=map[int]bool{ 2:true, 4:false, } fmt.Println(b) //直接输出b map[2:true 4:false] fmt.Printf("type:%T\n",b) // type:map[int]bool fmt.Printf("b:%#v\n",b) //b:map[int]bool{2:true, 4:false} }
1.3、判断map中键值是否存在
value, ok := map[key] //VALUE OK都可以自定义
func main() { test := make(map[string]int) test["张三"] = 90 test["小明"] = 100 // 如果key存在,那么ok为true,v则为对应的值;不存在ok为false,v为值类型的零值 2个函数可以自定义 v, ok := test["老王"] //上述 fmt.Println(v,ok)//0 false 此时老王不在键值里,所以是false if ok { fmt.Println(test) //存在就输出test map[小明:100 张三:90] } else { fmt.Println("查无此人") //因为不存在老王,所以输出查无此人 t, ook := test["小明"] fmt.Println(t,ook)//100 true if ook { fmt.Println(test) //存在,所以输出t map[小明:100 张三:90] } else { fmt.Println("查无此人") } fmt.Println(test) //map[小明:100 张三:90] }
1.4、map的遍历
Go语言中使用 for range 遍历map
func main() { test := make(map[string]int) test["张三"] = 90 test["小明"] = 100 test["娜扎"] = 60 for k, v := range test { fmt.Println(k, v) //张三 90 小明 100 娜扎 60 //map是无序的,键值对和添加的顺序无关 } }
map是无序的,键值对和添加的顺序无关
但我们只想遍历key的时候,可以按下面的写法:
func main() { test := make(map[string]int) test["张三"] = 90 test["小明"] = 100 test["娜扎"] = 60 for k := range scoreMap { fmt.Println(k) // 张三 小明 娜扎 } for _,V:= range test { fmt.Println(V) // 90 100 60 } }
注意: 遍历map时的元素顺序与添加键值对的顺序无关。
1.5、使用delete()函数删除键值对
使用delete()内建函数从map中删除一组键值对,delete()函数的格式如下:
delete(map, key)
其中,
- map:表示要删除键值对的map
- key:表示要删除的键值对的键
示例代码如下:
func main(){ test := make(map[string]int) test["张三"] = 90 test["小明"] = 100 test["娜扎"] = 60 delete(test, "小明")//将小明:100从map中删除 for k,v := range test{ fmt.Println(k, v) } }
1.6、 生成随机数遍历排序(math/rand随机数包)
package main import ( "fmt" "math/rand" //随机数包 "sort" //排序包 "time" //时间包 ) func main() { rand.Seed(time.Now().UnixNano()) //初始化随机数种子,如果不设置当前时间,每次运行得到的结果一直,是伪随机数,.UnixNano()代表纳秒时间 var test = make(map[string]int, 200) for i := 0; i < 100; i++ { key := fmt.Sprintf("test%d", i) //生成test开头的字符串 %03d表示占位符有3个,以十进制表示 value := rand.Intn(100) //生成0~99的随机整数 test[key] = value } //取出map中的所有key存入切片keys var keys = make([]string, 0, 200) for key := range test { //遍历key生成的test开头的字符串 keys = append(keys, key) } //对切片进行排序 sort.Strings(keys) //按照排序后的key遍历map for _, key := range keys { fmt.Println(keys), test[key]) } }
1.7 元素为map类型的切片
func main() { var test = make([]map[string]string,3) //完成了切片初始化,定义容量为3 test[0] = make(map[string]string,8) //对切片中的map元素进行初始化 test[1] = make(map[string]string,8) test[2] = make(map[string]string,8) test[0]["name"] = "切片元素" test[1]["name2"] = "切片元素2" test[2]["name3"] = "切片元素3" fmt.Println(test) //对切片中的map元素进行初始化 }
1.8 值为切片的map
func main() { var test = make(map[string][]int, 3) //初始化map,但切片没有初始化 fmt.Println(test) //map[] test["GO"] = make([]int,8) //完成切片初始化,此时切片容量长度为8 test["GO"][0] = 100 //定义切片容量为0的内容为100 test["GO"][2] = 300 fmt.Println(test) }
1.9 map统计某个词汇出现才次数
func main() { var test = "test go test run test" var sum = make(map[string]int,20) //定义一个map fenge := strings.Split(test, " ") //以空格为间隔 for _, test2 := range fenge { //遍历test函数里的内容 v, ok := sum[test2] if ok { //有这个单词就+1 sum[test2] = v + 1 }else { //出现一次就算1 sum[test2] = 1 fmt.Print(sum) //map[test:1]map[go:1 test:1]map[go:1 run:1 test:2] } } for k, v :=range sum { //再遍历map fmt.Println(k, v) //go 1 run 1 test 3 } }
6、函数
1.1 函数的定义和调用
Go语言中支持函数、匿名函数和闭包,函数是组织好的、可重复使用的、用于执行指定任务的代码块
Go语言中定义函数使用func
关键字,具体格式如下:
func 函数名(参数)(返回值){ 函数体 }
其中:
- 函数名:由字母、数字、下划线组成。但函数名的第一个字母不能是数字。在同一个包内,函数名也称不能重名(包的概念详见后文)。
- 参数:参数由参数变量和参数变量的类型组成,多个参数之间使用
,
分隔。 - 返回值:返回值由返回值变量和其变量类型组成,也可以只写返回值的类型,多个返回值必须用
()
包裹,并用,
分隔。 - 函数体:实现指定功能的代码块。
func test() { //定义函数test fmt.Println("test go") } func main(){ test() //直接调用,输出test go }
1.2 参数
函数的参数中如果相邻变量的类型相同,则可以省略类型
func test(name string) { //定义函数test 和name函数,数据类型为string fmt.Println("test go",name) } func main(){ n := "woo" test(n) //直接调用,输出test go woo }
func test(a,b int)int { //定义函数test return a + b } func main(){ c :=test(10,30) //传入函数test里a和b的值 fmt.Println(c) //40 } // -------------------- func test(a,b int) (sum int) { //定义函数test sum = a + b return } func main(){ c :=test(10,30) //传入函数test里a和b的值 fmt.Println(c) //40 }
1.3 可变参数
可变参数是指函数的参数数量不固定。Go语言中的可变参数通过在参数名后加...
来标识。
注意:可变参数通常要作为函数的最后一个参数。
func test(a... int)int { //定义函数test ret := 0 for _, arg := range a { //遍历a里的数据 ret = ret + arg } return ret } func main (){ r1 := test() r2 := test(10) r3 := test(10,20) fmt.Println(r1,r2,r3) // 0 10 30 }
固定参数和可变参数的一起调用,但可变参数要放在固定参数的后面
func test(a int,b ...int) int { //定义函数test ret := a for _, arg := range b { //遍历b的数据 ret = ret + arg } return ret } func main (){ r1 := test(0) //此时a=0 ,b=[] r2 := test(10) //a=10,b=[] r3 := test(10,20)//a=10,b=[20] r4 := test(10,20,30)//a=10,b=[20 30] fmt.Println(r1,r2,r3,r4) // 0 10 30 60 }
1.4 返回值
Go语言中函数支持多返回值,函数如果有多个返回值时必须用()
将所有返回值包裹起来。
func calc(x, y int) (int, int) { sum := x + y sub := x - y return sum, sub } func main (){ x,y := calc(20,10) fmt.Println(x,y) //30 10 }
1.5 defer语句 延迟处理
Go语言中的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 }
1.6 变量作用域
1、全局变量
全局变量是定义在函数外部的变量,它在程序整个运行周期内都有效。 在函数中可以访问到全局变量。
//定义全局变量num var num int64 = 10 func test() { fmt.Printf("num=%d\n", num) //函数中可以访问全局变量num } func main() { test() //num=10 }
2、局部变量
1、函数内定义的变量无法在该函数外使用
func test() { //定义一个函数局部变量a,仅在该函数内生效 var a int64 = 100 fmt.Printf("x=%d\n", a) //X=100 } func main() { TEST() fmt.Println(x) // 此时无法使用变量x,在外层不能访问函数的局部变量 }
2、如果局部变量和全局变量重名,优先访问局部变量。
var num int64 = 10 func test() { num := 100 fmt.Printf("num=%d\n", num) // 函数中优先使用局部变量 } func main() { testNum() // num=100 }
3、循环语句中定义的变量,也是只在for语句块中生效
func test() { for i := 0; i < 10; i++ { fmt.Println(i) //变量i只在当前for语句块中生效 } //fmt.Println(i) //此处无法使用变量i }
1.7 函数作为参数
函数可以作为参数:
func add(x, y int) int { //定义一个add函数,x,y为int类型的参数,返回值也为int类型 return x + y //返回值 } //定义了一个名为op,类型为func的函数,接收2个int类型参数,返回值也为int类型 func calc (x, y int, op func(int, int)int) int { return op(x,y) } func sub(x, y int) int { return x - y } func main(){ r1 := calc(100, 200, add) //把函数作为参数传入calc里 fmt.Println(r1) //300 r2 := calc(100, 200, sub) fmt.Println(r2) //-100 }
1.8 匿名函数
匿名函数就是没有函数名的函数,匿名函数的定义格式如下:
func(参数)(返回值){ 函数体 }
func main() { // 将匿名函数赋值给变量add add := func(x, y int) { //匿名函数里有2个参数 fmt.Println(x + y) //30 } add(10, 20) // 通过变量调用匿名函数 //自执行函数:匿名函数定义完再结尾加()直接执行,不需要赋值 func(x, y int) { fmt.Println(x + y) }(10, 20) //30 }
1.9 闭包
闭包指的是一个函数和与其相关的引用环境组合而成的实体。
简单来说,闭包=函数+引用环境
func a() func(){ //定义了函数a的返回值是一个函数 name :="woo" return func(){ //返回函数a fmt.Println("hello",name) //输出 } } func main(){ //闭包=函数+外层变量的引用 r := a() //r就是一个闭包 r() //执行a函数内部的匿名函数 }
func test(suffix string) func(string) string { //test函数接受suffix函数类型的参数,返回值也是函数,接受string类型参数,返回值也是string return func(name string) string { //返回了一个匿名函数,匿名函数接受名字为name的参数,返回值也是string,满足func(string) string的要求 if !strings.HasSuffix(name, suffix) { //strings.HasSuffix 判断是否已经以suffix结尾 return name + suffix //返回 } return name } } func main() { jpg := test(".jpg") //将jpg参数传给test函数,此时test函数的参数为.jpg ret := jpg("test") txt := test(".txt") fmt.Println(ret) //test.jpg fmt.Println(txt("test")) //test.txt }
func calc(base int) (func(int) int, func(int) int) { //定义一个calc名字的函数,接受2个函数类型的参数,返回值也是函数 add := func(i int) int { //将函数赋值给add变量 base += i //调用了外部的base函数,此时等于base加上i后再赋值给base return base //返回base的值,此时base已经不是原来的base了,而加上i以后的值 } sub := func(i int) int { base -= i //此时等于base减去i后再赋值给base return base } return add, sub } func main() { f1, f2 := calc(10) ret := f1(20) fmt.Println(ret)//10+20 =30 ret2 := f2(15) //30-15=15 fmt.Println(ret2) fmt.Println(f1(1), f2(3)) //15+1 16-3 //15(base)+1(add)=16 16-3(sub)=3 }
1.10内置函数介绍
内置函数 | 介绍 |
---|---|
close | 主要用来关闭channel |
len | 用来求长度,比如string、array、slice、map、channel |
new | 用来分配内存,主要用来分配值类型,比如int、struct。返回的是指针 |
make | 用来分配内存,主要用来分配引用类型,比如chan、map、slice |
append | 用来追加元素到数组、slice中 |
panic和recover | 用来做错误处理 |
panic/recover
Go语言中目前(Go1.12)是没有异常机制,但是使用panic/recover
模式来处理错误。 panic
可以在任何地方引发,但recover
只有在defer
调用的函数中有效。 首先来看一个例子:
package main import "fmt" 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() //func A funcB() //recover in B funcC() //func C }
recover()
必须搭配defer
使用。defer
一定要在可能引发panic
的语句之前定义。
7、指针
区别于C/C++中的指针,Go语言中的指针不能进行偏移和运算,是安全指针。
要搞明白Go语言中的指针需要先知道3个概念:指针地址、指针类型和指针取值。
任何程序数据载入内存后,在内存都有他们的地址,这就是指针。而为了保存一个数据在内存中的地址,我们就需要指针变量。
比如,“永远不要高估自己”这句话是我的座右铭,我想把它写入程序中,程序一启动这句话是要加载到内存(假设内存地址0x123456),我在程序中把这段话赋值给变量A
,把内存地址赋值给变量B
。这时候变量B
就是一个指针变量。通过变量A
和变量B
都能找到我的座右铭。
Go语言中的指针不能进行偏移和运算,因此Go语言中的指针操作非常简单,我们只需要记住两个符号:&
(取地址)和*
(根据地址取值)。
指针地址和指针类型
每个变量在运行时都拥有一个地址,这个地址代表变量在内存中的位置。Go语言中使用&
字符放在变量前面对变量进行“取地址”操作。 Go语言中的值类型(int、float、bool、string、array、st ruct)都有对应的指针类型,如:*int
、*int64
、*string
等。
取变量指针的语法如下:
ptr := &v // v的类型为T
其中:
- v:代表被取地址的变量,类型为
T
- ptr:用于接收地址的变量,ptr的类型就为
*T
,称做T的指针类型。*代表指针。
1、指针地址
func main() { 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 此时b也有自己的内存地址 }
我们来看一下b := &a
的图示:
2、指针取值
在对普通变量使用&操作符取地址后会获得这个变量的指针,然后可以对指针使用*操作,也就是指针取值,代码如下。
func main() { //指针取值 a := 10 b := &a // 取变量a的地址,将指针保存到b中 fmt.Printf("a:%d ptr:%p\n", a, &a) //a:10 ptr:0x11418098 fmt.Printf("b:%p type:%T\n", b, b) //b:0x11418098 type:*int c := *b // 指针取值(根据指针去内存取值) fmt.Printf("type of c:%T\n", c) // type of c:int fmt.Printf("value of c:%v\n", c) //10 因为b取了变量a的地址,而a=10 }
- 对变量进行取地址(&)操作,可以获得这个变量的指针变量。
- 指针变量的值是指针地址。
- 对指针变量进行取值(*)操作,可以获得指针变量指向的原变量的值。
3、指针传值
func modify1(x int) { x = 100 } func modify2(x *int) { *x = 300 } func main() { a := 10 modify1(a) fmt.Println(a) // 10 modify2(&a) fmt.Println(a) // 300 }
4、new函数和make函数
new
new是一个内置的函数,它的函数签名如下:
func new(Type) *Type
其中,
- Type表示类型,new函数只接受一个参数,这个参数是一个类型
- *Type表示类型指针,new函数返回一个指向该类型内存地址的指针。
new函数不太常用,使用new函数得到的是一个类型的指针,并且该指针对应的值为该类型的零值。
func main() { var a *int //定义函数a是一个指针类型的nuil a = new(int) *a = 10 fmt.Println(*a) //10 }
make函数
make也是用于内存分配的,区别于new,它只用于slice、map以及channel的内存创建,而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了。make函数的函数签名如下:
func make(t Type, size ...IntegerType) Type
make函数是无可替代的,我们在使用slice、map以及channel的时候,都需要使用make进行初始化,然后才可以对它们进行操作。
示例中var b map[string]int
只是声明变量b是一个map类型的变量,需要像下面的示例代码一样使用make函数进行初始化操作之后,才能对其进行键值对赋值:
func main() { var b map[string]int b = make(map[string]int, 10) b["沙河娜扎"] = 100 fmt.Println(b) }
new与make的区别
- 二者都是用来做内存分配的。
- make只用于slice、map以及channel的初始化,返回的还是这三个引用类型本身;
- 而new用于类型的内存分配,并且内存对应的值为类型零值,返回的是指向类型的指针。
8、类型别名和自定义类型
特别注意:
结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问)。
1、定义类型
在Go语言中有一些基本的数据类型,如string
、整型
、浮点型
、布尔
等数据类型, Go语言中可以使用type
关键字来定义自定义类型。
自定义类型是定义了一个全新的类型。我们可以基于内置的基本类型定义,也可以通过struct定义。例如:
//将test定义为int类型 type test int
通过type
关键字的定义,test就是一种新的类型,它具有
int`的特性。
type test int //定义一个基于int类型的test自定义类型 func main() { var woo test //woo是 test自定义类型 fmt.Printf("type:%T value:%v\n", woo, woo) //type:main.test value:0 }
2、类型别名
类型别名是Go1.9
版本添加的新功能。
类型别名规定:TypeAlias只是Type的别名,本质上TypeAlias与Type是同一个类型。就像一个孩子小时候有小名、乳名,上学后用学名,英语老师又会给他起英文名,但这些名字都指的是他本人。
type TypeAlias = Type
我们之前见过的rune
和byte
就是类型别名,他们的定义如下:
type byte = uint8 type rune = int32
type test int //定义一个基于int类型的test自定义类型 type tt = int //int类型别名 func main() { var woo test //woo是 test自定义类型 fmt.Printf("type:%T value:%v\n", woo, woo) //type:main.test value:0 var wo tt fmt.Printf("type:%T value:%v\n", wo, wo) //type:int value:0 }
3、结构体
Go语言中的基础数据类型可以表示一些事物的基本属性,但是当我们想表达一个事物的全部或部分属性时,这时候再用单一的基本数据类型明显就无法满足需求了,Go语言提供了一种自定义数据类型,可以封装多个基本数据类型,这种数据类型叫结构体,英文名称struct
。 也就是我们可以通过struct
来定义自己的类型了。
Go语言中通过struct
来实现面向对象。
1、结构体的定义
使用type
和struct
关键字来定义结构体,具体代码格式如下:
type 类型名 struct { 字段名 字段类型 字段名 字段类型 … }
其中:
- 类型名:标识自定义结构体的名称,在同一个包内不能重复。
- 字段名:表示结构体字段名。结构体中的字段名必须唯一。
- 字段类型:表示结构体字段的具体类型。
举个例子,我们定义一个Person
(人)结构体,代码如下:
type person struct { name string city string age int8 }
同样类型的字段也可以写在一行,
type person1 struct { name, city string age int8 }
4、结构体实例化
当结构体实例化时,才会真正地分配内存。也就是必须实例化后才能使用结构体的字段。
结构体本身也是一种类型,我们可以像声明内置类型一样使用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} }
4.1匿名结构体
在定义一些临时数据结构等场景下还可以使用匿名结构体。
package main import ( "fmt" ) func main() { var user struct{Name string; Age int} user.Name = "小王子" user.Age = 18 fmt.Printf("%#v\n", user) // struct { Name string; Age int }{Name:"小王子", Age:18} }
5、指针类型结构体
通过使用new
关键字对结构体进行实例化,得到的是结构体的地址。
(区别于直接使用结构体,new建立的是指针,可以做到类似引用传递的效果)
格式如下:
type person struct { name, city string age int8 } func main(){ var p2 = new(person) p2.name = "woo" //Go语言中支持对结构体指针直接使用`.`来访问结构体的成员。 p2.age = 30 p2.city = "广州" fmt.Printf("%T\n", p2) //*main.person fmt.Printf("p2=%#v\n", p2) //p2=&main.person{name:"woo", city:"广州", age:30} }
5.1取结构体的地址实例化
使用&
对结构体进行取地址操作相当于对该结构体类型进行了一次new
实例化操作。
type person struct { name, city string age int8 } func main(){ p2 := &person{} // 等同于var p2 = new(person) fmt.Printf("p2=%#v\n", p2) //p2=&main.person{name:"", city:"", age:0} p2.name = "woo" p2.age = 30 p2.city = "广州" fmt.Printf("p1=%v\n", p2) //p1=&{woo 广州 30} fmt.Printf("%T\n", p2) //*main.person fmt.Printf("p2=%#v\n", p2) //p2=&main.person{name:"woo", city:"广州", age:30} }
p2.name = "woo"`其实在底层是`(*p3).name = "woo" 只不过
6、结构体初始化
没有初始化的结构体,其成员变量都是对应其类型的零值。
type person struct { name string city string age int8 } func main() { var p4 person fmt.Printf("p4=%#v\n", p4) //p4=main.person{name:"", city:"", age:0} }
1、键值对初始化
使用键值对对结构体进行初始化时,键对应结构体的字段,值对应该字段的初始值。
type person struct { name string city string age int8 } func main(){ p5 := person{ name: "小王子", city: "北京", age: 18, } fmt.Printf("p5=%#v\n", p5) //p5=main.person{name:"小王子", city:"北京", age:18}
使用& 对结构体指针进行键值对初始化
p6 := &person{ name: "小王子", city: "北京", age: 18, } fmt.Printf("p6=%#v\n", p6) //p6=&main.person{name:"小王子", city:"北京", age:18}
当某些字段没有初始值的时候,该字段可以不写。
p7 := &person{ city: "北京", } fmt.Printf("p7=%#v\n", p7) //p7=&main.person{name:"", city:"北京", age:0}
2、使用值的列表初始化
初始化结构体的时候可以简写,也就是初始化的时候不写键,直接写值:
p8 := &person{ "沙河娜扎", "北京", 28, } fmt.Printf("p8=%#v\n", p8) //p8=&main.person{name:"沙河娜扎", city:"北京", age:28}
使用这种格式初始化时,需要注意:
- 必须初始化结构体的所有字段。
- 初始值的填充顺序必须与字段在结构体中的声明顺序一致。
- 该方式不能和键值初始化方式混用。
7、结构体内存布局
结构体占用一块连续的内存。
type test struct { a int8 b int8 c int8 d int8 } n := test{ 1, 2, 3, 4, } fmt.Printf("n.a %p\n", &n.a) fmt.Printf("n.b %p\n", &n.b) fmt.Printf("n.c %p\n", &n.c) fmt.Printf("n.d %p\n", &n.d)
输出:
n.a 0xc0000a0060 n.b 0xc0000a0061 n.c 0xc0000a0062 n.d 0xc0000a0063
8、结构体的内存布局⭐
1、结构体大小
结构体是占用一块连续的内存,一个结构体变量的大小是由结构体中的字段决定。
type Foo struct { A int8 // 1 B int8 // 1 C int8 // 1 } var f Foo fmt.Println(unsafe.Sizeof(f)) // 3
2、内存对齐
但是结构体的大小又不完全由结构体的字段决定,例如:
type Bar struct { x int32 // 4 y *Foo // 8 z bool // 1 } var b1 Bar fmt.Println(unsafe.Sizeof(b1)) // 24
很显然结构体变量b1
的内存布局和上图中的并不一致,实际上的布局应该如下图所示,灰色虚线的部分就是内存对齐时的填充(padding)部分。
Go 在编译的时候会按照一定的规则自动进行内存对齐。之所以这么设计是为了减少 CPU 访问内存的次数,加大 CPU 访问内存的吞吐量。如果不进行内存对齐的话,很可能就会增加CPU访问内存的次数。例如下图中CPU想要获取b1.y
字段的值可能就需要两次总线周期。
因为 CPU 访问内存时,并不是逐个字节访问,而是以字(word)为单位访问。比如 64位CPU的字长(word size)为8bytes,那么CPU访问内存的单位也是8字节,每次加载的内存数据也是固定的若干字长,如8words(64bytes)、16words(128bytes)等。
3、对齐保证
知道了可以通过内置unsafe
包的Sizeof
函数来获取一个变量的大小,此外我们还可以通过内置unsafe
包的Alignof
函数来获取一个变量的对齐系数,例如:
// 结构体变量b1的对齐系数 fmt.Println(unsafe.Alignof(b1)) // 8 // b1每一个字段的对齐系数 fmt.Println(unsafe.Alignof(b1.x)) // 4:表示此字段须按4的倍数对齐 fmt.Println(unsafe.Alignof(b1.y)) // 8:表示此字段须按8的倍数对齐 fmt.Println(unsafe.Alignof(b1.z)) // 1:表示此字段须按1的倍数对齐
unsafe.Alignof()
的规则如下:
- 对于任意类型的变量 x ,
unsafe.Alignof(x)
至少为 1。 - 对于 struct 类型的变量 x,计算 x 每一个字段 f 的
unsafe.Alignof(x.f)
,unsafe.Alignof(x)
等于其中的最大值。 - 对于 array 类型的变量 x,
unsafe.Alignof(x)
等于构成数组的元素类型的对齐倍数。
在了解了上面的规则之后,我们就可以通过调整结构体 Bar 中字段的顺序来减少其大小:
type Bar2 struct { x int32 // 4 z bool // 1 y *Foo // 8 } var b2 Bar2 fmt.Println(unsafe.Sizeof(b2)) // 16
此时结构体 Bar2 变量的内存布局示意图如下:
或者将字段顺序调整为以下顺序。
type Bar3 struct { z bool // 1 x int32 // 4 y *Foo // 8 } var b3 Bar3 fmt.Println(unsafe.Sizeof(b3)) // 16
此时结构体 Bar3 变量的内存布局示意图如下:
总结一下:在了解了Go的内存对齐规则之后,我们在日常的编码过程中,完全可以通过合理地调整结构体的字段顺序,从而优化结构体的大小。
4、结构体内存布局的特殊场景
空结构体字段对齐
如果结构或数组类型不包含大小大于零的字段(或元素),则其大小为0。两个不同的0大小变量在内存中可能有相同的地址。
由于空结构体struct{}
的大小为 0,所以当一个结构体中包含空结构体类型的字段时,通常不需要进行内存对齐。例如:
type Demo1 struct { m struct{} // 0 n int8 // 1 } var d1 Demo1 fmt.Println(unsafe.Sizeof(d1)) // 1
但是当空结构体类型作为结构体的最后一个字段时,如果有指向该字段的指针,那么就会返回该结构体之外的地址。为了避免内存泄露会额外进行一次内存对齐。
type Demo2 struct { n int8 // 1 m struct{} // 0 } var d2 Demo2 fmt.Println(unsafe.Sizeof(d2)) // 2
示意图:
在实际编程中通过灵活应用空结构体大小为0的特性能够帮助我们节省很多不必要的内存开销。
例如,我们可以使用空结构体作为map的值来实现一个类似 Set 的数据结构。
var set map[int]struct{}
我们还可以使用空结构体作为通知类channel的元素,例如Go源码src/cmd/internal/base/signal.go
中。
// src/cmd/internal/base/signal.go // Interrupted is closed when the go command receives an interrupt signal. var Interrupted = make(chan struct{})
以及 src/net/pipe.go
中都有类似的使用示例。
// src/net/pipe.go // pipeDeadline is an abstraction for handling timeouts. type pipeDeadline struct { mu sync.Mutex // Guards timer and cancel timer *time.Timer cancel chan struct{} // Must be non-nil }
原子操作在32位平台要求强制内存对齐
在 x86 平台上原子操作需要强制内存对齐是因为在 32bit 平台下进行 64bit 原子操作要求必须 8 字节对齐,否则程序会 panic,下面是Go源码src/atomic/doc.go
中的说明。
// src/atomic/doc.go // BUG(rsc): On 386, the 64-bit functions use instructions unavailable before the Pentium MMX. // // On non-Linux ARM, the 64-bit functions use instructions unavailable before the ARMv6k core. // // On ARM, 386, and 32-bit MIPS, it is the caller's responsibility // to arrange for 64-bit alignment of 64-bit words accessed atomically. // The first word in a variable or in an allocated struct, array, or slice can // be relied upon to be 64-bit aligned.
这里可以参照groupcache库中的实际应用,示例代码如下。
type Group struct { name string getter Getter peersOnce sync.Once peers PeerPicker cacheBytes int64 // limit for sum of mainCache and hotCache size // mainCache is a cache of the keys for which this process // (amongst its peers) is authoritative. That is, this cache // contains keys which consistent hash on to this process's // peer number. mainCache cache // hotCache contains keys/values for which this peer is not // authoritative (otherwise they would be in mainCache), but // are popular enough to warrant mirroring in this process to // avoid going over the network to fetch from a peer. Having // a hotCache avoids network hotspotting, where a peer's // network card could become the bottleneck on a popular key. // This cache is used sparingly to maximize the total number // of key/value pairs that can be stored globally. hotCache cache // loadGroup ensures that each key is only fetched once // (either locally or remotely), regardless of the number of // concurrent callers. loadGroup flightGroup _ int32 // force Stats to be 8-byte aligned on 32-bit platforms // Stats are statistics on the group. Stats Stats } // ... // Stats are per-group statistics. type Stats struct { Gets AtomicInt // any Get request, including from peers CacheHits AtomicInt // either cache was good PeerLoads AtomicInt // either remote load or remote cache hit (not an error) PeerErrors AtomicInt Loads AtomicInt // (gets - cacheHits) LoadsDeduped AtomicInt // after singleflight LocalLoads AtomicInt // total good local loads LocalLoadErrs AtomicInt // total bad local loads ServerRequests AtomicInt // gets that came over the network from peers }
Group
结构体中通过添加一个int32
字段强制让Stats
字段在32bit平台也是8字节对齐的。
5、fasle sharing
结构体内存对齐除了上面的场景外,在一些需要防止CacheLine伪共享的时候,也需要进行特殊的字段对齐。例如sync.Pool
中就有这种设计:
type poolLocal struct { poolLocalInternal // Prevents false sharing on widespread platforms with // 128 mod (cache line size) = 0 . pad [128 - unsafe.Sizeof(poolLocalInternal{})%128]byte }
结构体中的pad
字段就是为了防止false sharing而设计的。
当不同的线程同时读写同一个cache line上不同数据时就可能发生false sharing。false sharing会导致多核处理器上严重的系统性能下降。具体的可以参考 伪共享(False Sharing)。
如注释所说这里之所以使用128字节进行内存对齐是为了兼容更多的平台。
6、hot path
hot path 是指执行非常频繁的指令序列。
在访问结构体的第一个字段时,我们可以直接使用结构体的指针来访问第一个字段(结构体变量的内存地址就是其第一个字段的内存地址)。
如果要访问结构体的其他字段,除了结构体指针外,还需要计算与第一个值的偏移(calculate offset)。在机器码中,偏移量是随指令传递的附加值,CPU 需要做一次偏移值与指针的加法运算,才能获取要访问的值的地址。因为访问第一个字段的机器代码更紧凑,速度更快。
下面的代码是标准库sync.Once
中的使用示例,通过将常用字段放置在结构体的第一个位置上减少CPU要执行的指令数量,从而达到更快的访问效果。
// src/sync/once.go // Once is an object that will perform exactly one action. // // A Once must not be copied after first use. type Once struct { // done indicates whether the action has been performed. // It is first in the struct because it is used in the hot path. // The hot path is inlined at every call site. // Placing done first allows more compact instructions on some architectures (amd64/386), // and fewer instructions (to calculate offset) on other architectures. done uint32 m Mutex }
参考链接:https://stackoverflow.com/questions/59174176/what-does-hot-path-mean-in-the-context-of-sync-once
9、构造函数
Go语言的结构体没有构造函数,我们可以自己实现。 例如,下方的代码就实现了一个person
的构造函数。 因为struct
是值类型,如果结构体比较复杂的话,值拷贝性能开销会比较大,所以该构造函数返回的是结构体指针类型。
type person stuct { name, city string age int8 } func newPerson(name, city string, age int8) person { return person{ name: name, city: city, age: age, } } func main (){ woo :=newPerson("测试","广州",int8(22)) fmt.Printf("类型为:%T" value:%#v\n, woo, woo) }
10、方法和接收者
Go语言中的方法(Method)
是一种作用于特定类型变量的函数。这种特定类型变量叫做接收者(Receiver)
。接收者的概念就类似于其他语言中的this
或者 self
。
方法的定义格式如下:
func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) { 函数体 }
其中,
- 接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名称首字母的小写,而不是
self
、this
之类的命名。例如,Person
类型的接收者变量应该命名为p
,Connector
类型的接收者变量应该命名为c
等。 - 接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型。
- 方法名、参数列表、返回参数:具体格式与函数定义相同。
举个例子:
//Person 结构体 type Person struct { name string age int8 } //NewPerson 构造函数 func NewPerson(name string, age int8) *Person { return &Person{ name: name, age: age, } } //Dream 是一个Person类型的构造函数 func (p Person) Dream() { fmt.Printf("%s的梦想是学好Go语言!\n", p.name) } func main() { p1 := NewPerson("小王子", 25) p1.Dream() //相当于 (*p1).Dream() }
方法与函数的区别是,函数不属于任何类型,方法属于特定的类型。
11、指针类型的接收者
指针类型的接收者由一个结构体的指针组成,由于指针的特性,调用方法时修改接收者指针的任意成员变量,在方法结束后,修改都是有效的。这种方式就十分接近于其他语言中面向对象中的this
或者self
。 例如我们为Person
添加一个SetAge
方法,来修改实例变量的年龄。
//Person 结构体 type Person struct { name string age int8 } //NewPerson 构造函数 func NewPerson(name string, age int8) *Person { return &Person{ name: name, age: age, } } func (p *Person) SetAge(newAge int8) { p.age = newAge } func main() { p1 := NewPerson("Woo", 25) fmt.Println(p1.age) // 25 p1.SetAge(30) fmt.Println(p1.age) // 30
12、值类型的接收者
当方法作用于值类型接收者时,Go语言会在代码运行时将接收者的值复制一份。在值类型接收者的方法中可以获取接收者的成员值,但修改操作只是针对副本,无法修改接收者变量本身。
// SetAge2 设置p的年龄 // 使用值接收者 func (p Person) SetAge2(newAge int8) { p.age = newAge } func main() { p1 := NewPerson("小王子", 25) p1.Dream() fmt.Println(p1.age) // 25 p1.SetAge2(30) // (*p1).SetAge2(30) fmt.Println(p1.age) // 25 }
13、 什么时候应该使用指针类型接收者
- 需要修改接收者中的值
- 接收者是拷贝代价比较大的大对象
- 保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者。
14、基于内置类型定义自定义类型添加方法
在Go语言中,接收者的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法。 举个例子,我们基于内置的int
类型使用type关键字可以定义新的自定义类型,然后为我们的自定义类型添加方法。
//MyInt 将int定义为自定义MyInt类型 type MyInt int //SayHello 为MyInt添加一个SayHello的方法 func (m MyInt) SayHello() { fmt.Println("Hello, 我是一个int。") } func main() { var m1 MyInt m1.SayHello() //Hello, 我是一个int。 m1 = 100 fmt.Printf("%#v %T\n", m1, m1) //100 main.MyInt }
只能用type关键字自定义类型,不能通过func来新建一个类型
15、结构体的匿名字段
结构体允许其成员字段在声明时没有字段名而只有类型,这种没有名字的字段就称为匿名字段。
//Person 结构体Person类型 type Person struct { string int } func main() { p1 := Person{ "小王子", 18, } fmt.Printf("%#v\n", p1) //main.Person{string:"北京", int:18} fmt.Println(p1.string, p1.int) //北京 18 }
注意:这里匿名字段的说法并不代表没有字段名,而是默认会采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个。
15、结构体嵌套
一个结构体中可以嵌套包含另一个结构体或结构体指针,就像下面的示例代码那样。
//Address 地址结构体 type Address struct { Province string City string } //User 用户结构体 type User struct { Name string Gender string Address Address //嵌套另一个结构体 } func main() { user1 := User{ Name: "小王子", Gender: "男", Address: Address{ Province: "山东", City: "威海", }, } fmt.Printf("user1=%#v\n", user1)//user1=main.User{Name:"小王子", Gender:"男", Address:main.Address{Province:"山东", City:"威海"}} }
16、嵌套匿名字段
上面user结构体中嵌套的Address
结构体也可以采用匿名字段的方式,例如:
//Address 地址结构体 type Address struct { Province string City string } //User 用户结构体 type User struct { Name string Gender string Address //匿名字段 } func main() { var user2 User user2.Name = "小王子" user2.Gender = "男" user2.Address.Province = "山东" // 匿名字段默认使用类型名作为字段名 user2.City = "威海" // 匿名字段可以省略 fmt.Printf("user2=%#v\n", user2) //user2=main.User{Name:"小王子", Gender:"男", Address:main.Address{Province:"山东", City:"威海"}} } fmt.Println(users2.Province) //直接访问匿名结构体中的字段 //山东
当访问结构体成员时会先在结构体中查找该字段,找不到再去嵌套的匿名字段中查找。
17、嵌套结构体的字段名冲突
嵌套结构体内部可能存在相同的字段名。在这种情况下为了避免歧义需要通过指定具体的内嵌结构体字段名。
//Address 地址结构体 type Address struct { Province string City string CreateTime string } //Email 邮箱结构体 type Email struct { Account string CreateTime string } //User 用户结构体 type User struct { Name string Gender string Address //嵌套了上面的2个结构体 Email } func main() { user3 := User{ Name : "沙河娜扎", Gender : "男", Address: Address{ Province : "广东", City : "广州", CreateTime : "2023-3-3", }, Email :Email { Account : "213@WOO.com", CreateTime :"2023-3-2", }, } fmt.Println(user3.Address.CreateTime) //要指定具体的内嵌结构体字段名 2023-3-3 fmt.Println(user3.Email.CreateTime) //2023-3-2 }
18、结构体的“继承”
//Animal 动物 type Animal struct { name string } func (a *Animal) move() { //定义了一个move类型的方法,a变量接受animol指针, fmt.Printf("%s会动!\n", a.name) } //Dog 狗 type Dog struct { Feet int8 *Animal //通过嵌套匿名结构体实现继承,嵌套了一个Animal的指针类型的结构体,里面有move } func (d *Dog) wang() { //定义一个wang方法,d接受Dog类型的指针 fmt.Printf("%s会汪汪汪~\n", d.name) } func main() { d1 := &Dog{ Feet: 4, Animal: &Animal{ //注意嵌套的是结构体指针 name: "乐乐", }, } d1.wang() //乐乐会汪汪汪~ d1.move() //乐乐会动! }
19、结构体与JSON序列化(反序列化)⭐
type Student struct { ID int Gender string Name string } //Class 班级 type Class struct { Title string Students []*Student } func main() { c := &Class{ //创建命名为c的结构体变量指针 Title: "101", Students: make([]*Student, 0, 200), //创建一个student切片,初始元素为0.容量为200 } for i := 0; i < 10; i++ { stu := &Student{ Name: fmt.Sprintf("stu%02d", i), //在student里添加10个数据 Gender: "男", ID: i, } c.Students = append(c.Students, stu) //用append添加 } fmt.Printf("%+v\n", c) //{Title:101 Students:[{ID:0 Gender:男 Name:stu00} {ID:1 Gender:男 Name:stu01} {ID:2 Gender:男 Name:stu02}]} //JSON序列化:结构体-->JSON格式的字符串 data, err := json.Marshal(c) if err != nil { fmt.Println("json marshal failed") return } fmt.Printf("json:%s\n", data) //JSON反序列化:JSON格式的字符串-->结构体,注意,如果如果一个结构体中的首字母是大写,那它就是对外可见的,如果结构体里的字母不是大写的,那么在反序列化的时候,在json包里就是不可见的,那么打印的结果就会不一样 str := `{"Title":"101","Students":[{"ID":0,"Gender":"男","Name":"stu00"},{"ID":1,"Gender":"男","Name":"stu01"},{"ID":2,"Gender":"男","Name":"stu02"},}` //要注意,用的`号(波浪键) 而且不能有错别字,不然转换会有问题 c1 := &Class{} err = json.Unmarshal([]byte(str), c1) if err != nil { fmt.Println("json unmarshal failed!") return } fmt.Printf("%#v\n", c1) //main.Class{Title:"101", Students:[]main.Student{main.Student{ID:0, Gender:"男", Name:"stu00"}, main.Student{ID:1, Gender:"男", Name:"stu01"}, main.Student{ID:2, Gender:"男", Name:"stu02"}}} //如果tlitle是小写,输出就会变成下面这样 //main.Class{title:"", Students:[]main.Student{main.Student{ID:0, Gender:"男", Name:"stu00"}, main.Student{ID:1, Gender:"男", Name:"stu01"}, main.Student{ID:2, Gender:"男", Name:"stu02"}}} }
20、结构体标签(Tag)
Tag
是结构体的元信息,可以在运行的时候通过反射的机制读取出来。 Tag
在结构体字段的后方定义,由一对反引号包裹起来,具体的格式如下:
`key1:"value1" key2:"value2"`
结构体tag由一个或多个键值对组成。键与值使用冒号分隔,值用双引号括起来。同一个结构体字段可以设置多个键值对tag,不同的键值对之间使用空格分隔。
注意事项: 为结构体编写Tag
时,必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,通过反射也无法正确取值。例如不要在key和value之间添加空格。
例如我们为Student
结构体的每个字段定义json序列化时使用的Tag:
//Student 学生 type Student struct { ID int `json:"id"` //通过指定tag实现json序列化该字段时的key 如果是数据库,就用 `db:"id"`,exel表就用 `xml:"idd"` Gender string //json序列化是默认使用字段名作为key name string //私有不能被json包访问 } func main() { s1 := Student{ ID: 1, Gender: "男", name: "沙河娜扎", } data, err := json.Marshal(s1) if err != nil { fmt.Println("json marshal failed!") return } fmt.Printf("json str:%s\n", data) //json str:{"id":1,"Gender":"男"} }
9、接口 interface
在Go语言中接口(interface)是一种类型,一种抽象的类型。相较于之前章节中讲到的那些具体类型(字符串、切片、结构体等)更注重“我是谁”,接口类型更注重“我能做什么”的问题。接口类型就像是一种约定——概括了一种类型应该具备哪些方法,在Go语言中提倡使用面向接口的编程方式实现解耦。
type dog struct{} func (d dog) say() { fmt.Println("汪汪汪~") } type cat struct{} func (c cat) say() { fmt.Println("喵喵喵~") } type person struct { name string } func (p person) say() { fmt.Println("啊啊啊~") } // 接口不管你是什么类型,它只管你要实现什么方法 // 定义一个类型,一个抽象的类型,只要实现了say()这个方法的类型都可以称为sayer类型 type sayer interface { say() } // 打的函数 func da(arg sayer) { arg.say() // 不管是传进来的是什么,我都要打Ta,打Ta Ta就会叫,就要执行Ta的say方法 } func main() { c1 := cat{} da(c1) // 喵喵喵~ d1 := dog{} da(d1)//汪汪汪~ p1 := person{ name: "娜扎", } da(p1) //啊啊啊~ }
1、接口的定义
在 Go 语言中接口是一个非常重要的概念和特性,使用接口类型能够实现代码的抽象和解耦,也可以隐藏某个功能的内部实现,但是缺点就是在查看源码的时候,不太方便查找到具体实现接口的类型。
每个接口类型由任意个方法签名组成,接口的定义格式如下:
type 接口类型名 interface{ 方法名1( 参数列表1 ) 返回值列表1 方法名2( 参数列表2 ) 返回值列表2 … }
其中:
- 接口类型名:Go语言的接口在命名时,一般会在单词后面添加
er
,如有写操作的接口叫Writer
,有关闭操作的接口叫closer
等。接口名最好要能突出该接口的类型含义。 - 方法名:当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
- 参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以省略。
举个例子,定义一个包含Write
方法的Writer
接口。
type Writer interface{ Write([]byte) error }
当你看到一个Writer
接口类型的值时,你不知道它是什么,唯一知道的就是可以通过调用它的Write
方法来做一些事情。
PHP、Java等语言中也有接口的概念,不过在PHP和Java语言中需要显式声明一个类实现了哪些接口,在Go语言中使用隐式声明的方式实现接口。只要一个类型实现了接口中规定的所有方法,那么它就实现了这个接口。
interface
在go
中是一种神奇的存在,interface{}
可以代表所有类型的基类,interface
也可以定义为类的方法模板,只不过在Go
中是隐式的实现。
type Animal struct { //Animal结构体 } type Cat struct { //嵌套了Animal结构体的子结构体Cat Animal } type Dog struct { Animal } type behavior interface { //behavior接口类型,其中包含了eat()和run()两个方法。 eat() run() } func (c Cat) eat() { //Cat实现的是eat方法 fmt.Println("cat eat") } func (c Cat) run() { //Cat实现的是run方法 fmt.Println("cat run") } func (d Dog) eat() { fmt.Println("dog run") } func (d Dog) run() { fmt.Println("dog run") } func main(){ list := make([]behavior,2) //创建了一个list切片,切片类型为behavior,长度为2 list[0] = Cat{} //将Cat类型的值赋值给第一个元素 list[1] = Dog{} //即便它们使用不同的方法,但只要实现了behavior接口中定义的方法,就可以被视为相同的类型 for _,v := range list{ //遍历输出 v.run() //所以在调用的时候,会分别调用Cat和Dog类型中实现的run和eat方法 v.eat() } } //cat run //cat eat //dog run //dog run
典型的“不关心它是什么,只关心它能做什么”的场景
2、值接收者实现接口
我们定义一个Dog
结构体类型,并使用值接收者为其定义一个Move
方法。
// Dog 狗结构体类型 type Dog struct{} // Move 使用值接收者定义Move方法实现Mover接口 func (d Dog) Move() { fmt.Println("狗会动") }
此时实现Mover
接口的是Dog
类型。
var x Mover // 声明一个Mover类型的变量x var d1 = Dog{} // d1是Dog类型 x = d1 // 可以将d1赋值给变量x x.Move() var d2 = &Dog{} // d2是Dog指针类型 x = d2 // 也可以将d2赋值给变量x x.Move()
从上面的代码中我们可以发现,使用值接收者实现接口之后,不管是结构体类型还是对应的结构体指针类型的变量都可以赋值给该接口变量。
3、指针接收者实现接口
我们再来测试一下使用指针接收者实现接口有什么区别。
// Cat 猫结构体类型 type Cat struct{} // Move 使用指针接收者定义Move方法实现Mover接口 func (c *Cat) Move() { fmt.Println("猫会动") }
此时实现Mover
接口的是*Cat
类型,我们可以将*Cat
类型的变量直接赋值给Mover
接口类型的变量x
。
var c1 = &Cat{} // c1是*Cat类型 x = c1 // 可以将c1当成Mover类型 x.Move()
但是不能给将Cat
类型的变量赋值给Mover
接口类型的变量x
。
// 下面的代码无法通过编译 var c2 = Cat{} // c2是Cat类型 x = c2 // 不能将c2当成Mover类型
由于Go语言中有对指针求值的语法糖,对于值接收者实现的接口,无论使用值类型还是指针类型都没有问题。但是我们并不总是能对一个值求址,所以对于指针接收者实现的接口要额外注意。
4、一个类型实现多个接口
一个类型可以同时实现多个接口,而接口间彼此独立,不知道对方的实现。例如狗不仅可以叫,还可以动。我们完全可以分别定义Sayer
接口和Mover
接口,具体代码示例如下。
// Sayer 接口 type Sayer interface { Say() } // Mover 接口 type Mover interface { Move() }
Dog
既可以实现Sayer
接口,也可以实现Mover
接口。
type Dog struct { Name string } // 实现Sayer接口 func (d Dog) Say() { fmt.Printf("%s会叫汪汪汪\n", d.Name) } // 实现Mover接口 func (d Dog) Move() { fmt.Printf("%s会动\n", d.Name) }
同一个类型实现不同的接口互相不影响使用。
var d = Dog{Name: "旺财"} var s Sayer = d var m Mover = d s.Say() // 对Sayer类型调用Say方法 m.Move() // 对Mover类型调用Move方法
5、空接口的定义
空接口是指没有定义任何方法的接口类型。因此任何类型都可以视为实现了空接口。也正是因为空接口类型的这个特性,空接口类型的变量可以存储任意类型的值。
我们不能对一个空接口值调用任何方法,否则会产生panic
package main import "fmt" // 空接口 var x interface{} // 声明一个空接口类型变量x // Dog 狗结构体 type Dog struct{} func main() { x = "你好" // 字符串型 fmt.Printf("type:%T value:%v\n", x, x) //type:string value:你好 x = 100 // int型 fmt.Printf("type:%T value:%v\n", x, x) //type:int value:100 x = true // 布尔型 fmt.Printf("type:%T value:%v\n", x, x) //type:main.Dog value:{} x = Dog{} // 结构体类型 fmt.Printf("type:%T value:%v\n", x, x) //type:main.Dog value:{} }
6、空接口的应用
1、空接口作为函数的参数
使用空接口实现可以接收任意类型的函数参数。
// 空接口作为函数参数 func show(a interface{}) { fmt.Printf("type:%T value:%v\n", a, a) }
2、空接口作为map的值
使用空接口实现可以保存任意值的字典。
// 空接口作为map值 var studentInfo = make(map[string]interface{}) studentInfo["name"] = "沙河娜扎" studentInfo["age"] = 18 studentInfo["married"] = false fmt.Println(studentInfo)
7、 类型断言(确定接口类型)
接口值可能赋值为任意类型的值,那我们如何从接口值获取其存储的具体数据呢?
我们可以借助标准库fmt
包的格式化打印获取到接口值的动态类型。
而想要从接口值中获取到对应的实际值需要使用类型断言,其语法格式如下。
x.(T)
其中:
- x:表示接口类型的变量
- T:表示断言
x
可能是的类型。
该语法返回两个参数,第一个参数是x
转化为T
类型后的变量,第二个值是一个布尔值,若为true
则表示断言成功,为false
则表示断言失败。
举个例子:
var n Mover = &Dog{Name: "旺财"} v, ok := n.(*Dog) //当猜的类型不对时,ok=false, if ok { fmt.Println("类型断言成功") v.Name = "富贵" // 变量v是*Dog类型 } else { fmt.Println("类型断言失败") }
如果对一个接口值有多个实际类型需要判断,推荐使用switch
语句来实现。
// justifyType 对传入的空接口类型变量x进行类型断言 func justifyType(x interface{}) { switch v := x.(type) { case string: fmt.Printf("x is a string,value is %v\n", v) case int: fmt.Printf("x is a int is %v\n", v) case bool: fmt.Printf("x is a bool is %v\n", v) default: fmt.Println("unsupport type!") } }
由于接口类型变量能够动态存储不同类型值的特点,只有当有两个或两个以上的具体类型必须以相同的方式进行处理时才需要定义接口。切记不要为了使用接口类型而增加不必要的抽象,导致不必要的运行时损耗。
小技巧: 下面的代码可以在程序编译阶段验证某一结构体是否满足特定的接口类型。
// 摘自gin框架routergroup.go type IRouter interface{ ... } type RouterGroup struct { ... } var _ IRouter = &RouterGroup{} // 确保RouterGroup实现了接口IRouter
上面的代码中也可以使用var _ IRouter = (*RouterGroup)(nil)
进行验证。
10、error接口和错误处理
error
接口只包含一个方法——Error
,这个函数需要返回一个描述错误信息的字符串。
当一个函数或方法需要返回错误时,我们通常是把错误作为最后一个返回值。
func main() { configFile, err := ioutil.ReadFile("config.txt") if err != nil { //判断是否一致 log.Fatalln("读取配置文件错误:", err) }
由于 error 是一个接口类型,默认零值为nil
。所以我们通常将调用函数返回的错误与nil
进行比较,以此来判断函数是否返回错误。
1、错误结构体类型
可以自己定义结构体类型,实现error接口。
// OpError 自定义结构体类型 type OpError struct { Op string } // Error OpError 类型实现error接口的方法 func (e *OpError) Error() string { return fmt.Sprintf("无权执行%s操作", e.Op) } func main() { filename := "readonly.txt" file, err := os.OpenFile(filename, os.O_RDONLY, 0644) //os.O_RDONLY代表只读模式打开,0644代表文件权限 //用os.openfile打开文件并赋值给file if err != nil { fmt.Println("打开文件失败:", err) return } defer file.Close() //将file.Close()推迟到函数返回之前执行时,也就是说,无论后面的代码是否执行成功,都会保证在下一个函数返回之前确保文件被关闭,避免资源泄漏 err = os.Remove(filename) //删除文件,在执行完毕后关闭,并释放资源,避免内存泄漏 if err != nil { opErr := &OpError{Op: "删除"} //创建一个自定义结构的类型,并赋值给opErr变量 fmt.Println(opErr.Error()) return } fmt.Println("文件删除成功") }
举例2:
type error interface { Error() string }
只要结构体实现了这个方法就行,实现方式如下
type errorString struct { s string } func (e *errorString) Error() string { return e.s } // 多一个函数当作构造函数 func New(text string) error { return &errorString{text} }
所以我们只要扩充下自定义 error
的结构体字段就行了。
这个自定义异常可以在报错的时候存储一些信息,供外部程序使用
type FileError struct { Op string Name string Path string } // 初始化函数 func NewFileError(op string, name string, path string) *FileError { return &FileError{Op: op, Name: name, Path: path} } // 实现接口 func (f *FileError) Error() string { return fmt.Sprintf("路径为 %v 的文件 %v,在 %v 操作时出错", f.Path, f.Name, f.Op) }
调用
f := NewFileError("读", "README", "/home/how_to_code/README") fmt.Println(f.Error())
输出
路径为 /home/how_to_code/README 的文件 README,在 读 操作时出错
11、反射⭐
Go语言中的变量是分为两部分的:
- 类型信息:预先定义好的元信息。
- 值信息:程序运行过程中可动态变化的。
反射是指在程序运行期间对程序本身进行访问和修改的能力。程序在编译时,变量被转换为内存地址,变量名不会被编译器写入到可执行部分。在运行程序时,程序无法获取自身的信息。
支持反射的语言可以在程序编译期间将变量的反射信息,如字段名称、类型信息、结构体信息等整合到可执行文件中,并给程序提供接口访问反射信息,这样就可以在程序运行期间获取类型的反射信息,并且有能力修改它们。
Go程序在运行期间使用reflect包访问程序的反射信息。
之前提过空接口, 空接口可以存储任意类型的变量,那我们如何知道这个空接口保存的数据是什么呢? 反射就是在运行时动态的获取一个变量的类型信息和值信息。
1、reflect.TypeOf()
在Go语言中,使用reflect.TypeOf()函数可以获得任意值的类型对象(reflect.Type),程序通过类型对象可以访问任意值的类型信息。
package main import ( "fmt" "reflect" ) func reflectType(x interface{}) { //定义了reflectType函数接受一个参数为x, v := reflect.TypeOf(x) fmt.Printf("type:%v\n", v) //用反射机制获取对应的类型并打印出来 } func main() { var a float32 = 3.14 reflectType(a) // type:float32 //调用refle函数,将a和b作为参数传递给他 var b int64 = 100 reflectType(b) // type:int64 }
2、type name和type kind 种类
在反射中关于类型还划分为两种:类型(Type)
和种类(Kind)
。因为在Go语言中我们可以使用type关键字构造很多自定义类型,而种类(Kind)
就是指底层的类型,但在反射中,当需要区分指针、结构体等大品种的类型时,就会用到种类(Kind)
。 举个例子,我们定义了两个指针类型和两个结构体类型,通过反射查看它们的类型和种类。
package main import ( "fmt" "reflect" ) type myInt int64 //自定义一个类型myInt func reflectType(x interface{}) { t := reflect.TypeOf(x) fmt.Printf("type:%v kind:%v\n", t.Name(), t.Kind()) } func main() { var a *float32 // 指针 var b myInt // 自定义类型 var c rune // 类型别名 reflectType(a) // type: kind:ptr reflectType(b) // type:myInt kind:int64 reflectType(c) // type:int32 kind:int32 type person struct { //定义一个结构体 name string age int } type book struct{ title string } //空结构体 var d = person{ name: "沙河小王子", age: 18, } var e = book{title: "《跟小王子学Go语言》"} reflectType(d) // type:person kind:struct reflectType(e) // type:book kind:struct }
Go语言的反射中像数组、切片、Map、指针等类型的变量,它们的.Name()
都是返回空
。
在reflect
包中定义的Kind类型如下:
type Kind uint const ( Invalid Kind = iota // 非法类型 Bool // 布尔型 Int // 有符号整型 Int8 // 有符号8位整型 Int16 // 有符号16位整型 Int32 // 有符号32位整型 Int64 // 有符号64位整型 Uint // 无符号整型 Uint8 // 无符号8位整型 Uint16 // 无符号16位整型 Uint32 // 无符号32位整型 Uint64 // 无符号64位整型 Uintptr // 指针 Float32 // 单精度浮点数 Float64 // 双精度浮点数 Complex64 // 64位复数类型 Complex128 // 128位复数类型 Array // 数组 主要是后面这几种类型 Chan // 通道 Func // 函数 Interface // 接口 Map // 映射 Ptr // 指针 Slice // 切片 String // 字符串 Struct // 结构体 UnsafePointer // 底层指针 )
3、ValueOf 反射获取值
reflect.ValueOf()
返回的是reflect.Value
类型,其中包含了原始值的值信息。reflect.Value
与原始值之间可以互相转换。
reflect.Value
类型提供的获取原始值的方法如下:
方法 | 说明 |
---|---|
Interface() interface {} | 将值以 interface{} 类型返回,可以通过类型断言转换为指定类型 |
Int() int64 | 将值以 int 类型返回,所有有符号整型均可以此方式返回 |
Uint() uint64 | 将值以 uint 类型返回,所有无符号整型均可以此方式返回 |
Float() float64 | 将值以双精度(float64)类型返回,所有浮点数(float32、float64)均可以此方式返回 |
Bool() bool | 将值以 bool 类型返回 |
Bytes() []bytes | 将值以字节数组 []bytes 类型返回 |
String() string | 将值以字符串类型返回 |