go

go_demo

go_demo

Posted by DYC on December 22, 2024

go_demo

基础语法

values

Go 有各种值类型,包括字符串、整数、浮点数、布尔值等。这里是一些基本示例

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import "fmt"

func main() {
	fmt.Println("go" + "lang")
	fmt.Println("1+1 =", 1+1)
	fmt.Println("7.0/3.0 =", 7.0/3.0)
	fmt.Println(true && false)
	fmt.Println(true || false)
	fmt.Println(!true)
}

变量

在 Go 中,变量由编译器明确声明和使用,例如检查函数调用的类型正确性

var可以一次声明一个或多个变量,Go将判断初始化变量的类型,未进行初始化的声明变量为零值

:=可在函数内部使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

func main() {
    var a = "initial"
    fmt.Println(a)

    var b, c int = 1, 2
    fmt.Println(b, c)

    var d = true
    fmt.Println(d)

    var e int
    fmt.Println(e)

    f := "apple"
    fmt.Println(f)
}
常量

Go支持字符、字符串、布尔值、数字值的常量

const声明一个常量值,用const取代var即可数字常量没有类型,除非通过显式转换等方式指定类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
    "fmt"
    "math"
)

const s string = "constant"

func main() {
    fmt.Println(s)

    const n = 500000000

    const d = 3e20 / n
    fmt.Println(d)

    fmt.Println(int64(d))

    fmt.Println(math.Sin(n))
}
for

for是 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
package main

import "fmt"

func main() {
    i := 1
    for i <= 3 {
       fmt.Println(i)
       i++
    }
    for j := 0; j < 5; j++ {
       fmt.Println(j)
    }
    nums := []int{1, 2, 3}
    for i := range nums {
       fmt.Println("range", i)
    }

    for {
       fmt.Println("loop")
       break
    }
    for i := 1; i <= 6; i++ {
       if i%2 == 1 {
          continue
       }
       fmt.Println(i)
    }
}
If-else

可以有if一个不带 else 的语句,语句可以位于条件之前,此语句声明任何变量可以在该分支及后续分支使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import "fmt"

func main() {
	if 7%2 == 0 {
		fmt.Println("7 is even")
	}

	if 8%2 == 0 || 7%2 == 0 {
		fmt.Println("either 8 or 7 are even")
	}
	
	if num := 9; num < 0 {
		fmt.Println(num, "is negative")
	} else if num > 0 {
		fmt.Println(num, "is positive")
	} else {
		fmt.Println(num, "is zero")
	}

}

switch

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
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
package main

import (
	"fmt"
	"time"
)

func main() {
	i := 2
	fmt.Print("write", i, "as")
	switch i {
	case 1:
		fmt.Println("one")
	case 2:
		fmt.Println("two")
	case 3:
		fmt.Println("three")
	}

	switch time.Now().Weekday() {
  //可以使用逗号分隔同一`case`语句中的多个表达式
	case time.Saturday, time.Sunday:
		fmt.Println("it's the weekend")
	default:
		fmt.Println("it's a weekday")
	}

	t := time.Now()
  //`switch`不使用表达式是表达 if/else 逻辑的另一种方式,
	switch {
	case t.Hour() < 12:
		fmt.Println("it's before noon")
	default:
		fmt.Println("it's after noon")
	}

	whatAmI := func(i interface{}) {
    //类型`switch`比较的是类型而不是值。可以使用它来发现接口值的类型
		switch t := i.(type) {
		case bool:
			fmt.Println("I'm a bool")
		case int:
			fmt.Println("I'm an int")
		case string:
			fmt.Println("I'm a string")
		default:
			fmt.Printf("Don't know type %T\n", t)
		}
	}
	whatAmI(true)
	whatAmI("hey")
	whatAmI(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
package main

import "fmt"

func main() {
    var a [5]int
    fmt.Println(a)

    a[4] = 100
    fmt.Println(a[4])
    fmt.Println(len(a))

    b := [5]int{1, 2, 3, 4, 5}
    fmt.Println(b)
		//可以让编译器计算元素的数量...
    b = [...]int{1, 2, 3, 4, 5}
    fmt.Println(b)
    //[100 0 0 400 500]
  	//3: 400表示将3号位置设置为400,由于1号和2号位置没有设置,则默认为0
    b = [...]int{100, 3: 400, 500}
    fmt.Println(b)

    var twoD [2][3]int
    for i := 0; i < len(twoD); i++ {
       for j := 0; j < len(twoD[i]); j++ {
          twoD[i][j] = i + j
       }
    }
    fmt.Println(twoD)
    twoD = [2][3]int{
       {1, 2, 3},
       {2, 3, 4},
    }
    fmt.Println(twoD)

}
切片

切片实际上是一个结构体,结构体内部包含指向底层数组的指针、切片的长度、切片的容量

所以切片实际上是对底层数组的一个封装,自己并不存储数据

所以当使用append方法时,如果len>cap时,就会创建一个新的数组(一般两倍容量)进行迁移,但这个过程并不是并发安全的,所以如果多个goroutine在使用该切片的话,可以考虑加锁

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
package main

import (
	"fmt"
	"reflect"
	"slices"
)

func main() {
	var s []string
	//[] true true
	fmt.Println(s, s == nil, len(s) == 0)

	s = make([]string, 3)
	//[  ] false 3 3
	fmt.Println(s, s == nil, len(s), cap(s))

	s[0] = "a"
	s[1] = "b"
	s[2] = "c"
	fmt.Println(s)
	fmt.Println(s[2])
	fmt.Println(len(s))
	//需要接受返回值
  //append方法的返回值和原来的值是否是相同的地址,取决于切片的容量是否足够,如果不够就会创建新的数组进行迁移
	s = append(s, "d")
	s = append(s, "e", "f")
	//[a b c d e f]
	fmt.Println(s)

	c := make([]string, len(s))
  //这里我们创建一个与 长度相同的copy空切片并将其复制到中
	copy(c, s)
	fmt.Println(c)

	l := s[2:5]
	//[c d e]
	fmt.Println(l)
	//可以在一行中声明并初始化切片的变量
	t := []string{"a", "b", "c"}
	t2 := []string{"a", "b", "c"}
	//t==t2,这个用于判断两个切片中的内容是否相同
	if slices.Equal(t, t2) {
		fmt.Println("t==t2")
	}
	//如果想比较两个的地址的话,可以使用返回或者比较两个的指针,不能直接用t==t2进行比较
  //t和t2的地址不同
	if reflect.ValueOf(t).Pointer() == reflect.ValueOf(t2).Pointer() {
		fmt.Println("t and t2 share the same underlying array")
	} else {
		fmt.Println("t and t2 do not share the same underlying array")
	}
	if &t[0] == &t2[0] {
		fmt.Println("t and t2 share the same underlying array")
	} else {
		fmt.Println("t and t2 do not share the same underlying array")
	}
	//切片可以组成多维数据结构。与多维数组不同,内部切片的长度可以变化
	twoD := make([][]int, 3)
	for i := 0; i < len(twoD); i++ {
		inner := i + 1
		twoD[i] = make([]int, inner)
		for j := 0; j < inner; j++ {
			twoD[i][j] = i + j
		}
	}
	//[[0] [1 2] [2 3 4]]
	fmt.Println(twoD)
}

映射
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
package main

import (
	"fmt"
	"maps"
)

func main() {
	m := make(map[string]int)
	m["k1"] = 7
	m["k2"] = 13
	//map[k1:7 k2:13]
	fmt.Println(m)

	v1 := m["k2"]
	fmt.Println(v1)

	v3 := m["k3"]
	//0,如果键不存在, 则返回值类型的零值
	fmt.Println(v3)

	fmt.Println(len(m))
	//内置命令delete从映射中删除键/值对
	delete(m, "k2")
	//map[k1:7]
	fmt.Println(m)
	//要从map中删除所有clear键/值对
	clear(m)
	//map[]
	fmt.Println(m)
	//从映射中获取值时,可选的第二个返回值指示映射中是否存在该键。
  //这可用于区分缺失键和具有零值的键(如0或 )""。
  //这里我们不需要值本身,因此我们用空白标识符 _忽略它
	_, prs := m["k2"]
	//false
	fmt.Println(prs)

	n := map[string]int{"foo": 1, "bar": 2}
	fmt.Println(n)
	//map[k:v k:v]请注意,使用 打印时,地图会以表格形式出现fmt.Println
	n2 := map[string]int{"foo": 1, "bar": 2}
	if maps.Equal(n, n2) {
		//true
		fmt.Println("true")
	}
}

函数
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
package main

import "fmt"

func plus(a int, b int) int {
	//Go 需要显式返回,即它不会自动返回最后一个表达式的值
	return a + b
}

func plusPlus(a, b, c int) int {
	return a + b + c
}

func vals() (int, int) {
	//可以多返回值
	return 3, 7
}

func main() {

	res := plus(1, 2)
	fmt.Println(res)

	res = plusPlus(1, 2, 3)
	fmt.Println(res)

	a, b := vals()
	fmt.Println(a, b)
	_, c := vals()
	//7
	fmt.Println(c)

}

闭包

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
package main

import "fmt"

func intSeq() func() int {
	i := 0
	return func() int {
		i++
		return i
	}
}

func main() {
  //每次会创建一个新的状态
	nextInt := intSeq()
	//1
	fmt.Println(nextInt())
	//2
	fmt.Println(nextInt())
	//3
	fmt.Println(nextInt())
	newInt := intSeq()
	//1
	fmt.Println(newInt())
}

递归
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"

func fact(n int) int {
    if n == 0 {
       return 1
    }
    return n * fact(n-1)
}

func main() {
    fmt.Println(fact(5))
    var fib func(n int) int
    fib = func(n int) int {
       if n < 2 {
          return n
       }
       return fib(n-1) + fib(n-2)
    }
    fmt.Println(fib(10))
}
range函数
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
package main

import "fmt"

func main() {
    nums := []int{1, 2, 3}
    sum := 0
    for _, num := range nums {
       sum += num
    }
    fmt.Println(sum)
    for i, num := range nums {
       if num == 3 {
          //2
          fmt.Println(i)
       }
    }
    kvs := map[string]int{"a": 1, "b": 2}
    for k, v := range kvs {
       fmt.Println(k, v)
    }
    for k := range kvs {
       fmt.Println(k)
    }

    for i, c := range "go" {
       fmt.Println(i, c)
    }
}
指针

&i语法给出了 的内存地址i,即 的指针i

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import "fmt"

func zeroval(inval int) {
    inval = 0
}

func zeroptr(inptr *int) {
    *inptr = 0
}

func main() {
    i := 1
    fmt.Println(i)

    zeroval(i)
    fmt.Println(i)

    zeroptr(&i)
    fmt.Println(i)

}
strings

在 Go 中,rune 是一个类型,实际上是一个表示 Unicode 码点的整数(本质上是 int32 类型)。

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
package main

import (
    "fmt"
    "unicode/utf8"
)

func main() {
    const s = "Zbs"
    //3
    fmt.Println(len(s))
    for i := 0; i < len(s); i++ {
       //Z b s
       fmt.Printf("%c ", s[i])
    }
    fmt.Println()
    //3,用于计算字符串中 Unicode 字符(即 rune) 的数量
    fmt.Println(utf8.RuneCountInString(s))

    //如果没有%c,就会输出ascii的int值
    for idx, value := range s {
       //0 90
       //1 98
       //2 115
       fmt.Println(idx, value)
    }

    for i, w := 0, 0; i < len(s); i += w {
       //用于解码字符串中的下一个 Unicode 字符(rune),并返回该字符以及它的字节宽度
       runValue, width := utf8.DecodeRuneInString(s[i:])
       fmt.Println(width, runValue)
       w = width
       examineRunne(runValue)
    }

}
func examineRunne(r rune) {
    if r == 'b' {
       fmt.Println("b")
    } else if r == 'a' {
       fmt.Println("a")
    }
}
结构体

如果要将结构体作为函数的参数进行传递的时候,要考虑是否要修改原结构体的值,来确定结构体参数设置为指针类型还是值类型

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"

type person struct {
    name string
    age  int
}

func newPerson(name string, age int) *person {
    return &person{name, age}
}

func main() {
    fmt.Println(person{"Bob", 20})
    fmt.Println(person{name: "Bob", age: 20})
    fmt.Println(person{name: "Bob"})
    //&{Bob 20}
    fmt.Println(&person{name: "Bob", age: 20})
    //&{Bob 20}
  	//将新结构体的创建封装在构造函数中是一种惯用做法
    fmt.Println(newPerson("Bob", 20))

    s := person{name: "Bob", age: 20}
    fmt.Println(s.name)

    sp := &s
    //20
    fmt.Println(sp.age)
    sp.age = 30
    //30
    fmt.Println(s.age)

    dog := struct {
       name   string
       isGood bool
    }{
       "name",
       true,
    }
    fmt.Println(dog)
}
方法

通过结构体、方法、接口来实现面对对象的设计

通过结构体+方法实现类的组合

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
package main

import "fmt"

type rect struct {
    width, height int
}
//指针型接收者,这样方法内部的修改会影响到结构体的原字段
func (r *rect) area() int {
    return r.width * r.height
}
//值接收者,方法内部的修改不会影响到结构体的原字段
func (r rect) perim() int {
    return 2*r.width + 2*r.height
}

func main() {
    r := rect{width: 10, height: 5}

    fmt.Println("area: ", r.area())
    fmt.Println("perim:", r.perim())
		//Go 自动处理方法调用时值与指针之间的转换
    rp := &r
    fmt.Println("area: ", rp.area())
    fmt.Println("perim:", rp.perim())
  	//这里如果定义的是指针类型,那么r就只能传递的是地址
    var t *rect
    t = &r
    t.width = 0
    fmt.Println("area: ", t.area())
    fmt.Println("perim:", t.perim())
    fmt.Println("area: ", r.area())
    fmt.Println("perim:", r.perim())
}
接口

在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
package main

import (
    "fmt"
    "math"
)

type geometry interface {
    area() float64
    perim() float64
}

type rectt struct {
    width, height float64
}

type circle struct {
    radius float64
}
//在 Go 中,接口的实现依赖于方法接收者的类型,而方法的调用就需要按照接收者的类型来进行调用
func (r *rectt) area() float64 {
    return r.width * r.height
}
func (r *rectt) perim() float64 {
    return 2*r.width + 2*r.height
}

func (c circle) area() float64 {
    return math.Pi * c.radius * c.radius
}
func (c circle) perim() float64 {
    return 2 * math.Pi * c.radius
}
//如果变量具有接口类型,那么我们可以调用指定接口中的方法
//类似于java中的多态
func measure(g geometry) {
    fmt.Println(g)
}

func main() {
    r := rectt{width: 10, height: 5}
    c := circle{radius: 5}
  //circle和结构类型rect都实现了geometry接口,因此我们可以使用这些结构的实例作为的参数measure
  //因为实现接口的方法接收者是指针类型,所以这里就需要赋地址
    measure(&r)
    measure(c)
}
枚举

枚举是一种具有固定数量可能值的类型,每个值都有不同的名称。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
package main

import "fmt"

type ServerState int

const (
	//这里通过 iota 创建了四个常量,分别表示服务器的四种状态
	//iota 是 Go 中用于生成常量值的一个特殊关键字,它在每一行常量定义中会自动递增
	StateIdle ServerState = iota
	StateConnected
	StateError
	StateRetrying
)

var stateName = map[ServerState]string{
	StateIdle:      "idle",
	StateConnected: "connected",
	StateError:     "error",
	StateRetrying:  "retrying",
}

func (ss ServerState) String() string {
	return stateName[ss]
}

func main() {
	//这里虽然ServerState是int类型,但不能传int类型进去,否则编译器会报错,因为它们的类型不同。
	ns := transition(StateIdle)
	fmt.Println(ns)

	ns2 := transition(ns)
	fmt.Println(ns2)
}

// transition 模拟服务器的状态转换;它采用现有状态并返回新状态
func transition(s ServerState) ServerState {
	switch s {
	case StateIdle:
		return StateConnected
	case StateConnected, StateRetrying:

		return StateIdle
	case StateError:
		return StateError
	default:
		panic(fmt.Errorf("unknown state: %s", s))
	}
}

结构体嵌入
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
package main

import "fmt"

type base struct {
    num int
}

func (b base) describe() string {
    return fmt.Sprintf("base with num=%v", b.num)
}

type container struct {
  	//Acontainer 嵌入a base。嵌入看起来像一个没有名称的字段
    base
    str string
}

func main() {
		//当使用文字创建结构时,我们必须明确初始化嵌入;这里嵌入的类型用作字段名称
    co := container{
       base: base{
          num: 1,
       },
       str: "some name",
    }
		//这样就可以直接访问子结构体的子字段 例如co.num
    fmt.Printf("co={num: %v, str: %v}\n", co.num, co.str)

    fmt.Println("also num:", co.base.num)
		//由于container嵌入base, 的方法 base也成为 的方法。这里我们 直接container在 上调用嵌入的方法
    fmt.Println("describe:", co.describe())

    type describer interface {
       describe() string
    }
		//嵌入带有方法的结构可用于将接口实现赋予其他结构。这里我们看到 acontainer现在实现了 describer接口,因为它嵌入了base。
    var d describer = co
    fmt.Println("describer:", d.describe())
}
泛型
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
package main

import "fmt"

// SlicesIndex 使用了 类型约束,其中 S ~[]E 和 E comparable 是函数的类型参数约束
// S 必须是 切片类型,并且该切片的元素类型是 E
// ~ 运算符在这里的作用是允许类型别名,即允许 S 不仅仅是 []E 这样的原始切片类型,也可以是一个切片类型的类型别名,即如果后面使用这个方法的时候,传递参数时切片的类型不仅可以是E,也可以是其他类型
func SlicesIndex[S ~[]E, E comparable](s S, v E) int {
    //E comparable 限制了传入的元素类型 E 必须是可以通过 == 和 != 运算符进行比较的类型
    for i := range s {
       if v == s[i] {
          return i
       }
    }
    return -1
}

// List List[T any] 是一个泛型链表类型,T 表示链表中节点的元素类型,any 表示可以是任何类型
type List[T any] struct {
    head, tail *element[T]
}

// element[T any] 是链表中的节点类型
type element[T any] struct {
  //带的*说明是地址
    next *element[T]
    val  T
}
//为链表定义了两个方法
//我们可以像在常规类型上一样在泛型类型上定义方法,但必须保留类型参数。类型是List[T],而不是List
func (lst *List[T]) Push(v T) {
    if lst.tail == nil {
       lst.head = &element[T]{val: v}
       lst.tail = lst.head
    } else {
       lst.tail.next = &element[T]{val: v}
       lst.tail = lst.tail.next
    }
}

func (lst *List[T]) AllElements() []T {
    var elems []T
    for e := lst.head; e != nil; e = e.next {
       elems = append(elems, e.val)
    }
    return elems
}

func main() {
    var s = []string{"foo", "bar", "zoo"}

    fmt.Println("index of zoo:", SlicesIndex(s, "zoo"))

    _ = SlicesIndex[[]string, string](s, "zoo")

    lst := List[int]{}
    lst.Push(10)
    lst.Push(13)
    lst.Push(23)
    fmt.Println("list:", lst.AllElements())
}
range迭代器

迭代器函数将另一个函数作为参数,yield按照惯例调用该函数(但名称可以任意)。它将调用yield我们想要迭代的每个元素,并注意yield的返回值以防可能提前终止。

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
package main

import (
	"fmt"
	"iter"
	"slices"
)

type List[T any] struct {
	head, tail *element[T]
}

type element[T any] struct {
	next *element[T]
	val  T
}

func (lst *List[T]) Push(v T) {
	if lst.tail == nil {
		lst.head = &element[T]{val: v}
		lst.tail = lst.head
	} else {
		lst.tail.next = &element[T]{val: v}
		lst.tail = lst.tail.next
	}
}

func (lst *List[T]) All() iter.Seq[T] {
	return func(yield func(T) bool) {

		for e := lst.head; e != nil; e = e.next {
			if !yield(e.val) {
				return
			}
		}
	}
}
//迭代(Iteration) 不一定依赖于特定的数据结构,甚至不需要是有限的(比如无限序列)
func genFib() iter.Seq[int] {
	return func(yield func(int) bool) {
		a, b := 1, 1

		for {
			if !yield(a) {
				return
			}
			a, b = b, a+b
		}
	}
}

func main() {
	lst := List[int]{}
	lst.Push(10)
	lst.Push(13)
	lst.Push(23)
	//由于List.All返回一个迭代器,我们可以在常规range循环中使用它
	for e := range lst.All() {
		fmt.Println(e)
	}
	//像切片这样的包有许多有用的函数可以与迭代器一起使用
	all := slices.Collect(lst.All())
	fmt.Println("all:", all)
	
	for n := range genFib() {

		if n >= 10 {
			break
		}
		fmt.Println(n)
	}
}

链表以前的定义方式如下

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"

type ListNode struct {
	next *ListNode
	val  int
}

func main() {
	head := &ListNode{}
	node := head
	for node != nil {
		if node.next == nil {
			node.next = &ListNode{nil, 1}
			break
		}
		node = node.next
	}
	for head != nil {
		fmt.Println(head.val)
		head = head.next
	}
	
}

错误

在 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
package main

import (
	"errors"
	"fmt"
)

func f(arg int) (int, error) {
	if arg == 42 {
		//errors.Newerror使用给定的错误消息构造一个基本值
		return -1, errors.New("can't work with 42")
	}

	return arg + 3, nil
}
//标记错误是一个预先声明的变量,用于表示特定的错误情况
var ErrOutOfTea = fmt.Errorf("no more tea available")
var ErrPower = fmt.Errorf("can't boil water")

func makeTea(arg int) error {
	if arg == 2 {
		return ErrOutOfTea
	} else if arg == 4 {
		//这里是包装错误,因为在平时使用的过程中,希望不仅返回错误,还可以返回错误上下文信息
    //将一个错误包装到另一个错误中,并且保留原始错误信息,这样可以构建一个错误链
    //这里的函数makeTea就是一个错误,根据传递的参数来模拟出不同的错误
		return fmt.Errorf("making tea: %w", ErrPower)
	}
	return nil
}

func main() {
	for _, i := range []int{7, 42} {

		if r, e := f(i); e != nil {
			fmt.Println("f failed:", e)
		} else {
			fmt.Println("f worked:", r)
		}
	}

	for i := 1; i <= 5; i++ {
		if err := makeTea(i); err != nil {
			//errors.Is检查给定错误(或其链中的任何错误)是否与特定错误值匹配
			if errors.Is(err, ErrOutOfTea) {
				fmt.Println("We should buy new tea!")
			} else if errors.Is(err, ErrPower) {
				fmt.Println("Now it is dark.")
			} else {
				fmt.Printf("unknown error: %s\n", err)
			}
			continue
		}

		fmt.Println("Tea is ready!")
	}
}

自定义错误
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
package main

import (
	"errors"
	"fmt"
)

// 使用结构体自定义一个错误
type argError struct {
	arg     int
	message string
}

// 实现error接口
func (e *argError) Error() string {
	return fmt.Sprintf("%d - %s", e.arg, e.message)
}

func f1(arg int) (int, error) {
	if arg == 42 {
		//方法的接收者使用的是指针类型,所以这里要用&
		return -1, &argError{arg, "can't work with it"}
	}
	return arg + 3, nil
}

func main() {

	_, err := f1(42)
	var ae *argError
	//这里的errors.As会检查err是否是ae类型的错误,如果是的话,就会将err赋值给ae
	//errors.As赋值的时候,要求被赋值的变量必须是指针类型,这样才可以进行修改
	//所以这里传递的是ae的地址,尽管ae本来就是一个引用地址
	if errors.As(err, &ae) {
		fmt.Println(ae.arg)
		fmt.Println(ae.message)
	} else {
		fmt.Println("err doesn't match argError")
	}
}
/*
这里最后的输出是
42
can't work with it
*/
panic

panic 是 Go 中的一种机制,用于在程序遇到无法恢复的错误时终止程序的正常执行

一旦 panic 触发,当前函数停止执行,并向上级函数传播,如果没有 recover() 来捕获 panic,程序将终止并输出调用栈

在下面这段代码中,程序在执行完panic(“a problem”)后,就会停止执行后续的部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import "os"

func main() {

    //panic() 用于中止程序并抛出错误信息
    //panic 触发时,程序会立即终止,并打印出调用栈
    panic("a problem")

    _, err := os.Create("/tmp/file")
    if err != nil {
       panic(err)
    }
}

可以使用recover函数捕获panic并继续执行程序,其原理是:

  1. panic 会立即终止当前函数,并开始向上回溯调用栈(stack unwinding)。
  2. 直到 defer 中的 recover() 被调用,才可以捕获 panic 并恢复控制权。

recover() 只能捕获 panic 并防止程序崩溃,但 panic 发生后的代码不会再继续执行,输出:

1
2
File created successfully
Recovered from panic: a problem
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
    "fmt"
    "os"
)

func main() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered from panic:", r)
        }
    }()

    _, err := os.Create("/tmp/file")
    if err != nil {
        panic(err)
    }
    fmt.Println("File created successfully")

    panic("a problem")
}

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
package main

import (
	"fmt"
	"os"
)

func main() {

	f := createFile("/tmp/defer.txt")
	defer closeFile(f)
	writeFile(f)
}

func createFile(p string) *os.File {
	fmt.Println("creating")
	f, err := os.Create(p)
	if err != nil {
		panic(err)
	}
	return f
}

func writeFile(f *os.File) {
	fmt.Println("writing")
	fmt.Fprintln(f, "data")
}

func closeFile(f *os.File) {
	fmt.Println("closing")
	err := f.Close()

	if err != nil {
		panic(err)
	}
}

recover

Go 可以使用内置函数从 panic 中恢复

比如使用reover后,如果某个客户端连接出现严重错误,服务器不会崩溃。相反,服务器会关闭该连接并继续为其他客户端提供服务。事实上,这正是 Gonet/http 默认为 HTTP 服务器所做的

recover需要使用defer,不然是不能触发的

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"

func mayPanic() {
	panic("a problem")
}

func main() {

	defer func() {
		if r := recover(); r != nil {

			fmt.Println("Recovered. Error:\n", r)
		}
	}()

	mayPanic()

	fmt.Println("After mayPanic()")
}

字符串函数
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
package main

import (
	"fmt"
	s "strings"
)

var p = fmt.Println

func main() {
	//true,检查子字符串 es 是否存在于 "test" 中
	p("Contains:  ", s.Contains("test", "es"))
	//2,统计字符串 "test" 中子字符串 "t" 出现的次数
	p("Count:     ", s.Count("test", "t"))
	//true,检查字符串 "test" 是否以 "te" 开头
	p("HasPrefix: ", s.HasPrefix("test", "te"))
	//true,检查字符串 "test" 是否以 "st" 结尾
	p("HasSuffix: ", s.HasSuffix("test", "st"))
	//1,返回子字符串 "e" 在 "test" 中第一次出现的索引位置
	p("Index:     ", s.Index("test", "e"))
	//a-b,将 []string{"a", "b"} 用 "-" 连接为一个字符串
	p("Join:      ", s.Join([]string{"a", "b"}, "-"))
	//aaaaa,将字符串 "a" 重复 5 次
	p("Repeat:    ", s.Repeat("a", 5))
	//将字符串 "foo" 中的 "o" 替换成 "0",-1表示替换所有的
	p("Replace:   ", s.Replace("foo", "o", "0", -1))
	//1表示只替换第一个匹配的 "o" 为 "0"
	p("Replace:   ", s.Replace("foo", "o", "0", 1))
	//[]string{"a", "b", "c", "d", "e"},使用 "-" 作为分隔符拆分字符串 "a-b-c-d-e"
	p("Split:     ", s.Split("a-b-c-d-e", "-"))
	//将字符串 "TEST" 转换为小写
	p("ToLower:   ", s.ToLower("TEST"))
	//将字符串 "test" 转换为大写
	p("ToUpper:   ", s.ToUpper("test"))
}

字符串格式化

使用fmt.Printf进行输出时的一些输出格式

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
package main

import (
	"fmt"
	"os"
)

type point struct {
	x, y int
}

func main() {

	p := point{1, 2}
	//struct1: {1 2},以默认格式打印 p
	fmt.Printf("struct1: %v\n", p)
	//struct2: {x:1 y:2},结构体字段名 + 值
	fmt.Printf("struct2: %+v\n", p)
	//struct3: main.point{x:1, y:2},以 Go 语法的方式打印 p
	fmt.Printf("struct3: %#v\n", p)
	//type: main.point,打印变量的类型
	fmt.Printf("type: %T\n", p)
	//bool: true,打印布尔值 true 或 false
	fmt.Printf("bool: %t\n", true)
	//int: 123,以十进制格式打印整数
	fmt.Printf("int: %d\n", 123)
	//bin: 1110,以二进制格式打印整数
	fmt.Printf("bin: %b\n", 14)
	//char: !,打印对应 Unicode 码点的字符(33 对应 !)。
	fmt.Printf("char: %c\n", 33)
	//hex: 1c8,以小写十六进制打印整数
	fmt.Printf("hex: %x\n", 456)
	//float1: 78.900000,以浮点数格式(小数点形式)打印
	fmt.Printf("float1: %f\n", 78.9)
	//float2: 1.234000e+08,以科学计数法表示
	fmt.Printf("float2: %e\n", 123400000.0)
	fmt.Printf("float3: %E\n", 123400000.0)
	//str1: "string",打印字符串
	fmt.Printf("str1: %s\n", "\"string\"")
	//str2: "\"string\"",以带引号的字符串格式打印
	fmt.Printf("str2: %q\n", "\"string\"")
	//str3: 6865782074686973,以十六进制格式打印字符串的字节值
	fmt.Printf("str3: %x\n", "hex this")
	//pointer: 0xc00000e0c0,以十六进制格式打印变量的内存地址
	fmt.Printf("pointer: %p\n", &p)
	//width1: |    12|   345|,以宽度 6 右对齐打印整数,不足部分填充空格
	fmt.Printf("width1: |%6d|%6d|\n", 12, 345)
	//width2: |  1.20|  3.45|,以宽度 6 右对齐,保留 2 位小数
	fmt.Printf("width2: |%6.2f|%6.2f|\n", 1.2, 3.45)
	//width3: |1.20  |3.45  |,以宽度 6 左对齐,保留 2 位小数
	fmt.Printf("width3: |%-6.2f|%-6.2f|\n", 1.2, 3.45)
	//width4: |   foo|     b|,右对齐字符串,宽度 6
	fmt.Printf("width4: |%6s|%6s|\n", "foo", "b")
	//width5: |foo   |b     |,左对齐字符串,宽度 6
	fmt.Printf("width5: |%-6s|%-6s|\n", "foo", "b")
	//sprintf: a string,将格式化结果存储为字符串,不输出
	s := fmt.Sprintf("sprintf: a %s", "string")
	fmt.Println(s)
	//io: an error,格式化结果输出到指定的 io.Writer(这里是 os.Stderr)
	fmt.Fprintf(os.Stderr, "io: an %s\n", "error")
}

text/template

使用 text/template 包创建和解析模板,然后将不同的数据填充到模板中进行格式化输出

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
package main

import (
    "os"
    "text/template"
)

func main() {
    //创建名为 t1 的模板
    t1 := template.New("t1")
    //Parse("Value is \n") 解析模板字符串
    // 代表模板中的数据,会被 Execute() 时提供的数据替换
    t1, err := t1.Parse("Value is \n")
    if err != nil {
       panic(err)
    }
    //template.Must() 确保模板解析成功,如果失败就会调用panic终止程序
    t1 = template.Must(t1.Parse("Value: \n"))
    //这里就是对前面的t1模版进行填充数据
    //"some text" 被替换到 
    t1.Execute(os.Stdout, "some text")
    //5 被替换到 
    t1.Execute(os.Stdout, 5)
    //[]string{"Go", "Rust", "C++", "C#"} 将 []string 作为一个整体被打印
    t1.Execute(os.Stdout, []string{
       "Go",
       "Rust",
       "C++",
       "C#",
    })
    //Create 函数是一个模板辅助函数,可以根据 name 和 t 创建并解析一个新模板
    //template.Must() 保证解析成功
    Create := func(name, t string) *template.Template {
       return template.Must(template.New(name).Parse(t))
    }
    //使用模版辅助函数创建了t2模版
    t2 := Create("t2", "Name: \n")
    //使用结构体对t2模版填充数据
    //输出Name: Jane Doe
    t2.Execute(os.Stdout, struct {
       Name string
    }{"Jane Doe"})
    //使用map对模版填充数据
    //输出Name: Mickey Mouse
    t2.Execute(os.Stdout, map[string]string{
       "Name": "Mickey Mouse",
    })
    //创建if判断的模版
    t3 := Create("t3",
       "yes no \n")
    t3.Execute(os.Stdout, "not empty")
    t3.Execute(os.Stdout, "")
    //创建range遍历的模版
    t4 := Create("t4",
       "Range:  \n")
    t4.Execute(os.Stdout,
       []string{
          "Go",
          "Rust",
          "C++",
          "C#",
       })
}
正则表达式

下面是关于正则的一些函数使用

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
package main

import (
    "bytes"
    "fmt"
    "regexp"
)

func main() {

    //p:匹配字符 p,([a-z]+):匹配一个或多个小写字母,作为捕获组,ch:匹配字符 ch
    //即查找一个字符串,且以p开头,ch结尾
    //peach中含有p,故匹配该模式,因此 match 返回 true。
    match, _ := regexp.MatchString("p([a-z]+)ch", "peach")
    fmt.Println(match)
    //Compile() 解析正则表达式并返回 *Regexp 对象 r,后续可以使用 r 进行匹配和查找
    r, _ := regexp.Compile("p([a-z]+)ch")
    //使用r进行正则匹配,返回 true。
    fmt.Println(r.MatchString("peach"))
    //查找第一个匹配项:r.FindString(),peach是第一个,所以返回peach
    fmt.Println(r.FindString("peach punch"))
    //r.FindStringIndex("peach punch"):返回第一个匹配项的起始和结束索引。
    //peach在0-5的位置,所以输出[0,5]
    fmt.Println("idx:", r.FindStringIndex("peach punch"))
    //这里要求返回匹配字符串和捕获组(p和ch之间的字符串)
    //输出[peach ea]
    fmt.Println(r.FindStringSubmatch("peach punch"))
    //返回匹配字符串和捕获组的索引
    //[0 5 1 3]
    fmt.Println(r.FindStringSubmatchIndex("peach punch"))
    //r.FindAllString() 找到所有匹配项,-1 作为 n 的值,表示返回所有匹配项
    //[peach punch pinch]
    fmt.Println(r.FindAllString("peach punch pinch", -1))
    //返回所有匹配项的索引
    fmt.Println("all:", r.FindAllStringSubmatchIndex(
       "peach punch pinch", -1))
    //2代表只返回找到最多 2 个匹配项
    fmt.Println(r.FindAllString("peach punch pinch", 2))
    //匹配 byte 切片,返回true
    fmt.Println(r.Match([]byte("peach")))
    //和regexp.Compile效果差不多,但是当遇到匹配不到的时候会panic
    r = regexp.MustCompile("p([a-z]+)ch")
    fmt.Println("regexp:", r)
    //字符串替换,将 peach 替换为 <fruit>
    //输出  a <fruit>
    fmt.Println(r.ReplaceAllString("a peach", "<fruit>"))
    //r.ReplaceAllFunc() 将匹配项通过 bytes.ToUpper 进行转换
    in := []byte("a peach")
    out := r.ReplaceAllFunc(in, bytes.ToUpper)
    //输出 a PEACH
    fmt.Println(string(out))
}
JSON

将基本数据类型、切片、map、结构体编码为 JSON(Marshal)。

将 JSON 字符串解码为 Go 结构体或 map(Unmarshal)。

通过 EncoderDecoder 进行 JSON 的流式编码和解码。

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
package main

import (
    "encoding/json"
    "fmt"
    "os"
    "strings"
)

type response1 struct {
    Page   int
    Fruits []string
}

type response2 struct {
    Page   int      `json:"page"`
    Fruits []string `json:"fruits"`
}

func main() {
    //将布尔值 true 编码为 JSON
    bolB, _ := json.Marshal(true)
    fmt.Println(string(bolB))
    //将整数 1 编码为 JSON
    intB, _ := json.Marshal(1)
    fmt.Println(string(intB))
    //将浮点数 2.34 编码为 JSON
    fltB, _ := json.Marshal(2.34)
    fmt.Println(string(fltB))
    //将字符串 gopher 编码为 JSON
    strB, _ := json.Marshal("gopher")
    fmt.Println(string(strB))
    //将字符串切片编码为 JSON
    slcD := []string{"apple", "peach", "pear"}
    slcB, _ := json.Marshal(slcD)
    fmt.Println(string(slcB))
    //将 map 编码为 JSON
    mapD := map[string]int{"apple": 5, "lettuce": 7}
    mapB, _ := json.Marshal(mapD)
    fmt.Println(string(mapB))
    //结构体没有使用 JSON 标签,所以字段名以大写形式输出
    //输出  {"Page":1,"Fruits":["apple","peach","pear"]}
    //这里要将值传给json.Marshal,使用指针可以避免拷贝整个结构体数据
    res1D := &response1{
       Page:   1,
       Fruits: []string{"apple", "peach", "pear"}}
    res1B, _ := json.Marshal(res1D)
    fmt.Println(string(res1B))
    //response2 使用了 json 标签: Page 被映射为 page Fruits 被映射为 fruits
    //输出  {"page":1,"fruits":["apple","peach","pear"]}
    res2D := &response2{
       Page:   1,
       Fruits: []string{"apple", "peach", "pear"}}
    res2B, _ := json.Marshal(res2D)
    fmt.Println(string(res2B))

    byt := []byte(`{"num":6.13,"strs":["a","b"]}`)

    //interface{} 作为值的类型,表示键对应的值可以是任意类型
    var dat map[string]interface{}
    //将 JSON 字节数组解析为 map
    if err := json.Unmarshal(byt, &dat); err != nil {
       panic(err)
    }
    fmt.Println(dat)
    //dat["num"] 被解析为 float64,通过类型断言访问
    num := dat["num"].(float64)
    fmt.Println(num)

    strs := dat["strs"].([]interface{})
    str1 := strs[0].(string)
    fmt.Println(str1)
    //将 JSON 解析到 res
    //这里不需要修改数据之类的,所以可以直接是值类型
    str := `{"page": 1, "fruits": ["apple", "peach"]}`
    res := response2{}
    json.Unmarshal([]byte(str), &res)
    fmt.Println(res)
    fmt.Println(res.Fruits[0])

    //它将数据编码成 JSON 格式,并写入到指定的 io.Writer
    enc := json.NewEncoder(os.Stdout)
    d := map[string]int{"apple": 5, "lettuce": 7}
    //enc.Encode(d) 将 d 编码为 JSON 并写入标准输出
    enc.Encode(d)
    //将字符串转换为 io.Reader
    dec := json.NewDecoder(strings.NewReader(str))
    //解析 JSON 并存储到 res1
    res1 := response2{}
    dec.Decode(&res1)
    fmt.Println(res1)
}
XML
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
package main

import (
    "encoding/xml"
    "fmt"
)

type Plant struct {
    //指定 XML 根元素的名称为 <plant>...<plant/>
    XMLName xml.Name `xml:"plant"`
    //将 Id 编码为 XML 属性
    Id int `xml:"id,attr"`
    //将 Name 映射到 XML 标签 <name>
    Name   string   `xml:"name"`
    Origin []string `xml:"origin"`
}

// fmt.Stringer 接口只有一个方法
func (p Plant) String() string {
    return fmt.Sprintf("Plant id=%v, name=%v, origin=%v",
       p.Id, p.Name, p.Origin)
}

func main() {
    //创建 Plant 实例
    coffee := &Plant{Id: 27, Name: "Coffee"}
    coffee.Origin = []string{"Ethiopia", "Brazil"}
    //将 coffee 结构体编码为格式化的 XML
    out, _ := xml.MarshalIndent(coffee, " ", "  ")
    /*
     <plant id="27">
       <name>Coffee</name>
       <origin>Ethiopia</origin>
       <origin>Brazil</origin>
     </plant>

    */
    fmt.Println(string(out))
    //添加 XML 头信息
    fmt.Println(xml.Header + string(out))

    var p Plant
    //解析 XML 数据到 Plant 结构体
    if err := xml.Unmarshal(out, &p); err != nil {
       panic(err)
    }
    fmt.Println(p)

    tomato := &Plant{Id: 81, Name: "Tomato"}
    tomato.Origin = []string{"Mexico", "California"}

    //定义嵌套结构 Nesting
    type Nesting struct {
       XMLName xml.Name `xml:"nesting"`
       Plants  []*Plant `xml:"parent>child>plant"`
    }

    nesting := &Nesting{}
    nesting.Plants = []*Plant{coffee, tomato}
    //编码嵌套 XML 结构
    //xml.MarshalIndent() 将 nesting 结构体编码为格式化的 XML 字符串
    out, _ = xml.MarshalIndent(nesting, " ", "  ")
    fmt.Println(string(out))
}
time

关于时间类的一些用法

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
package main

import (
	"fmt"
	"time"
)

func main() {
	p := fmt.Println
	//返回当前的本地时间,类型为 time.Time
	now := time.Now()
	p(now)
	//time.Date() 创建一个特定的时间
	then := time.Date(
		2009, 11, 17, 20, 34, 58, 651387237, time.UTC)
	p(then)

	p(then.Year())
	p(then.Month())
	p(then.Day())
	p(then.Hour())
	p(then.Minute())
	p(then.Second())
	p(then.Nanosecond())
	//获取时区
	p(then.Location())

	p(then.Weekday())
	////时间比较,返回布尔值
	p(then.Before(now))
	p(then.After(now))
	p(then.Equal(now))
	//计算时间差
	diff := now.Sub(then)
	p(diff)
	//将时间差转换为不同单位
	p(diff.Hours())
	p(diff.Minutes())
	p(diff.Seconds())
	p(diff.Nanoseconds())
	//向 then 添加 diff 时间差。
	p(then.Add(diff))
	p(then.Add(-diff))
}

Epoch
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
    "fmt"
    "time"
)

func main() {

    now := time.Now()
    fmt.Println(now)
    //Unix 时间戳(Unix Timestamp) 是自 1970 年 1 月 1 日 00:00:00 UTC(Unix 纪元)以来经过的时间
    fmt.Println(now.Unix())
    fmt.Println(now.UnixMilli())
    fmt.Println(now.UnixNano())
    //将时间戳转化为time时间
    fmt.Println(time.Unix(now.Unix(), 0))
    fmt.Println(time.Unix(0, now.UnixNano()))
}
time格式化和解析
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
package main

import (
    "fmt"
    "time"
)

func main() {
    p := fmt.Println

    t := time.Now()
    //获取当前时间并格式化为 RFC3339 格式
    p(t.Format(time.RFC3339))

    //解析一个时间字符串为 time.Time 类型
    //time.RFC3339:指定解析的时间格式。
    //"2012-11-01T22:08:41+00:00":时间字符串,表示一个符合 RFC3339 格式的时间。
    t1, e := time.Parse(
       time.RFC3339,
       "2012-11-01T22:08:41+00:00")
    p(t1)

    //自定义格式化
    p(t.Format("3:04PM"))
    p(t.Format("Mon Jan _2 15:04:05 2006"))
    p(t.Format("2006-01-02T15:04:05.999999-07:00"))
    form := "3 04 PM"
    //"8 41 PM":要解析的时间字符串。
    t2, e := time.Parse(form, "8 41 PM")
    p(t2)
    //通过 fmt.Printf() 手动构造时间字符串,按自定义格式输出
    fmt.Printf("%d-%02d-%02dT%02d:%02d:%02d-00:00\n",
       t.Year(), t.Month(), t.Day(),
       t.Hour(), t.Minute(), t.Second())
    //ansic := "Mon Jan _2 15:04:05 2006":这是一种预定义的时间格式,表示星期几、月份、日期和时间。
    ansic := "Mon Jan _2 15:04:05 2006"
    //这里是不匹配的,解析失败,返回错误信息
    _, e = time.Parse(ansic, "8:41PM")
    p(e)
}
random
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
package main

import (
    "fmt"
    "math/rand/v2"
)

func main() {
		//rand.IntN返回一个随机的intn 0 <= n < 100
    fmt.Print(rand.IntN(100), ",")
    fmt.Print(rand.IntN(100))
    fmt.Println()
		//返回一个float64 f, 0.0 <= f < 1.0
    fmt.Println(rand.Float64())
		//这可用于生成其他范围内的随机浮点数5.0 <= f' < 10.0。
    fmt.Print((rand.Float64()*5)+5, ",")
    fmt.Print((rand.Float64() * 5) + 5)
    fmt.Println()
		//使用种子创建 PCG 随机源
  	//创建一个 PCG 伪随机源(更高质量的伪随机算法)。
    s2 := rand.NewPCG(42, 1024)
  	//构造一个 rand.Rand 实例,封装该源
    r2 := rand.New(s2)
  	//从自定义生成器 r2 中生成伪随机整数
  	//这样设置固定的种子可以让你每次运行程序得到相同的随机序列,常用于测试
    fmt.Print(r2.IntN(100), ",")
    fmt.Print(r2.IntN(100))
    fmt.Println()

    s3 := rand.NewPCG(42, 1024)
    r3 := rand.New(s3)
    fmt.Print(r3.IntN(100), ",")
  	//这里 r3 的种子和 r2 一致,所以输出结果也会和 r2 一模一样
    fmt.Print(r3.IntN(100))
    fmt.Println()
}
数字解析

将字符串转化为数字类型

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
package main

import (
    "fmt"
    "strconv"
)

func main() {
    //将字符串 "1.234" 解析为一个 64 位浮点数(float64)
    f, _ := strconv.ParseFloat("1.234", 64)
    fmt.Println(f)
    //将 "123" 解析为 int64,0 表示自动检测进制(默认 10 进制)
    i, _ := strconv.ParseInt("123", 0, 64)
    fmt.Println(i)
    //"0x1c8" 是一个十六进制数字,前缀 0x 被自动识别
    d, _ := strconv.ParseInt("0x1c8", 0, 64)
    fmt.Println(d)
    //ParseUint:与 ParseInt 类似,但解析为 无符号整数 uint64
    u, _ := strconv.ParseUint("789", 0, 64)
    fmt.Println(u)
    //Atoi("135") 是 ParseInt 的简化版,固定解析为十进制的 int
    k, _ := strconv.Atoi("135")
    fmt.Println(k)
    //试图将字符串 "wat" 转换为整数,失败
    _, e := strconv.Atoi("wat")
    fmt.Println(e)
}
URL解析
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
package main

import (
    "fmt"
    "net"
    "net/url"
)

func main() {
    //s 是一个 PostgreSQL 数据库连接字符串
    s := "postgres://user:pass@host.com:5432/path?k=v#f"
    //使用 url.Parse() 解析它,结果是一个 *url.URL 类型的结构体
    u, err := url.Parse(s)
    if err != nil {
       panic(err)
    }
    //提取协议部分,协议部分 "postgres" 通常表示连接的类型或使用的服务。
    fmt.Println(u.Scheme)
    //u.User 包含用户名和密码
    fmt.Println(u.User)
    fmt.Println(u.User.Username())
    p, _ := u.User.Password()
    fmt.Println(p)
    //u.Host 返回 主机名:端口
    fmt.Println(u.Host) //输出: host.com:5432
    //使用 net.SplitHostPort 将其拆分为主机和端口两部分
    host, port, _ := net.SplitHostPort(u.Host)
    fmt.Println(host)
    fmt.Println(port)
    //Path 是 URL 中的资源路径,常用于 REST API 或数据库名、文件路径等
    fmt.Println(u.Path)
    //Fragment 是 URL 的“锚点”部分,通常在网页中用来跳转到某个位置
    fmt.Println(u.Fragment)

    //u.RawQuery 直接得到查询字符串
    fmt.Println(u.RawQuery) //输出:k=v
    //url.ParseQuery 解析查询字符串为 map[string][]string 形式
    m, _ := url.ParseQuery(u.RawQuery)
    fmt.Println(m)         //输出:map[k:[v]]
    fmt.Println(m["k"][0]) //v
}
SHA256哈希值

属于加密哈希函数的一种,用于数据完整性验证、密码存储、数字签名等场景

特性 描述
输入长度 任意长度字符串(不限字符数)
输出长度 固定为 256 比特(即 64 个十六进制字符)
碰撞抗性 很难找到两个不同输入有相同哈希结果(抗碰撞)
雪崩效应 输入变一个字节,输出会完全不同
不可逆性 不可能从输出反推出原始输入(不用于加密,仅用于摘要/验证)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
    "crypto/sha256"
    "fmt"
)

func main() {
    s := "sha256 this string"
    //创建一个 sha256.Hash 对象(实现了 hash.Hash 接口)
    h := sha256.New()
    //将字符串转换为字节切片后,写入哈希器
  	//wirte函数是哈希器输入数据的唯一方式
    h.Write([]byte(s))
    //Sum(nil) 返回最终的哈希值(字节切片 []byte),参数 nil 表示不加任何前缀
    bs := h.Sum(nil)

    fmt.Println(s)
    fmt.Printf("%x\n", bs)
}
Base64编码

Base64是一种编码方式,不是加密算法,将任意二进制数据安全的转化成ASCII字符串

应用场景 示例
网络传输数据 把图片/文件编码成字符串传输或存储
HTTP/JSON 编码 避免二进制数据在传输中被误解释或截断
邮件 MIME 格式 附件、内嵌内容编码成安全的字符格式
URL 中传递数据 避免特殊字符冲突,使用 URL 安全版本编码

编码和加密的区别在于编码的目的是方便传输,并且编码是不安全的,任何人都可以decode回去

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 (
    b64 "encoding/base64"
    "fmt"
)

func main() {

    data := "abc123!?$*&()'-=@~"
    //将字符串转成字节切片后,使用 StdEncoding 编码为 Base64 字符串
    sEnc := b64.StdEncoding.EncodeToString([]byte(data))
    fmt.Println(sEnc)
    //把 Base64 字符串解码成原始字节,转回字符串后输出
    sDec, _ := b64.StdEncoding.DecodeString(sEnc)
    fmt.Println(string(sDec))
    fmt.Println()

    //使用 URLEncoding 编码方式,使用 - 和 _ 替代 + 和 /
    //更适合在 URL 中传递(不会被解释成特殊符号)
    uEnc := b64.URLEncoding.EncodeToString([]byte(data))
    fmt.Println(uEnc)
    // //将 URL 安全版本解码回原始字符串
    uDec, _ := b64.URLEncoding.DecodeString(uEnc)
    fmt.Println(string(uDec))
}
读取文件
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
package main

import (
    "bufio"
    "fmt"
    "io"
    "os"
)

func check(e error) {
    if e != nil {
       panic(e)
    }
}

func main() {

    //os.ReadFile() 适合用于小文件
    dat, err := os.ReadFile("/tmp/dat")
    check(err)
    fmt.Print(string(dat))

    //os.Open() 打开文件返回 *File
    f, err := os.Open("/tmp/dat")
    check(err)

    b1 := make([]byte, 5)
    //f.Read(b1) 从当前位置(开头)读取 5 字节
    n1, err := f.Read(b1)
    check(err)
    //这个方式读取之后,指针会移动到对应的位置,这里n1就变成了5
    fmt.Printf("%d bytes: %s\n", n1, string(b1[:n1]))

    //f.Seek(6, io.SeekStart) 将读取位置移到第 6 字节
    o2, err := f.Seek(6, io.SeekStart)
    check(err)
    //再读取 2 字节
    b2 := make([]byte, 2)
    //n2表示本次读完后,当前指针在的位置
    n2, err := f.Read(b2)
    check(err)
    //打印结果及偏移量为6
    fmt.Printf("%d bytes @ %d: ", n2, o2)
    fmt.Printf("%v\n", string(b2[:n2]))

    // 再次移动读取位置
    //从当前位置向后跳 2 字节
    _, err = f.Seek(2, io.SeekCurrent)
    check(err)
    // 从文件末尾往回跳 4 字节
    _, err = f.Seek(-4, io.SeekEnd)
    check(err)

    o3, err := f.Seek(6, io.SeekStart)
    check(err)
    b3 := make([]byte, 2)
    //io.ReadAtLeast 保证读取至少 2 字节,否则报错。
    n3, err := io.ReadAtLeast(f, b3, 2)
    check(err)
    fmt.Printf("%d bytes @ %d: %s\n", n3, o3, string(b3))
    //文件指针回到最开始,为下一步缓冲读取做准备
    _, err = f.Seek(0, io.SeekStart)
    check(err)
    //创建带缓存的读取器
    r4 := bufio.NewReader(f)
    //Peek(5) 读取前 5 字节,但不移动指针
    b4, err := r4.Peek(5)
    check(err)
    fmt.Printf("5 bytes: %s\n", string(b4))

    f.Close()
}
写入文件
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
package main

import (
    "bufio"
    "fmt"
    "os"
)

func check1(e error) {
    if e != nil {
       panic(e)
    }
}

func main() {

    //将字符串转化为切片
    d1 := []byte("hello\ngo\n")
    //以权限模式 0644(拥有者读写,其他人只读)创建文件 /tmp/dat1,写入字节切片
    err := os.WriteFile("/tmp/dat1", d1, 0644)
    check1(err)

    //创建文件 /tmp/dat2
    f, err := os.Create("/tmp/dat2")
    check1(err)
    //使用 defer f.Close() 确保程序结束时文件句柄被关闭
    defer f.Close()

    //byte写入文件后,就会变成char类型,这里10代表的就是\n
    d2 := []byte{115, 111, 109, 101, 10}
    //前面用create创建好文件后,现在就可以用write写入文件
    //n2代表当前指针的位置,为5
    n2, err := f.Write(d2)
    check1(err)
    fmt.Printf("wrote %d bytes\n", n2)

    //向文件继续追加字符串 "writes\n"
    //n3代表当前指针的位置,上次写入的内容最后是换行,所以n3是从新的一行开始计数,所以为7,指在\n的位置
    n3, err := f.WriteString("writes\n")
    check1(err)
    fmt.Printf("wrote %d bytes\n", n3)
    //强制文件内容写入磁盘
    f.Sync()

    //创建一个带缓冲的 Writer,减少频繁写入磁盘,提高效率
    //先将数据写入内存中的缓冲区,等缓冲区满或显式调用 Flush() 后再统一写入磁盘,极大减少了系统调用次数
    //将多个小写入合并成一次大写入,不仅减少 I/O 次数,还能利用磁盘的顺序写入特性,提升整体吞吐量
    w := bufio.NewWriter(f)
    //向缓冲区追加字符串 "buffered\n"
    n4, err := w.WriteString("buffered\n")
    check1(err)
    fmt.Printf("wrote %d bytes\n", n4)
    //强制将缓冲区的数据写入到文件
    //为了防止程序中断,数据消失,所以一定要加这句话,可以加一个defer
    w.Flush()

}
行过滤器

读取用户从标准输入(键盘)输入的每一行内容,并将其转换为大写再打印出来

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 (
    "bufio"
    "fmt"
    "os"
    "strings"
)

func main() {

    //创建一个 Scanner 实例,用于从标准输入 os.Stdin 读取内容
    scanner := bufio.NewScanner(os.Stdin)

    //会读取一行内容,如果成功读取到,就进入循环体
    for scanner.Scan() {

       //获取当前这行文本(类型是 string)
       ucl := strings.ToUpper(scanner.Text())

       fmt.Println(ucl)
    }

    //这部分是对 scanner.Scan() 的错误处理
    //如果在读取过程中发生 I/O 错误,这里会捕捉并打印错误信息,然后退出程序
    if err := scanner.Err(); err != nil {
       fmt.Fprintln(os.Stderr, "error:", err)
       os.Exit(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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package main

import (
    "fmt"
    "path/filepath"
    "strings"
)

func main() {

    //将多个路径元素合成一个合法路径,自动处理路径分隔符
    p := filepath.Join("dir1", "dir2", "filename")
    fmt.Println("p:", p)

    //自动清理多余的斜杠。这里"dir1//"和"filename"拼接后,多余的/会被处理掉
    fmt.Println(filepath.Join("dir1//", "filename"))
    //.. 表示上一级目录,经过规范化后相当于直接是dir1/filename
    fmt.Println(filepath.Join("dir1/../dir1", "filename"))

    //filepath.Dir(p) 获取路径中的目录部分
    fmt.Println("Dir(p):", filepath.Dir(p))
    //filepath.Base(p) 获取路径中的最后一部分(即文件名)
    fmt.Println("Base(p):", filepath.Base(p))

    //IsAbs 判断路径是否是绝对路径
    //"dir/file" 是相对路径,输出 false
    fmt.Println(filepath.IsAbs("dir/file"))
    //"/dir/file" 是绝对路径(以根 / 开头),输出 true
    fmt.Println(filepath.IsAbs("/dir/file"))

    filename := "config.json"
    //提取文件名的扩展名,即.json
    ext := filepath.Ext(filename)
    fmt.Println(ext)
    //去掉文件名末尾的指定后缀(这里是.json),得到基础文件名 "config"
    fmt.Println(strings.TrimSuffix(filename, ext))
    // 计算两个路径之间的相对关系
    rel, err := filepath.Rel("a/b", "a/b/t/file")
    if err != nil {
       panic(err)
    }
    //从 a/b 到 a/b/t/file,相对路径是 t/file
    fmt.Println(rel)

    rel, err = filepath.Rel("a/b", "a/c/t/file")
    if err != nil {
       panic(err)
    }
    //从 a/b 到 a/c/t/file
    //需要先返回上一级到 a,再进入 c/t/file,所以结果是 ../c/t/file
    fmt.Println(rel)
}
目录

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
package main

import (
    "fmt"
    "io/fs"
    "os"
    "path/filepath"
)

func main() {
    //创建一个名为 subdir 的目录
    //权限为 0755(即拥有者可读写执行,组用户和其他用户可读执行)
    err := os.Mkdir("subdir", 0755)
    check(err)

    //程序退出前,递归删除 subdir 及其所有内容,保证环境清理,避免临时文件残留
    defer os.RemoveAll("subdir")

    //定义了一个匿名函数,其作用是创建一个空文件
    createEmptyFile := func(name string) {
       d := []byte("")
       check(os.WriteFile(name, d, 0644))
    }

    //通过该函数,创建一个file
    createEmptyFile("subdir/file1")

    //创建多级目录 subdir/parent/child,MkdirAll 会自动创建不存在的父目录
    err = os.MkdirAll("subdir/parent/child", 0755)
    check(err)

    createEmptyFile("subdir/parent/file2")
    createEmptyFile("subdir/parent/file3")
    createEmptyFile("subdir/parent/child/file4")

    //读取 subdir/parent 目录下的文件和子目录列表
    c, err := os.ReadDir("subdir/parent")
    check(err)

    fmt.Println("Listing subdir/parent")
    for _, entry := range c {
       //IsDir() 判断当前条目是目录还是文件
       fmt.Println(" ", entry.Name(), entry.IsDir())
    }

    //更改当前工作目录到 subdir/parent/child
    err = os.Chdir("subdir/parent/child")
    check(err)

    //在新的目录下读取目录
    c, err = os.ReadDir(".")
    check(err)

    fmt.Println("Listing subdir/parent/child")
    for _, entry := range c {
       fmt.Println(" ", entry.Name(), entry.IsDir())
    }

    //从当前 child 目录,向上移动三级,回到程序最初的工作目录
    err = os.Chdir("../../..")
    check(err)

    fmt.Println("Visiting subdir")
    //使用 filepath.WalkDir 遍历整个 subdir 目录树
    //对每一个文件或目录调用 visit 函数
    err = filepath.WalkDir("subdir", visit)
}
//visit 函数输出每个节点的路径和是否是目录
func visit(path string, d fs.DirEntry, err error) error {
    if err != nil {
       return err
    }
    fmt.Println(" ", path, d.IsDir())
    return nil
}
临时文件和目录

在程序执行过程中,我们经常需要创建一些在程序退出后不再需要的数据。 临时文件和目录非常适合用于此目的,因为它们不会随着时间的推移污染文件系统

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
package main

import (
    "fmt"
    "os"
    "path/filepath"
)

func main() {

    //在默认的临时目录(通常是 /tmp/)下创建一个临时文件,文件名前缀是
    //"" 表示使用系统默认临时目录
    f, err := os.CreateTemp("", "sample")
    check(err)

    fmt.Println("Temp file name:", f.Name())

    //在程序结束时,自动删除刚才创建的临时文件,保证资源清理
    defer os.Remove(f.Name())

    //向临时文件中写入字节数据 [1, 2, 3, 4]
    //这里写入的是原始二进制数据,不是文本
    _, err = f.Write([]byte{1, 2, 3, 4})
    check(err)

    //在系统默认临时目录下创建一个临时目录,前缀是 "sampledir"
    //系统会在前缀后面加一个随机数,保证不重名
    dname, err := os.MkdirTemp("", "sampledir")
    check(err)
    fmt.Println("Temp dir name:", dname)

    //程序退出时递归删除整个临时目录及其内部所有内容
    defer os.RemoveAll(dname)

    //在刚刚创建的临时目录 dname 中,新建一个文件 "file1"
    fname := filepath.Join(dname, "file1")
    //向文件中写入字节数据
    err = os.WriteFile(fname, []byte{1, 2}, 0666)
    check(err)
}
嵌入命令

在编译时将文件或文件夹内容打包进可执行文件,并在运行时读取这些内嵌资源

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 (
    "embed"
)

// 将文件内容以字符串的格式嵌入
// //go:embed folder/single_file.txt
var fileString string

// 以字节数组的形式嵌入
// //go:embed folder/single_file.txt
var fileByte []byte

// 将目录下以.hash结尾的文件一起嵌入到一个folder中
// //go:embed folder/single_file.txt
//
//go:embed folder/*.hash
var folder embed.FS

func main() {

    print(fileString)
    print(string(fileByte))

    //读取内嵌的 file1.hash 和 file2.hash 文件内容
    content1, _ := folder.ReadFile("folder/file1.hash")
    print(string(content1))

    content2, _ := folder.ReadFile("folder/file2.hash")
    print(string(content2))
}
测试和基准测试
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
package main

import (
    "fmt"
    "testing"
)

func IntMin(a, b int) int {
    if a < b {
       return a
    }
    return b
}

func TestIntMinBasic(t *testing.T) {
    ans := IntMin(2, -2)
    if ans != -2 {

       t.Errorf("IntMin(2, -2) = %d; want -2", ans)
    }
}

//定义了一个测试用例表,每个元素是一组输入 (a, b) 和期望输出 want

func TestIntMinTableDriven(t *testing.T) {
    var tests = []struct {
       a, b int
       want int
    }{
       {0, 1, 0},
       {1, 0, 0},
       {2, -2, -2},
       {0, -1, -1},
       {-1, 0, -1},
    }

    for _, tt := range tests {

       testname := fmt.Sprintf("%d,%d", tt.a, tt.b)
       // t.Run 用于在测试报告中清晰标记每组测试,便于排查问题
       t.Run(testname, func(t *testing.T) {
          ans := IntMin(tt.a, tt.b)
          if ans != tt.want {
             t.Errorf("got %d, want %d", ans, tt.want)
          }
       })
    }
}

// 定义了一个基准测试函数,以 Benchmark 开头

func BenchmarkIntMin(b *testing.B) {
    for i := 0; i < b.N; i++ {
       IntMin(1, 2)
    }
}
命令行参数

通过标准库 os 包中的 Args 变量来读取命令行参数

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
package main

import (
    "fmt"
    "os"
)

func main() {

    //os.Args 是一个字符串切片([]string),包含了命令行输入的所有内容
    //os.Args[0] 永远是程序本身的路径
    argsWithProg := os.Args
    //切片截取,从第一个实际参数开始
    argsWithoutProg := os.Args[1:]

    //取命令行的第 4 个元素
    //如果不够,这里就会报运行时错误
    arg := os.Args[3]

    fmt.Println(argsWithProg)
    fmt.Println(argsWithoutProg)
    fmt.Println(arg)
}

/*
	$ go build command-line-arguments.go
	$ ./command-line-arguments a b c d
	[./command-line-arguments a b c d]
	[a b c d]
	c
*/
命令行flag包

flag包相比于直接使用Args包,可以自动解析参数、设定默认值、生成帮助信息,是开发命令行程序时的常规做法

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
package main

import (
    "flag"
    "fmt"
)

func main() {

    //定义一个字符串类型的命令行参数 -word,默认值是 "foo",帮助信息是 "a string"
    //flag.String 返回的是一个指向字符串的指针 *string
    wordPtr := flag.String("word", "foo", "a string")

    //定义一个整型的命令行参数 -numb
    numbPtr := flag.Int("numb", 42, "an int")
    //定义一个布尔型的命令行参数 -fork
    //默认值是false,所以如果没有出现-fork就是false,出现了就是true
    forkPtr := flag.Bool("fork", false, "a bool")

    var svar string
    //另一种定义参数的方式,直接将参数值存入已有变量 svar
    flag.StringVar(&svar, "svar", "bar", "a string var")

    //开始实际解析命令行输入,必须调用 flag.Parse(),否则定义的参数不会生效
    flag.Parse()

    fmt.Println("word:", *wordPtr)
    fmt.Println("numb:", *numbPtr)
    fmt.Println("fork:", *forkPtr)
    fmt.Println("svar:", svar)
    fmt.Println("tail:", flag.Args())
}

/*
    $ go build command-line-flags.go

    $ ./command-line-flags -word=opt -numb=7 -fork -svar=flag
    word: opt
    numb: 7
    fork: true
    svar: flag
    tail: []

    $ ./command-line-flags -word=opt
    word: opt
    numb: 42
    fork: false
    svar: bar
    tail: []

    $ ./command-line-flags -word=opt a1 a2 a3
    word: opt
    ...
    tail: [a1 a2 a3]

    ***请注意,该flag包要求所有标志都出现在位置参数之前
    $ ./command-line-flags -word=opt a1 a2 a3 -numb=7
    word: opt
    numb: 42
    fork: false
    svar: bar
    tail: [a1 a2 a3 -numb=7]



*/
命令行子命令

利用flag包解析子命令,

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
package main

import (
    "flag"
    "fmt"
    "os"
)

func main() {

    //创建一个新的 FlagSet,名字是 "foo"
    //flag.ExitOnError 表示如果解析失败,则程序直接退出
    fooCmd := flag.NewFlagSet("foo", flag.ExitOnError)
    //给foo子命令定义两个参数
    fooEnable := fooCmd.Bool("enable", false, "enable")
    fooName := fooCmd.String("name", "", "name")

    //同样的 创建另一个子命令bar
    barCmd := flag.NewFlagSet("bar", flag.ExitOnError)
    barLevel := barCmd.Int("level", 0, "level")

    //如果没有输入任何子命令(os.Args[0] 是程序名,os.Args[1] 才是第一个实际参数)
    //直接提示并退出
    if len(os.Args) < 2 {
       fmt.Println("expected 'foo' or 'bar' subcommands")
       os.Exit(1)
    }

    //根据第一个实际参数的内容,决定走哪条子命令逻辑
    switch os.Args[1] {

    case "foo":
       fooCmd.Parse(os.Args[2:])
       fmt.Println("subcommand 'foo'")
       fmt.Println("  enable:", *fooEnable)
       fmt.Println("  name:", *fooName)
       fmt.Println("  tail:", fooCmd.Args())
    case "bar":
       barCmd.Parse(os.Args[2:])
       fmt.Println("subcommand 'bar'")
       fmt.Println("  level:", *barLevel)
       fmt.Println("  tail:", barCmd.Args())
    default:
       fmt.Println("expected 'foo' or 'bar' subcommands")
       os.Exit(1)
    }

}

/*
$ go build command-line-subcommands.go
$ ./command-line-subcommands foo -enable -name=joe a1 a2
subcommand 'foo'
  enable: true
  name: joe
  tail: [a1 a2]


$ ./command-line-subcommands bar -level 8 a1
subcommand 'bar'
  level: 8
  tail: [a1]


$ ./command-line-subcommands bar -enable a1
flag provided but not defined: -enable
Usage of bar:
  -level int
        level
*/
环境变量
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
package main

import (
    "fmt"
    "os"
    "strings"
)

func main() {

    //调用 os.Setenv(key, value) 设置一个环境变量
    //将foo设为1
    os.Setenv("FOO", "1")
    //os.Getenv(key) 用于获取指定环境变量的值,如果没有就返回空
    fmt.Println("FOO:", os.Getenv("FOO"))
    fmt.Println("BAR:", os.Getenv("BAR"))

    fmt.Println()
    //os.Environ() 返回一个字符串切片([]string)
    //其中每个元素是 "key=value" 格式的环境变量字符串
    for _, e := range os.Environ() {
       //这个split是将环境变量按照=分成两部分,最多两部分
       pair := strings.SplitN(e, "=", 2)
       //输出第一部分
       fmt.Println(pair[0])
    }
}
日志
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
package main

import (
    "bytes"
    "fmt"
    "log"
    "os"

    "log/slog"
)

func main() {

    //使用标准日志器直接输出一行普通日志
    log.Println("standard logger")

    //LstdFlags设置标准时间戳,Lmicroseconds(微秒级时间)
    log.SetFlags(log.LstdFlags | log.Lmicroseconds)
    log.Println("with micro")

    //Lshortfile(输出调用日志语句的文件名和行号),便于定位
    log.SetFlags(log.LstdFlags | log.Lshortfile)
    log.Println("with file/line")

    //创建一个新的日志器对象 mylog,日志前缀my:,使用标准时间戳
    mylog := log.New(os.Stdout, "my:", log.LstdFlags)
    mylog.Println("from mylog")

    //修改日志前缀为 "ohmy:"
    mylog.SetPrefix("ohmy:")
    mylog.Println("from mylog")

    //创建一个内存缓冲区 bytes.Buffer
    var buf bytes.Buffer
    //将日志输出到内存中
    buflog := log.New(&buf, "buf:", log.LstdFlags)

    buflog.Println("hello")

    fmt.Print("from buflog:", buf.String())

    //创建一个新的 slog 日志器,使用 JSON 格式输出,目标是标准错误输出
    //jsonHandler 是用来定义日志输出格式和输出位置的
    jsonHandler := slog.NewJSONHandler(os.Stderr, nil)
    myslog := slog.New(jsonHandler)
    //使用结构化日志输出一条 Info 级别日志,仅含消息
    myslog.Info("hi there")
    //输出带有键值对字段的 Info 级别日志
    myslog.Info("hello again", "key", "val", "age", 25)
}
http客户端
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
package main

import (
    "bufio"
    "fmt"
    "net/http"
)

func main() {

    //通过 http.Get 发送一个简单的 HTTP GET 请求
    resp, err := http.Get("https://gobyexample.com")
    if err != nil {
       panic(err)
    }
    //防止连接泄露
    defer resp.Body.Close()
    //查看http响应状态
    fmt.Println("Response status:", resp.Status)
    //创建一个scanner,从响应体中逐行获取文本
    scanner := bufio.NewScanner(resp.Body)
    for i := 0; scanner.Scan() && i < 5; i++ {
       fmt.Println(scanner.Text())
    }
    //检查在扫描过程中是否有错误
    if err := scanner.Err(); err != nil {
       panic(err)
    }
}
http server
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
package main

import (
    "encoding/json"
    "fmt"
    "net/http"
)

// w http.ResponseWriter是响应写入器,用于返回响应内容,是一个接口,包含一些方法可以写响应体、响应头、状态码等
// req *http.Request是请求对象,包含客户端发来的所有http请求数据,使用指针类型是因为请求体的值很大,使用指针传递避免复制开销
func hello(w http.ResponseWriter, req *http.Request) {
    // fmt.Fprintf(w, ...) 表示将内容直接写入响应体
    fmt.Fprintf(w, "hello\n")
}

func headers(w http.ResponseWriter, req *http.Request) {

    // req.Header 是一个 map[string][]string 类型
    // 即每个请求头字段可能有多个值
    // 外层循环遍历所有请求头字段(如 "User-Agent"、"Accept" 等)
    for name, headers := range req.Header {
       // 内层循环处理同一字段的多个值(如果有多个,例如 cookie 或 accept-language)
       for _, h := range headers {
          fmt.Fprintf(w, "%v: %v\n", name, h)
       }
    }
}

// json 路由处理函数
func jsonHandler(w http.ResponseWriter, req *http.Request) {

    // 设置响应头,说明响应内容类型为 application/json
    w.Header().Set("Content-Type", "application/json")

    // 构造一个 JSON 响应数据,可以是结构体或 map
    data := map[string]interface{}{
       "message": "Hello, JSON!",
       "success": true,
       "status":  200,
    }

    // 使用 json.NewEncoder 直接将 data 编码为 JSON 并写入响应体
    json.NewEncoder(w).Encode(data)
}

func main() {

    // 主函数注册路由并启动服务
    // HandleFunc 将特定路径绑定到一个处理函数(handler)上
    http.HandleFunc("/hello", hello)
    http.HandleFunc("/headers", headers)
    http.HandleFunc("/json", jsonHandler) // 注册新增的 /json 路由

    // 启动一个 http 服务监听本地 8090 端口
    //只有请求这个端口的 HTTP 请求,才会被当前这段程序接收和处理
    // nil 代表使用默认的 http.DefaultServeMux 作为路由器
    //http.DefaultServeMux 是 Go 标准库中提供的默认 HTTP 请求多路复用器
    //它负责将 HTTP 请求根据请求路径分发给注册的对应处理函数
    http.ListenAndServe(":8090", nil)
}

req包含很多内容,下面是其一些使用方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
1获取 GET 查询参数
name := req.URL.Query().Get("name")
2. 获取 POST 表单字段
req.ParseForm()
email := req.Form.Get("email")
3. 读取原始请求体 JSON):
body, _ := io.ReadAll(req.Body)
4. 获取请求头
ua := req.Header.Get("User-Agent")
5. 获取 Cookie
cookie, err := req.Cookie("session_id")
6. 获取客户端 IP
ip := req.RemoteAddr // 可能还要考虑 X-Forwarded-For 头
7. 使用上下文
ctx := req.Context()
select {
case <-ctx.Done():
    // 请求被取消
}
context

这段代码演示了 Go 中如何在 HTTP 服务中使用 Context 机制 来处理请求的取消(取消上下文),这对于长时间运行的 HTTP handler 非常重要,尤其在客户端断开连接或主动取消请求时,服务端可以优雅退出处理逻辑

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
package main

import (
    "fmt"
    "net/http"
    "time"
)

// 这个函数模拟一个10s的长时间处理,如果客户端在等待期间中断连接,服务端会检测到这个请求取消,通过context提前退出处理逻辑,并记录
func hello1(w http.ResponseWriter, req *http.Request) {

    ctx := req.Context()
    fmt.Println("server: hello handler started")
    defer fmt.Println("server: hello handler ended")

    select {
    //如果10s内都没有被取消就输出
    case <-time.After(10 * time.Second):
       fmt.Fprintf(w, "hello\n")
    //如果客户端在这10s内关闭连接或取消请求,就会触发这个分支
    //这里是底层的tcp连接被关闭后,go的http服务检测到这个关闭后
    //就会自动调用context这个逻辑,使得状态变化
    case <-ctx.Done():
       //获取错误类型
       err := ctx.Err()
       fmt.Println("server:", err)
       internalError := http.StatusInternalServerError
       //向客户端返回一个带错误信息的http响应
       http.Error(w, err.Error(), internalError)
    }
}

func main() {

    http.HandleFunc("/hello", hello1)
    http.ListenAndServe(":8090", nil)
}
spawning-processes

在go中编写shell命令并执行

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
package main

import (
    "fmt"
    "io"
    "os/exec"
)

func main() {

    //创建一个命令对象,准备执行date
    dateCmd := exec.Command("date")

    //output会执行命令,并返回标准输出字节切片
    dateOut, err := dateCmd.Output()
    if err != nil {
       panic(err)
    }
    fmt.Println("> date")
    fmt.Println(string(dateOut))
    //执行一个带错误参数的命令
    _, err = exec.Command("date", "-x").Output()
    if err != nil {
       switch e := err.(type) {
       case *exec.Error:
          fmt.Println("failed executing:", err)
       case *exec.ExitError:
          fmt.Println("command exit rc =", e.ExitCode())
       default:
          panic(err)
       }
    }
    //使用grep hello并向其写入数据(通过管道)
    grepCmd := exec.Command("grep", "hello")
    //向grep提供输入流
    grepIn, _ := grepCmd.StdinPipe()
    //从grep读取输出流
    grepOut, _ := grepCmd.StdoutPipe()
    grepCmd.Start()
    //写入多行文本后关闭输入
    grepIn.Write([]byte("hello grep\ngoodbye grep"))
    grepIn.Close()
    //读取输出结果
    grepBytes, _ := io.ReadAll(grepOut)
    grepCmd.Wait()

    fmt.Println("> grep hello")
    fmt.Println(string(grepBytes))
    //用bash -c执行一个复合命令字符串
    lsCmd := exec.Command("bash", "-c", "ls -a -l -h")
    //执行lsCmd,并一次性读取它的标准输出内容到变量中
    lsOut, err := lsCmd.Output()
    if err != nil {
       panic(err)
    }
    fmt.Println("> ls -a -l -h")
    fmt.Println(string(lsOut))
}
execing-processes
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 (
	"os"
	"os/exec"
	"syscall"
)

// 这个方式是想用另一个(可能是非 Go 的)进程完全替换当前的 Go 进程
// 是一个底层的、不可逆的进程替换过程
func main() {

	//在 $PATH 中查找 ls 命令的完整路径,如 /bin/ls
	binary, lookErr := exec.LookPath("ls")
	if lookErr != nil {
		panic(lookErr)
	}

	//构造命令参数列表
	args := []string{"ls", "-a", "-l", "-h"}
	//获取当前进程的环境变量
	env := os.Environ()
	//使用 syscall.Exec 替换当前进程
	//即使用指定的ls进程完全替换当前go程度进程
	execErr := syscall.Exec(binary, args, env)
	if execErr != nil {
		panic(execErr)
	}
}

Signals

有时我们希望 Go 程序能够智能地处理Unix 信号。例如,我们可能希望服务器在收到 时正常关闭SIGTERM,或者命令行工具在收到 时停止处理输入SIGINT。以下是如何在 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
package main

import (
    "fmt"
    "os"
    "os/signal"
    "syscall"
)

// 这段代码监听两种信号,SIGINT(中断,即Ctrl+C)今儿SIGTERM(终止,即kill)
// 当收到这些信号后,就会打印信号类型,并退出程序
func main() {

    //创建一个缓冲为1的通道,用于接收信号,类型是os.Signal
    sigs := make(chan os.Signal, 1)

    //注册监听信号类型,表示只监听下面这两种信号,当程序收到后,就会将其送入通道中
    signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)

    //创建另一个通道用于通知程序退出
    done := make(chan bool, 1)

    //启动一个goroutine处理信号
    //使用这样的方式,可以在这个goroutine增加更多逻辑,主goroutine专注控制流程即可
    go func() {
       sig := <-sigs
       fmt.Println()
       fmt.Println(sig)
       done <- true
    }()

    fmt.Println("awaiting signal")
    <-done
    fmt.Println("exiting")
}
exit
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import (
    "fmt"
    "os"
)

// os.Exit() 会立即终止进程,跳过所有正常退出的流程
// 不会执行 defer、panic、连goroutine的资源释放流程都不会跑完
func main() {

    //注册一个延迟执行的打印语句
    defer fmt.Println("!")

    //强制退出程序,退出码为3
    os.Exit(3)
}

并发

Goroutines

goroutine是一个轻量级的执行线程

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
package main

import (
	"fmt"
	"time"
)

func ff(from string) {
	for i := 0; i < 3; i++ {
		fmt.Println(from, ":", i)
	}
}

func main() {
	//这是一个普通的函数调用,运行在 main goroutine 中
	//因此它会顺序执行并完成后才继续向下执行
	ff("direct")

	//main 线程不会等待它完成,直接继续执行下一行代码。
	go ff("goroutine")

	//启动一个 goroutine 进行匿名函数调用
	go func(msg string) {
		fmt.Println(msg)
	}("going")

	//这个 Sleep 的目的是给 goroutine 运行的时间
	//否则 main 可能会直接结束,导致 goroutine 没机会运行
	//即使是goroutine正在执行,只要主线程结束,goroutine也会被中断
	time.Sleep(time.Second)
	fmt.Println("done")
}

但是这里使用的sleep方法不太好,最好还是使用waitgroup

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
package main

import (
	"fmt"
	"sync"
)

func ff(from string) {
	for i := 0; i < 3; i++ {
		fmt.Println(from, ":", i)
	}
}

func main() {
	//使用waitgroup的方式
	var wg sync.WaitGroup

	ff("direct")
	//添加两个要等待的goroutine
	wg.Add(2)

	go func() {
		//这里需要加wg.Done(),用于wg的计数,要两次主线程才会结束等待,否则就会死锁
		defer wg.Done()
		ff("goroutine")
	}()

	go func(msg string) {
		defer wg.Done()
		fmt.Println(msg)
	}("going")
	//主线程在这个位置等待
	wg.Wait()
	fmt.Println("done")
}

Channels

Channel用于goroutine之间的消息通信

无缓冲通道会阻塞发送方和接收方,会一直等待发送方和接收方都准备好了,两边才会解除阻塞,进行执行下去

下面执行过程分析

  • Step 1: go func() { messages <- "ping" }() 启动一个 goroutine,并试图发送 "ping"
  • Step 2: 由于 messages 是无缓冲通道,messages <- "ping" 不能立即完成,它必须等 msg := <-messages 执行时,才会真正发送数据。
  • Step 3: msg := <-messagesmain goroutine 中执行,此时 messages <- "ping" 解除阻塞,数据传递发生,两个 goroutine 同时继续执行。
  • Step 4: "ping" 被打印,程序结束。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import "fmt"

func main() {

	//这创建了一个无缓冲通道,默认情况下,发送和接收操作都会阻塞,直到对方准备好
	messages := make(chan string)

	//创建一个goroutine,将消息传入通道
	//由于 messages 是无缓冲通道,发送操作会阻塞,直到 main 线程接收数据
	go func() { messages <- "ping" }()
	//将消息从通道中取出来
	//main 线程在这里阻塞,等待 messages 通道里有数据
	msg := <-messages
	fmt.Println(msg)
}

缓冲通道

🔹 无缓冲通道 = 生产者 & 消费者必须同时就绪

🔹 带缓冲通道 = 允许生产者先存储数据,消费者稍后取出

对于缓冲通道而言,如果通道为空,那就会阻塞消费者,如果通道是满的,就会阻塞生产者

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"
	"time"
)

func main() {
	messages := make(chan string, 2) // 创建一个缓冲区大小为 2 的通道

	// 生产者 goroutine
	go func() {
		messages <- "hello"
		fmt.Println("Sent: hello") // 立即执行,不会阻塞
		messages <- "world"
		fmt.Println("Sent: world") // 立即执行,不会阻塞
		// 由于缓冲区已满,下面的发送会阻塞,直到有消费者接收数据
		messages <- "!"
		fmt.Println("Sent: !") // 这行代码会等待
	}()

	// 让生产者先执行一会儿
	time.Sleep(2 * time.Second)

	// 消费者取出数据
	fmt.Println("Received:", <-messages)
	fmt.Println("Received:", <-messages)
	fmt.Println("Received:", <-messages) // 这里才会解除第三个消息的阻塞
}

正常情况下生产者消费者 只能一次发送或接收一个数据,但也可以通过传切片、或者使用range 通道的方式一次传递或者接收多个数据

通道同步

通过让主线程作为接收者阻塞接收消息来达到线程的同步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
	"fmt"
	"time"
)

func worker(done chan bool) {
	fmt.Println("worker")
	time.Sleep(time.Second)
	fmt.Println("done")

	done <- true

}

func main() {
	done := make(chan bool, 1)
	go worker(done)
	<-done

}

通道方向

当使用通道作为函数参数时,可以指定通道的方向,来提高程序的安全性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "fmt"

func ping(pings chan<- string, msg string) {
	pings <- msg
}

func pong(pings <-chan string, pongs chan<- string) {
	msg := <-pings
	pongs <- msg
}

func main() {
	pings := make(chan string, 1)
	pongs := make(chan string, 1)
	ping(pings, "passed message")
	pong(pings, pongs)
	fmt.Println(<-pongs)
}

select

将channel和select结合起来

select 语句使得一个 goroutine 可以等待多个 channel 的操作(接收或发送)。它会阻塞直到其中一个 case 可以执行(即某个 channel 完成了接收或发送)。如果有多个 case 同时准备好,select 会随机选择其中一个执行

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"
    "time"
)

func main() {

    c1 := make(chan string)
    c2 := make(chan string)

    go func() {
        time.Sleep(1 * time.Second)
        c1 <- "one"
    }()
    go func() {
        time.Sleep(2 * time.Second)
        c2 <- "two"
    }()
		//如果单独接收两个那就会3s,而使用select会并行处理两个通道,这样只会用2s
    for i := 0; i < 2; i++ {
        select {
        case msg1 := <-c1:
            fmt.Println("received", msg1)
        case msg2 := <-c2:
            fmt.Println("received", msg2)
        }
    }
}
超时

这里是模拟一个超时的场景,即在调用其他应用的过程中,虽然需要该应用的返回值,但是主线程有极其严格的时间要求,所以当超过规定时间后,主线程就会决定进行超时处理,放弃返回值

这里使用到的就是select,<-time.After 等待在 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
39
40
41
42
package main

import (
	"fmt"
	"time"
)

func main() {
	//这里使用的缓冲通道,这样goroutine在将数据放入通道后,就可以结束
	c1 := make(chan string,1)
	go func() {
		//模型应用需要2s才能返回结果
		time.Sleep(2 * time.Second)
		c1 <- "result 1"
	}()
	//select是每次只会选择一个case里面的执行,然后就会跳过
	//所以这里即便res没有从通道中接收值,也不会阻塞,在执行过超时处理之后,就会往下进行执行
	select {
	case res := <-c1:
		fmt.Println(res)
    //如果超过1s,就会执行这条语句,进行超时处理
	case <-time.After(1 * time.Second):
		fmt.Println("timeout 1")
	}

	c2 := make(chan string, 1)
	go func() {
		time.Sleep(2 * time.Second)
		c2 <- "result 2"
	}()
	select {
	case res := <-c2:
		fmt.Println(res)
	case <-time.After(3 * time.Second):
		fmt.Println("timeout 2")
	}
}

/*
timeout 1
result 2
*/
非阻塞通道操作

select 还可以配合 default 语句来实现 非阻塞操作。如果所有的 case 都不能立刻执行,select 会进入 default,避免阻塞

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
package main

import "fmt"

func main() {
	messages := make(chan string)
	signals := make(chan bool)

	select {
	case msg := <-messages:
		fmt.Println("received message", msg)
	default:
		fmt.Println("no message received")
	}

	msg := "hi"
	select {
	case messages <- msg:
		fmt.Println("sent message", msg)
	default:
		fmt.Println("no message sent")
	}

	select {
	case msg := <-messages:
		fmt.Println("received message", msg)
	case sig := <-signals:
		fmt.Println("received signal", sig)
	default:
		fmt.Println("no activity")
	}
}

关闭通道

关闭通道可以用于不知道通道内容有多少,要使用for循环一直接收的情况

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
package main

import "fmt"

func main() {
	jobs := make(chan int, 5)
	done := make(chan bool)

	go func() {
    //这里使用死循环,一直从通道中获取值
    //只有通道已经关闭,且没有剩余数据时的时候more就会为false
		for {
      //使用的是有缓冲的通道,所以作为接收者,当通道有内容就会进行下去,没有就会阻塞
			j, more := <-jobs
			if more {
				fmt.Println("received job", j)
			} else {
				fmt.Println("received all jobs")
				done <- true
				return
			}
		}
	}()
	//因为使用的是缓冲通道,所以可以一次性全放进通道,往下走下去
	for j := 1; j <= 3; j++ {
		jobs <- j
		fmt.Println("sent job", j)
	}
  //关闭通道
	close(jobs)
	fmt.Println("sent all jobs")
	//使用通道阻塞的方式等goroutine结束
	<-done
	//只有通道已经关闭,且没有剩余数据时,ok为false
	_, ok := <-jobs
	fmt.Println("received more jobs:", ok)
}
range通道
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import "fmt"

func main() {

	queue := make(chan string, 2)
	queue <- "one"
	queue <- "two"
	close(queue)
	//关闭通道后,也可以继续遍历,从通道中取值
	for elem := range queue {
		fmt.Println(elem)
	}
}

计数器

我们经常希望在未来的某个时间点执行 Go 代码,或者以某个间隔重复执行。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
package main

import (
	"fmt"
	"time"
)

func main() {

	//创建了一个定时器 timer1,它将在 2 秒后触发,并通过 timer1.C 发送一个值
	timer1 := time.NewTimer(2 * time.Second)
	//用于阻塞当前 goroutine,直到定时器的 channel C 被触发(也就是 2 秒后)
	<-timer1.C
	fmt.Println("Timer 1 fired")

	timer2 := time.NewTimer(2 * time.Second)
	go func() {
		//然后启动一个新的 goroutine 来监听 timer2.C
		//这里如果定时器是被关闭的,通道就会立刻关闭,返回0值,后面的就不执行了
		fmt.Println("1")
		//语句立即返回并继续执行代码,defer 语句确实没有机会执行
		defer fmt.Println("2")
		<-timer2.C
		fmt.Println("Timer 2 fired")
	}()
	//调用会尝试停止定时器。
	//如果定时器还没有触发,它会成功停止,返回 true。
	//如果定时器已经触发,它会返回 false
	time.Sleep(time.Second)
	stop2 := timer2.Stop()
	if stop2 {
		fmt.Println("Timer 2 stopped")
	}

	time.Sleep(2 * time.Second)
}
/*
Timer 1 fired
1
Timer 2 stopped
*/
ticker

timer用于想未来某段时间后做一件事,ticker用于想要定期重复做某件事

ticker和timer有类似的机制,通过通道来发送消息,表示状态

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"
    "time"
)

func main() {

    ticker := time.NewTicker(500 * time.Millisecond)
    done := make(chan bool)

    go func() {
       for {
          select {
          case <-done:
             return
          //每隔500 * time.Millisecond发送一次
          case t := <-ticker.C:
             fmt.Println("Tick at", t)
          }
       }
    }()
		//主线程会在这阻塞一段时间,以便ticker多发送几次
    time.Sleep(1600 * time.Millisecond)
  	//关闭ticker
    ticker.Stop()
    done <- true
    fmt.Println("Ticker stopped")
}
worker-pools

主要用于处理大量并发任务,同时又要控制资源使用

它解决的核心问题是:有很多任务要处理,但不能无限制地开启 goroutine 或线程,否则会耗尽资源

本质上相当于java中的线程池,通过通道来传递任务,这段代码的作用就是创建了三个线程,不断的从jobs中取任务执行

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
package main

import (
    "fmt"
    "time"
)

func worker1(id int, jobs <-chan int, results chan<- int) {
    for j := range jobs {
       fmt.Println("worker", id, "started job", j)
       time.Sleep(time.Second)
       fmt.Println("worker", id, "finished job", j)
       results <- j * 2
    }
}

func main() {
    const numJobs = 5
    jobs := make(chan int, numJobs)
    results := make(chan int, numJobs)
    for w := 1; w <= 3; w++ {
      //启动了 3 个并发的 worker(worker1 ~ worker3),它们共享同一个 jobs 通道
       go worker1(w, jobs, results)
    }
    for j := 1; j <= numJobs; j++ {
      //向 jobs 通道发送 5 个任务(1 到 5)
       jobs <- j
    }
  	//发送完任务后关闭 jobs 通道,通知所有 worker 没有任务了
    close(jobs)
    for a := 1; a <= numJobs; a++ {
      //主线程逐次从通道中拿取结果,主要用于阻塞主线程
       <-results
    }
}
WaitGroups

要等待多个 goroutine 完成,我们可以使用WaitGroups

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
package main

import (
	"fmt"
	"sync"
	"time"
)

func worker2(id int) {
	fmt.Printf("Worker %d starting\n", id)

	time.Sleep(time.Second)
	fmt.Printf("Worker %d done\n", id)
}

func main() {

	var wg sync.WaitGroup

	for i := 1; i <= 5; i++ {
		//启动多个 goroutine 并增加每个 goroutine 的 WaitGroup 计数器
		wg.Add(1)
		//创建一个闭包,这个的目的是因为waitgroup需要使用done才能算结束一个
		//所以是需要defer wg.Done(),为了解偶,不将无关代码写到worker2中,所以就另创建了一个func
		go func() {
			defer wg.Done()
			worker2(i)
		}()
	}

	wg.Wait()

}

限流

在服务端处理请求时,限流用于防止服务过载。它通过控制单位时间内处理的请求数量,避免系统资源被耗尽。

Go 的 time.Tickchannel 的组合可以实现非常简洁的限流逻辑。

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
package main

import (
    "fmt"
    "time"
)

func main() {

    //1、基本限流
    //创建一个缓冲通道 requests,模拟要处理的 5 个请求。
    requests := make(chan int, 5)
    for i := 1; i <= 5; i++ {
       requests <- i
    }
    //用于关闭通道,当你通过 range 遍历通道时,如果不关闭通道,接收方就无法知道数据何时结束
    close(requests)
    //limiter 是一个定时器通道,每 200ms 发出一个事件。作用是限制处理速率为 每 200ms 一个请求
    limiter := time.Tick(200 * time.Millisecond)

    //每次处理请求前,从 limiter 中读取一个值,相当于“等待许可”,确保处理频率不会超过限制
    for req := range requests {
       <-limiter
       fmt.Println("request", req, time.Now())
    }
    //2、突发限流
    //创建一个限制器,类似于创建一个数量为3的令牌桶
    burstyLimiter := make(chan time.Time, 3)

    for i := 0; i < 3; i++ {
       burstyLimiter <- time.Now()
    }
    //后台协程每 200ms 向 burstyLimiter 添加一个许可(如果没满),以便持续补充“突发令牌”。
    go func() {
       for t := range time.Tick(200 * time.Millisecond) {
          burstyLimiter <- t
       }
    }()
    //创建一个大小为5的任务需求
    burstyRequests := make(chan int, 5)
    for i := 1; i <= 5; i++ {
       burstyRequests <- i
    }
    close(burstyRequests)
    //遍历任务需求,只有在令牌桶中有令牌的时候,才会去执行任务,以此来限流
    for req := range burstyRequests {
       <-burstyLimiter
       fmt.Println("request", req, time.Now())
    }
}
原子计数器

用并发安全的方式统计一个变量在多协程中被累加的总次数

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
package main

import (
    "fmt"
    "sync"
    "sync/atomic"
)

func main() {

    //定义一个 atomic.Uint64 类型变量 ops
    //确保多线程环境下读写变量是原子的,即不可分割的操作,不会产生竞争条件(data race)
    var ops atomic.Uint64

    var wg sync.WaitGroup

    for i := 0; i < 50; i++ {
       wg.Add(1)

       go func() {
          for c := 0; c < 1000; c++ {
						
             ops.Add(1)
          }

          wg.Done()
       }()
    }

    wg.Wait()
		//50000
    fmt.Println("ops:", ops.Load())
}
互斥锁

通过 sync.Mutex 来实现互斥锁,确保多个 goroutine 在并发修改资源时不会发生数据竞争

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
package main

import (
    "fmt"
    "sync"
)

type Container struct {
    //互斥锁,保护 counters 变量
    mu sync.Mutex
    //存储计数器的 map,键是字符串,值是对应的计数
    counters map[string]int
}

func (c *Container) inc(name string) {
    //加锁,防止其他 goroutine 同时访问 counters
    //这样保证同一时间只有一个线程可以访问map
    c.mu.Lock()
    defer c.mu.Unlock()
    c.counters[name]++
}

func main() {
    c := Container{

       counters: map[string]int{"a": 0, "b": 0},
    }

    var wg sync.WaitGroup
    //doIncrement 是一个闭包(closure),用于递增指定 name 的计数器 n 次
    doIncrement := func(name string, n int) {
       for i := 0; i < n; i++ {
          c.inc(name)
       }
       wg.Done()
    }

    wg.Add(3)
    go doIncrement("a", 10000)
    go doIncrement("a", 10000)
    go doIncrement("b", 10000)

    wg.Wait()
    fmt.Println(c.counters)
}
状态 Goroutines

在go中不仅可以使用互斥锁来实现多线程下资源共享,还可以使用goroutine 和通道的内置同步功能来实现相同的结果,通过通信并让每段数据都归 1 个 goroutine 所有

这种方式实现并发安全的原理是通过单个goroutine的func来管理state这个资源,其他多个goroutines想要访问都是通过将请求放入对应的通道中,由这个单个的goroutinue进行处理

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
package main

import (
    "fmt"
    "math/rand"
    "sync/atomic"
    "time"
)

// 用于读取 map 的操作
type readOp struct {
    key int
    //用于返回读取的值
    resp chan int
}

// 用于写入 map 的操作
type writeOp struct {
    key int
    val int
    //用于确认写入成功
    resp chan bool
}

func main() {
    //通过 atomic 来进行原子计数,确保并发安全
    var readOps uint64
    var writeOps uint64
    //用于接收读请求的通道
    reads := make(chan readOp)
    //用于接收写请求的通道
    writes := make(chan writeOp)
  	//这个函数的作用就是不断接收请求,对state进行修改
    go func() {
       //维护一个 map[int]int 作为共享状态
       var state = make(map[int]int)
       for {
          //通过 select 来监听 reads 和 writes 通道
          select {
          //接收到读请求后,将state中对应的值传给resp
          case read := <-reads:
             read.resp <- state[read.key]
          //接收到写请求后,将要写的值记录在state中,并修改通道中的状态
          case write := <-writes:
             state[write.key] = write.val
             write.resp <- true

          }
       }
    }()
    for r := 0; r < 100; r++ {
       go func() {
          for {
             //创建一个读请求
             read := readOp{
                key:  rand.Intn(5),
                resp: make(chan int),
             }
             //发送读请求
             reads <- read
             //等待返回值
             <-read.resp
             atomic.AddUint64(&readOps, 1)
             time.Sleep(time.Millisecond)
          }
       }()
    }
    for w := 0; w < 10; w++ {
       go func() {
          for {
             //创建一个写请求
             write := writeOp{
                key:  rand.Intn(5),
                val:  rand.Intn(100),
                resp: make(chan bool)}
             //发送写请求
             writes <- write
             <-write.resp
             atomic.AddUint64(&writeOps, 1)
             time.Sleep(time.Millisecond)
          }
       }()
    }

    time.Sleep(time.Second)
    //获取readOps的值
    readOpsFinal := atomic.LoadUint64(&readOps)
    fmt.Println("readOps:", readOpsFinal)
    writeOpsFinal := atomic.LoadUint64(&writeOps)
    fmt.Println("writeOps:", writeOpsFinal)

}

排序

基本排序
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"
	"slices"
)

func main() {

	strs := []string{"c", "a", "b"}
  //对切片进行升序排序
	slices.Sort(strs)
	fmt.Println("Strings:", strs)

	ints := []int{7, 2, 4}
	slices.Sort(ints)
	fmt.Println("Ints:   ", ints)
	//检查切片是否已经排序
	s := slices.IsSorted(ints)
	fmt.Println("Sorted: ", s)
}

自定义排序

即自定义排序,想要让集合按照自己规定的方式进行排序

可以使用cmp.Compare()➕slices.SortFunc(slice, func(a, b T) int)实现

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
package main

import (
    "cmp"
    "fmt"
    "slices"
)

func main() {
    fruits := []string{"peach", "banana", "kiwi"}

    lenCmp := func(a, b string) int {
       //升序
       //return cmp.Compare(len(a), len(b))
       //降序
       return cmp.Compare(len(b), len(a))
    }

    slices.SortFunc(fruits, lenCmp)
    fmt.Println(fruits)

    type Person struct {
       name string
       age  int
    }

    people := []Person{
       Person{name: "Jax", age: 37},
       Person{name: "TJ", age: 25},
       Person{name: "Alex", age: 72},
    }

    strcutcmp := func(a, b Person) int {
       return cmp.Compare(a.age, b.age)
    }
    slices.SortFunc(people, strcutcmp)
    fmt.Println(people)
}