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

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

structure cannot be used outside of a function

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由三个部分构成:指针、长度和容量。A slice consists of three parts: a pointer, a length, and a capacity.

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)

    First, check if the new capacity (cap) is greater than twice the old capacity (old.cap). If it is, the final capacity (newcap) will be equal to the new capacity (cap).

  • 否则判断,如果旧切片的长度小于1024,则最终容量(newcap)就是旧容量(old.cap)的两倍,即(newcap=doublecap)

    check if the length of the old slice is less than 1024. If it is, the final capacity (newcap) will be twice the old capacity (old.cap), i.e. (newcap = doublecap).

  • 否则判断,如果旧切片长度大于等于1024,则最终容量(newcap)从旧容量(old.cap)开始循环增加原来的 1/4,即(newcap=old.cap,for {newcap += newcap/4})直到最终容量(newcap)大于等于新申请的容量(cap),即(newcap >= cap)

    the length of the old slice is greater than or equal to 1024, the final capacity (newcap) will be increased by 1/4 of the old capacity (old.cap) in a loop starting from the old capacity (newcap = old.cap, for {newcap += newcap/4}) until the final capacity (newcap) is greater than or equal to the new capacity (cap), i.e. (newcap >= cap).

  • 如果最终容量(cap)计算值溢出,则最终容量(cap)就是新申请容量(cap)

    If the calculated value of the final capacity (cap) overflows, then the final capacity (cap) will be equal to the new capacity (cap) that was initially requested.

  • 新切片指向的数组是一个全新的数组。并且 cap 容量也发生了变化

    The new slice points to a completely new array. Additionally, the capacity (cap) has also changed.

append 操作

  • slice底层数组是否有足够的容量来保存新添加的元素。如果有足够空间的话,直接扩展slice

    Check if the underlying array of the slice has enough capacity to add the new elements. If there is enough space, the slice is directly expanded.

  • 没有足够的增长空间的话,会先分配一个足够大的slice用于保存新的结果,先将输入的x复制到新的空间,然后添加y元素

    If there is not enough space,a new slice with enough capacity is first allocated to store the new result. The input elements (x) are then copied to the new space, followed by the addition of the element (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

  • new 函数用于创建指定类型的零值对象,并返回指向该对象的指针。
  • new 函数只接受一个参数,即要创建的对象的类型,返回一个指向新分配的零值对象的指针。
  • 由于 new 函数返回的是指针,通常用于创建值类型(如结构体、基本类型)的对象。
  • new 函数分配的内存会被初始化为零值(即类型的默认值)。

make

  • make 用于内置引用类型(切片、map 和通道)
  • make 函数接受两个或三个参数,具体取决于要创建的对象类型. make([]T, length, capacity) make(map[K]V, capacity) make(chan T, capacity)
  • 函数返回的是一个已经初始化并准备好使用的对象,而不是指针。

总结:

  • new 用于值类型对象的创建,返回的是指向新分配对象的指针,并将对象初始化为零值。
  • make 用于引用类型对象的创建,返回的是已初始化并准备好使用的对象,不返回指针。
  • new 适用于创建结构体、基本类型等值类型对象。
  • make 适用于创建切片、映射、通道等引用类型对象,并可以指定初始长度或容量。

函数作为参数

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()
}