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并继续执行程序,其原理是:
panic
会立即终止当前函数,并开始向上回溯调用栈(stack unwinding)。- 直到
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
)。
通过 Encoder
和 Decoder
进行 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
并发
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 := <-messages
在main
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.Tick
和 channel
的组合可以实现非常简洁的限流逻辑。
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)
}