go的历史与语言优势
go语言本身非常年轻,发布于2009年,C语言是上世纪70年代诞生的语言,C++是80年代诞生的,python是1991年诞生的,java是1995年诞生的,相比而言,go是目前流行语言中最年轻的,是在这些众多语言肩膀上诞生的语言,是由google支持的开源语言,属于静态的编译型语言,由三位语言专家共同编写。
go语言的优势
-
简单易学:只有25个关键字
-
开发速度快:比java好多了,和python差不多
-
性能好:是编译型语言,编译完是字节码,“一处编译,到处运行”;同时可以很方便地引用C语言
-
支持高并发:使用协程,考虑了很多并发场景和资源消耗的问题。
-
生态环境好:很多优秀开源项目是用go做的,例如docker,K8S,ETCD,console,codis,TaiDB,都是基于golang开发的。
go的容器
数组(Array)
-
数组的类型,取决于数组中的元素类型和数组的长度,其中数组的长度不可变
-
[N]T,N表示数组的长度,T表示数组的元素类型,比如[10]int
-
注意数组的长度和类型,只要有一项不一样,就不能算作一样的数组,属于不同的类型。
-
数组类型是值类型,不是引用类型,故函数传递数组参数的时候是值拷贝,函数内部使用的是数组的备份,不会影响初始的数组。
-
数据声明时未显式初始化的元素会隐式初始化为类型对应的0(如数字0、空字符串、false等)。
切片(Slice)
-
[]T,T表示切片中的元素类型
-
切片的结构:
type slice struct{
Data unsafe.Pointer
Len int
Cap int
}
-
切片包含了对一段底层数组的动态引用,以及长度和容量
-
长度表示已经有几个元素,容量表示一共能放多少元素
-
定义的一般方法
-
a := make([]int,4,6)
-
第一个是切片类型,第二个数是len,第三个数是capacity,再append 只能在len之后append。
-
-
定义的其他方法
-
var num = [10]int{0,1,2,3,4,5,6,7,8,9}
-
b := num[2:5:8]
-
第一个是start,第二个数是end,第三个数是capacity
-
-
越容量访问会报panic
-
注意slice赋值时底层数组是否相同,若直接将sliceA的一部分赋给sliceB,则修改一方时另一方会跟着改动。
-
为避免这种浅拷贝,可以使用Go的内置函数copy,可以做到深拷贝。
-
slice1 := []int{1,2,3,4,5,6}
-
slice2 := make([]int,3)
-
copy(slice2,slice[1:4])
-
前者是destination,后者是source
-
-
还有个常用函数是append,用于向切片后面加元素
-
slice1 := make([]int,0,10)
-
slice1 = append(slice,1,2,3)
-
注意如果加元素后超出容量后就会造成切片的扩容:整个容量在1024以下时,每次扩容*2;超过1024后,每次增加四分之一的容量。还要注意每次扩容go会新开辟一块空间,将原来的内容拷贝过来,所以这个操作还是会占据一定性能的。
-
-
注意slice[i]是引用改动,但是for-range(for _,n := range slice{用n进行操作})循环是值改动,不会对slice产生实际影响。
映射(Map)
-
映射的定义:map[k]T
-
key,value结构:students["Jack"] = 11
-
可用delete删除元素:delete(students,"Jack")
-
也用make生成:students := make(map[string]int)
-
注意遍历顺序不是插入顺序(指for-range方法 for k,v := range students {})
-
golang没有内置集合(Set)类型,用Map来实现Set
-
数组、切片、映射这几个容器都不是并发安全的。
结构体与接口
结构体(Struct)
-
struct 用来自定义复杂数据结构,可以包含多个字段(属性),可以嵌套其他结构体,也可以定义方法。
-
封装性:字段首字母大写包外可访问、小写包外不可访问
-
继承:用聚合实现继承(把父类放到子类里)
-
tag:定义特定格式中的形式或约束条件,书写:` `
-
结构体的方法:收参是A结构体就是A结构体的方法。
-
结构体的初始化:
-
直接初始化:person := Person{}
-
使用new:返回的是指针
-
-
类似数组,也有值传递和引用传递的区别,不传指针就没法改
-
注意,指针和非指针对字段的访问都是用.:person.Age =
接口(Interface)
-
接口描述了某个类型有哪些方法,或者说一个接口类型,定义了一个方法集。
-
通过接口可以实现多:
type Animal interface { Shout() } type Dog struct{} type Cat struct{} func (d Dog) Shout(){ fmt.PrintIn("汪汪") } func (c Cat) Shout(){ fmt.PrintIn("喵") }
-
空接口可以被认为是很多其他语言中的any类型;空接口中没有任何方法,所以任何类型都实现了空接口==》
-
为什么PrintIn什么都能打印?
-
func Println(a ... interface{}) (n int,err error)
-
因为里面入参是一堆空接口,而go中万物都是空接口
-
补充:类型断言
var x interface{} = 123
n,ok := x.(int)
fmt.Println(n,ok) //123 ,true
a = x.(float64) //将产生一个恐慌
协程与管道
协程(Goroutine)
-
go不直接支持创建系统线程,协程是Go程序内部唯一的并发实现方式。
-
起协程的语句:go func(){......}()
-
注意主协程(main)结束后,此程序也就退出了,即使还有一些其他协程在运行。
-
从古至今,多线程的优化有好多种方式,比较常见的一种是reactor模型:线程池中存储大量线程,需要多创建一个线程来执行任务时就从线程池中选取一个线程来用。
-
而Go语言采用的是另一个思路:有几个核就跑几个线程,只是某个线程上面有很多协程;协程的切换是不会像线程切换那样有操作系统层面上的开销的,例如线程切换需要切换虚拟地址空间、切换内核栈、切换硬件上下文、CPUcache需要失效,而切换协程完全没有这些开销。
-
协程底层的实现原理:基于GMP模型:
-
G:goroutines 表示一个协程
-
M:machine 表示一个线程
-
P:processor 管理器,通过队列管理协程
-
基于GMP模型,协程运行在线程上
-
一个协程中的信息:运行栈+寄存器数值(PC,BP,SP)
-
协程的切换,仅仅需要改变寄存器的数值,cpu便会从需要切换的协程指定位置继续运行
-
协程与线程的比例关系:N:M
协程:线程 | 含义 | 优点 | 缺点 |
---|---|---|---|
1:1 | 一个协程在一个线程上运行(其实就是传统的多线程) | 利用多核 | 上下文切换比较慢(reactor模型,代价较大) |
N:1 | 多个协程在一个线程上运行 | 上下文切换较快 | 1.无法充分利用多核2.饥饿,如果一个协程不结束,其余协程阻塞 |
N:M | 多个协程在多个线程上运行 | 充分利用多核,上下文切换快 | 对实现要求更高 |
-
协程调度器的设计策略(减少开销、兼顾公平):
-
复用线程(避免频繁的创建、销毁线程,而是对线程的复用)
-
work stealing机制:当本线程无可运行的协程时,尝试从其他线程绑定的P偷取G,而不是销毁线程。
-
hand off机制:当本线程因为G进行系统调用阻塞时,线程释放绑定的P,把P转移给其他空闲的线程执行。
-
-
利用并行:GOMAXPROCS设置P的数量
-
抢占:限制协程执行时长,不会出现饿死现象
-
全局协程队列:多个线程全满时可以塞入全局协程队列,它是链表实现的,可以塞很多协程,不用怕没地方放协程。
-
-
常用的同步控制机制:WaitGroup
-
开发过程中,经常遇到多task之间的同步问题。例如,多个子task并发完成一部分任务,主task等待他们最后结束。
-
var wg sync.WaitGroup ;
for i := 0;i<3;i++ {
wg.Add(1)
go func(i int){
wg.Done()
}(i)
}
wg.Wait()
管道(Channel)
-
并发模型CSP,全称Communicationg Sequential Processes。它的核心观念是将两个并发执行的实体通过管道连接起来,所有的消息都通过管道传输。
-
管道(通道),也是一种Go的数据同步技术。它可以被看作是在一个程序内部的一个先进先出(FIFO:first in first out) 数据队列。
-
管道的操作有读、写和关闭。
-
定义:ch := make(chan string)
-
读:a = <- ch
-
写: ch <- "hello"
-
写一个已经关闭的channel会引发panic
-
-
管道分类:无缓冲管道&缓冲管道
-
无缓冲管道:长度为0的channel,为不带buffer的channel
-
ch := make(chan int)
-
不会发生额外的拷贝
-
读在写前
-
-
有缓冲管道:长度大于0的channel,为带buffer的channel
-
ch := make(chan int,10)
-
会发生额外的拷贝
-
写在读前 ch<- 1
-
缓冲区最大为65535
-
-
管道元素的传递,是复制,非缓冲区管道复制了1次,缓冲区管道复制了2次
-
例如面试常考的:请用管道实现交替打印AB:
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
ch3 := make(chan string)
go printA(ch1, ch2)
go printB(ch1, ch2, ch3)
<-ch3
}
func printA(ch1, ch2 chan string) {
for i := 0; i < 100; i++ {
<-ch2
fmt.Println(i, "A")
ch1 <- "print A"
}
}
func printB(ch1, ch2, ch3 chan string) {
ch2 <- "begin"
for i := 0; i < 100; i++ {
<-ch1
fmt.Println(i, "B")
if i != 99 {
ch2 <- "print B"
} else {
ch3 <- "end"
}
}
}
几项最佳实践
1.关于init()函数
Golang中init()函数是个特殊的函数,它的执行晚于变量的初始化而早于main()函数
-
同一个包中的同一个文件中,可以有多个init()函数,按照顺序执行。
-
而不同包的init函数按照包导入的依赖关系决定执行顺序。
乱使用init()函数,会使得代码的可阅读性和可理解性变差。建议不使用init(),或者克制使用。
2.关于切片追加
当使用切片追加时,总是将append的结果保存在相同切片中。如果需要新切片,先copy再追加
例如下述代码,初学者很难判断出来x,y,z都是0124
func main() {
x := []int{}
x = append(x, 0)
x = append(x, 1)
x = append(x, 2)
y := append(x, 3)
z := append(x, 4)
fmt.Println(x, y, z)
}
3.关于切片构建
一般切片的构建,使用make()函数来完成,如果预知切片的大小,最好直接指定容量,否则一开始写个容量0,后面疯狂append,每次append都要新开空间然后复制,性能极差;所以一开就开够。
注意一个小陷阱:使用make构造切片时,如果只写一个参数的话,默认这个数既是len,也是capacity,所以感觉使用make指定好了容量,但是其实一append还是整个开空间复制,所以用make一定要写中间的0,不要偷懒。
当不知道cap时,要赋值一个适当的cap,既不能太大(占用内存空间),也不能太小(不断拷贝和调用gc)。
4.关于指针
Go中有两种指针:
1.限制型指针,类似 var ptr *int,这种指针只能寻址,但不能通过加减地址来任意读写内存;
2.无限制指针,unsafe.pointer,这种指针类似于C语言中的void*,可以突破Go类型系统的限制,任意读写内存。
对于第一种指针的用法:
-
Go的参数传递是值传递的,需要有结构体内部进行修改,使用指针。
-
注意,如果不是修改的情况,建议不要使用指针,好处是避免结构体内的field被任意或者不小心修改。
-
当结构体或者容器数据很大时,可以考虑用指针来提高性能
-
但是不那么大的话,尽量用值。因为传指针Go要进行逃逸分析,所以传递指针的性能并不一定比传值性能更好,因为要进行逃逸分析。
5.关于恐慌(Panic)
Go中,当有运行时异常时,会报恐慌;也可以用panic()函数来直接触发恐慌。
发生panic后,后面的代码将停止运行,当前函数或方法会直接return。
Go中,延迟(defer)函数可以在函数return前执行,一般会将panic的recover逻辑放在defer函数中。
如果启子协程,如果想防止因为子协程中发生panic而导致整个程序终止,建议在启动子协程的开始增加恢复panic的延迟函数;延迟函数就是前有defer修饰的函数,是在主函数return前一定会执行的方法,所以一旦panic了就会立即执行defer函数,可以在不知道哪里可能会panic时用,并且一旦起协程就写defer也是一个好的编程习惯。
func main() {
go some()
go func() {
defer func() {
if err := recover(); err != nil {
fmt.Println("panic is recovered")
}
}()
fmt.Println("Do something!")
panic("this is a panic")
}()
time.Sleep(2 * time.Second)
fmt.Println("end")
}