Go 语言入门
变量定义
1 | var a int // 定义 int 变量 a |
TIPS 运行 GO 的 main 函数,所在包的包名也要是 main
1 | package main // |
内建变量
bool
布尔型string
字符串型(u)int
,(u)int8
,(u)int16
,(u)int32
,(u)int64
,(u)intptr
数字表示int位数,不加就默认为当前计算机位数,ptr
表示指针,u表示无符号byte
字节型,8位1字节rune
Go语言的字符型,32位4个字节float32
,float64
浮点型,数字为位数complex64
,complex128
复数类型,前者实部和虚部都是float32
,后者两个都是float64
强制类型转换
Go语言类型转换是强制的
常量的定义
与变量的定义有很多相似的地方,不过要用 const
代替 var
定义常量的时候可以指定类型,也可以不指定类型,若不指定类型,就是个位置类型的文本,在使用的时候会进行推断
1 | const a, b = 3, 4 |
定义枚举类型
Go 语言没有枚举值,只能用 const
来模拟
1 | const ( |
iota
是一个预先定义的标识符,在 const
表达式中(通常在括号内),用来表示当前 const
规范的非类型整数顺序数,索引为 0
条件语句
if
1 | const filename = "in.txt" |
Go的条件语句的特点
- if 条件里可以赋值,且赋值的变量作用域只在这个 if 语句里
- 条件语句部分不需要括号,并且可以用
;
来分隔多个条件
上面的例子可以进一步修改为
1 | if contents, err := ioutil.ReadFile(filename); err == nil { |
switch
1 | func eval(a, b int, op string) int { |
Go 语言中 switch 的特点:
- swtich 会自动 break,除非使用
falllthrough
- switch 后可以没有表示,在 case 中写表达式也可以
循环语句
for
去其他语言的 for 循环的区别在于没有 ( )
,并且三个部分各自都可以省略
1 | sum := 0 |
Go 语言中没有 while,如果把 for 玄幻的初始化和递增条件省略,就类似于 while
如果把三个部分都省略,那么就相当于死循环
1 | for { |
函数
多个返回值
Go 的函数可以返回多个值
1 | func div(a, b int) (int, int) { |
此外,还可以给返回值命名,可以增强代码可读性,也可以为编辑器的补全提供帮助。
若指明了返回值名称,则可以对返回值进行直接赋值,最后只需要一个 return 即可(该用法仅用于非常简单的函数,否则影响可读性)
1 | func div(a, b int) (q, r int) { |
函数式编程
1 | func apply(op func(int, int) int, a, b int) int { |
使用
1 | apply(func(a int, b int) int { |
可变参数列表
Go 语言没有默认参数
、函数重载
,但有可变参数列表
1 | func sum(numbers ...int) int { |
指针
Go 语言的指针不能进行运算,这也是他简单的地方
Go 语言只有值传递一种方式(与值传递对应的是类似与 C++ 的引用传递,这个才 Go 中是没有的)
1 | func swap(a, b *int) { |
数组
1 | var arr1 [5]int // 定义长度为 5 的整型数组,默认值都为 0 |
数组的遍历,可以使用 range
关键字,可以获得下标和值
1 | arr := [...]int{2, 4, 6, 8, 10} |
如果只需要值不需要下标,则可以用 __
代替
1 | for _, v := range arr { ... } |
若只要下标,则可以直接写成
1 | for i range arr { ... } |
Go 中的数组是值类型,也就是说数组传递给函数后,会拷贝数组内容,修改数组的值不会改变原数组的内容(大部分语言的数组都是引用传递,而 Go 则是值传递)。此外[10]int
和 [20]int
是不同类型,也就是说实参数组的长度要与形参数组的长度相一致,否则会报错。
要实现引用传递,则需要用到指针
1 | func foo(arr *[5]int) { ... } |
可见使用数组还是相当麻烦的,所以在 Go 中使用的更多的是切片(Slice)
切片
1 | arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7} |
切片生成的是数组的一个视图,修改切片会改变原数组,在函数中传递数组时,若要方便修改原数组,可以使用切片
1 | func foo(arr []int) { ... } // 方括号内什么也没有就表示切片 |
注
- slice 可以向后扩展,但是不能向前扩展
- s[i] 不可以超越 len(s),向后扩展不可以超越底层数组 caps(s)
- 向slice中添加元素,如果长度没超过 cap,则会覆盖原数组的内容,若超过cap,系统则会重新分配更大的底层数组
- 由于值传递,使用 append 有可能会修改 slice 的 len 或 cap ,因此必须接受 append 的返回值,即
newSlc := append(slc, newValue)
切片的创建方式
1 | s1 := []int{2, 4, 6, 8} // 定义并初始化切片,[]内什么都不加为切片,加...或数字为数组 |
切片的操作
切片的复制
1 | copy(s1, s2) // 把 s2 复制到 s1 上 |
切片元素的删除
1 | s = append(s[:3], [4:]) // 删除中间元素(第三个) |
Map
定义形式如下
1 | map[K]V |
- map 使用哈希表,必须可以比较相等
- 除了 slice、map、function 的内建类型都可以作为key
- 自建类型(Struct 类型)也可以作为 key,前提是没有 slice、map、function
面向对象
Go 语言面向对象仅支持封装,不支持继承和多态;Go语言没有class,只有 struct
结构的创建和简单使用
1 | package main |
Q 在工厂方法中返回的局部变量的地址,该局部变量是创建在堆上还是在栈上?
A 因为 Go 语言有垃圾回收机制,具体是在堆上还是在栈上,由编译器来决定。若是函数中的局部变量,那么建在栈上;若将局部变量的地址返回给外部调用,那么这个局部变量就会键在堆上,并参与垃圾回收,当不再使用时,就会被回收掉。所以退出函数后,局部变量就会被销毁,这是不一定的。
为结构定义方法
1 | func (node treeNode) print() { |
(node treeNode)
显示定义和命名方法接收者,随便叫什么都可以
因为在Go语言中都是值传递,那么如果要在函数内修改值的话,值传递是无法生效的,因此可以使用指针作为方法接收者
1 | func (node *treeNode) setValue(value int) { |
- 只有使用指针才可以改变结构内容
nil
指针也可以调用方法
值接收者 VS 指针接收者
- 要改变内容时必须使用指针接收者
- 结构过大时也考虑使用指针接收者(因为值接收者是对值的拷贝,结果过大则消耗越大)
- 一致性:如果有指针接收者,最好都是指针接收者(一个建议,保持一致有利于阅读以及代码维护)
- 值接收者 是 Go 语言特有的
- 值/指针接收者均可接收值/指针
封装
名字一般使用CamelCase,首字母大写为 public,首字母小写为 private。
封装是对包而言的,每个目录可以有很多个包,但只能有一个main包,main 包包含可执行入口。
为结构定义的方法必须在同一个包内,但可以是不同文件。
扩充类型
使用别名
1
2
3
4
5
6
7
8
9
10
11
12type Queue []int //Queue 本质是一个int切片
// 扩展 []int,使其具有队列的功能
/* 入队 */
func (q *Queue) Push(v int) { // 设计修改内容的要使用指针接收者
*q = append(*q, v)
}
/* 出队 */
func (q *Queue) Pop() int {
head := (*q)[0]
*q = (*q)[1:]
return head
}使用组合
利用组合为之前的 treeNode对象扩展一个遍历方法
1 | // 定义新的结构体,使用组合的方式扩展类型 |
接口
接口的定义
使用者定义接口
1 | package main |
实现接口
在接口所在目录下新建目录,创建新文件用来实现接口方法,当前目录结构为
1 | LearnGo>innofang.io>innofang>interfaces |
只要实现了接口方法,就被认为实现了接口,接口实现如下
1 | package real |
使用接口实现
1 | func main() { |
接口里面有什么
接口变量里面的内容有两种情况:
- 实现者的类型和实现者的值
- 实现者的类型和实现者的指针,其中指针指向实现者
具体是哪个,可以自由选择,因此在使用接口变量的时候,不要使用接口的地址,因为接口内部包含有指针
- 接口变量自带指针
- 接口变量同样采用值传递,几乎不需要使用接口的指针
- 指针接收者实现只能以指针方式使用;值接收者都可
若接口方法实现时使用了指针接收者,那么在定义该接口实习时,需要取接口地址
1 | // 修改 real 包下的 Get 方法实现,使用指针接收者 |
那么在定义该接口实现者时,就需要使用取地址符
1 | func main() { |
查看接口变量
再定义一个接口实现者
1 | package mock |
查看接口变量有两种方式
Type Switch
1 | switch v := r.(type) { // 获取类型 |
Type Assertion
1 | realRetriever := r.(*real.Retriever) // 通过 |
表示任何类型 :interface{}
1 | type Quue []interface{} // 该slice就可以接收任何类型的值 |
接口的组合
假设有另一个结构体 Poster,现在要把含有 Get 方法的 Retriever 和含有 Post 方法的 Poster 进行组合
1 | func RetrieverPoster interface { |
现在对于实现者来说,他可以自己实现 Post 方法和 Get 方法了
使用者不管定义者如何定义,它只管实现它要的方法,不需要关心使用的是什么接口
只要是一个类型就可以实现接口,这也是 Go 语言灵活的地方
defer 调用
何时使用 defer 调用
- Open/Close
- Lock/Unlock
- PrintHeader/PrintFooter
被 defer 修饰的语句会确保在函数结束时发生,无论这个程序是否被提前终止,如果有多个 defer 会有一个栈来存储语句,采用先进后出的顺序执行语句
错误处理
1 | file, err := os.Open("abc.txt") |
panic
- 停止当前函数执行
- 一直向上返回,执行每一层的 defer
- 如果没有遇见 recover,程序退出
recover
- 仅在 defer 调用中使用
- 获取 panic 的值
- 如果无法处理,可重新 0panic
1 | package main |
表格驱动测试
- 分离的测试数据和测试逻辑
- 明确的出错信息
- 可以部分失败
- Go 语言的语法使得我们更易实践表格驱动测试
1 | func Test(t *testing.T) { |
代码覆盖率
在测试文件所在目录下执行
1 | go test -coverprofile=c.out // 运行测试代码 |
性能测试
1 | func Becnchmark(b *testing.B) { |
命令行在测试文件所在目录下执行
1 | go test -bench . |
性能调优
在测试文件所在目录下执行
1 | go test -bench . -cpuprofile cpu.out |
生成了二进制文件 cpu.out
,接着使用命令
1 | go tool pprof cpu.out |
进入交互式命令行,可以使用 help
查看,最简单的方式是输入 web
查看二进制文件。内容上,看到的方框越大,说明耗时越久,优化方框大的部分可以使程序性能更有。
Goroutine
协程
- 轻量级“线程”
- 非抢占式多任务处理,由协程序主动交出控制权
- 编译器/解释器/虚拟机层面的多任务
- 多个协程可能在一个或多个线程上运行
因为是非抢占式的 ,若不切换协程,则会一直执行一个协程(io操作会自动切换协程)。因此对于自定义协程,需要注意有没有手动交出控制权
1 | func main() { |
goroutine 的定义
- 任何函数只需要加上
go
就能送给调度器运行 - 不需要在定义时区分是否是异步函数
- 调度器在合适的点进行切换
- 调试时,在命令行使用
-race
来检测数据访问冲突go run -race goroutine.go
goroutine 可能的切换点
- I/O, select
- channel
- 等待锁
- 函数调用(有时)
- runtime.Gosched()
只是参考,不能保证切换,不能保证在其他地方不切换
通道
指的是 goroutine 之间的通道,可以让 goroutine 之间互相通信。每个通道都有与其相关的类型,即通道允许传输的数据类型。通道的零值为 nil,需要使用内置 make 方法来定义
1 | // 声明通道 |
通道的注意点
Channel 通道在使用的时候,有以下几个注意点:
- 用于 goroutine,传递消息
- 通道,每个都有相关联的数据类型,nil chan 是无法使用的
- 使用通道传递数据:<-
- chan <- data,发送数据到通道。向通道中写
- data <- chan,从通道中获取数据。从通道中读数据
- 阻塞
- 发送数据:chan <- data,阻塞,直到另一条 goroutine 读取数据来接触阻塞
- 读取数据:data <- chan,阻塞,知道另一条 goroutine 写入数据来接触阻塞
- Channel 是同步的,意味着同一时间只有一条 goroutine 来操作
注 通道是 goroutine 之间的连接,所以通道的发送和接收必须处在不同的 goroutine 中
1 | data := <- a // 从通道 a 中读取数据 |
定义方式
1 | // var c chan int // c == nil |
关闭 channel
channel 可以被发送方 close,但是 channel 即使被 close 了,也会接收数据,只不过收到的是 zeroValue
1 | func worker(id int, c chan int) { |
Select
select 是 Go 中的一个控制结构,select 语句类似于 switch 语句,但是 select 会随机执行一个可执行的 case。如果没有 case 可运行,若设置了 default 则会执行 default,否则它将阻塞,直到有 case 可执行。语法结构上与 switch 语句很相似
1 | select { |
- 每个 case 都必须是一个通信
- 所有 channel 表达式都会被求值
- 所有被发送的表达式都会被求值
- 如果有多个 case 都可以运行,select 会随机公平地选出一个执行,其他不会执行
- 若此刻没有 case 语句可以执行时,若有default语句,则执行该语句;否则 select 将阻塞,知道有某个通信可以运行,Go 不会重新对 channel 或值进行求值
1 | func main() { |