基础
1 | go help # `go` 命令帮助 |
数据类型
布尔
bool
常用操作:&& || ! == !=
整型
Go 语言提供了 5 种有符号、5 种无符号、1 种指针、1 种单字节、1 种单个 unicode 字符(unicode 码点),共 13 种整数类型,零值均为 0
变量的声明:如果没有赋值 默认值为 0
int, uint, rune, int8, int16, int32, int64, uint8, uint16, uint32, uint64, byte, uintptr
无符号
- uint:根据操作系统位数不同在内存中占的字节也不同 uint64 或 uint32,如果出现了负数 会溢出从最小值变成最大值
- uint8: 无符号 8 位整型 (0 到 255)
- uint16: 无符号 16 位整型 (0 到 65535)
- uint32:无符号 32 位整型 (0 到 4294967295)
- uint64:无符号 64 位整型 (0 到 18446744073709551615)
有符号
- int:根据操作系统位数不同在内存中占的字节也不同 int64 或 int32
- int8: 有符号 8 位整型 (-128 到 127)
- int16: 有符号 16 位整型 (-32768 到 32767)
- int32: 有符号 32 位整型 (-2147483648 到 2147483647)
- int64: 有符号 64 位整型 (-9223372036854775808 到 9223372036854775807)
其他
byte: 类似 uint8,1 字节
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39package main
import "fmt"
func main0201() {
//byte字符类型 同时也是uint8的别名
var a byte = 'a'
//所有的字符都对应ASCII中的整型数据
//'0'对应的48 'A'对应的65 'a' 对应的97
//fmt.Println(a)
//%c是一个占位符 表示打印输出一个字符
fmt.Printf("%c\n", a)
fmt.Printf("%c\n", 97)
fmt.Printf("%T\n", a)
var b byte = '0' //字符0 对应的ASCII值为为48
fmt.Printf("%c\n", 48)
fmt.Printf("%c\n", b)
}
func main0202() {
var a byte = 'a'
//将小写字母转成大写字母输出
fmt.Printf("%c", a-32)
}
func main() {
//转义字符 \n 换行
//var a byte = '\n'
//\0 对应的ASCII 值为0 用于字符串的结束标志
//\t 对应的ASCII 值为9 水平制表符 一次跳八个空格
var a byte ='\t'
//fmt.Println(a)
fmt.Printf("%c",a)
}rune: 类似 int32
uintptr:无符号整型,用于存放一个指针,32 位或 64 位
定义 int 和 int64 需要使用类型转换才可以计算
浮点型
通过自动推到类型创建的浮点型变量 默认类型为 float64
- float32: 有效位数 6 或 7,第 7 位不一定有效,前 6 位一定有效,示例:π 有效 3.14159
- float64:有效位数 15
注意:浮点型不能进行==或!=比较,可选择使用两个浮点数的差在一定区间内则认为相等
字符串
string
- 可解析字符串:通过双引号(“)来创建,不能包含多行,支持特殊字符转义序列
- 原生字符串:通过反引号(`)来创建,可包含多行,不支持特殊字符转义序列
特殊字符:
1 | \a:响铃 |
iota
特殊常量,可以认为是一个可以被编译器修改的常量。在每一个 const 关键字出现时,被重置为 0,然后在下一个 const 出现之前,每出现一次 iota,值自动加 1。
iota 生成器用于初始化一系列相同规则的常量。
实力:
1 | package main |
数组 和 切片
数组在定义时即固定长度,切片在定义时可以指定长度和容量,也可以不指定。
切片是长度可变的数组(具有相同数据类型的数据项组成的一组长度可变的序列),切片由三部分组成:
- 指针:指向切片第一个元素指向的数组元素的地址
- 长度:切片元素的数量
- 容量:切片开始到结束位置元素的数量
注意:数组是 值类型,切片是 引用类型
注意:切片长度不能超过容量。
关于…
1 | arr := [...]int {0,1,2,3,4,5,6,7} |
如果没有三个点,右边类型推导赋值是切片,有三个点推导的类型是数组,数组个数由后面实际初始化个数确定,数组是长度不能改变的
指针
pointer
指针是用来存储变量地址的变量。
该使用值还是指针,取决于是否现需要修改原始数据。
声明:*type
1
var p *int // 声明指针变量 默认值为0x0 (nil) 空指针
初始化:&variable / new()
1
2
3
4var a = 123
var p := &a // 初始化
var p2 = new(int) // 初始化,默认为0操作:*pointerVariable
1
2
3var a = 123
var p := &a
*p = 456 // 赋值
数组指针
数组指针操作数组,可以省略*
示例:
1 | arr := [...]int{1, 2, 3} |
上例中,(*p)[0] 可以简化为 p[0]
切片指针
因为切片是引用类型,所以*不能省略
示例:
1 | slice := []int{1, 2, 3} |
指针变量作为函数参数
示例:
1 | package main |
map
映射是存储一系列无序的 key/value 对,通过 key 来对 value 进行操作(增、删、改、查)。
映射的 key 只能为可使用 == 运算符的 值类型(字符串、数字、布尔、数组),value 可以为任意类型
示例:
1 | names := map[string]string{"Go001": "小明"} // 一定要进行初始化,不能只声明类型 |
获取元素的数量
len()
判断 key 是否存在
通过 key 访问元素时可接收两个值,第一个值为 value,第二个值为 bool 类型表示元素是否存在,若存在为 true,否则为 false
删除
delete(map, key)
1 | delete(names, "Go001") |
遍历
for range
1 | package main |
自定义类型
1 | type TypeName Format |
Format 可以是任意内置类型、函数签名、结构体、接口。
结构体
struct
结构体是 值类型。
1 | package main |
匿名结构体
1 | me := struct { |
命名嵌入 & 匿名嵌入
命名嵌入:
调用的时候属性名不能省略。
1 | type A struct { |
匿名嵌入:
结构体类型和属性名必须一致,调用的时候可以省略属性名。
1 | type A struct { |
指针类型嵌入
结构体嵌入(命名&匿名)类型也可以为结构体指针。
使用属性为指针类型,底层共享数据结构,当底层数据发生变化,所有引用都会发生影响。
使用属性为值类型,则在复制时发生拷贝,两者不相互影响
命名嵌入指针结构体:
1 | type A struct { |
匿名嵌入指针结构体:
1 | type A struct { |
可见性
结构体首字母大写则包外可见(公开),否者仅包内可访问(内部)
结构体属性名首字母大写包外可见(公开),否者仅包内可访问(内部)
组合:
- 结构体名首字母大写,属性名大写:结构体可在包外使用,且访问其大写的属性名
- 结构体名首字母大写,属性名小写:结构体可在包外使用,且不能访问其小写的属性名
- 结构体名首字母小写,属性名大写:结构体只能在包内使用,属性访问在结构体嵌入时由被嵌入结构体(外层)决定,被嵌入结构体名首字母大写时属性名包外可见,否者只能在包内使用
- 结构体名首字母小写,属性名小写:结构体只能在包内使用
结构体和指针
结构体是值类型,指针是引用类型。
1 | type File struct { |
channel
不要通过共享内存来通信,而通过通信来共享内存。
基本定义与使用
channel 必须通过 make 创建,var c chan int
这种形式创建的是 nil channel,是不能用的,无论收发都会被堵塞。
1 | // 无缓存channel |
读取和写入
可通过操作符 <-
和 ->
对管道进行读取和写入操作,当写入无缓冲区管道或由缓冲区管道已满时写入则会阻塞直到管道中元素被其他例程读取。同理,当管道中无元素时读取时也会阻塞到管道被其他例程写入元素。
关闭
1 | // func close(c chan<- Type) |
有缓存和有缓存的区别
示例:
1 | // 发送者 |
输出:
1 | deadlock |
使用无缓存通道,稍不留神就会死锁。
有缓存和无缓存channel,除了有无缓存空间外,其阻塞策略也不同。
在无缓存通道下,发送者的发送操作将阻塞,直到接收者执行接受操作。同样接受者的接受操作将阻塞,直到发送者执行发送操作。发送者的发送操作和接受者的接受操作是同步的。
在有缓存通道下,如果缓存空间满了,那么发送者的发送操作将阻塞。直到接受者取走数据,有了缓存空间,发送者的发送操作才会继续执行。如果缓存是空的,那么接受者的接受操作将阻塞。
上面的示例中,有两种解决方法:
使用有缓存通道。
1
ch := make(chan int, 1)
还是使用无缓存通道,但要先创建接受者,再发送数据。
1
2
3
4
5
6
7
8
9
10
11
12
13func main() {
ch := make(chan int)
// 接收者
go func() {
v := <-ch // 这里阻塞
fmt.Println("收到:", v)
}()
// 发送者
ch <- 1
fmt.Println("发送:", 1)
}
range 遍历
select-case 监听多个 channel
使用select-case对多个channel进行监听(写入或者读取),则可以使用 select-case
示例:
1 | func done() <-chan int { |
每个case语句都必须是管道操作,即channel的读或者写;
在一个select-case操作中,select检查所有的case语句,只要是合法的管道操作,这个case就可以执行,select会随机执行一个可以执行的case,其他的忽略;
case语句中如果有函数,则会执行一遍这个函数(函数中对管道的操作不会真的执行),上例中,done() 2秒后返回管道,所以select-case需要等2秒后才会随机选择一个case进行执行;
如果使用time.After,则无需等待,因为time.Sleep是同步,而time.After是异步:
1
2
3
4
5
6
7
8
9
10
11
12
13
14func main() {
ch01 := make(chan int, 1)
ch02 := make(chan int, 1)
ch01 <- 0
ch02 <- 0
select {
case c := <-ch01:
fmt.Println("ch01: ", c)
case c := <-ch02:
fmt.Println("ch02: ", c)
case <-time.After(2 * time.Second):
fmt.Println("2 time.Second")
}
}如果所有的case都不可执行,就走default,如果没有default,则整个select-case就会一直堵塞
select-case 通常搭配 for 使用,在某种情况下,通过break退出for循环(或return退出函数)
只读和只写管道
chan<- type
单向只写管道,只能写数据到管道里面
<-chan type
单向只读管道,只能从管道里面读出数据
函数
参数
在声明函数中若存在多个连续形参类型相同可只保留最后一个参数类型名。
运算符…声明可变参数函数或在调用时传递可变参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14package main
import "fmt"
func test(a, b int, args ...int) {
fmt.Println(a, b, args) // 1 2 [3 4 5]
fmt.Printf("%T\n", args) // []int 切片类型
}
func main() {
// 两种调用方式
test(1, 2, 3, 4, 5)
test(1, 2, []int{1, 2, 3}...)
}
返回值
多返回值
1 | package main |
命名返回值
1 | package main |
匿名函数
闭包
值类型 & 引用类型
值类型和引用类型的差异在于赋值同类型新变量后,对新变量进行修改是否能够影响原来的变量,若不能影响则为值类型,若能影响则为引用类型
- 值类型:数值、布尔、字符串、指针、数组、结构体
- 引用类型:切片、映射、接口等
- 值类型:基本数据类型 int 系列, float 系列, bool, string 、数组和结构体 struct
- 引用类型:指针、slice 切片、map、管道 chan、interface 、函数都是引用类型
值传递 & 引用传递
- 默认就是值值传递
- 传引用类型(最常用的是切片[]byte 和指针)作为参数,就是引用传递,可以在函数内修改函数外的变量
如果要直接修改传入的参数,一定要引用传递
错误处理
error 接口
go 语言通过 error 接口实现错误处理的标准模式,通过使用函数返回值列表中的最后一个值返回错误信息,将错误的处理交由程序员主动进行处理。
error 接口的初始化方法:
- errors.New
- fmt.Errorf
1 | package main |
defer
defer 关键字用户声明函数,不论函数是否发生错误都在函数执行最后执行(return 之前),类似 php 中的析构函数。
若使用 defer 声明多个函数,则按照声明的顺序,先声明后执行(堆)。
常用来做资源释放,记录日志等工作。
1 | package main |
输出:
1 | defer 02 |
panic 和 recover 函数
go 语言提供 panic 和 recover 函数用于处理运行时错误:
panic 抛出异常,中断代码继续往下执行,异常在 defer 中“冒泡”,recover 只能用于 defer 语句声明的函数,用于捕获“冒泡”的异常,被 recover 捕获的异常就不继续往上“冒泡”了,但是不会阻止 defer 的继续执行。
1 | package main |
输出:
1 | err: this is panic |
panic 抛出异常,在 defer 中向上冒泡,要注意以下几点:
- panic 后面的代码就不执行了,包括 defer,异常只在 panic 之前的 defer 中冒泡
- 只有 panic 抛出的异常,才会在 defer 中冒泡,return、log.Fatal 等中止程序运行的语句,会直接中止,并不会执行 defer
接口
interface
接口是引用类型。
接口是自定义类型,是对是其他类型行为的抽象。
1 | type Sender interface { |
若两个接口声明同样的函数签名,则两个接口完全等价。
当接口 A 包含接口 B 中声明的所有函数时(接口 A 函数是接口 B 函数的父集),则接口 A 的对象赋值给接口 B 的对象,只不过接口 B 对象只能调用接口 B 中定义的函数(方法)。
示例:
1 | // 定义Sender接口,声明Send和SendAll两个方法 |
匿名接口
在定义变量时将类型指定为接口的函数签名的接口,此时叫匿名接口。匿名接口常用于初始化一次接口变量的场景。
1 | var singleSender interface { |
接口匿名嵌入
接口之中也可以嵌入已存在的接口,从而实现接口的扩展。
1 | type A interface { |
类型断言 & 查询
断言
格式:v,ok := x.(类型)
,x 必须为接口类型,x 转换类型得到 v,ok 表示转换类型是否成功,如果不设置第二个参数,也就是 ok,断言失败时会直接造成一个 panic,如果 x 为 nil 同样也会 panic
1 | type A interface { |
查询
格式:x.(type)
,x 必须为接口类型
只能用于 switch 的判断
示例:参考下面的空接口示例
空接口
不包含任何函数签名的接口叫空接口,空接口声明的变量可以赋值为任何类型的变量(任意接口)
使用场景
常声明函数参数类型为 interface{}
,用于接收任意类型的变量
go 中没有泛型,空接口搭配 switch 可以部分实现泛型的功能
1 | func printTypes(vs ...interface{}) { |
类型转换
golang 中的类型转换分为强制类型转换、类型断言、以及“向上造型”
这里先只介绍 强制类型转换:
等价类型
1
type_name(expression)
type_name 为类型,expression 为表达式。
什么是等价类型?即实现了相同的方法,[]byte 和 string 是等价类型,int32 和 int64 也是等价类型,但是 int 和 string 不是等价类型。
1
不等价类型
使用 strconv 包
fmt 格式化输入输出
https://studygolang.com/static/pkgdoc/pkg/fmt.htm
占位符
流程控制
条件
选择
循环
for
go 中的循环只有 for 和 for range
1 | package main |
for range
1 | package main |
break 和 continue
break 和 continue 语句和其他语言中的 break 和 continue 无异
类 while
for 子语句只保留条件子语句,此时类似于其他语言中的 while 循环
1 | package main |
死循环
for 子语句全部省略,则为无限循环(死循环),常与 break 与 continue 结合使用,类似其他语言中的 while(true) {}
1 | package main |
label 和 goto
可以通过 goto 语句任意跳转到当前函数指定的 label 位置。
包
Go 源文件都需要在开头使用 package 声明所在包。
包名使用简短的小写字母,常与所在目录名保持一致,一个包中可以由多个 Go 源文件,但必须使用相同包名。
main 包用于声明告知编译器将包编译为二进制可执行文件,在 main 包中的 main 函数是程序的入口,无返回值,无参数。
成员可见性
Go 使用名称首字母大小写来判断对象(常量、变量、函数、类型、结构体、方法等)的访问权限,首字母大写标识包外可见(公开的),否者仅包内可访(内部的)。
init 函数
init 函数用于初始化包,无返回值,无参数。建议每个包只定义一个,相当于 php 中的构造函数__construct()。
Go 不允许包导入但未使用,在某些情况下需要初始化包,使用空白符作为别名进行导入,从而使得包中的 init 函数可以执行。
1 | import _ "fmt" |
Go 包管理
标准包:
Go 语言标准库文档中文版 | Go 语言中文网 | Golang 中文社区 | Golang 中国 (studygolang.com)
第三方包查找地址:
https://godoc.org
https://gowalker.org/
方法
方法是为特定类型定义的,只能由该类型调用的函数。
方法是添加了接受者的函数,接受者必须是自定义的类型。
1 | func (t type) method(params) returns {} |
调用:通过点 . 调用。
1 | type Dog struct { |
接受者为指针
1 | func (dog *Dog) SetName(name string) { |
该使用值接收者还是指针接收者,取决于是否现需要修改原始结构体:
- 若不需要修改则使用值,若需要修改则使用指针
- 若存在指针接收者,则所有方法使用指针接收者
对于接收者为指针类型的方法,需要注意在运行时若接收者为 nil 会发生错误。
匿名嵌入
若结构体匿名嵌入带有方法的结构体时,则在外部结构体可以调用嵌入结构体的方法
1 | package main |
反射
反射是指在运行时动态的访问和修改任意类型对象的结构和成员,在 go 语言中提供 reflect 包提供反射的功能,
每一个变量都有两个属性:类型(Type)和 值(Value)
变量、interface{}、reflect.Value 相互转换
- interface{} -> reflect.Value
- reflect.Value -> interface{}
- interface{} -> 原来的变量类型
1 | func test(b interface{}) { |
Go Modules
Go Modules 的出现就是为了淘汰 GOPATH
设置如下环境变量:
1 | export GOPATH=/data/go |
最常用:go mod tidy
查缺补漏
单元测试 & 性能测试
标准库 testing 可以单元测试和性能测试