Three-day Thinking in Go (Day 1)

15 minute read

最近在公司内部参加了一个go programming的培训,一共三天,这个系列算作是我的个人笔记。即回顾所学的知识,又在整理的时候做了更多的查阅和阅读。当然这三天不能期待太多,都是非常基础的东西,所以适合有编程经验但是并不熟悉Go的读者。 希望日后时间从这个三天大纲式的笔记做一些延伸。

安装 (Mac)

建议是从 Homebrew 开始

安装 Homebrew

1ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

安装 Go

1brew install go

设置环境变量

查看Go的版本

go version

查看GO的当前环境变量

go env

确保 GO111MODULE=on 这样我们可以使用 go mod

设置 Workspace

在你的 shell (比如 .zshrc) 中设置

1export GOPATH=$HOME/go  # go 会把这个当作是workspace
2export PATH="$GOPATH/bin:$PATH"
3export GO111MODULE=on  # 如果你的go version是 1.11/1.12 你需要这个

设置完了记得 source一下 让其生效。

1source ~/.zshrc

Hello World 测试

mkdir -p ~/threedaygo/hello
cd ~/threedaygo/hello
go mod init github.com/threedaygo/hello

go mod 应该现在是go开发中 de facto 的依赖管理

创建一个 main.go

内容为

1package main
2
3import "fmt"
4
5func main() {
6    fmt.Println("Hello, let's do go in three days!")
7}

跑一下,确保可以看到输出

go run main.go
1go build main.go  # build 一个bianry在当前folder
2./main
1go install main.go 
2which main   # 应该在你的go path 的bin folder 下
3main  # 应该会执行

安装 golint

 go get -u golang.org/x/lint/golint

在你的 main.go 里加入

1func hello_word() {}

跑 linter

1golint main.go 
2# 应该会有: 不应该用undersocre来命名的警告

类型

原始类型

  • string
  • bool
  • int, int[8,16,32,64], int 和 int32等 不是一个type
  • uint, uint[8,16,32,64], 同理 uint 和 uint32等 也不是一个type
  • int/uint的大小其实根据CPU决定的
  • float32 flot64
  • complex64 complex128
  • byte (uint8) 可代表了一个ASCII字符
  • rune (int32) 可代表一个UTF-8字符
 1// 用var声明变量,变量会被初始化zero value
 2// type在变量名后
 3// 变量名用的驼峰
 4var a int
 5var str string // string的空值是“”
 6
 7// 用:= 简洁声明赋值
 8a := 10
 9
10// 声明并赋值
11var b int8 = 10
12var d = 10.2 // compiler infer
13fmt.Printf("%T\n", d) // float64
14
15// var block 声明多个
16var (
17    a1 int
18    str1 string
19    b1 float64
20)
21
22// 用back tick声明多行string
23a := `大家
2425`
26

阅读资料 变量的命名

string

string在go中作为基本类型, 是一个双字的结构

这里的字,通常指的是字长单位,即CPU做处理的自然数据单位,通常CPU里的数据传输是一个字长,寄存器的大小也是一个字长。比如目前主流的CPU字长应该是64bit

由于string使用一个数组来做数据存储,所以string是immutable的 string支持UTF8,所以这个数据array的单位应该是rune

常量 const

  • 编译时即有值
  • 有两种 typed 和 untyped
  • 如果是untyped则会用256bits最高精度来装载
  • 如果是typed则会用对应的type
  • typed的常量可以bei
 1const (
 2    pi = 3.14  //untyped,  float kind
 3    double = 2  //untyped, int kind
 4    piT float64 = pi  // => 3.14 float64
 5    doubleT int32 = double  // => 2, int32
 6    a  // 沿用前面的值和type => 2, int32
 7    b = 1  // => 1, int
 8)
 9
10
11func main() {
12	var (
13		a1 float32 = 2.0
14		a2 int     = 10
15	)
16	fmt.Println("R1:", a1*pi) // float32变量和一个untyped float kind的常量数字相乘,ok
17	fmt.Println("R2:", a2*pi) // int变量和一个untyped float kind常量数字相乘,报错
18	fmt.Println("R3:", a2*double) // int变量和一个untyped int kind常量数字相乘,ok
19	fmt.Println("R4:", a2*doubleT) // int变量和一个float64 type常量相乘,报错
20}
21

iota

  • 一个自增的赋值器
  • 自增开始于下一个行
  • 可以用 _ 来跳过
  • Const block 会重设 iota
 1  // 类型定义 (注意区分类型别名)
 2  type Status int
 3
 4  const (
 5    CREATED Status = iota + 1 // => 0 + 1
 6    RUNING                    // => 1 + 1
 7    STOPPED                   // => 2 + 1
 8    _                         // => 3 + 1
 9    _                         // => 4 + 1
10    ERROR                     // => 5 + 1
11  )
12
13  // const block 会重设iota
14  const (
15      Monday int = iota // 0
16      Tuesday           // 1
17      Wednesday         // 2
18  )

阅读资料 Rob Pike 在Go Blog 写了一些为何要设计成这样,感兴趣的应该读一读。

归纳一下的话就是

  • numeric type 不能混用在运算中,因为这个是很多bug的来源
  • 但是constant则没有这么严格,constant 不像是变量,更像是一个数字

类型提升 (type promotion)

  • 没有自动type conversion,变量的话需要手动convert
  • untyped constant 如果匹配的话,会自动提升类型
  • 可以用 type定义自己的type
  • 区分 type 定义和 type别名(alias)

结构体

从C继承过来的

  • 关键字是 type 和 struct
  • 可以做 type compose
  • 用点来访问结构体成员
  • struct也是初始化为0值

%#v 来打印struct

指针

指针也是从C集成过来

  • 指针初始化是 new(T) *T 应该对等c的malloc
  • * dereference
  • & 取地址
  • 指针的size取决于cpu
  • 打印指针可以用%p
 1	a := 10
 2	fmt.Printf("a: %-10T %p:%v\n", a, &a, a)
 3
 4	b := &a
 5	fmt.Printf("b: %-10T %p:%p:%v\n", b, &b, b, *b)
 6
 7	var c *int
 8	c = &a
 9	fmt.Printf("c: %-10T %p:%p:%v\n", c, &c, c, *c)
10	
11	var d = new(int)
12	*d = a
13	fmt.Printf("d: %-10T %p:%p:%v\n", d, &d, d, *d)

函数

  • 函数也是一个类型
  • 函数输入可以是多个变量,返回也可以是多个变量
  • 函数命名一般用驼峰
  • 通过首字母是否大小,决定函数是否可以包外使用
  • _ 省略不需要return
  • named return
  • func可以被赋给一个变量
  • clean code, 注意变量的和return变量的数目
  • func call 会有stack frame
  • 匿名函数
 1// 函数可以做函数的输入变量
 2pow := func(i int) int {
 3    return i*i
 4}
 5fmt.Println(pow(100))
 6
 7// 匿名函数
 8say := func(i int) func() string {
 9    return func() string {
10        return fmt.Sprintf("this is a %d", i)
11    }
12}
13
14// func 有type
15type multiplier func(i int) int

控制语句

这里和其他的编程语言并没有很多不同

  • 循环只有一个关键字 for
  • for range
  • defer
  • goto
  • {} 来定义scope
  • switch 的case 默认会break
  • switch break( to branch)
  • switch fallthrough/default
  • panic
  • recover

集合

数组

  • 数组是一片连续分配的内存
  • 数组是mutable
  • 但是静态的,不能增长或者缩小
  • typed,element必须是声明的type
  • zero based index
  • 初始化为0值
  • cap
  • len
  • 对于数组, len==cap
  • 数组是静态的, 大小在编译的时候是已知的,所以分配在栈上
 1// 声明
 2var a [5]int
 3var b [2]bool
 4
 5// 赋值声明
 6c := [3]float32{1,2,3}
 7
 8e := [3]float64{0: 1.1, 2:2.2}
 9fmt.Printf("e = %#v\n", e) // => e = [3]float64{1.1, 0, 2.2}
10
11d := [...]float64{0: 1.1, 3:2.2}
12fmt.Printf("d = %#v\n", d) // => d = [4]float64{1.1, 0, 0, 2.2}
13
14// 可以对一数组切片
15fmt.Printf("d = %#v\n", d[1:3])
16fmt.Printf("d = %#v\n", d[1:])
17fmt.Printf("d = %#v\n", d[:2])
18fmt.Printf("d = %#v\n", d[:])
19
20// 循环访问数组
21for i :=0 ; i < len(d); i++ {
22    fmt.Printf("d[%d] %p:%s\n", i, &d[i], d[i])
23}
24
25for i, v := range d {
26    fmt.Printf("d[%d] %p:%s\n", i, &v, v)
27}
28
29// 多维数组
30e := [3][3]int{ {1, 1, 1}, {2, 2, 2}, {3, 3, 3} }
31for i := 0; i < len(e); i++ {
32  for j := 0; j < len(e[i]); j++ {
33    fmt.Printf("% d ", e[i][j])
34  }
35  fmt.Println()
36}

切片

  • 利用数组来提供数据
  • 自身是一个引用类型
  • 可以增长
  • 声明和array数组,但是 []中没有长度
  • len,cap,append
  • 通过 make 来创建
  • 是一个3字结构 (指向数组的指针,切片的长度,切片的容量)

切片如何扩容?

切片相当于一个小的struct,指向切片的第一个元素

这里也有一个增长因子,当元素小于1024,每次需要扩容的时候都是翻倍,超过之后则是1.25。 如果增长之后,超过了原有数组的容量,go会开辟新的内存,把值拷贝过来,然后切片指向新的位置。

 1package main
 2
 3import (
 4	"fmt"
 5)
 6
 7func main() {
 8	a := [3]int{1, 2, 3}
 9	b := a[2:]
10	c := a[2:]
11	for i := 1; i < 16; i++ {
12		fmt.Printf("b %T, %p, %p, %#v\n", b, b, &b, b)
13		b = append(b, i)
14    }
15	c[0] = 4
16	fmt.Printf("c %T, %p, %p, %#v\n", c, c, &c, c)
17    fmt.Printf("a %T, %p, %#v\n", a, &a, a)
18	fmt.Printf("b %T, %p, %p, %#v\n", b, b, &b, b)
19}
20
21
22// b []int, 0xc000014030, 0xc00000c0a0, []int{3}
23// b []int, 0xc00002c060, 0xc00000c0a0, []int{3, 1}
24// b []int, 0xc000014060, 0xc00000c0a0, []int{3, 1, 2}
25// b []int, 0xc000014060, 0xc00000c0a0, []int{3, 1, 2, 3}
26// b []int, 0xc00007c040, 0xc00000c0a0, []int{3, 1, 2, 3, 4}
27// b []int, 0xc00007c040, 0xc00000c0a0, []int{3, 1, 2, 3, 4, 5}
28// b []int, 0xc00007c040, 0xc00000c0a0, []int{3, 1, 2, 3, 4, 5, 6}
29// b []int, 0xc00007c040, 0xc00000c0a0, []int{3, 1, 2, 3, 4, 5, 6, 7}
30// b []int, 0xc000078100, 0xc00000c0a0, []int{3, 1, 2, 3, 4, 5, 6, 7, 8}
31// b []int, 0xc000078100, 0xc00000c0a0, []int{3, 1, 2, 3, 4, 5, 6, 7, 8, 9}
32// b []int, 0xc000078100, 0xc00000c0a0, []int{3, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
33// b []int, 0xc000078100, 0xc00000c0a0, []int{3, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}
34// b []int, 0xc000078100, 0xc00000c0a0, []int{3, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}
35// b []int, 0xc000078100, 0xc00000c0a0, []int{3, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13}
36// b []int, 0xc000078100, 0xc00000c0a0, []int{3, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14}
37// c []int, 0xc000014030, 0xc00000c0c0, []int{4}
38// a [3]int, 0xc000014020, [3]int{1, 2, 4}
39// b []int, 0xc000078100, 0xc00000c0a0, []int{3, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}

这个结果怎么理解呢?一开始的slice大小为0 当我们开始append的时候,扩容就开始了,所以创建了一个新的backing struct,这里可以看出这里的扩容大概是2倍每次。 因在扩容的时候发生了拷贝,所以slice b下面已经不在是a数组做backing。而slice c还是,所以修改c的数据就体现在a, 但是b丝毫不受影响

  • key-value
  • 通过对key的hash决定value的位置
  • key的类型,只要是comparable的即可(==)
  • 所以,map,slice func 不能成为map的key
  • 不是concurrency safe的
  • 读取是unorder的
 1m1 := map[string]int{}
 2m2 := make(map[string]int)
 3m3 := make(map[int]bool, 10)
 4m4 := map[string]int{"a": 1, "b": 2, "c": 3}
 5
 6fmt.Printf("m1 = %#v\n", m1)
 7fmt.Printf("m2 = %#v\n", m2)
 8fmt.Printf("m3 = %#v\n", m3)
 9fmt.Printf("m4 = %#v\n", m4)
10
11
12// 写
13m1["a"] = 1
14m1["b"] = 2
15
16// 读
17if v, ok := m1["c"]; ok {
18  fmt.Println(v, ok)
19} else {
20  fmt.Println("Not found!")
21}
22
23// 更新
24if v, ok := m["c"]; ok {
25  m["c"] = v + 1
26}
27
28// 删除
29delete(m, "b")
30
31// Map的元素是没办法addressable的
32type worker struct {
33  firstName, lastName string
34}
35
36m := map[string]worker{
37  "a": {
38    firstName: "b",
39    lastName:  "c",
40  },
41}
42
43// 以下操作会报错
44m["a"].firstName = "d" // => cannot assign to struct field m["a"].firstName in map
45
46// 解决方法1 替换这个value
47e := m["a"]
48e.firstName = "d"
49m["a"] = e
50
51// 解决办法2 是放struct的指针到map中
52m := map[string]*worker{
53  "a": &worker{
54    firstName: "b",
55    lastName:  "c",
56  },
57}
58m["a"].firstName = "d"
59fmt.Println(m["a"].firstName)
60

GO入门系列

  1. 系列1
  2. 系列2
  3. 系列3

wechat-qrcode