golang五十度灰阅读(高级篇)

五十度灰文章是之前实习时候大佬推荐的,最近才仔细的去阅读学习,这里主要是高级篇部分阅读解析

前言

网上找到golang五十度灰的英文原版,以及翻译版,但是翻译版完全是google翻译的结果,这里主要是阅读英文原版并且结合自己的理解给出解释,可以看作是翻译版,但是我四级都没过:(

其次这里只有高级篇,因为初中级的看google翻译版都没啥问题,几个难点,有时间会记录一下。

总的来说高级篇是需要你对go有一定的源码阅读或者底层理解才能更好的理解这里出现的问题,当然你记住了也没有任何问题,也是可以提高自己的代码水平。

原文参考:英文/中文

1、Using Pointer Receiver Methods On Value Instances

如果需要使用指针变量作为方法的参数,那么调用者的地址必须是可取的

但是并不是所有的变量都是可取址的,比如Map中的元素、通过interface引用的变量

Fails:

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
package main

import "fmt"

type data struct {
name string
}

func (p *data) print() {
fmt.Println("name:",p.name)
}

type printer interface {
print()
}

func main() {
d1 := data{"one"}
d1.print() //ok

var in printer = data{"two"} //error
in.print()

m := map[string]data {"x":data{"three"}}
m["x"].print() //error
}

Works:

1
2
3
func (p data) print() {
fmt.Println("name:",p.name)
}

2、Updating Map Value Fields

Map中无法更新struct的某个字段值,因为Map元素是不可取址的

Fails:

1
2
3
4
5
6
7
8
9
10
package main

type data struct {
name string
}

func main() {
m := map[string]data {"x":{"one"}}
m["x"].name = "two" //error
}

Compile Error:

cannot assign to m[“x”].name

但是slice元素是可以取址的,即可修改的:

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import "fmt"

type data struct {
name string
}

func main() {
s := []data {{"one"}}
s[0].name = "two" //ok
fmt.Println(s) //prints: [{two}]
}

修复Map中的错误有两个办法:

Works one 是采用临时变量

1
2
3
4
5
6
7
8
9
10
11
12
package main
import "fmt"
type data struct {
name string
}
func main() {
m := map[string]data {"x":{"one"}}
r := m["x"]
r.name = "two"
m["x"] = r
fmt.Printf("%v",m) //prints: map[x:{two}]
}

Works two 是采用指针作为值的map

1
2
3
4
5
6
7
8
9
10
package main
import "fmt"
type data struct {
name string
}
func main() {
m := map[string]*data {"x":{"one"}}
m["x"].name = "two" //ok
fmt.Println(m["x"]) //prints: &{two}
}

思考题,下面代码会发生什么?

1
2
3
4
5
6
7
8
9
10
package main

type data struct {
name string
}

func main() {
m := map[string]*data {"x":{"one"}}
m["z"].name = "what?" //???
}

结果是:编译成功,但是运行panic

原因是:m[“z”]的value是一个nil,所以自然就不能赋值

3、”nil” Interfaces and “nil” Interfaces Values

interface只有当类型和值都为nil时才能满足”interface{} == nil”

interface的类型和值会根据创建对应interface变量的类型和值进行变化,如果将某类型变量的零值赋予interface变量,那么interface变量并不与nil或者其零值相等

fails:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import "fmt"

func main() {
var data *byte
var in interface{}

fmt.Println(data,data == nil) //prints: <nil> true
fmt.Println(in,in == nil) //prints: <nil> true

in = data
fmt.Println(in,in == nil) //prints: <nil> false
//'data' is 'nil', but 'in' is not 'nil'
}

当你的函数返回interface时,可能会产生下面情况:

Incorrect:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

func main() {
doit := func(arg int) interface{} {
var result *struct{} = nil

if(arg > 0) {
result = &struct{}{}
}

return result
}

if res := doit(-1); res != nil {
fmt.Println("good result:",res) //prints: good result: <nil>
//'res' is not 'nil', but its value is 'nil'
}
}

Works:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import "fmt"

func main() {
doit := func(arg int) interface{} {
var result *struct{} = nil

if(arg > 0) {
result = &struct{}{}
} else {
return nil //return an explicit 'nil'
}

return result
}

if res := doit(-1); res != nil {
fmt.Println("good result:",res)
} else {
fmt.Println("bad result (res is nil)") //here as expected
}
}

我认为更好的解决办法应该是将interface变量进行断言后判断,这样就不需要特别关注返回nil了,但是可能会出现断言panic,也是需要注意的问题

4、Stack and Heap Variables

在golang中,无论是new()还是make()申请的变量,变量所处的内存地址是由编译器决定的,编译器根据变量的大小和escap analysis的结果来决定其位置

所以在代码中,我们可以返回局部变量的引用,但是在c或者c++中是不行的。

同时可以在go build或者go run上传入-mgc flag,例如:go run -gcflags -m main.go

5、GOMAXPROCS, Concurrency, and Parallelism

在go1.5之前,go只有一个工作线程,在go1.5及以后,我们可以通过runtime.GOMAXPROCS()来设置工作线程的数量,并且默认是num of cpu即CPU核数(超线程就翻倍,超线程本质上是一个规定:如果一个cpu内核在两个线程之间切换的时间少于某个值,则大致认为两个线程是并行的)

GOMAXPROCS可以设置的值在go1.10之后不再受限制,之前在1.5之后是256,1.9是1024

当你的GOMAXPROCS的值不超过num of cpu时,可以认为是并行的,超过之后则是并发的,这涉及到GMP的调度,简单说GOMAXPROCS的值就是go在后台启动的系统线程数量的最小值,可以参考Golang 的 协程调度机制 与 GOMAXPROCS 性能调优也谈goroutine调度器

在五十度灰的原文中,其实只是表达了GOMAXPROCS的使用,但是我们应该了解其底层机制才能读懂。

6、Read and Write Operation Reordering

Go可能会重排某些操作,但是它会保证在同一个goroutine中的整体行为是不变的,即最后结果一致,但是它并不会保证多个goroutine的执行顺序。

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
package main

import (
"runtime"
"time"
)

var _ = runtime.GOMAXPROCS(3)

var a, b int

func u1() {
a = 1
b = 2
}

func u2() {
a = 3
b = 4
}

func p() {
println(a)
println(b)
}

func main() {
go u1()
go u2()
go p()
time.Sleep(1 * time.Second)
}

上面的例子多执行几次会得到不同的结果

1
2

3
4

0
2

0
0

1
4

02的结果表明b在a之前更新了,这是重排的结果,但是不影响最后的整体行为

这里主要提及的问题就是多个goroutine之间共享变量的读写顺序问题,可以通过sync包提供的锁机制或者channel来解决。

7、Preemptive Scheduling

在goroutine的调度中,可能存在某个goroutine一直保持运行,使得调度器无法运行,比如包含一个空的for循环的goroutine

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import "fmt"

func main() {
done := false

go func(){
done = true //不会执行
}()

for !done {
}
fmt.Println("done!")
}

for循环可以不是空的,但是只要不包含触发调度器执行的代码,就会出现这种情况

调度器会在GC、“go“声明、channel阻塞操作、阻塞系统调度和lock操作后运行,还有一种情况是在调用非内联函数时执行,具体调度还是可以参考也谈goroutine调度器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import "fmt"

func main() {
done := false

go func(){
done = true
}()

for !done {
fmt.Println("not done!") //not inlined
}
fmt.Println("done!")
}

可以通过传入“-m“ gcflags来查询你在for循环中调用的函数是否是内联的,例如:go build -gcflags -m main.go

此外,如果你是单纯的计算任务,又需要调度器调度,可以使用runtime.Goshed()方法来显示调度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
"fmt"
"runtime"
)

func main() {
done := false

go func(){
done = true
}()

for !done {
runtime.Gosched()
}
fmt.Println("done!")
}

Cgo篇

下面是Cgo部分,严格讲与高级篇无关,对在go中调用c无需求的可以跳过

8、Import C and Multiline Import Blocks

在Go中使用C需要用到Cgo,可以通过import “C“实现导入C中的包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

/*
#include <stdlib.h>
*/
import (
"C"
)

import (
"unsafe"
)

func main() {
cs := C.CString("my go string")
C.free(unsafe.Pointer(cs))
}

你也可以采用import块来导入,但是导入块中不能包含其他包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

/*
#include <stdlib.h>
*/
import (
"C"
"unsafe"
)

func main() {
cs := C.CString("my go string")
C.free(unsafe.Pointer(cs))
}

Compile Error:

./main.go:13:2: could not determine kind of name for C.free

9、No blank lines Between Import C and Cgo Comments

在导入C包上方的注释必须与import语句无空格,否则会编译报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

/*
#include <stdlib.h>
*/

import "C"

import (
"unsafe"
)

func main() {
cs := C.CString("my go string")
C.free(unsafe.Pointer(cs))
}

Compile Error:

./main.go:15:2: could not determine kind of name for C.free

10、Can’t Call C Functions with Variable Arguments

你不能直接调用带有不定数量参数的c函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

/*
#include <stdio.h>
#include <stdlib.h>
*/
import "C"

import (
"unsafe"
)

func main() {
cstr := C.CString("go")
C.printf("%s\n",cstr) //not ok
C.free(unsafe.Pointer(cstr))
}

Compile Error:

./main.go:15:2: unexpected type: …

你可以将其包裹在已知参数数量的函数中调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

/*
#include <stdio.h>
#include <stdlib.h>

void out(char* in) {
printf("%s\n", in);
}
*/
import "C"

import (
"unsafe"
)

func main() {
cstr := C.CString("go")
C.out(cstr) //ok
C.free(unsafe.Pointer(cstr))
}