go rule

变量

Go 语言变量名由字母、数字、下划线组成,其中首个字母不能为数字

var ( a int b bool str string ) 这种因式分解关键字的写法一般用于声明全局变量,一般在func 外定义。

当一个变量被var声明之后,系统自动赋予它该类型的零值:

int 为 0 float 为 0.0 bool 为 false string 为空字符串”” 指针为 nil 记住,这些变量在 Go 中都是经过初始化的。

声明赋值

声明

1
var s string

声明赋值 := 结构不能在函数外使用

1
2
3
4
5
i := 100                  // an int
var boiling float64 = 100 // a float64

var i, j, k int                 // int, int, int
var b, f, s = true, 2.3, "four" // bool, float64, string

作用域的坑

声明赋值

1
2
3
4
5
6
7
8
9
10
11

func main() {  
    x := 1
    fmt.Println(x)     // prints 1
    {
        fmt.Println(x) // prints 1
        x := 2         // 两个x不一样了
        fmt.Println(x) // prints 2
    }
    fmt.Println(x)     // prints 1 (不是2)
}

表达式new(T)将创建一个T类型的匿名变量,初始化为T类型的零值,然后返回变量地址,返回的指针类型为*T。

1
2
3
4
p := new(int)   // p, *int 类型, 指向匿名的 int 变量
fmt.Println(*p) // "0"
*p = 2          // 设置 int 匿名变量的值为 2
fmt.Println(*p) // "2"
1
2
3
4
5
x := 1
p := &x         // p, of type *int, points to x
fmt.Println(*p) // "1"
*p = 2          // equivalent to x = 2
fmt.Println(x)  // "2"

生命周期

变量的生命周期指的是在程序运行期间变量有效存在的时间间隔。对于在包一级声明的变量来说,它们的生命周期和整个程序的运行周期是一致的。而相比之下,局部变量的声明周期则是动态的: 每次从创建一个新变量的声明语句开始,直到该变量不再被引用为止,然后变量的存储空间可能被回收。函数的参数变量和返回值变量都是局部变量。它们在函数每次被调用的时候创建。

常量

1
2
3

显式类型定义 const b string = "abc"
隐式类型定义 const b = "abc"

枚举 从0开始 每遇到一次 const 关键字,iota 就重置为 0

1
2
3
4
5
const (
    a = iota
    b = iota
    c = iota
)

数组

数组长度也是数组类型的一部分,所以[5]int[10]int是属于不同类型的。

1
2
3
4
5
6
a := [2]int{1, 2}
b := [...]int{1, 2}
c := [2]int{1, 3}
fmt.Println(a == b, a == c, b == c) // "true false false"
d := [3]int{1, 2}
fmt.Println(a == d) // compile error: cannot compare [2]int == [3]int
1
2
3
4
5
6
7
8
9
10
11
12
13
type Currency int

const (
    USD Currency = iota // 美元
    EUR                 // 欧元
    GBP                 // 英镑
    RMB                 // 人民币
)

//表示数组的长度是根据初始化值的个数来计算  通过索引赋值
symbol := [...]string{USD: "$", EUR: "€", GBP: "£", RMB: "¥"}

fmt.Println(RMB, symbol[RMB]) // "3 ¥"

把一个大数组传递给函数会消耗很多内存。有两种方法可以避免这种现象:

  • 传递数组的指针
  • 使用数组的切片

切片

一直觉得这是定义数组另一种方式‍

切片是引用,本身就是一个指针。所以它们不需要使用额外的内存并且比使用数组更有效率,所以在 Go 代码中切片比数组更常用

多个切片如果表示同一个数组的片段,它们可以共享数据;因此一个切片和相关数组的其他切片是共享存储的

一个slice由三个部分构成:指针、长度和容量。

1
2
3
4
5
// 类似
type IntSlice struct {
    ptr      *int
    len, cap int
}
1
2
3
4
5
var x = []int{2, 3, 5, 7, 11}
var runes []rune
x := []int{}
var slice1 []type = make([]type, len,[cap])

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func main(){
    s := []int{5}
    s = append(s, 7)
    s = append(s, 9)
    x := append(s, 11)
    y := append(s, 12)
   fmt.Println(s, x, y)
}

[5 7 9] [5 7 9 12] [5 7 9 12]


func main(){
	s := []int{5, 7, 9} // len 3 cap 3
	x := append(s, 11) // cap 扩容返回新指针
	y := append(s, 12)
	fmt.Println(s,x,y)
}

[5 7 9] [5 7 9 11] [5 7 9 12]

Go 中切片扩容的策略是这样的:

  • 首先判断,如果新申请容量(cap)大于2倍的旧容量(old.cap),最终容量(newcap)就是新申请的容量(cap)
  • 否则判断,如果旧切片的长度小于1024,则最终容量(newcap)就是旧容量(old.cap)的两倍,即(newcap=doublecap)
  • 否则判断,如果旧切片长度大于等于1024,则最终容量(newcap)从旧容量(old.cap)开始循环增加原来的 1/4,即(newcap=old.cap,for {newcap += newcap/4})直到最终容量(newcap)大于等于新申请的容量(cap),即(newcap >= cap)
  • 如果最终容量(cap)计算值溢出,则最终容量(cap)就是新申请容量(cap)
  • 新切片指向的数组是一个全新的数组。并且 cap 容量也发生了变化

append 操作

  • slice底层数组是否有足够的容量来保存新添加的元素。如果有足够空间的话,直接扩展slice
  • 没有足够的增长空间的话,会先分配一个足够大的slice用于保存新的结果,先将输入的x复制到新的空间,然后添加y元素

和数组不同的是,slice之间不能比较,因此我们不能使用==操作符来判断两个slice是否含有全部相等元素

slice唯一合法的比较操作是和nil比较

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
	slice := []int{10, 20, 30, 40}
	for index, value := range slice {
		fmt.Printf("value = %d , value-addr = %x , slice-addr = %x\n", value, &value, &slice[index])
	}
}


value = 10 , value-addr = c4200aedf8 , slice-addr = c4200b0320
value = 20 , value-addr = c4200aedf8 , slice-addr = c4200b0328
value = 30 , value-addr = c4200aedf8 , slice-addr = c4200b0330
value = 40 , value-addr = c4200aedf8 , slice-addr = c4200b0338

如果用 range 的方式去遍历一个切片,拿到的 Value 其实是切片里面的值拷贝。所以每次打印 Value 的地址都不变。通过 &slice[index] 获取真实的地址

map

map 是引用类型 未初始化的 map 的值是 nil

1
2
3
var map1 map[string]int
map3 := map[string]string{}
var map1 = make(map[keytype]valuetype)

key可以用 == 或者 != 操作符比较的类型

数组、切片和结构体不能作为 key (含有数组切片的结构体不能作为 key,只包含内建类型的 struct 是可以作为 key 的)

value 可以是任意类型的;通过使用空接口类型,我们可以存储任意值

通过 key 在 map 中寻找值是很快的,比线性查找快得多,但是仍然比从数组和切片的索引中直接读取要慢 100 倍;所以如果你很在乎性能的话还是建议用切片来解决问题。

Go 语言中,通过哈希查找表实现 map,用链表法解决哈希冲突。

通过 key 的哈希值将 key 散落到不同的桶中,每个桶中有 8 个 cell。哈希值的低位决定桶序号,高位标识同一个桶中的不同 key。

当向桶中添加了很多 key,造成元素过多,或者溢出桶太多,就会触发扩容。扩容分为等量扩容和 2 倍容量扩容。扩容后,原来一个 bucket 中的 key 一分为二,会被重新分配到两个桶中。

扩容过程是渐进的,主要是防止一次扩容需要搬迁的 key 数量过多,引发性能问题。触发扩容的时机是增加了新元素,bucket 搬迁的时机则发生在赋值、删除期间,每次最多搬迁两个 bucket。

查找、赋值、删除的一个很核心的内容是如何定位到 key 所在的位置,需要重点理解。一旦理解,关于 map 的源码就可以看懂了。

深度解密Go语言之map

struct

1
2
3
4
5
type identifier struct {
    field1 type1
    field2 type2
    ...
}

new 函数给一个新的结构体变量分配内存,它返回指向已分配内存的指针:var t *T = new(T) 表达式 new(Type) 和 &Type{} 是等价的。

匿名成员

匿名成员的数据类型必须是命名的类型或指向一个命名的类型的指针

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
type Base struct {
	basename string
}
type Derive struct { // 内嵌 匿名成员
	Base
	adf int
}
type Derive1 struct { // 内嵌, 这种内嵌与上面内嵌有差异
	*Base
	adf int
}


type Derive2 struct { // 聚合
	base Base
	adf  int
}

func main() {
    // 匿名嵌入的特性,我们可以直接访问叶子属性而不需要给出完整的路径
	a := Derive{}
	getType(a.basename)
	b := Derive1{}
	getType(b.basename)
	// 必须显式访问这些叶子成员
	c := Derive2{}
	getType(c.base.basename)

}

但是构造时还是要写清楚

1
2
3
4
5
6
7
8
9
10

w = Wheel{Circle{Point{8, 8}, 5}, 20}

w = Wheel{
    Circle: Circle{
        Point:  Point{X: 8, Y: 8},
        Radius: 5,
    },
    Spokes: 20,
}

实际上,外层的结构体不仅仅是获得了匿名成员类型的所有成员,而且也获得了该类型导出的全部的方法

错误处理

自定义错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var ErrNil = errors.New("redigo: nil returned")
func Sqrt(f float64) (z float64, err error) {
	if f < 0 {
		return 0, ErrNil
	}
	return 0,nil
}

func main() {
	_,err := Sqrt(-1)
	if err != nil{
		fmt.Print(err.Error())
	}
}

错误处理

  • 失败的原因只有一个/没有失败时,不使用error
  • error应放在返回值类型列表的最后
  • 错误值统一定义,方便上层函数要对特定错误value进行统一处理
  • 当上层函数不关心错误时,建议不返回error
  • 当尝试几次可以避免失败时,不要立即返回错误
  • 在程序部署后,应恢复异常避免程序终止 recover

panic 处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func main() {
	n := foo()
	fmt.Println("main received", n)
}

func foo() int {
	defer func() {
		if err := recover(); err != nil {
			fmt.Println(err)
			debug.PrintStack()
		}
	}()
	m := 1
	panic("foo: fail")
	m = 2
	return m
}
  • A call to recover stops the unwinding and returns the argument passed to panic.
  • If the goroutine is not panicking, recover returns nil.

The only code that runs while unwinding is inside deferred functions, recover is only useful inside such functions. 只能在 defer 中发挥作用的函数,在其他作用域中调用不会发挥作用。

  • panic 只会触发当前 Goroutinedefer
  • recover 只有在 defer 中调用才会生效, recover 只有在发生 panic 之后调用才会生效

defer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func dr() int {
	var i int
	defer func() {
		i++
		fmt.Println("a defer2:", i) // 打印结果为 a defer2: 2
	}()
	defer func() {
		i++
		fmt.Println("a defer1:", i) // 打印结果为 a defer1: 1
	}()
	return i
}

func main(){
		fmt.Println(df())
}


a defer1: 1
a defer2: 2
0

defer、return、返回值三者的执行顺序应该是:return最先给返回值赋值;接着defer开始执行一些收尾工作;最后RET指令携带返回值退出函数。

控制结构

if

&&、   或 !
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
if condition {
	return x
}
return y


if condition {
	// do something	
} else {
	// do something	
}


if condition1 {
	// do something	
} else if condition2 {
	// do something else	
} else {
	// catch-all or default
}


if value, ok := readData(); ok {

}

if err := file.Chmod(0664); err != nil {
	fmt.Println(err)
	return err
}

switch

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
func main() {
	var num1 int = 100

	switch num1 {
	case 98, 99:
		fmt.Println("It's equal to 98")
	case 100: 
		fmt.Println("It's equal to 100")
	default:
		fmt.Println("It's not equal to 98 or 100")
	}
}

func main() {
	var num1 int = 7

	switch {
	    case num1 < 0:
		    fmt.Println("Number is negative")
	    case num1 > 0 && num1 < 10:
		    fmt.Println("Number is between 0 and 10")
	    default:
		    fmt.Println("Number is 10 or greater")
	}
}

for

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
for i:=0; i<5; i++ {
	for j:=0; j<10; j++ {
		println(j)
	}
}

func main() {
	var i int = 5

	for i >= 0 {
		i = i - 1
		fmt.Printf("The variable i is now: %d\n", i)
	}
}


函数

new make 区别

new 和 make 均是用于分配内存:new 用于值类型和用户定义的类型,如自定义结构,

  • make 用于内置引用类型(切片、map 和通道)。它们的用法就像是函数,但是将类型作为参数:new(type)、make(type)。
  • new(T) 分配类型 T 的零值并返回其地址,也就是指向类型 T 的指针。它也可以被用于基本类型:v := new(int)。

make(T) 返回类型 T 的初始化之后的值,因此它比 new 进行更多的工作。 new() 是一个函数,不要忘记它的括号。二者都是内存的分配(堆上), 但是make只用于slice、map以及channel的初始化(非零值);而new用于类型的内存分配,并且内存置为零。

函数作为参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main
import (
    "fmt"
)
func main() {
    callback(1, Add)
}
func Add(a, b int) {
    fmt.Printf("The sum of %d and %d is: %d\n", a, b, a+b)
}
func callback(y int, f func(int, int)) {
    f(y, 2) // 实际上是 Add(1, 2)
}

闭包

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
package main
import (
    "fmt"
)
func addNumber(x int) func(int) {
    fmt.Printf("x: %d, addr of x:%p\n", x, &x)
    return func(y int) {
        k := x + y
        x = k
        y = k
        fmt.Printf("x: %d, addr of x:%p\n", x, &x)
        fmt.Printf("y: %d, addr of y:%p\n", y, &y)
    }
}
func main() {
    addNum := addNumber(5)
    addNum(1)
    addNum(1)
    addNum(1)
    fmt.Println("---------------------")
    addNum1 := addNumber(5)
    addNum1(1)
    addNum1(1)
    addNum1(1)
}

可变参数

参数列表的最后一个参数类型之前加上省略符号...,这表示该函数会接收任意数量的该类型参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func sum(vals...int) int {
    total := 0
    for _, val := range vals {
        total += val
    }
    return total
}

fmt.Println(sum())           // "0"
fmt.Println(sum(3))          // "3"
fmt.Println(sum(1, 2, 3, 4)) // "10"

values := []int{1, 2, 3, 4}
fmt.Println(sum(values...)) // "10"

测试

测试程序必须属于被测试的包 文件名 *_test.go 不会被普通的 Go 编译器编译, 所以当放应用部署到生产环境时它们不会被部署;只有 Gotest 会编译所有的程序:普通程序和测试程序

必须导入 testing 包,并写一些名字以 TestZzz 打头的全局函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import (
	"fmt"
	"testing"
)

func n() { fmt.Println(a) }

func m() {
	a := "O"
	fmt.Println(a)
}
func TestA(t *testing.T) {
	n()
	m()
	n()
}
func TestB(t *testing.T) {
	n()
	m()
	n()
}