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
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 := <-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)
}