Go Notes

Go 笔记

2024.12.5

特点

  • 简洁
  • 并发

程序开头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main // 定义包名
import ( // 导入库
"fmt"
"math/cmplx"
)
func main(){
var num1 int;
fmt.Print("hello go")
fmt.Printf("print number: %d", num1)
// %d 数字,%c 字符,%f 浮点数,%s字符串
}
// go.mod 定义go的版本还有一些库
// go文件:main.go
// 编译运行:go run main.go

  • 编译为可执行文件
    1
    2
    3
    4
    5
    6
    7
    8
    // windows platform, app.exe is executable files
    set GOOS=windows
    set GOARCH=amd64
    go build -o bin/app.exe main.go
    // Linux platform
    set GOOS=linux
    set GOARCh=amd64
    go build -o bin/app main.go

数据类型

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// variable: var name data_type
var object1 int

// byte, rune,字符类型
type byte = uint8 // 底层是unsigned int 8,代表ASCII的一个字符
type rune = int32 // 底层是int32,代表一个unicode字符
var ch byte = 'A'
var ch byte = 65 // 相当于A
var ch byte = '\x41' // 65的十六进制表达

var letter = rune('好') // 把字符'好'转换为rune类型
// unicode.IsLetter()是否字符,unicode.IsDigit()是否数字, unicode.IsSpace()是否空白符号

// string字符串
// string初始化后不可变,不能修改部分。只能重新赋值
var str string = "hello go"

// int, int8, int16,..., int64; signed
// uint, uint8,..., uint64, uintptr; unsigned
// uintptr, go和C交互的时候可以用,存放指针
var num1 int64

// float32, float64(常用)
// bool, true, false,不参与计算和类型转换

// 只可以显式类型转换
// []byte <-> []rune
s1 := "localhost:8080"
strbyte := []byte(s1)
strbyte[len(s1) - 1] = '1'
s2 := string(strbyte) // 此时s2为localhost:8081

// 常量,只能是bool,整型,浮点型,byte,rune
const PI float64 = 3.1415926

// complex
g := 0.8+1.5i

// iota,批量定义,常量生成器,从定义开始,值从0加一
type month int
const (
jan = 1 month = 1 + iota
feb = 2
march = 3
)

// 定义const Pi全局变量,首字母大写可以对所有package生效,可以通过main.Pi进行访问

// 指针,空指针为nil
a := 10
var p *int = &a // 可以通过*p访问
var ptr *int = new(int) // 或者通过new申请空间

// go简洁类型的写法,可以自动推导类型,只能在函数内部使用
a := 5
b := 4.3
var(
val1 = 100
val2 = 1
)

函数

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
func function_name ([param list]) [return_type] {}
func swap(x, y int) (int, int) {return y, x}
var swap func(x, y int) (int, int) // 类似一个函数指针
swap = func(x, y int) (int, int){
return y, x
}
// 下例说明函数可以作为变量,参数传递,返回值,返回值可以取名字
func call(swap func(x, y int) (a int, b int)) func(x int){
a = 10 // 返回值同时也可以编辑
y, _ := swap(10, 20)
return func(x int, y int){
return y, x
}
}
// 可变长函数参数
func max(arr ...int) (int)
// go中参数传递是值传递,实际上为copy。若传递指针地址则类似为引用传递
// 和引用传递不完全一样,还是会申请新空间存放指针
// 闭包和匿名函数
generator := gen("msz-006")
name, hp := generator()
func gen(name string) func() (string, int){
return func() (string, int){
return name, 150
}
}
// 闭包会保存当前的状态,并在每次调用时保留这些状态,直到下一次调用(核心特性)
// 里面的参数保存在堆上,不在栈上。闭包也会保存外部函数的局部变量
func counter() func() int {
count := 0 // 外部函数的局部变量,该闭包捕获并保存了外部变量 count
return func() int { // 这个return值就是一个闭包
count++
return count
}
}
c := counter()
fmt.Println(c()) // 输出 1
fmt.Println(c()) // 输出 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
26
27
28
29
30
31
// for循环
sum := 0
for i:= 0; i < 10; i++ {
sum += i
}

var s = []int{1, 2, 3}
for index, element := range(s){ // 也可以 for index, _ = range(s) 或者 for _, element = range(s)
fmt.Printf("(%d, %d)", index, element)
}
// while循环
sum := 1
for sum < 100 {
sum += sum
}
// if else
if x < 0 {}
if i := 5; i < 10 {
return i // 可以在判断前执行一个简短语句,作用域只在if中
}
else{
return i // if前定义的i同时也可以在else中使用
}
// switch, 其中go为每个case后会默认添加break语句,也有default
switch os := runtime.GOOS; os{
case "mac":
case "linux":
default:
}
// defer 语句会将函数推迟到外层函数返回之后执行。推迟调用的函数其参数会立即求值,但直到外层函数返回前该函数都不会被调用
// 被defer推迟的函数会放入一个栈中,先进后出

结构体,切片,映射

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
43
44
45
46
47
48
49
50
51
52
53
54
55
type vertex struct {
x int
y int
}
v := vertex(1, 2) // 可通过v.x, v.y访问
p := &v
p.x = 3 // 允许通过隐式访问,实际上是(*p).x = 3
ptr = &vertex(3, 4) // 创建*vertex指针

// 数组
var arr [10]int // 数组大小是固定的,但是可以使用切片,类似数组的引用,不存储任何数据
// 修改切片的数据会影响到底层数组,同时所有相关的切片也会被修改,因为是相当于对底层数据的引用
arr1 := [6]int{1, 2, 3, 4, 5, 6}
var s []int = arr1[1:4] // 一个数组切片,包含第一个元素不包含最后一个,s = {2, 3, 4},可以类似python使用arr1[:4], arr1[1:], arr1[:]
q := []int{2, 3, 4} // 无长度,切片字面量
// 切片长度:len(), 切片容量:cap()

s := []int{2, 3, 5, 7, 11, 13}
s = s[:0] // s = {}
s = s[:4] // s = {2, 3, 5, 7}
s = s[2:] // s = {5, 7}

// 切片零值为nil,长度容量为0,没有底层数组
// 使用make创造切片
s := make([]int, 5) // len(s) = 5
s := make([]int, 0, 5) // len = 0, cap = 5
board1 := [][]string {
[]string{".", "."},
[]string{"-", "-"},
}

var s []int
s = append(s, 0) // 后插,s = {0}
s = append(s, 2, 3, 4) // 插入多个元素, s = {0, 2, 3, 4}

// 动态创建二维数组
row, col := 3, 4
arr := make([][]int, row)
for i := range(arr) {
arr[i] = make([]int, col)
}

// map映射
var m map[string]int // 键是string,值是int
m = make(map[string]int)
m["bella"] = 6

var m = map[string]int{ // 或者
"bella": 1,
"john": 2
}
m["sico"] = 3 // 修改或插入元素
elem = m["sico"] // 获取元素
delete(m, "sico") // 删除元素
elem, ok := m["sico"] // 检测是否存在这个键,不存在ok为false

方法和接口

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
// 方法就是一类带特殊的'接收者'参数的函数,方法只是个带接收者参数的函数
// Abs方法拥有一个名字为v,类型为Vertex的接收者。Abs是方法,v是接收者
// 接收者的类型定义和方法声明必须在同一包内
type vertex struct {
x, y float64
}
func(v vertex) Abs() float64 { // 传入的v是拷贝传递
return math.Sqrt(v.x * v.x + v.y * v.y)
}
v := vertex{3, 4}
fmt.Println(v.Abs())

// 带值接收者的方法,同时使用指针,保证是引用传递
func(v *vertex) Scale(f float64){
v.x = v.x * f // 由于使用的是指针,所以原始的v的x,y值也会被修改
v.y = v.y * f
}
v.Scale(10) // 传入参数f
// 接收者为指针的的方法被调用时,接收者既能是值又能是指针
// 反之也一样,值为接收者的方法被调用时,接收者既能为值又能为指针
var v vertex
v.Scale(5)
p := &v
p.Scale(5) // 对于上面定义的Scale函数,这四条语句都可以通过编译,接收者的类型多变
// 尽量悬着指针接收者,可以避免拷贝复制的开销,同时还能修改其值

// 接口类型的定义为一组方法签名
type Abs_interface interface {
Abs() float64
}
func (v *vertex2d) Abs() float64 {}
func (v *vertex3d) Abs() float64 {}
var a Abs_interface
v1 := vertex2d(3, 4)
v2 := vertex3d(3, 4, 5)
a = &v1 // a vertex2d实现了接口Abs_interface
a = &v2 // a vertex3d实现了接口Abs_interface

// 接口,隐式实现
type AbsI interface {Abs()}
func(v vertex) Abs() {} // 此方法表示类型vertex实现了接口AbsI
var obj1 AbsI = vertex{3, 4} // obj1实现接口AbsI,值是vertex{3, 4}

// 接口值可以用作函数的参数或返回值,接口值是包含值和具体类型的元组(value, type)
// 对于接口值为nil的,仍然可以进行调用,因为接口不为nil只是值为nil,可以写一些if来处理
var i I // I是一个接口
var v vertex
i = v // 接口值为nil,但是接口不为nil,可以正常调用i.Abs()
// 接口为nil,不知道类型,调用报错
var i I
i.Abs() // 报错,未指明该调用哪个具体方法的类型

// 空接口,可以保存任何类型值,可以用来处理未知类型的值
var i interface{} // 打印输出为(<nil>, <nil>)
i = 42 // 打印输出为(42, int)
i = "hello" // // 打印输出为(hello, string)

// 类型断言 提供了访问接口值底层具体值的方式
var i interface{} = "hello"
f, ok := i.(float64) // 输出f和ok为0 false, 因为i不是float64类型
s, ok := i.(string) // 输出为hello true
f = i.(float64) // 报错

// 类型选择是一种按顺序从几个类型断言中选择分支的结构
func do(i interface{}) {
switch v := i.(type) { // type是一个关键字
case int:
fmt.Printf("type is int")
case string:
fmt.Printf("type is string")
default:
fmt.Printf("I dont know the tyep")
}
}
do(21) // type is int
do("hello") // type is string

// fmt包中定义的Stringer是最普遍的接口之一
type Stringer interface{
String() string
}
func (p person) String() string {
return fmt.Sprintf(p.name) // fmt.Sprintf()是fmt包中的一个函数,用于格式化字符串并返回结果,将格式化后的字符串返回给调用者,而不输出
}

错误

1
2
3
4
5
// go使用error值表示错误状态,error也是一个内建接口
type error interface {
Error() string
}
i, err := strconv.Atoi("42") // 如果err不是nil代表有错误信息,如果err为nil表示成功

Readers

1
2
3
4
// io包制定了io.Reader接口,表示数据流的读取端,需要import{"io"}
// io.Reader接口的Read方法
func (T) Read(b []byte) (n int, err error)
// Read用数据填充给定的字节切片并返回填充的字节数和错误值。在遇到数据流的结尾时,它会返回一个io.EOF错误

泛型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 类型参数,使用comparable
// comparable是一个有用的约束,它能让我们对任意满足该类型的值使用 == 和 != 运算符
func Index[T comparable] (arr []T, target T) int { // 函数返回target在arr中的下标
for i, val := range arr {
// val和target的类型为T,它拥有comparable可比较的约束,
if val == target{
return i
}
}
return -1
}
nums := []int{10, 20, 15, -10}
fmt.Println(Index(nums, 15)) // 可以使用在整数切片上,也可以使用在字符串切片上

// 泛型类型,下面定义了一个任意类型的单链表
type List[T any] struct {
next *List[T]
val T
}

Go协程

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
// Go程(goroutine)是由Go运行时管理的轻量级线程,提供并行运行的能力
// Go程在相同的地址空间中运行,因此在访问共享的内存时必须进行同步
go f(x, y, z) // 启动一个新的Go协程并执行

func display(n int){
for i := 0; i < n; i++ {
fmt.Printf("%d ", i)
time.Sleep(100 * time.Millisecond)
}
}
go display(5)
display(5)

// 信道,带有类型的管道,可以使用信道操作符 <- 来发送或者接收值
ch := make(chan int) // 信道在使用前需要创建
ch <- v // 将v发送至信道ch
v := <- ch // 从ch接收值,赋予v
// 默认情况下,发送和接收操作在另一端准备好之前都会阻塞。这使得Go程可以在没有显式的锁或竞态变量的情况下进行同步
func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- sum // 发送sum到信道c
}
s := []int{7, 2, 8, -9, 4, 0}
go sum(s[:len(s)/2], c) // 求-9,4,0的和,结果-5
go sum(s[len(s)/2:], c) // 求7,2,8的和,结果17
x, y := <-c, <-c // 从信道c接收
fmt.Println(x, y, x+y) // 最后结果是-5,17,12

// 带有buffer缓冲的信道
ch := make(chan int, 1000) // 容量1000,只有信道的缓冲区满后,向其发送数据才会阻塞。当缓冲区为空,接收方会阻塞

// 可以通过close(ch)关闭一个信道,表示没有需要发送的值,接收者可以进行测试
// 只应由发送者关闭信道,而不应由接收者关闭。向一个已经关闭的信道发送数据会引发程序panic
v, isOpen := <- ch // 若没有值可以接收且信道已被关闭,isOpen为false

// select语句让一个Go程可以等待多个通信操作
// select会阻塞到某个分支可以继续执行为止,这时就会执行该分支。当多个分支都准备好时会随机选择一个执行
func fibonacci(c, quit chan int) {
x, y := 0, 1
for {
select {
case c <- x:
x, y = y, x+y
case <-quit:
fmt.Println("quit")
return
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
go func() {
for i := 0; i < 10; i++ {
fmt.Println(<-c)
}
quit <- 0
}()
fibonacci(c, quit)
}
// fibonacci函数和go程是同时执行的,通过Go的并发特性协同工作
// 所以每次向c写入一个数字的时候,就会触发fibonacci函数里面的select语句,执行那条语句,表示这个分支准备好了
// 直到10次以后循环结束,向quit写入一个0,select发现该分支准备好了,就执行,然后return返回

// 当select中的其它分支都没有准备好时,default分支就会执行
for {
select {
case <-tick:
fmt.Println("tick.")
case <-boom:
fmt.Println("BOOM!")
return
default:
fmt.Println(" .")
time.Sleep(50 * time.Millisecond)
}
}

// Go的互斥锁,import{"sync"},Go提供了sync.Mutex互斥锁类型和两个方法:Lock,Unlock
// 在代码前调用Lock,在代码后调用Unlock保证一段代码互斥执行
type SafeCounter struct { // SafeCounter 是并发安全的
mu sync.Mutex
v map[string]int
}
func (c *SafeCounter) Inc(key string) { // Inc 对给定键的计数加一
c.mu.Lock() // 锁定使得一次只有一个 Go 协程可以访问映射 c.v
c.v[key]++
c.mu.Unlock()
}
func (c *SafeCounter) Value(key string) int { // Value 返回给定键的计数的当前值
c.mu.Lock() // 锁定使得一次只有一个 Go 协程可以访问映射 c.v
defer c.mu.Unlock()
return c.v[key]
}
func main() {
c := SafeCounter{v: make(map[string]int)}
for i := 0; i < 1000; i++ {
go c.Inc("somekey")
}
time.Sleep(time.Second)
fmt.Println(c.Value("somekey"))
}

知识点

  1. 内存管理
    • 动态分配的全局栈
    • goroutine的本地堆栈
    • bss和data两部分存放全局变量和静态变量
    • text存放代码
    • stack存放局部变量和函数参数(用于静态内存分配),可以理解为函数运行在一个线程中,每个线程有一个栈区不共享,栈区大小在编译期间就是已知的
    • heap用于动态内存分配,比如指针,数组。所有线程共享堆
  2. goroutine(go协程),初始为2kb,动态增减,这个是语言特色