A Tour of Go

Introduction

入了长亭的区块链实习,目前组里的主要项目是公链审计。

挺多用Go来implement的公链的,语言都不会,审个???

不过神奇的是,虽然没接触过go,还是能够看得懂一些go代码的。

So, I’m here to pick up Golang.

img

Notes

  • go里可以不用写 ;,舒服。

  • import,最好用带括号(factored import statement)的写法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    // Yes
    import (
    	"fmt"
    	"math"
    )
    
    // No
    import "fmt"
    import "math"
    

    甚至都不需要逗号隔开

  • import进来的,也就是被exported的,首字母必须得大写。

    e.g. math.pi是不行的,要math.Pi,不然会报错cannot refer to unexported name xxx

  • math/rand的实现:

    It looks more like Knuth’s random number generator algorithm from the art of computer programming Vol 2, using a very large LSFR of 607 terms with a tap at 273.

    Ref:https://groups.google.com/forum/#!topic/golang-nuts/RZ1G3_cxMcM

    deterministc

    要安全的话,用crypto/rand

  • Go里面,类型(type)都是放在变量名(variable name)的后面的。

    原因:https://blog.golang.org/gos-declaration-syntax

    简单来说,就是C里面的变量声明在某些情况下(数组指针,函数指针)会很混乱,让人搞不清楚,所以Go的作者就想换一种姿势。

    e.g. int (*(*fp)(int (*)(int, int), int))(int, int); 很难看出来这个是一个函数指针的声明。

    (这个指针所指向的函数有2个参数,第1个参数是另外一个函数指针(有2个int参数,返回值是int),第2个参数是int,返回值是另另外一个函数指针(2个int参数,返回值是int))

    Screen Shot 2020-04-14 at 4.40.04 PM

    但Go里把变量名直接放在最前面,就很清晰了:f func(func(int,int) int, int) func(int, int) int

    Screen Shot 2020-04-14 at 4.45.03 PM

  • 函数的形参(function parameters)如果有多个且同种类型的话,可以简写。

    e.g.

    1
    
    func xxx(x int, y int)
    

    可以简写为

    1
    
    func xxx(x, y int)
    
  • 跟C里面函数的返回值很不一样的一点就是,Go的函数可以有很多个返回值(虽然实际上C也能返回多个)。

    甚至可以事先命名好函数返回的变量名。

    1
    2
    3
    4
    5
    
    func split(sum int) (x, y int) {
    	x = sum * 4 / 9
    	y = sum - x
    	return
    }
    
  • 可以用var来声明变量,需要指明类型(type)。

    可以在声明(declaration)的时候就初始化(initialization),提供初始值了就可以不用指明类型了。

    1
    2
    3
    
    var i, j int = 1, 2
    
    var c, python, java = true, false, "no!"
    
  • 在函数里,可以用:=来初始化一个变量(short assignment statement)。

    但是在函数外就不可以。

    1
    
    c, python, java := true, false, "no!"
    
  • 如果声明了,但没给初始值,会被给予零值(zero value)。

  • Go里的基本类型(basic types)

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
    bool
    
    string
    
    int  int8  int16  int32  int64
    uint uint8 uint16 uint32 uint64 uintptr
    
    byte // alias for uint8
    
    rune // alias for int32
         // represents a Unicode code point
    
    float32 float64
    
    complex64 complex128
    
  • Go里的格式化字符串有点意思的。https://gobyexample.com/string-formatting

  • Go里面似乎没有自动类型转换,必须都得手动来。

    syntax: T(v)

    e.g. uint(7.0),注意和C里面的不太一样(uint) 7.0

    不过函数传参的时候貌似会自动转换。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    package main
    
    import "fmt"
    
    func f(x float64) float64 {
        return x;
    }
    
    func main() {
    	fmt.Println(f(4));		   // output: 4
        fmt.Printf("%T", f(4));    // output: float64
    }
    
  • 可以用const关键词来定义一个常量,不能和:=一起用。


  • Go里只有一种循环结构,用for来写。

    for initial statement; loop condition; update statement { ... }

    不用在for后面跟上()了,但{}是一定要有的。

    上面for后面的三条语句,可以为空;当只有loop condition的时候,可以省去两个分号(变成了while);甚至可以什么都没有,那么就是一个无限循环。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    package main
    
    import (
    	"fmt"
    	"math"
    )
    
    func Sqrt(x float64) float64 {
    	var z float64 = 1.0;
    	for ; math.Abs(z*z - x) > 1e-12; z -= (z*z - x) / (2*x) {
    		fmt.Println(z);
    	}
    	return z
    }
    
    func main() {
    	fmt.Println(Sqrt(2))
    }
    
  • if语句跟for类似,条件判断不需要(),但一定要有{}

    甚至还可以给if的判断之前来一条短语句(应该就是:=

    1
    
    if a := 1; b { ... }
    
  • Go里的switch,跟C里的差不多。

    if一样,可以带一条短语句(short statement)

    有两点很给力:1. 只会调到某一条分支里,(自动break)执行完了这条分支就完事了;2. switch的对象可以不是常量或整型(C里要求必须是一个常量或整型)。

    分支跳转比对的时候,是从上往下的,e.g.,在下面这个例子中,如果i==0那么f()是不会执行的

    1
    2
    3
    4
    
    switch i {
    	case 0:
    	case f():
    }
    

    还能无条件switch,相当于switch true

  • defer,中文名:推迟。

    能让一个函数不执行,直到这条语句所在的函数返回了。

    defer的函数,会被压进一个栈(stack)里,直到所在的函数返回了,才会把这些函数一个一个地弹出来(last-in-first-out)。

    e.g.

     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"
    
    func main() {
    	fmt.Println("counting")
    
    	for i := 0; i < 10; i++ {
    		defer fmt.Println(i)
    	}
    
    	fmt.Println("done")
    }
    // output:
    // counting
    // done
    // 9
    // 8
    // 7
    // 6
    // 5
    // 4
    // 3
    // 2
    // 1
    // 0
    

    这玩意儿第一次见啊,肯定有一些特殊的需求所以才引进来的吧。


  • Go也有指针(pointer),也用*, &,基本差不多,但不支持指针运算。

  • 也有结构体(struct)

    A struct is a collection of fields.

    1
    2
    3
    4
    
    type Vertex struct {
        X int
        Y int
    }
    

    初始化一个结构体:v := Vectex{1, 2}

    可以通过.来访问里面的field:v.X;结构体的指针也是用.来访问内部的field(而非C中的->)。

    1
    2
    3
    
    v := Vertex{1, 2}
    p := &v
    p.X = 1e9
    

    (*p).X当然也可以,不过太麻烦了。

    可以给结构体里某个指定的field赋值:

    1
    2
    3
    4
    
    v1 = Vertex{1, 2}  // has type Vertex
    v2 = Vertex{X: 1}  // Y:0 is implicit
    v3 = Vertex{}      // X:0 and Y:0
    p  = &Vertex{1, 2} // has type *Vertex
    
  • 数组(Arrays),[n]T就是一个类型为T大小为n的数组。

    var a [10]int,a就是一个能够容纳10整数的数组。

    数组的长度是这个类型(type)的一部分([2]int[3]int就是两种不同的类型),因此数组的长度是无法改变的。

  • 但是可以用切片(slice),[]T就是一个元素类型为T的切片。跟python里的slice差的不多。

    1
    2
    3
    
    primes := [6]int{2, 3, 5, 7, 11, 13}
    
    var s []int = primes[1:4] // [3, 5, 7]
    

    可以把slice理解为一个指向数组的指针。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    
    func main() {
    	names := [4]string{
    		"John",
    		"Paul",
    		"George",
    		"Ringo",
    	}
    	fmt.Println(names)
    
    	a := names[0:2]
    	b := names[1:3]    // a和b都指向同一个数组
    	fmt.Println(a, b)
    
    	b[0] = "XXX"       // names,a,b,都会改变。
    	fmt.Println(a, b)
    	fmt.Println(names)
    }
    

    slice是有长度(切片的时候的长度)和容量(底层数组的长度)的:len(s)cap(s)

    跟python的slice还是有一些区别的,Go里的slice底层数组是不会变的,如果切片范围超过了当前该切片的length,如果范围没有超过容量的话,那么就会从底层数组里扩充出一些元素(扩容)。很神奇

     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
    
    package main
    
    import "fmt"
    
    func main() {
    	s := []int{2, 3, 5, 7, 11, 13}
    	printSlice(s)
    
    	// Slice the slice to give it zero length.
    	s = s[:0]			// 长度为0
    	printSlice(s)
    
    	// Extend its length.
    	s = s[:4]           // 从底层数组中扩充
    	printSlice(s)
    
    	// Drop its first two values.
    	s = s[2:]		    // 在上一个slice里再切片
    	printSlice(s)
    }
    
    func printSlice(s []int) {
    	fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
    }
    
    // Output:
    // len=6 cap=6 [2 3 5 7 11 13]
    // len=0 cap=6 []
    // len=4 cap=6 [2 3 5 7]
    // len=2 cap=4 [5 7]
    

    slice的零值是nil,长度和容量都为0,无底层数组。

    (Go的非basic type的零值都是nil??猜测,待确定。)

    可以使用make来分配(allocate)一个slice

    1
    2
    3
    4
    5
    6
    
    a := make([]int, 5)  // len(a)=5
    
    b := make([]int, 0, 5) // len(b)=0, cap(b)=5
    
    b = b[:cap(b)] // len(b)=5, cap(b)=5
    b = b[1:]      // len(b)=4, cap(b)=4
    

    woc,这个cap到底是怎么变的啊,有点晕。

    slice的全面讲解:https://www.cnblogs.com/qcrao-2018/p/10631989.html(别看汇编部分,直接看图就完事了)

    也有多维数组、slice。

    可以用append对slice添加元素:func append(s []T, vs ...T) []T

  • for循环可以搭配上range,对slice或者map进行迭代(iterate)

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    package main
    
    import "fmt"
    
    var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}
    
    func main() {
    	for i, v := range pow {
    		fmt.Printf("2**%d = %d\n", i, v)
    	}
    }
    

    跟python里的enumerate很像。

    可以用_来省略掉i, v中的一个,或者只想要下标的话可以只给出一个i

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    package main
    
    import "fmt"
    
    func main() {
    	pow := make([]int, 10)
    	for i := range pow {
    		pow[i] = 1 << uint(i) // == 2**i
    	}
    	for _, value := range pow {
    		fmt.Printf("%d\n", value)
    	}
    }
    
  • map,hash table,类似于python的dict

    map的零值也是nil,一个nil map无键值,且无法添加键值。

    make生成一个(ready for use)map

    1
    
    m := make(map[string] uint)
    

    map的读写操作和python的dict差不多,删除:delete(m, key)

    可以用两个变量来获取某个键值及是否存在该键值:key, ok = m[key]

  • Go里的函数也是值(values),可以用来作为函数的参数或者返回值。

    function closures

     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 "fmt"
    
    func adder() func(int) int {
    	sum := 0
    	return func(x int) int {
    		sum += x
    		return sum
    	}
    }
    
    func main() {
    	pos, neg := adder(), adder()
    	for i := 0; i < 10; i++ {
    		fmt.Println(
    			pos(i),
    			neg(-2*i),
    		)
    	}
    }
    // Output:
    // 0 0
    // 1 -2
    // 3 -6
    // 6 -12
    // 10 -20
    // 15 -30
    // 21 -42
    // 28 -56
    // 36 -72
    // 45 -90
    

    posneg里的sum变量是不同的,但是会保持那个状态。

    用这个来写斐波那且数列:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    package main
    
    import "fmt"
    
    // fibonacci is a function that returns
    // a function that returns an int.
    func fibonacci() func() int {
    	a, b := 0, 1
    	return func() int {
    		a, b = b, a + b
    		return a
    	}
    }
    
    func main() {
    	f := fibonacci()
    	for i := 0; i < 10; i++ {
    		fmt.Println(f())
    	}
    }
    

  • Go没有类(class),舒服。

    但是可以为某种类型(type)定义一个方法(method)。

    A method is a function with a special receiver argument.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    package main
    
    import (
    	"fmt"
    	"math"
    )
    
    type Vertex struct {
    	X, Y float64
    }
    
    func (v Vertex) Abs() float64 {
    	return math.Sqrt(v.X*v.X + v.Y*v.Y)
    }
    
    func main() {
    	v := Vertex{3, 4}
    	fmt.Println(v.Abs())
    }
    

    Abs方法就是专门为类型Vertex所定义的。

    不难看出为某种类型T定义一个方法,就是在func xxx()中间加入(t T)(receiver)。

    方法(method)仍然是函数(function)。

    只能给当前包(package)中的类型定义方法,无法为其他包中的类型定义。因此无法给basic types(e.g. int, float64, string)定义方法。

    也给可以给指针类型(pointer)定义方法,这样就可以在这个方法里对这个类型的变量进行内部的修改。

    v.Abs() == (&v).Abs()

  • 指针传参 vs 值传参

    指针传参可以:1.方便修改传入的参数 2.避免拷贝,提高效率

  • interface类型,定义了一系列方法的签名,作为接口,以便被后续某个实现了这些方法的变量赋值。

     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
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    
    package main
    
    import (
    	"fmt"
    	"math"
    )
    
    type Abser interface {
    	Abs() float64
    }
    
    func main() {
    	var a Abser
    	f := MyFloat(-math.Sqrt2)
    	v := Vertex{3, 4}
    
    	a = f  // a MyFloat implements Abser
    	a = &v // a *Vertex implements Abser
    
    	// In the following line, v is a Vertex (not *Vertex)
    	// and does NOT implement Abser.
    //	a = v                 // Error: "... Vertex does not implement Abser"
    
    	fmt.Println(a.Abs())
    }
    
    type MyFloat float64
    
    func (f MyFloat) Abs() float64 {
    	if f < 0 {
    		return float64(-f)
    	}
    	return float64(f)
    }
    
    type Vertex struct {
    	X, Y float64
    }
    
    func (v *Vertex) Abs() float64 {
    	return math.Sqrt(v.X*v.X + v.Y*v.Y)
    }
    

    A type implements an interface by implementing its methods. There is no explicit declaration of intent, no “implements” keyword.

    Implicit interfaces decouple the definition of an interface from its implementation, which could then appear in any package without prearrangement.

    一个interface类型的变量,可以被视为:(value, type)这样一个二元组。

    这个value可以是nil(某个type变量没初始化赋值),仍然可以去调用这个接口的方法,只不过传入的receiver是nil罢了,但这个接口变量并不是nil

    接口变量可以是nil,这样它内部的valuetype都不存在了(都是nil),调用这个接口变量的某个方法会报错run-time error。

    可以有空接口:interface{},用来处理未知类型的变量。

  • Go会对没有使用过的变量报错:... xxx declared but not used

  • 有了接口变量,但想要把里面的value取出来怎么办?

    type assertions可以解决这个问题。

    t := i.(T)i是某个接口变量,T是某个类型,如果i里面的那个type的确是T的话,就会把i里面的value赋值给t,否则会触发panic。

    也可以让i.(T)返回两个值,第二个用来判断是否成功:t, ok := i.(T)。失败的话,t = 0, ok = false。这里的语法和从map里取出某个值很类似。

  • 如果猜不到i里面的那个type到底是什么呢?

    type switches

    1
    2
    3
    4
    5
    6
    7
    8
    
    switch v := i.(type) {
    case T:
        // here v has type T
    case S:
        // here v has type S
    default:
        // no match; here v has the same type as i
    }
    

    可以对i里面的type进行一个一个猜测。注意i.(type)括号中的是type而非某种具体的类型。

  • Stringer是一个常见的interface,定义在fmt包中。

    1
    2
    3
    
    type Stringer interface {
        String() string
    }
    

    可以对某个类型实现一下String,这样就可以赋值给Stringer接口变量,然后被fmt中的某些函数所用。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    
    package main
    
    import "fmt"
    
    type IPAddr [4]byte
    
    func (ip IPAddr) String() string {
    	return fmt.Sprintf("%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3])
    }
    
    func main() {
    	hosts := map[string]IPAddr{
    		"loopback":  {127, 0, 0, 1},
    		"googleDNS": {8, 8, 8, 8},
    	}
    	for name, ip := range hosts {
    		fmt.Printf("%v: %v\n", name, ip)
    	}
    }
    // Output:
    // loopback: 127.0.0.1
    // googleDNS: 8.8.8.8
    
  • Go也能处理错误,用一个内置的error接口类型来实现。

    1
    2
    3
    
    type error interface {
        Error() string
    }
    

    某些函数会返回一个error值,需要对此进行处理。

    1
    2
    3
    4
    5
    6
    
    i, err := strconv.Atoi("42")
    if err != nil {
        fmt.Printf("couldn't convert number: %v\n", err)
        return
    }
    fmt.Println("Converted integer:", i)
    

    error == nil说明成功,否则就是失败。

Summary

还有一点interfaceconcurrency的内容还没看。。不过应该基本算是能看懂一些go代码了吧。

Reference

https://tour.golang.org/


2020.04.17 更新

  • 特殊函数func init() { ... },早于main函数(entry point)执行,晚于包中变量的初始化,主要用来做一些(额外的)初始化操作。

    更多请参考:

  • Function type 理解起来就是对某一群由同一个函数签名(function signature)的函数的总称?

    • 一个只需1min就能看懂的讲解:https://www.jianshu.com/p/fc4902159cf5
  • Go写多线程似乎很容易,直接go xxx()

    但是写多线程的话,就要关注底层数据的一个读写问题了,要尽量避免出现多个线程同时读写同一个数据 之类的问题。


2020.04.18 更新

  • 函数可以接受任意数量的参数(variadic function)。

    需要用三个.来表示一个函数能接受任意数量的参数;传参的时候,如果是一个slice,也需要用...来传多个参数(类似于Python里的解包?)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    func foo(is ...int) {
      for i := 0; i < len(is); i++ {
          fmt.Println(is[i])
      }
    }
    
    func main() {
        foo([]int{9,8,7,6,5}...)
    }
    

    Ref: https://stackoverflow.com/questions/16248241/concatenate-two-slices-in-go


2020.04.28 更新

  • golang是一个强类型(strong-typed)语言。不同类型之间进行比较会被认为是invalid。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    
    package main
    
    import (
    	"fmt"
    )
    
    func main() {
    	var (
    		a uint = 1
    		b int = 2
    	)
    	fmt.Println(a<b)
    
    }
    

    运行,会报错:invalid operation: a < b (mismatched types uint and int)

  • 记录两段看上去很标准的多线程操作:

    ethash数据集的生成过程:

     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
    33
    34
    35
    36
    37
    38
    39
    40
    41
    
    start := time.Now()
    
    // Generate the dataset on many goroutines since it takes a while
    threads := runtime.NumCPU()
    size := uint64(len(dataset))
    
    var pend sync.WaitGroup
    pend.Add(threads)
    
    var progress uint32
    for i := 0; i < threads; i++ {
        go func(id int) {
            defer pend.Done()
    
            // Create a hasher to reuse between invocations
            keccak512 := makeHasher(sha3.NewLegacyKeccak512())
    
            // Calculate the data segment this thread should generate
            batch := uint32((size + hashBytes*uint64(threads) - 1) / (hashBytes * uint64(threads)))
            first := uint32(id) * batch
            limit := first + batch
            if limit > uint32(size/hashBytes) {
                limit = uint32(size / hashBytes)
            }
            // Calculate the dataset segment
            percent := uint32(size / hashBytes / 100)
            for index := first; index < limit; index++ {
                item := generateDatasetItem(cache, index, keccak512)
                if swapped {
                    swap(item)
                }
                copy(dataset[index*hashBytes:], item)
    
                if status := atomic.AddUint32(&progress, 1); status%percent == 0 {
                    logger.Info("Generating DAG in progress", "percentage", uint64(status*100)/(size/hashBytes), "elapsed", common.PrettyDuration(time.Since(start)))
                }
            }
        }(i)
    }
    // Wait for all the generators to finish and return
    pend.Wait()
    

    ethash挖矿寻找nonce的过程:

     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
    33
    34
    35
    36
    
    var (
        pend   sync.WaitGroup
        locals = make(chan *types.Block)
    )
    for i := 0; i < threads; i++ {
        pend.Add(1)
        go func(id int, nonce uint64) {
            defer pend.Done()
            ethash.mine(block, id, nonce, abort, locals)
        }(i, uint64(ethash.rand.Int63()))
    }
    // Wait until sealing is terminated or a nonce is found
    go func() {
        var result *types.Block
        select {
            case <-stop:
            // Outside abort, stop all miner threads
            close(abort)
            case result = <-locals:
            // One of the threads found a block, abort all others
            select {
                case results <- result:
            default:
                ethash.config.Log.Warn("Sealing result is not read by miner", "mode", "local", "sealhash", ethash.SealHash(block.Header()))
            }
            close(abort)
            case <-ethash.update:
            // Thread count was changed on user request, restart
            close(abort)
            if err := ethash.Seal(chain, block, results, stop); err != nil {
                ethash.config.Log.Error("Failed to restart sealing after update", "err", err)
            }
        }
        // Wait for all miners to terminate and return the block
        pend.Wait()
    }()