New Gopher Must-Knows

一份直接了当的新手 Gopher 必知清单。

说明:下列要点均在我阅读过官方文档、看书、写代码过程中捡个人认为重要的部分总结而来。也是老规矩,不定时更新。

工具篇

go get 的工作原理是什么?

其作用是获取远程包源码,分两步:先 clone 代码到 src 目录下,然后执行 go install

注意需要根据不同的开源平台(的域名)采用不同的源代码控制工具,因此要想获取这些平台的源码,必须先安装并配置相应的代码控制工具:

  • GitHub:git
  • Google Code: Mercurial, Git, Subversion, hg
  • BitBucket: Mercurial, Git
  • Launchpad: Bazaar

执行 go build 时如何忽略部分文件?

  • 通过文件名前缀

_ 或者 . 开头的 go 文件,go build 会自动忽略。

  • 通过文件名后缀

如果源代码针对不同的操作系统需要做不同的处理,那么可以用不同的操作系统代号作为代码文件名后缀来区分,go build 时会根据当前的操作系统类型来选择性编译相应的文件名。

比如,如果在 Linux 系统上编译下面的文件,那么只有 a_linux.go 才会被编译,其他文件均会被忽略。

1
2
3
4
a_linux.go
a_darwin.go
a_windows.go
a_freebsd.go

这里再次体现了 Go Style,Go 中很多这样的简单直接的约定,而不是选择复杂的配置步骤。

  • 通过代码文件首行跳过标记。

在一些 go 示例代码中,可能会在源文件第一行看见这样的代码注释:

1
// +build OMIT

+build is implemented by the Go tool, not the compiler, to filter files passed to the compiler for build or test.

当这种注释存在的时候,运行 go build 时会提示 can’t load package: package xxx: build constraints exclude all Go files in /path/to/project 这样的提示,这种情况下想要构建 build 成功,有两种方式:

  • 取消想要编译的源文件中第一行的注释

  • go build 时手动指定要编译的文件路径,如果有多个含有 OMIT 注释的源文件要编译,则一个个跟在命令最后,比如:

1
2
3
# 1.go/2.go/3.go 均是 main package 下含有 +build OMIT 注释的源文件

go build ./1.go ./2.go ./3.go

此时如果构建成功,则生成文件是含有 main() 函数的源文件名。

提交代码前如何统一清理编译文件?

编译文件,临时文件等一般不会纳入版本控制中。Go 代码开发过程中难免会生成一些编译文件。

还好 Go 自身也考虑到了这一点,提供了 go clean 工具:

1
go clean -i -n

环境篇

每个 GOPATH 下面必须要有哪三个目录,各自有何用?

  • src:存放源代码(.go/.c/.h/.s/…)
  • pkg:编译后生成的文件(.a)
  • bin:编译后生成的可执行文件。通常也把此目录设置到环境变量 PATH 中。

GOPATH 是否可以配置多个?

是。设置环境变量的时候,Linux/Unix 使用冒号 : 隔开,Windows 使用分号 ; 隔开。比如:

1
export GOPATH=/home/user1/go:/data/user2/go

如何一次性将多个 $GOPATH/bin 目录添加到 bin 目录中?

1
export PATH="$PATH:${GOPATH//://bin:}/bin}"

语法篇

什么是原始字符串字面量?

Raw string literals are character sequences between back quotes string_literals. Within the quotes, any character is legal except back quote.

结构定义中字段最后的反斜线和字符串代表什么意思?(标签)

1
2
3
4
5
6
7
8
type Student struct {
Name string `json:"Name" xml:"Name"`
Gender string `json:"Gender" xml:"Gender"`
XYZ string "well this is called tag, any string is permitted as a tag"
Age1 int // absent tag <=> empty tag
Age2 int "" // empty tag <=> absent tag
_ []byte "ceci n'est pas un champ de structure(this is not a structure field)"
}

这其实是所谓的 “字符串字面量标签”,官方 FAQ 定义如下:

A field declaration may be followed by an optional string literal tag, which becomes an attribute for all the fields in the corresponding field declaration. (结构中每个字段的定义后面可以跟一个可选的字符串字面量标签,该标签将成为相应字段声明中所有字段的一个属性。)

An empty tag string is equivalent to an absent tag. The tags are made visible through a reflection interface and take part in type identity for structs but are otherwise ignored. (空标签和无标签是等价的。标签可以通过反射接口被使用,并会参与结构的类型判断,但在其他情况下会被忽略。)

我们可以通过标签,给 struct 附加一些元信息,并通过反射机制类获取其值。

标签一般在结构需要编码、解码成其他格式,或者从数据库存储、获取数据的时候,指示具体的字段要如何转换。

当然了,元信息的内容是可以任意的,你既可以专门为某个包准备,也可以写来自己个人使用。

其中,当我们需要通过 reflect/#StructTag 获取其数据时,元信息的格式必须是规范的键值对:

1
2
3
type User struct {
Name string `json:"name,omitempty" xml:"-"`
}

键值对中的 K 代表了 V 的包名,比如 json:"name,omitempty" 的 K/包名就是 encoding/json,V/数据就是 name,omitempty。同一个 K 的多个信息之间使用英文逗号 , 隔开。

多个键值对之间使用空格来分隔。

当 V 是 "-" 时,表示忽略对当前字段处理。比如这里的 xml:"-" 即是代表了当要与 XML 格式数据转换时不要 marshal 和 unmarshal Name 字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type User struct {
Name string `mytag:"MyName"`
Email string `mytag:"MyEmail"`
}

u := User{"Bob", "bob@mycompany.com"}
t := reflect.TypeOf(u)

for _, fieldName := range []string{"Name", "Email"} {
field, found := t.FieldByName(fieldName)
if !found {
continue
}
fmt.Printf("\nField: User.%s\n", fieldName)
fmt.Printf("\tWhole tag value : %q\n", field.Tag)
fmt.Printf("\tValue of 'mytag': %q\n", field.Tag.Get("mytag"))
}

https://stackoverflow.com/questions/10858787/what-are-the-uses-for-tags-in-go

Go 是否有三目运算符?

NO.

Go: 我的关键字很少,数量还没英文字母多呢!(25)所以我不支持三目很奇怪吗?…

rune/byte 的原始类型是什么?rune 变量是否可以与 int32/int64 变量相加?

rune: int32,byte:uint8。

rune 变量能和 int32 变量相加,和 int64 不能。

使用 iota 的注意事项?

  • iota 是用来声明 enum 的,默认开始值是 0
  • const 每增加一行加 1
  • 每遇到一个 const 关键字 iota 就会重置
  • iota 在每一行的值都相同

[3]int[4]int 类型相同吗?

不相同,因为长度也是数组的一部分。这也体现了 Go 数组长度不能改变的特点。

[...]int{1,2,3} 是变长数组吗?

不是。这只是数组的另一种写法,用 ... 代替数组长度时,Go 会自动根据元素个数来计算长度。

嵌套数组内层数组的类型能不能省略?

能,但是要类型一致。比如:

1
2
3
4
5
6
7
8
9
10
d2 := [2][4]int{
[4]int{1,2,3,4},
[4]int{5,6,7,8},
}

// 可简写为
d2 := [2][4]int{
{1,2,3,4},
{5,6,7,8},
}

slice 如何实现动态数组?

slice 并不是真正意义上的动态数组,而是一个引用类型,指向底层数组,因此可以通过引用改变其中值。

从概念上讲,slice 像一个包括以下三个元素的结构体:

  • 一个指针:指向数组中 slice 指定的开始位置
  • 长度 len:即 slice 的长度
  • 最大长度 cap:slice 开始位置到数组最后位置的长度

需要注意的是,只有当一个 slice 没有剩余空间时,才会动态分配空间,该 slice 将指向新分配的数组空间,此后该 slice 便不能够再更改原数组的值了。因此在 slice 空间充足的时候才能影响原数组的值。

举例说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var arr = [3]int{
1, 2, 3,
}

s1 := arr[1:2]
s2 := arr[2:]
fmt.Printf("arr => %v: %p\n", arr, &arr)
fmt.Printf("s1 before => %v: %p\n", s1, s1)
fmt.Printf("s2 before => %v: %p\n", s2, s2)

s11 := append(s1, 4)
s22 := append(s2, 5)
s222 := append(s2, 6)

fmt.Printf("s1 after => %v: %p\n", s1, s1)
fmt.Printf("s2 after => %v: %p\n", s2, s2)
fmt.Printf("s11 => %v: %p\n", s11, s11)
fmt.Printf("s22 => %v: %p\n", s22, s22)
fmt.Printf("s222 => %v: %p\n", s222, s222)

输出如下:

1
2
3
4
5
6
7
8
arr => [1 2 3]: 0xc42009a000
s1 before => [2]: 0xc42009a008
s2 before => [3]: 0xc42009a010
s1 after => [2]: 0xc42009a008
s2 after => [4]: 0xc42009a010
s11 => [2 4]: 0xc42009a008
s22 => [4 5]: 0xc420084030
s222 => [4 6]: 0xc420084040### 如何指明一个 slice 的容量?

可以看到,s11 仍然和 s1 前、后指向同一个内存地址,因为 s1 容量是 2,初始化时 s1 只用了一个长度,因此还剩 1 个长度,append 一次后长度还是没有超过最大容量(这里刚好等于,但是没有超过)。

而 s2 和 s22 却不一样了,因为 s2 本身容量只有 1,初始化是 s2 就占了一个,append 的时候已经超过最大容量,这里就发生了上面所说的重新分配,s22 其实指向的是新开辟的一个数组空间。s2 在 append 前后一样,因为 ss2 和 s2 指向不同,导致 s2 所指向的内存空间未受到影响。

map 和 slice 的区别?

  • map 的读取和设置和 slice 类似,只是 slice 的 index/下标只能是 int,而 map 的下标可以是 int、string 等所有完全定义了 ==!= 的类型。

  • map 不是 thread-save 的,多个 goruntines 读写时必须使用 mutex lock 机制。

  • slice 取值的时候返回值只有一个,map 有两个:

1
2
3
4
5
6
7
8
var s = []int{1, 2, 3}
var m map[string]int

v1 := s[1]
// v1, isset := s[1] // 报错:assignment mismatch: 2 variables but 1 values
v2, isset := m["404"]

fmt.Println(v1, v2, isset) // 2 0 false

make 和 new 的区别?

两个都是分配内存的,区别在于:

  • make 返回初始化后的非零值,new 返回指针。

new(T) 返回的是指针/内存地址 *T,指向一块分配了零值填充的 T 类型内存空间。

make(T) 返回的是有初始值(非零)的 T 类型内存空间,而不是 *T

  • make 只能用于 builtin 类型 slice,map,channel 的内存分配,new 用于各种类型的内存分配。

这三种类型特殊之处在于,指向数据结构的引用在使用前必须被初始化,在这些类型被初始化之前为 nil,make 初始化了内部数据结构,并填充适当的值。

什么是“零值”?

指“变量未填充前的默认值”,通常为 0(不同类型默认零值不同),而不是空值 nil

1
2
3
4
5
6
s1 := make([]int{}, 3)    // len(s1) = 3
s2 := make([]int{}, 0, 3) // len(s2) = 0, cap(s2) = 3

m := make(map[string]interface{})

ch := make(chan bool)

function 和 method 有什么区别?

A method is a function with an implicit first argument, called a receiver. (Rob Pike)

即,method 是有一个隐式参数(第一个)为 receiver(接受者)的 function,function 是外围函数,在概念上不属于任何 structure。

匿名字段

Go 的 structure 支持只提供类型,而不写字段名的方式,即匿名字段/嵌入字段,其特点如下:

  • 所有的内置类型和自定义类型都是可以作为匿名字段的。
  • 匿名字段类型名可以作为所属类型的字段名调用。
  • 当匿名字段中的字段名和所属类型的字段名冲突时,优先访问主类型的字段。
  • 当主类型有多个匿名字段且匿名字段之间出现字段名冲突,会出现编译错误 “duplicate field Base”
  • 匿名字段能够实现字段的继承,如果匿名字段实现了一个 method 那么主字段也能直接调用该 method。

接口是否可以嵌入?

可以。形式和嵌入字段类似。

method receiver 是不是指针有什么区别?

区别在于传递方式不同:

  • 不是指针(普通类型)是以值传递,即传递的是副本,不会对原实例对象发生操作。
  • 是指针则是以引用传递,可以更改实例对象。

此外,当作 receiver 作为指针传递时,不需要使用 & 来显式地转化为指针,Go 在你指定参数类型为 (receiver *T) 时就已经知道。

实用篇

如何检查一个 Map 中是否存在某个 Key?

1
2
3
if val, isset := dict["foo"]; isset {
// do something when key isset
}

什么是类型声明(type assertion)?

For an expression x of interface type and a type T, the primary expression x.(T) asserts that x is not nil and that the value stored in x is of type T.

即声明某个 interface{} 型变量的实际类型是 T。举例说明:

1
2
3
4
5
var x interface{} = 1
i, ok := x.(string)
fmt.Println(i, ":", ok) // , false
j, ok := x.(int)
fmt.Println(j, ":", ok) // 1, true

类型声明通常用在需要反向获取 interface{} 动态变量的实际类型的对象时的场景。使用类型声明的变量类型必须是 interface{}。

类型声明是一个表达式,有两个返回值。如果变量声明的类型是实际的数据类型,则第一个返回值就是该变量的值,第二个返回值为 true;否则第一个返回值是所声明类型的零值,第二个返回值是 false

类型声明是,必须判断第二个返回值,否则当断言失败时,会出现 runtime panic。

如何计算 slice 的容量?

slice 的长度是目前容量的元素个数(len(slice))。

slice 的容量(cap(slice))= 所指向数组的结束位置 - 所指向数组索引的起始位置,其中,数组的结束位置可以通过 slice 的第三个参数(Go1.2+)显式指定。

举例说明:

1
2
3
var arr [10]int
s1 := arr[1:3] // 容量为 9=10-1,目前长度为 2=3-1
s2 := arr[2:5:8] // 容量为 6=8-2,目前长度为 3=5-2

如何正确使用 goto

想不到 Go 居然还支持 goto:

1
2
3
4
5
6
7
8
9
func abc() {
i := 0
HELL: // 标签名,大小写敏感
fmt.Println(i)
i++
if i < 3 {
goto HELL
}
}

anyway, 正确使用就是尽量不使用。(:

fallthrough 有什么作用?

Go 里面 switch 默认相当于每个 case 后面都有 break,如果不匹配就跳出 switch,如果想要强制执行下一个 case,就可以使用 fallthrough:

1
2
3
4
5
6
7
8
9
10
11
s := 1
switch s {
case 2:
fmt.Println("NO")
fallthrough
case 3:
fmt.Println("NO")
fallthrough
default:
fmt.Println("NO")
}

如何获得 array 和 slice , map 的指针?

1
2
3
4
5
6
7
8
a := [...]int{1, 2, 3}
s := a[1:]
m := map[string]string{
"x": "y",
}
fmt.Printf("%p => %p\n", a, &a)
fmt.Printf("%p => %p\n", s, &s)
fmt.Printf("%p => %p\n", m, &m)

需要注意的是,array 本身是一个数组类型,要获取其只指针必须要通过 & 取地址符;而 slice/map 本身就是指向一块某种数据类型内存区域的指针,因此无需使用 &,使用 &slice 表示的是 slice 这个指针自身的地址。

struct 有几种声明使用方式?

四种:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type Person struct {
name string
age int
}

// 0. 初始化
var p0 Person
// 1. 按照顺序初始化并赋值,可以不写属性名
p1 := Person{"Li", 18}
// 2. 通过键值对的方式初始化并赋值,可以任意顺序(类似 JSON)
p2 := Person{age:18, name:"Li"}
// 3. 通过 new 函数分配指针初始化
p3 := new(Person)
p3.name = "cjli1"
(*p3).name = "cjli2"

fmt.Println(p0, p1, p2, p3)

如何获取一个动态变量的实际类型?

  • 使用 comma-ok 类型声明

适用于能确定变量类型时:

1
2
3
var x interface{} = 1
i, ok := x.(string)
j, ok := x.(int)
  • 使用 switch + .(type) 语法
1
2
3
4
5
6
7
8
9
var x interface{} = "test"
switch x := x.(type) {
case int:
// ...
case string:
// ...
default:
// ...
}

注意这里的 x.(type) 语法不能在 switch 之外任何逻辑时使用,如果要在 switch 外面判断一个动态变量的类型,只能使用 comma-ok 方式。

如何使用反射?

反射指的是检查程序在运行时的状态。Go 官方使用 reflect 包 来实现。

reflect 包使用步骤:

  • 首先把要反射的类型变量(所有类型都实现了 interface{} 空类型)转化为 reflect 对象:reflect.Type 或 reflect.Value。
  • 然后根据不同的情况调用 reflect 对象的不同方法。
1
2
3
4
5
6
7
8
var x int = 1
t := reflect.TypeOf(x) // 获取元数据
v := reflect.ValueOf(x) // 获取实际值

fmt.Println(t)
fmt.Println(v.Type()) // 获取值类型
fmt.Println(v.Kind() == reflect.Float64) // 判断值类型
fmt.Println(v.Int()) // 获取值

如何要修改反射的值,则必须传指针,否则会报错:

1
2
3
4
5
6
var x float64 = 3.14
// v := reflect.ValueOf(x)
// v.SetFloat(3.14159) // 报错
p := reflect.ValueOf(&x)
v := p.Elem()
v.SetFloat(3.14159) // 成功

goroutine 最少必知知识点

什么是 goroutine?为什么需要它?

goroutine 是通过 Go 的 runtime 管理的一个线程管理器,通过 go 关键字实现。多个 goroutine 运行在同一个进程里面,共享内存数据。

goroutine 的本质就是协程,比线程更轻量级、更易用、更高效。因此可以同时运行成千上万个并发任务。

goroutine 通信和 goroutine 内存共享

设计上有个原则:不要通过共享来通信,而要通过通信来共享。

channel 有什么用?如何用?

goroutine 运行在相同的地址空间,因此访问共享内存必须做好同步。channel 是 goroutine 之间的通信机制。

channel 类似与 Unix Shell 的双向管道,既可以通过它发送值,也可以通过它接受值。不过这些值只能是 channel 类型。

channel 定义时必须使用 make,也必须定义要发送到 channel 的值的类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func add(arr []int, ch chan int) {
sum := 0
for _, v := range arr {
sum += v
}

ch <- sum // send sum to channel ch
}

func main() {
arr := []int{1, 4, 7, 258, 36, 9}
ch := make(chan int)
go add(arr[:2], ch)
go add(arr[3:], ch)

x, y := <-ch, <-ch // receive sum from channel ch
fmt.Println(x, y) // 303 5
}
buffer channel

Go 也允许指定 channel 的缓冲区大小——channel 可以存多少个元素。比如:ch := mak(chan int, 10)` 创建了一个 10 个元素的 int 型 channel,在这个 channel 中,前 10 个元素可以无阻塞的写入,当写入第 11 个元素时,代码会被阻塞,直到其他 goroutine 从该 channel 中读取一些元素,腾出空间。

默认情况 channel 接受和发送数据都是阻塞的,除非另一端已经准备好,着使得 goroutine 的同步变得更简单,而不需要显式地 lock。所谓阻塞,即:读取时(<-ch)它将被阻塞,直到有数据可接受;发送时(ch<-val)它将被阻塞,直到数据被读走。这种 channel 称为“无缓冲channel”,当缓冲区大小为 0 的时候也是无缓冲 channel。

举例说明:

1
2
3
4
5
ch := make(chan int, 2)
ch <- 1
ch <- 2
fmt.Println(<-ch)
fmt.Println(<-ch)

这里如果把 channel 的缓冲区大小改为 1 或 0 时,就会报错:

fatal error: all goroutines are asleep - deadlock!

range

可以通过 range 实现像操作 slice 或者 map 一样操作缓冲类型 channel:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func fib(n int, ch chan int) {
x, y := 1, 1
for i := 0; i < n; i++ {
ch <- x
x, y = y, x+y
}

close(ch)
}

func main() {
ch := make(chan int, 7)
go fib(cap(ch), ch)
for v := range ch {
fmt.Println(v)
}
}

这个例子中,通过 range,可以不断循环地读取 channel 里面的数据,直到该 channel 被显式地关闭。

close

前面的例子中,生产者(往 channel 写数据的)通过 close(ch) 显式地关闭了 channel,关闭之后便无法继续往里面发送任何数据了。在消费者(从 channel 读数据的)可以通过 v, ok := <-ch 来测试 ok 是否为 true 从而判断 channel 是否被关闭。

注意:原则上只能在生产者的地方关闭 channel,而不是消费的地方,否则容易 panic。 但是如果非要从消费者那里关闭 channel ,或者在多个生产者的一个关闭 channel,则需要使用 panic/recover 机制来安全地发送值到 channel 中。

详见:https://go101.org/article/channel-closing.html

select

简而言之,select 用于选择不同类型的通讯。当存在多个 channel 的时候,需要使用 select 来监听 channels 上的数据流动/事件。

select 默认是阻塞的,只有当监听的 channel 中有发送或接受可以进行时才会运行,当多个 channel 都准备好的时候,select 会随机选择一个执行。

select 支持 default 语法,当监听的所有 channel 都没准备好的时候,default 会执行,此时 select 不再阻塞。

超时

select 还可以用来处理超时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
c1 := make(chan int)
c2 := make(chan bool)

go func() {
for {
select {
case v := <-c1:
// do sth ...
case <- time.After(10 * time.Second)
// do sth when timeout 10 seconds
c2 <- true
break
}
}
}

<- c2 // 外层 goroutine 阻塞在这里直到超时

使用 runtime 包中处理 goroutine

  • runtime.Goexit():退出当前 goroutine 但是 defer 函数还会继续调用。
  • runtime.Gosched():让出当前 goroutine 的执行权限,调度器安排其他等待的任务运行,并在下次某个时候从该位置恢复执行。

当一个 goroutine 发生阻塞时,Golang 会自动地把与该 goroutine 处于同一个系统线程的其他 goroutine 转移到另一个系统线程上去,以使这些 goroutine 不阻塞。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
runtime.GOMAXPROCS(1)
exit := make(chan bool)

go func() {
defer close(exit)
go func() {
fmt.Println("2222")
}()
}()

for i := 0; i < 3; i++ {
fmt.Println("i=", i)

if i == 1 {
runtime.Gosched()
}
}

<-exit

说明:这里先设置 runtime.GOMAXPROCS(1) 表示强制性使用同一个逻辑核心来执行代码,否则 fmt.Println("2222") 在我的 MacBook 不一定能被执行。

  • runtime.GOMAXPROCS(n):设置可以同时运行逻辑代码的系统线程的最大数量,并返回之前的值,如果 n<1,则不会改变当前设置。

Go 1.5 之前,调度器仅使用单线程,也就是说只实现了并发。想要发挥多核处理器的并行,需要在我们的程序中显式调用 runtime.GOMAXPROCS(n) 来告诉调度器同时使用多个线程。

Go 1.5 之后,标识并发系统线程个数的 runtime.GOMAXPROCS 的初始值 1 改为了运行环境的 CPU 核数。

说明:多核 CPU 适合 CPU 密集型程序,但如果是 IO 密集型使用多核会增加 CPU 切换成本。

  • runtime.GOOS:返回正在运行的操作系统代号。
  • runtime.GOROOT():获取 GOROOT 环境变量。
  • runtime.NumCPU():获取系统的 CPU 核数。
  • runtime.NumGoroutine():返回正在执行和排队的任务总数。

经验篇

Go 的简洁性体现在哪些地方?

  • 面向对象机制的简洁:没有私有共有关键字,通过字段大小写来区分;没有继承等显式语法,而通过现有类型组合来顺便实现

  • 接口机制的简洁:不需要显式指定某个类型实现了某个接口,而是通过鸭子模型,只要类型实现了某个接口定义的所有方法,那么这个类型就自动实现了这个接口

如何解决 goroutine 之间的数据竞争问题(race detector)?

数据竞争是并发系统中最常见和最难调试类型的错误之一。

  • 数据竞争发生的条件?

当两个 goroutine 同时访问相同的变量并且访问中的至少一个是写入时,发生数据竞争。

  • golang race 的使用
1
2
3
4
$ go test -race mypkg
$ go run -race mysrc.go
$ go build -race mycmd
$ go install -race mypkg

详细参数见:https://golang.org/doc/articles/race_detector.html

说明:不要再生产环境使用 go race 检测并发场景,因为会很影响性能。

  • race 的原理

race的底层实现基于 ThreadSanitizerAlgorithm 算法,该算法的大体为,随着线程的时钟递增,创建与当前内存访问相对应的新的Shadow Word。然后状态机迭代存储在Shadow State中的所有Shadow Word。如果其中一个老的Shadow Word与新Shadow Word组成race,则data race被检测出。

  • race 的解决办法

channels,加锁(mutexes)和原子(atomic counters)。

Golang 是否有必要使用 Web 框架?

和 PHP/Python 不一样,Golang 本身的 net/http 包就已经能够处理请求和相应(HTTP 核心),就已经算一个小型 Web 框架了。因此很多时候只需要根据用途选择组件来组合开发 Web 应用就可以了。

Go is a different beast comparing to many other languages. While being general enough, Go was built for the modern web. It has all the built-in tools enough to accomplish most web programming tasks, and if it does not, the modularity of its package system makes it easy to mash up many external packages in a plug-and-play fashion.

It is understandable why Python needs web frameworks. It is a more general language without built-in response and request objects. When it comes to standard patterns to handle HTTP requests and responses in an application, it leaves that to each framework’s implementations.

Ironically, all frameworks tend to advertise with something along the line of “simple”, “fast”, “fancy”, and “powerful”, because they appeal to Go’s users. But the fact is they are not simple and unfancy. When an augmentation to something seems out of place and counter-intuitive, it is usually a sign that the thing alone is already fine and needs no further simplification(如果有一个事物,当往里面继续增加新东西之后,反而觉得莫名其妙或者反人类直觉时,这通常只能说明这个事物本身已经足够好,且不再需要更多的“简化”).

参考