作者:李骁
Go 奉行通过通信来共享内存,而不是共享内存来通信。所以,channel 是goroutine之间互相通信的通道,goroutine之间可以通过它发消息和接收消息。
channel是进程内的通信方式,因此通过channel传递对象的过程和调用函数时的参数传递行为比较一致,比如也可以传递指针等。
channel是类型相关的,一个channel只能传递(发送或接受 | send or receive)一种类型的值,这个类型需要在声明channel时指定。
默认的,信道的存消息和取消息都是阻塞的 (叫做无缓冲的信道)
使用make来建立一个通道:
var channel chan int = make(chan int)
// 或
channel := make(chan int)
Go中channel可以是发送(send)、接收(receive)、同时发送(send)和接收(receive)。
// 定义接收的channel
receive_only := make (<-chan int)
// 定义发送的channel
send_only := make (chan<- int)
// 可同时发送接收
send_receive := make (chan int)
定义只发送或只接收的channel意义不大,一般用于在参数传递中:
package main
import (
"fmt"
"time"
)
func main() {
c := make(chan int) // 不使用带缓冲区的channel
go send(c)
go recv(c)
time.Sleep(3 * time.Second)
close(c)
}
// 只能向chan里send数据
func send(c chan<- int) {
for i := 0;
作者:李骁
Concurrency is about dealing with lots of things at once.
Parallelism is about doing lots of things at once.并发: 指的是程序的逻辑结构。如果程序代码结构中的某些函数逻辑上可以同时运行,但物理上未必会同时运行。
并行: 并行是指程序的运行状态。并行则指的就是在物理层面也就是使用了不同CPU在执行不同或者相同的任务。
并发是在同一时间处理(dealing with)多件事情。并行是在同一时间做(doing)多件事情。并发的目的在于把当个 CPU 的利用率使用到最高。并行则需要多核 CPU 的支持。
Go 语言从语言层面上就支持了并发,goroutine是Go语言提供的一种用户态线程,有时我们也称之为协程。所谓的协程,某种程度上也可以叫做轻量线程,它不由os,而由应用程序创建和管理,因此使用开销较低(一般为4K)。我们可以创建很多的goroutine,并且它们跑在同一个内核线程之上的时候,就需要一个调度器来维护这些goroutine,确保所有的goroutine都使用cpu,并且是尽可能公平的使用cpu资源。调度器的主要有4个重要部分,分别是M、G、P、Sched,前三个定义在runtime.h中,Sched定义在proc.c中。
M (work thread) 代表了系统线程OS Thread,由操作系统管理。
P (processor) 衔接M和G的调度上下文,它负责将等待执行的G与M对接。P的数量可以通过GOMAXPROCS()来设置,它其实也就代表了真正的并发度,即有多少个goroutine可以同时运行。
G (goroutine) goroutine的实体,包括了调用栈,重要的调度信息,例如channel等。
在操作系统的OS Thread和编程语言的User Thread之间,实际上存在3中线程对应模型,也就是:1:1,1:N,M:N。
N:1 多个(N)用户线程始终在一个内核线程
作者:李骁
在前面我们讲了结构(struct)和接口(interface),但关于这两种类型中非常重要的的方法以及方法调用一直没有具体讲解。那么在这一章里,我们来仔细看看方法有那些奇妙之处呢?
在 Go 语言中,结构体就像是类的一种简化形式,那么面向对象程序员可能会问:类的方法在哪里呢?在 Go 中有一个概念,它和方法有着同样的名字,并且大体上意思相近:
Go 方法是作用在接收器(receiver)上的一个函数,接收器是某种类型的变量。因此方法是一种特殊类型的函数。
定义方法的一般格式如下:
func (recv receiver_type) methodName(parameter_list) (return_value_list) { ... }
在方法名之前,func 关键字之后的括号中指定接收器 receiver。
type A struct {
Face int
}
func (a A) f() {
fmt.Println("hi ", a.Face)
}
上面代码中,我们定义了结构体 A ,注意f()就是 A 的方法,(a A)表示接收器。
a 是 receiver 的实例,f()是它的方法名,那么方法调用遵循传统的 object.name 选择器符号:a.f()。
如果 recv 一个指针,Go 会自动解引用。如果方法不需要使用 recv 的值,可以用 _ 替换它,比如:
func (_ receiver_type) methodName(parameter_list) (return_value_list) { ... }
package main
import (
"fmt"
)
type MyInt int
func (m MyInt) p() {
fmt.Println("Now", m)
}
func main() {
var pp MyInt =
作者:李骁
Go 语言里有非常灵活的 接口 概念,通过它可以实现很多面向对象的特性。
接口定义了一组方法(方法集),但是这些方法不包含(实现)代码:它们没有被实现(它们是抽象的)。接口里也不能包含变量。
通过如下格式定义接口:
type Namer interface {
Method1(param_list) return_type
Method2(param_list) return_type
...
}
上面的 Namer 是一个 接口类型
(按照约定,只包含一个方法的)接口的名字由方法名加 [e]r 后缀组成,例如 Printer、Reader、Writer、Logger、Converter 等等。还有一些不常用的方式(当后缀 er 不合适时),比如 Recoverable,此时接口名以 able 结尾,或者以 I 开头。
Go 语言中的接口都很简短,通常它们会包含 0 个、最多 3 个方法。
注意:
类型不需要显式地声明它实现了某个接口,接口被隐式地实现,隐式接口解藕了实现接口的包和定义接口的包:互不依赖。
多个类型可以实现同一个接口,一个类型可以实现多个接口,实现了某个接口的类型,还可以有其它的方法。
接口类型是由一组方法定义的集合。接口类型的值可以存放实现这些方法的任何值。
类型(比如结构体)实现接口方法集中的所有方法,一定是接口方法集中所有方法。那么接口类型的值其实也可以存放该结构体的值。
如:
package main
import (
"fmt"
)
type A struct {
Face int
}
type B interface {
f()
}
func (a A) f() {
fmt.Println("hi ", a.Face)
}
func main() {
var s A = A{Face: 9}
s.f()
var b B = A{Face: 9} //接口类型可接受结构体的值,因为结构体实现了接口
b.f()
}
即使接口在类型之后才定义,二者处于不
作者:李骁
Go 通过结构体的形式支持用户自定义类型,或者叫定制类型。
一个带属性的结构体试图表示一个现实世界中的实体。
结构体是复合类型(composite types),当需要定义一个类型,它由一系列属性组成,每个属性都有自己的类型和值的时候,就应该使用结构体,它把数据聚集在一起。
然后(方法)可以访问这些数据,就好像它们是一个独立实体的一部分。
结构体是值类型,因此可以通过 new 函数来创建。
组成结构体类型的那些数据称为字段(fields)。每个字段都有一个类型和一个名字;在一个结构体中,字段名字必须是唯一的。
结构体定义的一般方式如下:
type identifier struct {
field1 type1
field2 type2
...
}
结构体里的字段都有 名字,像 field1、field2 等,如果字段在代码中从来也不会被用到,那么可以命名它为 _。
使用 new
使用 new 函数给一个新的结构体变量分配内存,它返回指向已分配内存的指针:var t *T = new(T),如果需要可以把这条语句放在不同的行(比如定义是包范围的,但是分配却没有必要在开始就做)。
var t *T
t = new(T)
写这条语句的惯用方法是:t := new(T),变量 t 是一个指向 T的指针,此时结构体字段的值是它们所属类型的零值。
声明 var t T 也会给 t 分配内存,并零值化内存,但是这个时候 t 是类型T。在这两种方式中,t 通常被称做类型 T 的一个实例(instance)或对象(object)。
同样的,使用点号符可以获取结构体字段的值:structname.fieldname。
在 Go 语言中这叫 选择器(selector)。无论变量是一个结构体类型还是一个结构体类型指针,都使用同样的 选择器符(selector-notation) 来引用结构体的字段:
type myStruct struct { i int }
var v my
作者:李骁
在Go 语言中,基础类型有下面几种:
bool byte complex64 complex128 error float32 float64
int int8 int16 int32 int64 rune string
uint uint8 uint16 uint32 uint64 uintptr
使用 type 关键字可以定义你自己的类型,你可能想要定义一个结构体,但是也可以给一个已经存在的类型的新的名字,然后你就可以在你的代码中使用新的名字(用于简化名称或解决名称冲突),称为自定义类型,如:
type IZ int
然后我们可以使用下面的方式声明变量:
var a IZ = 5
这里我们可以看到 int 是变量 a 的底层类型,这也使得它们之间存在相互转换的可能。
如果你有多个类型需要定义,可以使用因式分解关键字的方式,例如:
type (
IZ int
FZ float64
STR string
)
在 type TZ int 中,TZ 就是 int 类型的新名称(用于表示程序中的时区),称为自定义类型,然后就可以使用 TZ 来操作 int 类型的数据。使用这种方法定义之后的类型可以拥有更多的特性,且在类型转换时必须显式转换。
每个值都必须在经过编译后属于某个类型(编译器必须能够推断出所有值的类型),因为 Go 语言是一种静态类型语言。
在必要以及可行的情况下,一个类型的值可以被转换成另一种类型的值。由于 Go 语言不存在隐式类型转换,因此所有的转换都必须显式说明,就像调用一个函数一样(类型在这里的作用可以看作是一种函数):
valueOfTypeB = typeB(valueOfTypeA)
类型 B 的值 = 类型 B(类型 A 的值)
type TZ int 中,新类型不会拥有原类型所附带的方法;TZ 可以自定义一个方法用来输出更加人性化的时区信息。
type TZ = int
(这种写法应该才是真正的别名,type TZ int 其实是定义了新类型,这两种完
作者:李骁
Go 里面有三种类型的函数:
除了main()、init()函数外,其它所有类型的函数都可以有参数与返回值。
函数参数、返回值以及它们的类型被统称为函数签名。
函数重载(function overloading)指的是可以编写多个同名函数,只要它们拥有不同的形参或者不同的返回值,在 Go 里面函数重载是不被允许的。
如果需要申明一个在外部定义的函数,你只需要给出函数名与函数签名,不需要给出函数体:
func flushICache(begin, end uintptr)
函数也可以以申明的方式被使用,作为一个函数类型,就像:
type binOp func(int, int) int
在这里,不需要函数体 {}。
函数是一等值(first-class value):它们可以赋值给变量,就像下面一样:
add := binOp
这个变量知道自己指向的函数的签名,所以给它赋一个具有不同签名的函数值是不可能的。
函数值(functions value)之间可以相互比较:如果它们引用的是相同的函数或者都是 nil 的话,则认为它们是相同的函数。函数不能在其它函数里面声明(不能嵌套),不过我们可以通过使用匿名函数来破除这个限制。
没有参数的函数通常被称为 无参数函数(niladic function),就像 main.main()
Go 默认使用按值传递来传递参数,也就是传递参数的副本。函数接收参数副本之后,在使用变量的过程中可能对副本的值进行更改,但不会影响到原来的变量,比如 Function(arg1)。
如果你希望函数可以直接修改参数的值,而不是对参数的副本进行操作,你需要将参数的地址(变量名前面添加&符号,比如 &variable)传递给函数,这就是按引用传递,比如 Function(&arg1),此时传递给函数的是一个指针
作者:李骁
任何时候当你需要一个新的错误类型,都可以用 errors(必须先 import)包的 errors.New 函数接收合适的错误信息来创建,像下面这样:
err := errors.New("math - square root of negative number")
func Sqrt(f float64) (float64, error) {
if f < 0 {
return 0, errors.New ("math - square root of negative number")
}
}
用 fmt 创建错误对象:
通常你想要返回包含错误参数的更有信息量的字符串,例如:可以用 fmt.Errorf() 来实现:它和 fmt.Printf() 完全一样,接收有一个或多个格式占位符的格式化字符串和相应数量的占位变量。和打印信息不同的是它用信息生成错误对象。
比如在前面的平方根例子中使用:
if f < 0 {
return 0, fmt.Errorf("square root of negative number %g", f)
}
在多层嵌套的函数调用中调用 panic,可以马上中止当前函数的执行,所有的 defer 语句都会保证执行并把控制权交还给接收到 panic 的函数调用者。这样向上冒泡直到最顶层,并执行(每层的) defer,在栈顶处程序崩溃,并在命令行中用传给 panic 的值报告错误情况:这个终止过程就是 panicking。
标准库中有许多包含 Must 前缀的函数,像 regexp.MustComplie 和 template.Must;当正则表达式或模板中转入的转换字符串导致错误时,这些函数会 panic。
不能随意地用 panic 中止程序,必须尽力补救错误让程序能继续执行。
自定义包中的错误处理和 panicking,这是所有自定义包实现者应该遵守的最佳实践:
1)在包内部,总是应该从 panic 中 recover:不允许显式的超出包范围的 panic()
作者:李骁
switch var1 {
case val1:
...
case val2:
...
default:
...
}
switch {
case condition1:
...
case condition2:
...
default:
...
}
switch 语句的第二种形式是不提供任何被判断的值(实际上默认为判断是否为 true),然后在每个 case 分支中进行测试不同的条件。当任一分支的测试结果为 true 时,该分支的代码会被执行。
switch 语句的第三种形式是包含一个初始化语句:
switch initialization {
case val1:
...
case val2:
...
default:
...
}
switch result := calculate(); {
case result < 0:
...
case result > 0:
...
default:
// 0
}
变量 var1 可以是任何类型,而 val1 和 val2 则可以是同类型的任意值。类型不被局限于常量或整数,但必须是相同的类型;或者最终结果为相同类型的表达式。前花括号 { 必须和 switch 关键字在同一行。
您可以同时测试多个可能符合条件的值,使用逗号分割它们,例如:case val1,val2,val3。
一旦成功地匹配到某个分支,在执行完相应代码后就会退出整个 switch 代码块,也就是说您不需要特别使用 break 语句来表示结束。
如果在执行完每个分支的代码后,还希望继续执行后续分支的代码,可以使用 fallthrough 关键字来达到目的。
fallthrough强制执行后面的case代码,fallthrough不会判断下一条case的expr结果是否为true。
作者:李骁
map 是引用类型,可以使用如下声明:
var map1 map[keytype]valuetype
var map1 map[string]int
([keytype] 和 valuetype 之间允许有空格,但是 Gofmt 移除了空格)
在声明的时候不需要知道 map 的长度,map 是可以动态增长的。
未初始化的 map 的值是 nil。
key 可以是任意可以用 == 或者 != 操作符比较的类型,比如 string、int、float。所以数组、切片和结构体不能作为 key (译者注:含有数组切片的结构体不能作为 key,只包含内建类型的 struct 是可以作为 key 的),但是指针和接口类型可以。如果要用结构体作为 key 可以提供 Key() 和 Hash() 方法,这样可以通过结构体的域计算出唯一的数字或者字符串的 key。
value 可以是任意类型的;通过使用空接口类型,我们可以存储任意值,但是使用这种类型作为值时需要先做一次类型断言。map 也可以用函数作为自己的值,这样就可以用来做分支结构:key 用来选择要执行的函数。
map 传递给函数的代价很小:在 32 位机器上占 4 个字节,64 位机器上占 8 个字节,无论实际上存储了多少数据。
通过 key 在 map 中寻找值是很快的,比线性查找快得多,但是仍然比从数组和切片的索引中直接读取要慢 100 倍;所以如果你很在乎性能的话还是建议用切片来解决问题。
map 可以用 {key1: val1, key2: val2} 的描述方法来初始化,就像数组和结构体一样。
map 是 引用类型 的: 内存用 make 方法来分配。
map 的初始化:
var map1 = make(map[keytype]valuetype)
map 容量:
和数组不同,map 可以根据新增的 key-value 对动态的伸缩,因此它不存在固定长度或者最大限制。但是你也可以选择标明 map 的初始容量 capacity,就像这样:make(map[keytype]