0%

go语言学习

一、前言

此篇主要讲解go的通用语法和一些linux专属操作,windows专属操作可以看 go for windows

1. 环境

1
2
=> go version
go version go1.16.4 linux/amd64

2. 安装gvm版本管理工具

2.1. 安装

1
2
wget https://raw.githubusercontent.com/moovweb/gvm/master/binscripts/gvm-installer
bash gvm-installer
  • 默认生效到~/.bashrc,生效到zsh需要将下面一句话加到.zshrc最后
1
[[ -s "/home/test/.gvm/scripts/gvm" ]] && source "/home/test/.gvm/scripts/gvm"

2.2. 使用

1
2
3
4
5
6
7
8
# 查看可安装的版本
gvm listall
# 查看当前版本
gvm list
# 安装一个版本
gvm install go1.18.3
# 使用一个版本,回归默认就是 gvm use system
gvm use go1.18.3

二、语法相关

1. 基本类型

1.1. 去除变量未定义提示

1
_ = varA

1.2. int

1) 大小
  • 32位下int为4个字节,64位下int为8个字节
2) 类型转换
1
2
// 转string
fmt.Sprint(123)

1.2. 类型转换

float32 float64

1
2
3
// 转int
var f float32 = 1
var a int = int(f)

[]byte

1
2
3
// 转string
data := []byte("aaa")
str := string(data)

interface

类型判断

类型断言

1
2
3
4
5
var f interface{}

if _, ok = f.(string); !ok {
fmt.Println("f type is not string")
}

switch类型判断

1
2
3
4
5
6
7
8
9
10
11
12
13
var f interface{}

switch f := f.(type) {
case string:
fmt.Println("f type is string")
f = "xxx" // 这里的f为string类型

case int:
fmt.Println("f type is int")

default:
fmt.Println("f type is unknown")
}

1.2. string

1) 转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 转[]byte
var jsonStr string
jsonByte := []byte(jsonStr)

// 转byte
testByte := jsonStr[0]

// 转uint64
// The bitSize argument specifies the integer type
// that the result must fit into. Bit sizes 0, 8, 16, 32, and 64
// correspond to uint, uint8, uint16, uint32, and uint64.
var num uint64
num, err := strconv.ParseUint("1234", 10, 64)

// 转int64
// The bitSize argument specifies the integer type
// that the result must fit into. Bit sizes 0, 8, 16, 32, and 64
// correspond to int, int8, int16, int32, and int64.
var num uint64
num, err := strconv.ParseInt("1234", 10, 64)

2) 单引号,双引号,反引号的区别

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

import (
"fmt"
)

func main() {
fmt.Printf("%c %d\n", 'a', 'a') // 单引号代表byte
fmt.Printf("a\n") // 双引号可以输入转义字符
fmt.Printf("%s\n", `a\n`) // 反引号会忽略转义
}

输出

1
2
3
a 97
a
a\n

2) 字符串截取

1
2
3
4
5
6
7
outStr := "abcdefg"
fmt.Println(outStr[0]) // 97,被识别成byte打印了
fmt.Println(outStr[:3]) // abc
fmt.Println(outStr[3:5]) // de
fmt.Println(outStr[5:]) // fg

fmt.Println(outStr[:-2]) // 不支持,索引仅支持正数

3) 遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
s := "abc"
for i := range(s) {
fmt.Println(i)
}
/* 输出
0
1
2
*/

for i, v := range(s) {
fmt.Println(i, byte(v))
}
/* 输出
0 a
1 b
2 c
*/

4) 修改

  • string类型里面的元素是不可变的,除非赋值一个新的string
  • 如果想要修改string中某一个元素,相当于拷贝了两次
1
2
3
4
5
6
a := "abc"
tmp := []byte(a) // 做了一次拷贝
tmp[0] = 'z'
fmt.Println(a) // abc
a = string(tmp) // 又拷贝了一次
fmt.Println(a) // zbc

2. map

  • 底层使用哈希map实现而非红黑树

2.1. 一些基本操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/****** map初始化 ******/
xxxMap := make(map[string]interface{})
check := map[byte]byte{
')': '(',
'[': ']',
'{': '}',
}

/****** 判断map是否存在key ******/
if _, ok := xxxMap[key]; ok {
// 存在key
}

/****** 遍历map ******/
// key, value
for key, value := range xxxMap {
// 操作map
}
// 只要key
for key := range xxxMap {
// do something
}

2.2. map取不存在的值

  • 对map取值可以获取一个返回值也可以获取两个返回值用于判断是否存在
  • 如果不存在采用一个返回值会返回对应value的类型的默认值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
boolMap := map[int]bool{}
intMap := map[int]int{}
stringMap := map[int]string{}
sliceMap := map[int][]int{}
mapMap := map[int]map[int]int{}
interfaceMap := map[int]interface{}{}
fmt.Println(boolMap[1]) // false
fmt.Println(intMap[1]) // 0
fmt.Println(stringMap[1]) //
fmt.Println(sliceMap[1], sliceMap[1] == nil) // [] true
fmt.Println(mapMap[1], mapMap[1] == nil) // map[] true
fmt.Println(interfaceMap[1]) // <nil>
}

2.3. 底层实现

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
// /usr/lib/go/src/cmd/compile/internal/types/type.go
// Map contains Type fields specific to maps.
type Map struct {
Key *Type // Key type
Elem *Type // Val (elem) type

Bucket *Type // internal struct type representing a hash bucket
Hmap *Type // internal struct type representing the Hmap (map header object)
Hiter *Type // internal struct type representing hash iterator state
}

// /usr/lib/go/src/runtime/map.go
// A header for a Go map.
type hmap struct {
// Note: the format of the hmap is also encoded in cmd/compile/internal/reflectdata/reflect.go.
// Make sure this stays in sync with the compiler's definition.
count int // # live cells == size of map. Must be first (used by len() builtin)
flags uint8
B uint8 // log_2 of # of buckets (can hold up to loadFactor * 2^B items)
noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
hash0 uint32 // hash seed

buckets unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.
oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing
nevacuate uintptr // progress counter for evacuation (buckets less than this have been evacuated)

extra *mapextra // optional fields
}

// /usr/lib/go/src/cmd/compile/internal/types/type.go
// NewMap returns a new map Type with key type k and element (aka value) type v.
func NewMap(k, v *Type) *Type {
t := newType(TMAP)
mt := t.MapType()
mt.Key = k
mt.Elem = v
if k.HasTParam() || v.HasTParam() {
t.SetHasTParam(true)
}
if k.HasShape() || v.HasShape() {
t.SetHasShape(true)
}
return t
}

// /usr/lib/go/src/cmd/compile/internal/types/type.go
// New returns a new Type of the specified kind.
func newType(et Kind) *Type {
t := &Type{
kind: et,
width: BADWIDTH,
}
t.underlying = t
// TODO(josharian): lazily initialize some of these?
switch t.kind {
case TMAP:
t.extra = new(Map)
...
}
return t
}

注意事项

  • var xxx map[string]interface{}定义的xxx是一个空指针,没有指向任何地址,不能进行赋值
  • 初始化map需要使用make

3. array

3.1. 一些基本操作

1
2
3
4
5
6
7
8
/****** array初始化 ******/
arr0 := [...]int{0, 1}
arr1 := [...]int{0, 1}

/****** 遍历 ******/
for _, v := range arr0 {
fmt.Println(v)
}

5. 流程控制语句

5.1. 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
// 死循环
for {
...
}
// for i 形式
for i := 0; i < len(a); i++ {
...
}
// 遍历
for _, v := range(arr) {
...
}
// do while
for {
...
if expression {
break
}
}
// while
for {
if expression {
break
}
...
}

5.2. switch

  • case多条件写成逗号形式,c那样的写法不适用
1
2
3
4
5
6
7
8
9
switch v.key {
case "Maker":
...
case "OS":
...
// 多条件判断
case "linuxOS", "linuxKernel", "linuxArch":
...
}

6. 函数

6.1. 定义和返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 正常定义
func test(a int) int {
return a + 1
}
// 多返回值
func test(a int) (int, int) {
return a+1, a-1
}
// nil 返回初始化定义,仅对slice和map生效
func test(a int) []int {
return nil // 相当于return []int{}
}
// 返回值提前声明
func test(a int) (result int) {
result = a + 1
return
}

6.2. 值传递

(1) go函数都是值传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func changeDatas(intData int, strData string, arrData [3]int) {
intData += 2
strData += "bbb"
arrData[0] = 999
}

func main() {
intData := 1
strData := "aaa"
arrData := [...]int{1, 2, 3}
fmt.Println(intData, strData, arrData)
changeDatas(intData, strData, arrData)
fmt.Println(intData, strData, arrData)
}

输出

1
2
1 aaa [1 2 3]
1 aaa [1 2 3]

(2) slice、map、channel类型的看似引用传递的解释

slice
  • slice本质是一个结构体,内部包含三要素:长度、容量、数据首地址
  • 所以传递的也是这三个值,数据内容改变改的是地址里面的内容,外部可见,但是改了长度和容量,外部就不会变
1
2
3
4
5
6
7
8
9
10
11
12
func changeDatas(sliceData []int, mapData map[string]int) {
sliceData[0] = 999
mapData["test"] = 123
}

func main() {
sliceData := []int{1, 2, 3}
mapData := make(map[string]int)
fmt.Println(sliceData, mapData)
changeDatas(sliceData, mapData)
fmt.Println(sliceData, mapData)
}

输出

1
2
[1 2 3] map[]
[999 2 3] map[test:123]
map和channel都是指针
  • 本质上map和channel都是指针,所以改动外部都可见

7. 组合赋值

  • 组合赋值会先将值取出再赋值给前面的变量,所以go不需要swap方法
1
2
3
a, b := 1+1, 2+2
// 交换两个元素的值
a, b = b, a

8. i++、i–

1
2
3
i++		// 理解成 i+=1
j=i++ // error,i++是i+=1,是语句,非表达式,不能赋值
++i // error,不存在++i

9. const & iota

9.1. itoa

参考golang const 内itoa 用法详解及优劣分析

(1) 每次 const 出现时,都会让 iota 初始化为0

1
2
3
4
5
const a = iota // a = 0
const (
b = iota // b = 0
c // c = 1
)

(2) 自定义类型

自增长常量经常包含一个自定义枚举类型,允许你依靠编译器完成自增设置。

1
2
3
4
5
6
7
8
type Newtype int

const (
T1 Newtype = iota // 0
T2 // 1
T3 // 2
T4 // 3
)

(3) 可跳过的值

1
2
3
4
5
6
7
8
9
10
type AudioOutput int

const (
OutMute AudioOutput = iota // 0
OutMono // 1
OutStereo // 2
_
_
OutSurround // 5
)

(4) 位掩码表达式

1
2
3
4
5
6
7
8
9
type Allergen int

const (
IgEggs Allergen = 1 << iota // 1 << 0 which is 00000001
IgChocolate // 1 << 1 which is 00000010
IgNuts // 1 << 2 which is 00000100
IgStrawberries // 1 << 3 which is 00001000
IgShellfish // 1 << 4 which is 00010000
)

(5) 定义数量级

1
2
3
4
5
6
7
8
9
10
11
12
13
type ByteSize float64

const (
_ = iota // ignore first value by assigning to blank identifier
KB ByteSize = 1 << (10 * iota) // 1 << (10*1)
MB // 1 << (10*2)
GB // 1 << (10*3)
TB // 1 << (10*4)
PB // 1 << (10*5)
EB // 1 << (10*6)
ZB // 1 << (10*7)
YB // 1 << (10*8)
)

(6) 定义在一行的情况

  • 跟普通形式 没什么不同
  • iota 在下一行增长,而不是立即取得它的引用。
1
2
3
4
5
6
7
8
9
10
11
const (
Apple, Banana = iota + 1, iota + 2 // 0+1, 0+2
Cherimoya, Durian // 1+1, 1+2
Elderberry, Fig // 2+1, 2+2
)

/*
1, 2
2, 3
3, 4
*/

(7) 中间插队

中间插队时,iota 会被覆盖掉 不再继续自增。但是用另一个 iota 接一下,又会继续自增。
示例如下,中间插入了5、3和6,3下面有itoa接,6没有。

1
2
3
4
5
6
7
8
9
const(
a = iota // 0 itoa = 0
b = 5 // 5 itoa = 1
c = 3 // 3 itoa = 2
d = iota // 3 itoa = 3
e = 6 // 6
f // 6
g // 6
)

10. 并发

10.1. 基础概念

和线程的区别

  • OS线程(操作系统线程)一本都有固定的栈内存(通常为2MB)
  • 一个 goroutine 的栈在其生命周期开始时只有很小的栈(典型情况下2KB),goroutine 的栈不是固定的,他可以按需增大和缩小,grorutine的栈大小限制可以达到1GB,但极少情况下会到1GB。所以在Go语言中一次创建十万左右的 grorutine 也是可以的。

和线程的关系

  • 一个操作系统线程对应用户态多个goroutine。
  • go程序可以同时使用多个操作系统线程。
  • goroutine和OS线程是多对多的关系,即m:n。

其他

  • 1.5版本之前,go仅占用一个核执行
  • 1.5之后,默认使用所有核

10.2. sync.WaitGroup 等待退出

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

var wg sync.WaitGroup

func hello(i int) {
fmt.Println("Hello", i)
wg.Done()
}

func main() {
fmt.Println("Hello world, main")
for i := 1; i < 1000; i++ {
wg.Add(1)
go hello(i)
}
wg.Wait()
}

10.3. runtime.GOMAXPROCS(i int) 限定占用核心数

  • go可以设定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
package main

import (
"fmt"
"sync"
"runtime"
)

var wg sync.WaitGroup

func hello(i int) {
fmt.Println("Hello", i)
wg.Done()
}

func main() {
runtime.GOMAXPROCS(1)
fmt.Println("Hello world, main")
for i := 1; i < 1000; i++ {
wg.Add(1)
go hello(i)
}
wg.Wait()
}

10.4. 互斥锁

1

11. 面向对象编程

11.1. 属性和方法

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

import (
"fmt"
)

// Rectangle define class
type Rectangle struct {
X int
Y int
}

// Area define method
func (r *Rectangle) Area() int {
return r.X * r.Y
}

func main() {
rec := Rectangle{
X: 5,
Y: 10,
}
fmt.Println(rec.Area())
}

11.2. 访问权限

  • 首字母大小写可以控制访问权限
  • 不过小写对整个package都可以访问,外部不可以访问

11.3. 继承

  • 结构体里面直接定义另一个结构体就可以实现继承
  • 但是初始化不能直接初始化父类的属性
  • 同一个包内,大小写都可以访问到
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"

// Rectangle define class
type Rectangle struct {
x int
Y int
}

// Area define method
func (r *Rectangle) Area() int {
return r.x * r.Y
}

type testRec struct {
Rectangle // 继承属性,不写不会继承属性,但是可以实现方法
}

// Area 可以调用,也可以像下面这样直接重写
// func (r *testRec) Area() int {
// return r.x * r.Y
// }

func main() {
rec := testRec{}
rec.x = 10
rec.Y = 4
fmt.Println(rec.Area())
}

11.4. 多态

  • 多态在go里面更多把思想转成接口
  • 对外提供接口,但是存在不同实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 定义接口Product,定义接口存在方法Use
type Product interface {
Use()
}

// productA继承接口
type productA struct {
Product
}

// 也可以继承另一个结构体
type productB struct {
productA
}

11.5. 析构函数

  • go可以给类设置析构函数,但是析构时机是GC触发的时机
  • 如果进程直接退出,由操作系统回收内存,不会触发析构函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import (
"fmt"
"runtime"
)

type testT struct {
a int
next *testT
}

func main() {
var root testT
runtime.SetFinalizer(&root, func (tmp *testT) {
fmt.Println("hhh", tmp.a)
})
}

11.6. 纯虚函数

  • 对于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
type TestI interface {
TestFunc()
Print()
}

type TestImpl struct{}

func (t *TestImpl) Print() {
fmt.Println("TestImpl Print")
}

type TestImpl1 struct{}

func Constructor() TestI {
return &TestImpl1{} // 这里取地址
}

func (t *TestImpl1) TestFunc() {
fmt.Println("TestImpl1 TestFunc")
}

func (t *TestImpl1) Print() {
fmt.Println("TestImpl1 Print")
}

func main() {
a := &TestImpl{} // 不会报错,没有实现不会认为是继承关系
a.Print()
var b TestI = &TestImpl{} // 有这句话就会报错,因为TestImpl没有完全实现TestI
var c TestI = Constructor()
var d *TestI = &c
c.Print() // 可以正常调用,调用的是子类的方法
d.Print() // 报错,因为找不到 func (t *TestI)Print() 的实现
}

12. 指针

12.1. int转指针

  • go中限制了指针类型的转换,不允许将int转成指针类型
  • 但是,转换一下想法,用二级指针进行赋值即可
1
2
3
4
var tmp **testT
tmp1 := 0x111111111
tmp = (**testT)(unsafe.Pointer(&tmp1))
fmt.Printf("%p", *tmp)

13. GC 垃圾回收机制

13.1. 内存管理

程序在内存上被分为堆区、栈区、全局数据区、代码段、数据区五个部分。对于C++等早期编程语言栈上的内存由编译器管理回收,堆上的内存空间需要编程人员负责申请与释放。在Go中栈上内存仍由编译器负责管理回收,而堆上的内存由编译器和垃圾收集器负责管理回收,给编程人员带来了极大的便利性。

13.2. GC触发时机

触发GC有俩个条件,一是堆内存的分配达到控制器计算的触发堆大小,初始大小环境变量GOGC,之后堆内存达到上一次垃圾收集的2倍时才会触发GC。二是如果一定时间内没有触发,就会触发新的循环,该触发条件由runtime.forcegcperiod变量控制,默认为2分钟。

13.3. GC带来的便利和坑

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
import (
"fmt"
"runtime"
"time"
"strconv"
"unsafe"
)

type testT struct {
a int
next *testT
}

func testFunc(root *testT) (arr []string) {
for i := 0; i < 5; i++ {
var tmp testT
tmp.a = i
// 引用了局部变量的地址,此变量在编译时会放到堆上而不是栈上
root.next = &tmp
arr = append(arr, fmt.Sprintf("%p", &tmp))
// 这里加上析构函数查看触发时机
runtime.SetFinalizer(&tmp, func (tmp *testT) {
fmt.Println("hhh", tmp.a)
})
root = root.next
}
return
}

func main() {
var root testT

arr := testFunc(&root)
// 手动触发GC,这里什么都回收不掉,因为后面还在使用
for i := 0; i < 10; i++ {
time.Sleep(time.Second)
runtime.GC()
}
for e := &root; e != nil; e = e.next {
fmt.Println(e.a)
}

// 手动触发GC,GC一次回收一个变量
for i := 0; i < 10; i++ {
time.Sleep(time.Second)
runtime.GC()
}
// 如果没有下面这一句使用了root,上面就会连带root的内存也回收掉,不会等到函数退出,因为分析后续没有再使用局部变量
// 有下面这一句使用了root,上面就不会回收root和子节点
fmt.Println(root.a)
}

14. struct

14.1. String() 默认打印方法

  • 使用值传递实现的,打印原始和指针类型都可以输出
  • 使用指针传递实现的,必须使用指针类型打印才能输出,值传递输出不出来
1
2
3
4
5
6
7
8
9
10
11
12
13
14
type A struct {
name string
}

// 实现String方法后,打印时会默认调用此方法
func (a A) String() string {
return "A name: " + a.name
}

func main() {
// 原型模式就是需要提供clone的接口,实现对对象的拷贝
a := A{name: "a"}
fmt.Println(a) // A name: a 默认调用String方法
}

14.2. 初始化父类属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type ProductA struct {
Name string
}

type ProductB struct {
ProductA
}

func main() {
p := ProductB{
ProductA: ProductA{
Name: "ProductB",
},
}
fmt.Println(p.Name)
}

15. 常量

  • go中常量只能是数字、布尔、字符串,其他类型无法定义成常量

16. package 包

16.1. init函数

  • init函数不可被调用,是golang提供的引入包就会调用的一个初始化函数
  • 对于package main来说也是一样的,会在main函数调用前进行调用
  • init可以定义多个,按照定义顺序执行

17. channel

17.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
var wg sync.WaitGroup
var wgW sync.WaitGroup

func read(i int, ch chan int) {
time.Sleep(1 * time.Second)
// 这里防止了惊群,不会同一个数据给到多个消费者
for a := range ch {
fmt.Println(i, a)
}
wg.Done()
}

func write(i int, ch chan int) {
ch <- i
fmt.Println(i, " write")
wgW.Done()
}

func main() {
ch := make(chan int, 100)
for i := 0; i < 500; i++ {
wg.Add(1)
go read(i, ch)
}
for i := 0; i < 50; i++ {
wgW.Add(1)
go write(i, ch)
}
wgW.Wait()
close(ch) // 这里立刻退出,不能再写数据了,但是可以读
fmt.Println("close channel")
wg.Wait()
}

17.2. close后的特性

  • close不会阻塞,关闭后可读不可写,写会崩溃
  • close后,正在阻塞的消费者可以拿到一个默认值,for的方式会直接退出循环
  • close后,正在阻塞的生产者会崩溃

1) 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
func main() {
ch := make(chan int)
go func() {
fmt.Println("begin write")
ch <- 1
fmt.Println("end write")
}()
time.Sleep(time.Second)
close(ch)
fmt.Println("close done")
time.Sleep(time.Second)
fmt.Println("main done")
}

/*
begin write
close done
panic: send on closed channel

goroutine 18 [running]:
main.main.func1()
/path/to/main.go:12 +0x6c
created by main.main
/path/to/main.go:10 +0x6c
exit status 2
*/

2) close之后可以读,把没读完的读完就退出了,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
func main() {
ch := make(chan int, 10)
go func() {
fmt.Println("begin write")
ch <- 1
ch <- 2
ch <- 3
ch <- 4
ch <- 5
fmt.Println("end write")
}()
time.Sleep(time.Second)
close(ch)
fmt.Println("close done")
for v := range ch {
fmt.Println(v)
}
v := <-ch // 没数据拿到默认值
fmt.Println(v)
}

/*
begin write
end write
close done
1
2
3
4
5
0
*/

17.3. 缓冲与无缓冲

  • 无缓冲channel只负责数据的流转不负责存储数据,所以发送前必须有数据的接收者,否则发送会阻塞
  • 缓冲channel可以储存部分数据,发送在缓冲区满之前可以发完继续运行

17.4. 只读和只写chan

  • 只读和只写一般由一个可读可写的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
func productor(wch chan<- int) <-chan bool {
quit := make(chan bool)
go func() {
for i := 0; i < 10; i++ {
fmt.Println("product", i)
wch <- i
}
quit <- true
}()
return quit
}

func consumer(rch <-chan int) <-chan bool {
quit := make(chan bool)
go func() {
for v := range rch {
fmt.Println("consumer", v)
time.Sleep(100 * time.Millisecond)
}
quit <- true
}()
return quit
}

func main() {
rwCh := make(chan int, 10)
qw := productor(rwCh)
qr := consumer(rwCh)
<-qw
close(rwCh)
<-qr
}

17.5. 底层原理

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
// /usr/lib/go/src/runtime/chan.go
type hchan struct {
qcount uint // total data in the queue
dataqsiz uint // size of the circular queue
buf unsafe.Pointer // points to an array of dataqsiz elements
elemsize uint16
closed uint32
elemtype *_type // element type
sendx uint // send index
recvx uint // receive index
recvq waitq // list of recv waiters
sendq waitq // list of send waiters

// lock protects all fields in hchan, as well as several
// fields in sudogs blocked on this channel.
//
// Do not change another G's status while holding this lock
// (in particular, do not ready a G), as this can deadlock
// with stack shrinking.
lock mutex
}

// /usr/lib/go/src/cmd/compile/internal/types/type.go
// Chan contains Type fields specific to channel types.
type Chan struct {
Elem *Type // element type
Dir ChanDir // channel direction
}

// /usr/lib/go/src/cmd/compile/internal/types/type.go
// NewChan returns a new chan Type with direction dir.
func NewChan(elem *Type, dir ChanDir) *Type {
t := newType(TCHAN)
ct := t.ChanType()
ct.Elem = elem
ct.Dir = dir
if elem.HasTParam() {
t.SetHasTParam(true)
}
if elem.HasShape() {
t.SetHasShape(true)
}
return t
}

// /usr/lib/go/src/cmd/compile/internal/types/type.go
// New returns a new Type of the specified kind.
func newType(et Kind) *Type {
t := &Type{
kind: et,
width: BADWIDTH,
}
t.underlying = t
// TODO(josharian): lazily initialize some of these?
switch t.kind {
...
case TCHAN:
t.extra = new(Chan)
...
}
return t
}
  • 底层也是指针,所以传参传入的是指针,不用担心被拷贝

18. slice

18.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
/****** slice初始化 ******/
s0 := []int{0, 1}
s1 := []int{0, 1}
s2 := make([]int, 5) // len 5, cap 5
s3 := make([]int, 0, 10) // len 0, cap 10

/****** slice添加元素 ******/
// go对append有处理,如果赋值给原切片,且切片空间足够,不会新申请内存
// 赋值给原切片,空间不够,会申请cap的两倍大小空间
// 如果赋值给不同切片,会新申请内存,不会更改原切片
s0 = append(s0, 2)
s0 = append(s0, 3, 4)
// 切片合并,其中s1...意味着将s1拆分成元素传入append
// 相当于调用添加多个元素
s0 = append(s0, s1...)

/****** slice删除元素 ******/
s0 = s0[2:] // 截取2到末尾,删除前两个元素
s1 = s1[:len(s1)-2] // 开头到倒数第3个元素,删除后两个元素
s0 = s0[:0] // 清理slice,不改变cap

/****** 遍历 ******/
for _, v := range s0 {
fmt.Println(v)
}

18.2. 切片截取

1
2
3
4
5
6
7
8
testArr := []int{2, 7, 4, 9, 1, 4, 8}
fmt.Println(cap(testArr), testArr) // 7 [2 7 4 9 1 4 8]
testArr = testArr[:len(testArr)-1]
fmt.Println(cap(testArr), testArr) // 7 [2 7 4 9 1 4]
testArr = testArr[1:]
fmt.Println(cap(testArr), testArr) // 6 [7 4 9 1 4]
testArr = testArr[:0] // 清理slice,不改变容量
fmt.Println(cap(testArr), testArr) // 6 []
  • 可以认为切片的截取就是在原切片的基础上,返回了一个切片,data的首地址、len、cap分别改了一下,本质上复用了原始的地址空间
1
2
3
4
5
6
7
8
9
10
func main() {
a := []int{
1, 2, 3, 4, 5,
}
fmt.Println(&a[0], &a[1]) // 0x40000160c0 0x40000160c8
b := a[:3]
fmt.Println(&b[0], &b[1]) // 0x40000160c0 0x40000160c8 和a完全一样
b[0] = 3
fmt.Println(a) // [3 2 3 4 5] 改动会改动到a里面
}

18.3. 动态初始化二维数组(矩阵)

  • 矩阵的长宽不确定,构建的时候就需要一个一个进行构建,类似下面这样
1
2
3
4
seen := make([][]bool, r)
for i := 0; i < r; i++ {
seen[i] = make([]bool, c)
}

18.4. 底层实现原理

底层数据结构

  • slice本身是一个结构体
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
// /usr/lib/go/src/runtime/slice.go
type slice struct {
array unsafe.Pointer
len int
cap int
}

// /usr/lib/go/src/cmd/compile/internal/types/type.go
// NewSlice returns the slice Type with element type elem.
func NewSlice(elem *Type) *Type {
if t := elem.cache.slice; t != nil {
if t.Elem() != elem {
base.Fatalf("elem mismatch")
}
if elem.HasTParam() != t.HasTParam() || elem.HasShape() != t.HasShape() {
base.Fatalf("Incorrect HasTParam/HasShape flag for cached slice type")
}
return t
}

t := newType(TSLICE)
t.extra = Slice{Elem: elem}
elem.cache.slice = t
if elem.HasTParam() {
t.SetHasTParam(true)
}
if elem.HasShape() {
t.SetHasShape(true)
}
return t
}

扩容

  • 基于golang v1.20.3版本代码
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
// /usr/lib/go/src/runtime/slice.go
// growslice allocates new backing store for a slice.
//
// arguments:
//
// oldPtr = pointer to the slice's backing array
// newLen = new length (= oldLen + num)
// oldCap = original slice's capacity.
// num = number of elements being added
// et = element type
//
// return values:
//
// newPtr = pointer to the new backing store
// newLen = same value as the argument
// newCap = capacity of the new backing store
//
// Requires that uint(newLen) > uint(oldCap).
// Assumes the original slice length is newLen - num
//
// A new backing store is allocated with space for at least newLen elements.
// Existing entries [0, oldLen) are copied over to the new backing store.
// Added entries [oldLen, newLen) are not initialized by growslice
// (although for pointer-containing element types, they are zeroed). They
// must be initialized by the caller.
// Trailing entries [newLen, newCap) are zeroed.
//
// growslice's odd calling convention makes the generated code that calls
// this function simpler. In particular, it accepts and returns the
// new length so that the old length is not live (does not need to be
// spilled/restored) and the new length is returned (also does not need
// to be spilled/restored).
// newLen 期望多少
// num 要添加多少
// oldCap 原来有多少容量
func growslice(oldPtr unsafe.Pointer, newLen, oldCap, num int, et *_type) slice {
oldLen := newLen - num // 原来有多少数据(非容量)
... // 省略检查
newcap := oldCap
doublecap := newcap + newcap // 原始容量double
if newLen > doublecap {
newcap = newLen // 要添加的超过double,要多少给多少
} else {
const threshold = 256
if oldCap < threshold {
newcap = doublecap // 不超过double,并且小于256,直接double容量
} else {
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
for 0 < newcap && newcap < newLen {
// Transition from growing 2x for small slices
// to growing 1.25x for large slices. This formula
// gives a smooth-ish transition between the two.
newcap += (newcap + 3*threshold) / 4 // 超过256,每次添加 1/4的当前容量 + 3/4的256也就是192 容量
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = newLen
}
}
}
... // 省略内存对齐和溢出检查,capmem 是 newcap * sizeof(item) 内存对齐后的大小(字节)
var p unsafe.Pointer
if et.ptrdata == 0 {
p = mallocgc(capmem, nil, false)
// The append() that calls growslice is going to overwrite from oldLen to newLen.
// Only clear the part that will not be overwritten.
// The reflect_growslice() that calls growslice will manually clear
// the region not cleared here.
memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
} else {
// Note: can't use rawmem (which avoids zeroing of memory), because then GC can scan uninitialized memory.
p = mallocgc(capmem, et, true)
if lenmem > 0 && writeBarrier.enabled {
// Only shade the pointers in oldPtr since we know the destination slice p
// only contains nil pointers because it has been cleared during alloc.
bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(oldPtr), lenmem-et.size+et.ptrdata)
}
}
memmove(p, oldPtr, lenmem)

return slice{p, newLen, newcap}
}

19. import 导入包

19.1. 普通导入

1
2
3
4
5
6
7
import (
"fmt"
)

func main() {
fmt.Println("Hello, world")
}

19.2. 别名导入

1
2
3
4
5
6
7
import (
f "fmt"
)

func main() {
f.Println("Hello, world")
}

19.3. 隐藏导入

  • 不调用,仅导入
1
2
3
4
5
6
import (
_ "fmt"
)

func main() {
}

19.4. 不需要使用包名字调用

1
2
3
4
5
6
7
8

import (
. "fmt"
)

func main() {
Println("Hello, world")
}

20. 泛型

20.1. 泛型函数

  • 类似于c++的模板函数
1
2
func handle[T any](l T) {
}

20.2. 泛型的限制

  • 泛型不能使用a.(type)获取类型,只能使用reflect.TypeOf()来获取
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func GetType[T any](a T) {
// 编译不允许
b := a.(int)
fmt.Println(b)
// 编译不允许
switch a.(type) {
case int:
fmt.Println("a is int")
}

// 使用下面方式获取类型
b := reflect.TypeOf(a)
fmt.Println("a is", b)

switch b.Kind() {
case reflect.Int:
fmt.Println("a is int")
case reflect.Bool:
fmt.Println("a is bool")
}
}

20.3. 自定义泛型类型

comparable

  • comparable只能针对!===两个操作符
1
2
3
4
5
6
7
8
9
10
11
12
13
14
func CompareSlice[T comparable](l, r []T) bool {
if len(l) != len(r) {
return false
}
if len(l) == 0 {
return true
}
for i := range l {
if l[i] != r[i] {
return false
}
}
return true
}

自定义其他类型

  • 使用下面的方式定义限定只能下面几种类型传递
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type compareType interface {
int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | string
}

func CompareUnorderSlice[T compareType](l, r []T) bool {
if len(l) != len(r) {
return false
}
if len(l) == 0 {
return true
}

a := DeepCopy(l)
b := DeepCopy(r)
sort.Slice(a, func(i, j int) bool {
return a[i] < a[j]
})
sort.Slice(b, func(i, j int) bool {
return b[i] < b[j]
})

return CompareSlice(a, b)
}

衍生类型

1
2
3
4
5
6
7
8
9
10
11
func Add[T int](a, b T) T {
return a + b
}

type TestInt int

func main() {
var a TestInt = 1
var b TestInt = 2
fmt.Println(Add(a, b)) // 编译报错
}
  • 使用~修饰的类型,可以使用其衍生类型
1
2
3
4
5
6
7
8
9
10
11
func Add[T ~int](a, b T) T {
return a + b
}

type TestInt int

func main() {
var a TestInt = 1
var b TestInt = 2
fmt.Println(Add(a, b)) // 编译通过
}

官方定义的衍生类型 golang.org/x/exp/constraints

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
// Signed is a constraint that permits any signed integer type.
// If future releases of Go add new predeclared signed integer types,
// this constraint will be modified to include them.
type Signed interface {
~int | ~int8 | ~int16 | ~int32 | ~int64
}

// Unsigned is a constraint that permits any unsigned integer type.
// If future releases of Go add new predeclared unsigned integer types,
// this constraint will be modified to include them.
type Unsigned interface {
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
}

// Integer is a constraint that permits any integer type.
// If future releases of Go add new predeclared integer types,
// this constraint will be modified to include them.
type Integer interface {
Signed | Unsigned
}

// Float is a constraint that permits any floating-point type.
// If future releases of Go add new predeclared floating-point types,
// this constraint will be modified to include them.
type Float interface {
~float32 | ~float64
}

// Complex is a constraint that permits any complex numeric type.
// If future releases of Go add new predeclared complex numeric types,
// this constraint will be modified to include them.
type Complex interface {
~complex64 | ~complex128
}

// Ordered is a constraint that permits any ordered type: any type
// that supports the operators < <= >= >.
// If future releases of Go add new ordered types,
// this constraint will be modified to include them.
type Ordered interface {
Integer | Float | ~string
}

21. defer

21.1. defer调用时机和顺序

  • defer是出函数作用域才调用,下面的调用是在main end之后
1
2
3
4
5
6
7
8
func main() {
for i := 0; i < 5; i++ {
defer func(i int) {
fmt.Println(i)
}(i)
}
fmt.Println("main end")
}

21.2. defer和命名返回值

  • 命名返回值,就算没有显式赋值,defer拿到的也是最终的返回值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func aaa() int {
return 5
}

func testFunc() (a int) {
a = 4
defer func() {
fmt.Println(a) // 5
}()
return aaa()
}

func main() {
testFunc()
}

22. 作用域

22.1. 作用域变化的:=

  • 作用域相同,不会新建同名变量
  • 作用域不相同,同名变量会新建,外面的不会更改
1
2
3
4
5
6
7
8
9
10
11
12
func aaa() (int, int) {
return 5, 6
}

func main() {
a := 4
defer func() {
fmt.Println(a) // 5
}()
a, b := aaa()
fmt.Println(a, b) // 5 6
}
1
2
3
4
5
6
7
8
9
10
11
12
func aaa() (int, int) {
return 5, 6
}

func main() {
a := 4
if true {
a, b := aaa()
fmt.Println(a, b) // 5 6
}
fmt.Println(a) // 4
}

23. 跨平台

  • go可以在文件头部指定特定平台编译文件
  • 具体支持的平台和架构使用go tool dist list查看

仅在linux的amd64下编译

1
2
//go:build linux && amd64
// +build linux,amd64

在linux或mac下编译

1
2
//go:build linux || darwin
// +build linux darwin

三、GMP运行模型

参考一些好的帖子

1. 几个原则

  • G goroutine
  • M machine 也是线程的抽象
  • P process 协程调度器
  • M必须绑定一个P才能执行
  • P默认是cpu核心数,可以使用runtime.GOMAXPROCS(2)设置P的数量
  • 每个P上面有一个G的队列不超过256个,M绑定P后,从P的队列中取G运行
  • 新的G创建出来后,先加入P的本地队列中,P的本地队列满了就取一半放到全局队列中
  • M把某个P上面的G队列执行完了,会从全局队列拿一批放到本地队列或者从其他P的本地队列偷一半到自己的本地队列中
  • 如果G中某个系统调用阻塞了M,go底层hook了相关系统调用,会阻塞的将会把G和M绑定一起从P中排除,然后找空闲的M,没有空闲的M就直接新建一个M
  • go程序启动会设置M的最大数量,默认10000. 但是内核很难支持这么多的线程数,所以这个限制可以忽略
  • 可以手动设置M的最大数量,使用debug.SetMaxThreads(6)设置,但是超过此数量会直接崩溃而不是不建立继续运行

2. 调试方法

  • 设置环境变量后,执行程序就会显示调度器的状态
1
export GODEBUG=schedtrace=1000
1
SCHED 1001ms: gomaxprocs=16 idleprocs=15 threads=9 spinningthreads=0 needspinning=0 idlethreads=2 runqueue=0 [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
  • SCHED:调试信息输出标志字符串,代表本行是 goroutine 调度器的输出;
  • 1001ms:即从程序启动到输出这行日志的时间;
  • gomaxprocs=16: P的数量,默认的 P 的属性是和 cpu 核心数量默认一致
  • idleprocs=15: 处于 idle 状态的 P 的数量;通过 gomaxprocs 和 idleprocs 的差值,我们就可知道执行 go 代码的 P 的数量;
  • threads=9: os threads/M 的数量,包含 scheduler 使用的 m 数量,加上 runtime 自用的类似 sysmon 这样的 thread 的数量;
  • spinningthreads=0: 处于自旋状态的 os thread 数量;
  • needspinning=0: 需要自旋的 os thread 数量;
  • idlethreads=2: 处于 idle 状态的 os thread 的数量;
  • runqueue=0: Scheduler 全局队列中 G 的数量;
  • [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]: 16个P的本地队列中的G的数量。

四、标准库

1. encoding/json

1.1. 一些基本操作

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

import (
"encoding/json"
)

func main() {
/****** 将json字符串序列化成go对象 ******/
// 先marshal在unmarshal可以让一个未知的interface确定内部类型
// Unmarshal可以将一个json字符串解析到一个特定的格式下
// json.Unmarshal会将数字解析成float64
json.Unmarshal(jsonBytes, &target)

/****** 将go对象转成json字符串 ******/
var f interface{}
jsonBytes, err := json.Marshal(&f)
// 格式化的字符串,第二个是前缀,第三个是缩进符号
jsonIndentBytes, err := json.MarshalIndent(&f, "", " ")
}

1.2. key排序转json

  • go默认对map[string]interface{}转json按照key升序转化
  • 想要对key排序只能通过结构体的方式,转json会按照结构体的顺序转化
  • 为空的key不想输出,可以加上omitempty
1
2
3
4
5
6
7
8
9
type outT struct {
Type string `json:"type,omitempty"`
Name string `json:"name,omitempty"`
}

func main() {
var f outT
str, _ := json.Marshal(&f) // {}
}

1.3. 单纯想压缩json不修改key顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
a := `{ "z": 1, "a": 2 }`
// 使用下面的方式会排序
var tmp interface{}
json.Unmarshal([]byte(a), &tmp)
out, _ := json.Marshal(tmp)
fmt.Println(string(out)) // {"a":2,"z":1}

// 使用这种方式就可以直接压缩
out2 := &bytes.Buffer{}
json.Compact(out2, []byte(a))
fmt.Println(out2.String()) // {"z":1,"a":2}
}

注意事项

  • 数字类型在Unmarshal之后会转成float64,只能断言为float64

2. testing 单测

  • 单测文件要以xxx_test.go的格式,函数名以Testxxx的格式,package和要测试的文件属于同一个
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 service

import (
"encoding/json"
"errors"
"reflect"
"sort"
"testing"

"github.com/gin-gonic/gin"

. "github.com/agiledragon/gomonkey"
. "github.com/smartystreets/goconvey/convey"
)

func TestFunc(t *testing.T) {
sess := session.Session{}
var s *session.Session

// hook类的成员函数Commit
patch := ApplyMethod(reflect.TypeOf(s), "Commit", func(_ *session.Session) error { return nil })
defer patch.Reset()

// 使用Convey包裹,可以按照分组测试
Convey("TestFunc", t, func() {
Convey("正常流程", func() {
Convey("插入单个数据", func() {
sess.New()
...
// So确定
So(tmp, ShouldResemble, []string{"aaa"})
})
})
Convey("异常流程", func() {
Convey("GetSessionData出错", func() {
sess.New()
...
patch1 := ApplyMethod(reflect.TypeOf(s), "GetSessionData",
func(_ *session.Session, _ string) (interface{}, error) {
return nil, errors.New("testErr")
})
...
So(tmp, ShouldResemble, []string{})
patch1.Reset()
...
So(tmp, ShouldResemble, []string{"bbb"})
})
})
})
}

2.2. 性能分析

参考go 性能优化之 benchmark + pprof

  • 函数需要使用下面的定义
1
2
3
func BenchmarkXXX(b *testing.B) {
...
}
  • 使用go test命令跑
1
2
3
4
5
6
# -bench xxx 测试哪个函数
# -run none 不跑单测函数
# -benchmem 表示打印函数执行过程中的内存分配
# -cpuprofile xxx.out cpu文件路径
# -memprofile xxx.out 内存文件路径
go test -bench BenchmarkXXX -run none -benchmem -cpuprofile cpuprofile.out -memprofile memprofile.out [package]
  • 正常结束后可以使用下面命令生成报告,会自动打开浏览器
1
go tool pprof -http=":8081" cpuprofile.out

3. flag 控制台参数解析

3.1. 获取命令行参数

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

import (
"fmt"
"flag"
)

func main() {
// 在使用前先调用此函数解析参数,不然会返回空
flag.Parse()
// 打印第一个参数
fmt.Println(flag.Arg(0))
// 打印所有非flag的参数
fmt.Println(flag.Args())
// 打印参数个数
fmt.Println(flag.NArg())
}

输出

1
2
3
4
=> go run main.go a -a -b -asdfsaf cccc
a
[a -a -b -asdfsaf ccc]
5

4. path/filepath 目录操作库

4.1. 遍历目录下所有文件和目录,类似 find xxx

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 (
"flag"
"fmt"
"os"
"path/filepath"
)

func getFileList(path string) {
err := filepath.Walk(path, func(path string, f os.FileInfo, err error) error {
if f == nil {
return err
}
if f.IsDir() {
return nil
}
println(path)
return nil
})
if err != nil {
fmt.Printf("filepath.Walk() return %v\n", err)
}
}

func main() {
flag.Parse()
var root string
root = flag.Arg(0)
getFileList(root)
}

输出

1
2
3
4
5
6
7
8
9
10
11
12
=> go run main.go .
go.mod
main.go
=> go run main.go ./
go.mod
main.go
=> go run main.go ../
../test/go.mod
../test/main.go
=> go run main.go /path/to/test
/path/to/test/go.mod
/path/to/test/main.go

4.2. 路径处理

1
2
3
4
5
func test() {
filePath := "/home/test/aaaa/test.txt"
fmt.Println(filepath.Base(filePath)) // 文件名 test.txt
fmt.Println(filepath.Dir(filePath)) // 路径名 /home/test/aaaa
}

5. os/exec 命令行调用

5.1. 阻塞式等待返回,并输出stdout

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

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

func execShell(s string) (string, error) {
cmd := exec.Command("/bin/bash", "-c", s)

var out bytes.Buffer
cmd.Stdout = &out

err := cmd.Run()
if err != nil {
fmt.Printf("%v\n", err)
}

return out.String(), err
}

func main() {
fmt.Println(execShell("ls /"))
}

6. net/http http网络请求库

6.1. 发送post请求

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"
"io/ioutil"
"net/http"
"strings"
)

func main() {
resp, err := http.Post("https://1.1.1.1:1234/a/b", "application/x-www-form-urlencoded", strings.NewReader("mobileId=!@#$%^"))
if err != nil {
fmt.Println(err)
}

defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println(err)
}
fmt.Println(string(body))
}

6.2. 忽略https的证书错误

(1) 修改默认客户端

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

import (
"fmt"
"io/ioutil"
"net/http"
"strings"
"crypto/tls"
)

func main() {
http.DefaultClient.Transport = &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}

resp, err := http.Post("https://1.1.1.1:1234/a/b", "application/x-www-form-urlencoded", strings.NewReader("mobileId=!@#$%^"))
...
}

6.3. 302不跳转

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func main() {
tr := http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
c := http.Client{
Transport: &tr,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
// 返回这个错误,302就不会继续跳转
return http.ErrUseLastResponse
},
}

resp, err := c.Get("https://10.240.17.108:8080")
...
}

6.4. 获取tcp的四元组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func main() {
// 修改DialContext才能拿到连接信息,将conn返回前赋值给conn变量
var conn net.Conn
var connLock sync.Mutex
tr := http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
var dialer net.Dialer
c, err := dialer.DialContext(ctx, network, addr)
if err == nil {
connLock.Lock()
conn = c
connLock.Unlock()
}
return c, err
},
}

c := http.Client{ Transport: &tr }
c.Get("https://10.240.17.108:8080")
connLock.Lock()
fmt.Println(conn.LocalAddr().String(), conn.RemoteAddr().String())
connLock.Unlock()
}

7. strings 字符串操作库

7.1. 字符串替换

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

import (
"strings"
)

func main() {
outStr := '{"a":"123"}'
// 将json中的双引号全部替换为单引号
outStr = strings.ReplaceAll(outStr, `"`, `'`)
}

7.2. 裁剪两端字符串

  • 只能裁剪两端的字符串,中间的不会裁剪
1
2
outStr := "abffffffffabfffffffab"
outStr = strings.Trim(outStr, "ab") // ffffffffabfffffff

7.3. 字符串分割

Split

  • 根据某字符串分割
1
arr := strings.Split("a,b,c", ",")	// [a b c]

Fields

  • 根据空格分割,多个空格视为一个
1
arr := strings.Fields("a b  c   d") // [a b c d]

7.4. 字符串比较

Compare

1
2
3
if strings.Compare(event1[1], event2[0]) < 0 {
// do something
}

8. encoding/hex 十六进制操作库

8.1. 将十六进制字符串转成数组

1
arr, err := hex.DecodeString("aabbcc")	// [170 187 204]

8.2. 将[]byte按照十六进制打印成字符串

1
hexStr := hex.EncodeToString([]byte("123456"))	// 313233343536

9. io/ioutil io操作(到go1.16之后可以使用io或os库替代)

9.1. 文件读取

1
contentBytes, err := ioutil.ReadFile("/a/b/c.txt")

10. os 系统操作

10.1. 目录操作

1) 基本操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//判断目录是否存在
_, err := os.Stat("./")
if err != nil && os.IsNotExist(err) {
// 目录不存在
}

// 获取当前二进制执行路径
execFilePath, _ := os.Executable()
fmt.Println(execFilePath) // /home/xxx/local/go/tools/tool

// 获取temp目录路径
os.TempDir()

// 切换工作目录
err := os.Chdir("/tmp")

// 创建目录
err := os.Mkdir(currentPath+"/tmp", 0777) // 类似mkdir xxx/tmp
err := os.MkdirAll(currentPath+"/tmp/a/b", 0777) // 类似mkdir -p xxx/tmp/a/b

// 删除目录
err := os.Remove(currentPath + "/tmp") // 删除文件或空文件夹
err := os.RemoveAll(currentPath + "/tmp") // 删除目录及其子目录

10.2. 文件操作

1) 整个文件读取

1
contentBytes, err := os.ReadFile("/a/b/c.txt")

2) 按行读取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func (s *defaultParser) ParseFile(filePath string, handler func(log string)) {
f, err := os.Open(filePath)
if err != nil {
panic(err)
}
defer f.Close()

rd := bufio.NewReader(f)
for {
line, err := rd.ReadString('\n') //以'\n'为结束符读入一行
fmt.Println(line)
if err != nil || io.EOF == err {
break
}
}
}

3) 写入文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
f, err := os.OpenFile(outpath, os.O_WRONLY|os.O_CREATE, 0644)
if err != nil {
panic(err)
}
defer f.Close()
nw, err := f.Write(buf.Bytes())
if err != nil {
panic(err)
}
if nw != buf.Len() {
panic(fmt.Errorf("need to write %d, but write %d", buf.Len(), nw))
}
}

11. stronv

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 (
"strconv"
"fmt"
)
func main() {
// 使用ParseFloat解析浮点数,64是说明使用多少位
// 精度来解析
f, _ := strconv.ParseFloat("1.234", 64)
fmt.Println(f)

// 对于ParseInt函数,0 表示从字符串推断整型进制,
// 则表示返回结果的位数
i, _ := strconv.ParseInt("123", 0, 64)
fmt.Println(i)

// ParseInt能够解析出16进制的数字
d, _ := strconv.ParseInt("0x1c8", 0, 64)
fmt.Println(d)

// 还可以使用ParseUint函数
u, _ := strconv.ParseUint("789", 0, 64)
fmt.Println(u)

// Atoi是解析10进制整型的快捷方法
k, _ := strconv.Atoi("135")
fmt.Println(k)

// 解析函数在遇到无法解析的输入时,会返回错误
_, e := strconv.Atoi("wat")
fmt.Println(e)
}

12. net 网络库

12.1. udp发送和接收

客户端

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 (
"flag"
"fmt"
"io"
"log"
"net"
"os"
)

func main() {
flag.Parse()
if flag.NArg() < 2 {
log.Fatalln("Must input 2 params")
}
serverAddr := flag.Arg(0)
filePath := flag.Arg(1)

fmt.Printf("client, serverAddr: %s, filePath: %s\r\n", serverAddr, filePath)

conn, err := net.Dial("udp", serverAddr)
if err != nil {
log.Fatalln(err)
}
defer conn.Close()

f, err := os.Open(filePath)
if err != nil {
log.Fatalln(err)
}
defer f.Close()

buf := make([]byte, 50*1024)
count := 0
i := 0
for {
n, err := f.Read(buf)
if err != nil {
if err == io.EOF {
break
} else {
log.Fatalln(err)
}
}
sendSize, err := conn.Write(buf[:n])
if err != nil {
log.Fatalln(err)
}
count += sendSize

msg := make([]byte, 128)
_, err = conn.Read(msg)
if err != nil {
log.Fatalln(err)
}

fmt.Printf("%d, Send %d, check %s\r\n", i, n, string(msg))
i++
}

fmt.Printf("Send %s to %s success, size %d", serverAddr, filePath, count)
}

服务端

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 (
"flag"
"fmt"
"log"
"net"
"os"
)

func main() {
flag.Parse()
if flag.NArg() < 2 {
log.Fatalln("Must input 2 params")
}
serverAddr := flag.Arg(0)
filePath := flag.Arg(1)

fmt.Printf("Hello, main, serverAddr: %s, filePath: %s\r\n", serverAddr, filePath)

f, err := os.Create(filePath)
if err != nil {
panic(err)
}
defer f.Close()

addr, err := net.ResolveUDPAddr("udp", serverAddr)
if err != nil {
log.Fatalln(err)
}

conn, err := net.ListenUDP("udp", addr)
if err != nil {
log.Fatalln(err)
}
defer conn.Close()

i := 0
for {
data := make([]byte, 100*1024)
n, rAddr, err := conn.ReadFromUDP(data)
if err != nil {
fmt.Println(err)
continue
}

fmt.Printf("%d, Recieved from %s size %d\r\n", i, rAddr.String(), n)

f.Write(data[:n])
i++

_, err = conn.WriteToUDP([]byte(fmt.Sprintf("%d", n)), rAddr)
if err != nil {
fmt.Println(err)
continue
}
}
}

12.2. 给socket设置opt

1) 普通类型设置

1

2) 自定义结构体设置

  • 设置sockaddr结构体
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
func main() {
...
dialer := &net.Dialer{
Control: func(network, address string, c syscall.RawConn) error {
return c.Control(func(fd uintptr) {
type sockaddr struct {
family uint16
port uint16
addr [4]byte
sinZero [20]byte
}

// 自定义结构体
addr := &sockaddr{
family: unix.AF_INET,
addr: netip.MustParseAddr("1.2.3.4").As4(),
}

cmd := 0x248 // 自定义命令
_, _, e1 := unix.Syscall6(unix.SYS_SETSOCKOPT, fd, uintptr(unix.IPPROTO_IP), uintptr(cmd), uintptr(unsafe.Pointer(addr)), unsafe.Sizeof(*addr), 0)
if e1 != 0 {
panic(e1.Error())
}
})
},
}
conn, err := dialer.Dial("tcp", addr)
if err != nil {
panic(err)
}
defer conn.Close()
...
}

13. time 时间库

13.1. 基本操作

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

import (
"time"
)

func main() {
// 秒级时间戳
timestampS := time.Now().Unix()
// 当前时间向前一天
yestoday := time.Now().Add(-24 * time.Hour)
// 时间距离当前多久,不受系统时间变化而变化
du := time.Since(yestoday)
// 当前到此时间经过多久,不受系统时间变化而变化
du := time.Util(yestoday)
}

13.2. 时钟说明

参考 go中使用单调时钟获得准确的时间间隔

  • time结构体记录了墙上时间和单调时间
  • 使用time.Now()获取的会保存墙上时间和单调时间一起,计算Since的时候会使用单调时间进行计算
  • 使用时间字符串得到的不会有单调时间,会随着系统时间变化而变化

14. sort 排序

14.1. 基本类型排序

1) 升序

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

import (
"sort"
)

func main() string {
var queryKeyList []string
var intList []int

sort.Strings(queryKeyList)
sort.Ints(intList)
}

2) 降序

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

import (
"fmt"
"sort"
)

func main() {
testArr := []int{2, 7, 4, 9, 1, 4, 8}

sort.Sort(sort.Reverse(sort.IntSlice(testArr)))
fmt.Println(testArr)
}
  • 解释一下上面的代码,sort.IntSlice(testArr)是将[]int强转成sort.IntSlice类型,类型定义了sort需要的三个函数,本质上还是[]int
  • sort.IntSlicesort.Interface的一个子类实现
  • sort.Reverse(xxx)将传入的sort.Interface转成sort.reverse类型返回
  • 同样sort.reverse也是sort.Interface的一个子类实现,将Less()方法重写,反着调用原方法
  • sort.Sort()sort.Interface进行排序

14.2. 二分查找

1) 普通类型

  • sort包提供两个函数进行二分查找,并提供了一些基本类型的查找
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import (
"fmt"
"sort"
)

func main() {
a := []int{1, 2, 2, 4, 4, 5}

// 二分查找从最左边开始的第一个满足条件的索引
fmt.Println(sort.Search(len(a), func(i int) bool { return a[i] >= 2 })) // 1
// 二分查找从最左边开始第一个 func(i) <= 0 的索引,并返回对应索引是否 func(i) == 0
fmt.Println(sort.Find(len(a), func(i int) int { return 3 - a[i] })) // 3, false
// 基本类型可以直接调用,但是只支持升序的slice
// 源码就是调用 sort.Search(len(a), func(i int) bool { return a[i] >= 2 }
fmt.Println(sort.SearchInts(a, 2)) // 1
}

2) 字符串使用strings.Compare()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func main() {
all := []string{"a", "b", "c", "d", "e"}

// 升序
sort.Slice(all, func(i, j int) bool {
return strings.Compare(all[i], all[j]) < 0
})
fmt.Println(all)
fmt.Println(sort.Find(len(all), func(i int) int {
// 升序,要查找的字符串放前面
return strings.Compare("c", all[i])
}))

// 降序
sort.Slice(all, func(i, j int) bool {
return strings.Compare(all[i], all[j]) > 0
})
fmt.Println(all)
fmt.Println(sort.Find(len(all), func(i int) int {
// 降序,要查找的字符串放后面
return strings.Compare(all[i], "c")
}))
}

14.3. 任意类型的slice排序

1) 数字类型

  • 直接使用sort.Slice排序,需要实现Less的函数
1
2
3
4
5
6
7
type item struct {
val, count int
}
itemSlice := make([]item, 10)
...
// 为true,i向前;false,j向前。要满足相等时返回false
sort.Slice(itemSlice, func(i, j int) bool { return itemSlice[i].val < itemSlice[j].val })

2) 字符串类型

1
2
3
4
5
6
7
8
9
func main() {
all := []string{"a", "b", "c", "d", "e"}
sort.Slice(all, func(i, j int) bool {
// 升序就是小于0
// 降序就是大于0
return strings.Compare(all[i], all[j]) < 0
})
fmt.Println(all)
}

14.4. 自定义类型排序

  • 需要定义一个类型的三个接口,然后调用sort.Sort即可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type NameSlice struct {
names []string
heights []int
}

func (x NameSlice) Len() int { return len(x.names) }
// 为true,i向前;false,j向前。要满足相等时返回false
func (x NameSlice) Less(i, j int) bool { return x.heights[i] > x.heights[j] }
func (x NameSlice) Swap(i, j int) {
x.heights[i], x.heights[j] = x.heights[j], x.heights[i]
x.names[i], x.names[j] = x.names[j], x.names[i]
}

func sortPeople(names []string, heights []int) []string {
tmp := NameSlice{
names: names,
heights: heights,
}
sort.Sort(tmp)
return tmp.names
}

14.5. sort内置类型

  • sort内置了几个基本类型的slice,实现了几个方法,可以直接用,如IntSlice
1
2
3
4
5
6
7
8
9
10
// /usr/lib/go/src/sort/sort.go
// IntSlice attaches the methods of Interface to []int, sorting in increasing order.
type IntSlice []int

func (x IntSlice) Len() int { return len(x) }
func (x IntSlice) Less(i, j int) bool { return x[i] < x[j] }
func (x IntSlice) Swap(i, j int) { x[i], x[j] = x[j], x[i] }

// Sort is a convenience method: x.Sort() calls Sort(x).
func (x IntSlice) Sort() { Sort(x) }

15. image 图像库

15.1. png处理

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 (
"bytes"
"fmt"
"image"
"image/color"
"image/png" // 包含这个才能解析png图片
"io/ioutil"
"os"
)

func main() {
originByte, _ := ioutil.ReadFile("./test.png")
originBuffer := bytes.NewBuffer(originByte)
originImg, filename, err := image.Decode(originBuffer)
if err != nil {
return err.Error()
}
fmt.Println(filename) // png
imgBounds := img.Bounds()
imgX := imgBounds.Dx() // 宽度
imgY := imgBounds.Dy() // 高度

firstRgba := img.At(0, 0) // 获取一个位置的像素点
r, g, b, a := firstRgba.RGBA()

// 创建一个新的图片,范围是100x100
newImg := image.NewRGBA(image.Rectangle{
Min: image.Point{X: 0, Y: 0},
Max: image.Point{X: 100, Y: 100},
})
// 坐标 (5, 5) 涂黑
newImg.SetRGBA(5, 5, color.RGBA{
R: 0,
B: 0,
G: 0,
A: 255, // 不透明
})

// 保存到output.png
f, _ := os.Create("./output.png")
png.Encode(f, newImg)
defer f.Close()
}

16. container/list 双向链表

  • value可以是任意类型的值
  • list本身不能拷贝,因为里面的element有一个指针指向root的地址,如果拷贝,root会被复制一份,将会导致element失效
  • 需要将list放到结构体时,建议使用指针放置

16.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
// 初始化
tmp := list.New()
tmp.PushBack(1) // 向后插入
tmp.PushFront(3) // 向前插入
// 遍历
for e := tmp.Front(); e != nil; e = e.Next() {
fmt.Println(e.Value)
}
// 删除
for e := tmp.Front(); e != nil; e = e.Next() {
if e.Value == 3 {
tmp.Remove(e)
break
}
}
// 中间插入
for e := tmp.Front(); e != nil; e = e.Next() {
if e.Value == 3 {
// 向后插入
tmp.InsertAfter(1, e)
// 向前插入
tmp.InsertBefore(2, e)
break
}
}

踩坑记

1) list放到结构体里面一定要注意结构体拷贝的问题,建议在结构体里面使用指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type test struct {
head list.List
}

func main() {
tmp := test{}
tmp.head.PushFront(1)

tmp2 := tmp // 拷贝会导致里面的元素的root指向错误,导致下面的插入语句错误
tmp2.head.InsertAfter(2, tmp2.head.Front())
// 这里打印只有一个1,前面的插入失效
for e := tmp2.head.Front(); e != nil; e = e.Next() {
fmt.Println(e.Value)
}
}
  • 原因从源码查看
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
// src/container/list/list.go
// Element is an element of a linked list.
type Element struct {
// Next and previous pointers in the doubly-linked list of elements.
// To simplify the implementation, internally a list l is implemented
// as a ring, such that &l.root is both the next element of the last
// list element (l.Back()) and the previous element of the first list
// element (l.Front()).
next, prev *Element

// The list to which this element belongs.
list *List

// The value stored with this element.
Value any
}
...
// List represents a doubly linked list.
// The zero value for List is an empty list ready to use.
type List struct {
root Element // sentinel list element, only &root, root.prev, and root.next are used
len int // current list length excluding (this) sentinel element
}
...
// InsertAfter inserts a new element e with value v immediately after mark and returns e.
// If mark is not an element of l, the list is not modified.
// The mark must not be nil.
func (l *List) InsertAfter(v any, mark *Element) *Element {
if mark.list != l {
return nil
}
// see comment in List.Remove about initialization of l
return l.insertValue(v, mark)
}
  • 上面的Element里面有一个变量list指向root的地址,也就是下面InsertAfter判断的值
  • 当进行了拷贝之后,root本身是拷贝的,地址处于新的地址上,里面的元素的list还指向拷贝前的地址,自然和当前不相等,就会插入失败
  • 解决办法就是使用指针,这样拷贝的是指针,里面的内容不会变化
  • 上面的写法只是实例,实际场景经常会出现在构造函数构造完元素后返回了一个拷贝,如下面的错误写法
1
2
3
4
5
6
7
8
9
10
type Allocator struct {
memMap list.List
}

func Constructor(n int) Allocator {
res := Allocator{}
res.memMap.PushFront(1)
// 返回拷贝,外层拷贝时将memMap的内容拷贝过去,导致元素的list失效
return res
}

17. runtime 一些运行需要的变量和方法

17.1. 常用变量

1) runtime.GOOS 操作系统类型

1
2
3
4
5
6
7
8
9
10
11
12
func test() {
switch runtime.GOOS {
case "windows":
fmt.Println("windows")
case "linux":
fmt.Println("linux")
case "darwin":
fmt.Println("mac")
default:
log.Fatalln("unsupported os", runtime.GOOS)
}
}

18. io io相关操作

19. regexp 正则库

19.1. 提取字符串

  • 括号内的是要提取的字符串,输出为整体匹配到的字符串加括号匹配的字符串
1
2
3
4
5
6
7
8
9
10
11
func TestRegexDefault(t *testing.T) {
line := "[2022-12-26 18:49:24.631][ 25160: 6372][ info][aaa][bbb]testhhh\r\n"

defaultRegexStr := `^\[(20[0-9]{2}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3})\]\[[ ]*(\w+):[ ]*(\w+)\]\[[ ]*(\w+)\]\[(\w+)\]\[[^\]]*\](.*)`
matchRegex := regexp.MustCompile(defaultRegexStr)
params := matchRegex.FindStringSubmatch(line)
if len(params) == 0 {
t.Fatal(defaultRegexStr, " parse failed, params ", params)
}
fmt.Println(params) // ["[2022-12-26 18:49:24.631][ 25160: 6372][ info][aaa][bbb]testhhh\r" "2022-12-26" "18:49:24.631" "25160" "6372" "info" "aaa" "testhhh"]
}

20. archive/zip zip相关操作

20.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
func unzip(zipFile string, destDir string) error {
zipReader, err := zip.OpenReader(zipFile)
if err != nil {
return err
}
defer zipReader.Close()

for _, f := range zipReader.File {
fpath := filepath.Join(destDir, f.Name)
if f.FileInfo().IsDir() {
os.MkdirAll(fpath, os.ModePerm)
} else {
if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
return err
}

inFile, err := f.Open()
if err != nil {
return err
}
defer inFile.Close()

outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
if err != nil {
return err
}
defer outFile.Close()

_, err = io.Copy(outFile, inFile)
if err != nil {
return err
}
}
}
return nil
}

20.2. 压缩

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// srcFile could be a single file or a directory
func Zip(srcFile string, destZip string) error {
zipfile, err := os.Create(destZip)
if err != nil {
return err
}
defer zipfile.Close()

archive := zip.NewWriter(zipfile)
defer archive.Close()

filepath.Walk(srcFile, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}

header, err := zip.FileInfoHeader(info)
if err != nil {
return err
}


header.Name = strings.TrimPrefix(path, filepath.Dir(srcFile) + "/")
// header.Name = path
if info.IsDir() {
header.Name += "/"
} else {
header.Method = zip.Deflate
}

writer, err := archive.CreateHeader(header)
if err != nil {
return err
}

if ! info.IsDir() {
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
_, err = io.Copy(writer, file)
}
return err
})

return err
}

21. exec 执行命令

21.1. 打开浏览器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func openBrowser(url string) {
command := ""
args := []string{}
switch runtime.GOOS {
case "windows":
command = "start"
args = append(args, "/c")
args = append(args, "start")
case "linux":
command = "xdg-open"
case "darwin":
command = "open"
}
args = append(args, url)

fmt.Println("exec", command, args)
cmd := exec.Command(command, args...)
cmd.Start()
}

22. heap 堆

22.1. 小根堆

  • 基础类型小根堆直接使用sort.IntSlice等可以实现
1
2
3
4
5
6
7
8
9
10
11
type lhp struct{ sort.IntSlice }

func (l *lhp) Push(x interface{}) {
l.IntSlice = append(l.IntSlice, x.(int))
}
func (l *lhp) Pop() interface{} {
n := len(l.IntSlice)
x := l.IntSlice[n-1]
l.IntSlice = l.IntSlice[:n-1]
return x
}
  • 上面代码大致解释就是,使用hp继承sort.IntSlice的各种基础方法
  • 实现Push和Pop两个接口即可实现最小堆

22.2. 大根堆

  • 照抄小根堆,然后将Less重写一下就好了,j和i互调顺序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
type bhp struct{ sort.IntSlice }

func (l *bhp) Less(i, j int) bool {
return l.IntSlice.Less(j, i)
}
func (l *bhp) Push(x interface{}) {
l.IntSlice = append(l.IntSlice, x.(int))
}
func (l *bhp) Pop() interface{} {
n := len(l.IntSlice)
x := l.IntSlice[n-1]
l.IntSlice = l.IntSlice[:n-1]
return x
}

23. context

23.1. 关闭子协程的方式

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
func main() {
fatherCtx := context.Background()
childCtx, cancelChild := context.WithCancel(fatherCtx)
var wg sync.WaitGroup
wg.Add(1)
go func() {
for {
select {
case <-childCtx.Done():
fmt.Println("exit because:", context.Cause(childCtx))
wg.Done()
return
case <-time.After(1 * time.Millisecond):
fmt.Println("begin do something")
// 模拟函数执行了5s
time.Sleep(5 * time.Second)
}
}
}()
time.Sleep(1 * time.Second)
cancelChild()
fmt.Println("notify child quit done")
wg.Wait()
}

2) 定点关闭协程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func main() {
fatherCtx := context.Background()
childCtx, cancelChild := context.WithDeadline(fatherCtx, time.Now().Add(3*time.Second))
defer cancelChild()
var wg sync.WaitGroup
wg.Add(1)
go func() {
for {
select {
case <-childCtx.Done():
fmt.Println("exit because:", context.Cause(childCtx))
wg.Done()
return
case <-time.After(1 * time.Second):
fmt.Println("reach 1 s")
}
}
}()
fmt.Println("begin wait")
wg.Wait()
}

3) 定时关闭协程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func main() {
fatherCtx := context.Background()
childCtx, cancelChild := context.WithTimeout(fatherCtx, 3*time.Second)
defer cancelChild()
var wg sync.WaitGroup
wg.Add(1)
go func() {
for {
select {
case <-childCtx.Done():
fmt.Println("exit because:", context.Cause(childCtx))
wg.Done()
return
case <-time.After(1 * time.Second):
fmt.Println("reach 1 s")
}
}
}()
fmt.Println("begin wait")
wg.Wait()
}

4) 带原因的取消

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func main() {
fatherCtx := context.Background()
childCtx, cancelChild := context.WithCancelCause(fatherCtx)
var wg sync.WaitGroup
wg.Add(1)
go func() {
for {
select {
case <-childCtx.Done():
fmt.Println("exit because:", context.Cause(childCtx))
wg.Done()
return
case <-time.After(1 * time.Second):
fmt.Println("reach 1 s")
}
}
}()
cancelChild(fmt.Errorf("father notify"))
wg.Wait()
}

23.2. 携带上下文

  • 当前找不到会找父级的context
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func main() {
fatherCtx := context.Background()
childCtx := context.WithValue(fatherCtx, "test", "123")
child1Ctx, cancel := context.WithTimeout(childCtx, 3*time.Second)
defer cancel()
var wg sync.WaitGroup
wg.Add(1)
go func() {
for {
select {
case <-child1Ctx.Done():
fmt.Println("exit because:", context.Cause(child1Ctx))
wg.Done()
return
case <-time.After(1 * time.Second):
fmt.Println("reach 1 s, get test: ", child1Ctx.Value("test"))
}
}
}()
fmt.Println("begin wait")
wg.Wait()
}

24. os/signal 信号处理库

24.1. 捕获信号

1
2
3
4
5
6
7
func main() {
c := make(chan os.Signal, 10)
signal.Notify(c, syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT)
for s := range c {
fmt.Println("recieve signal", s)
}
}
  • 常见的操作对应的信号
操作信号值描述
ctrl+csyscall.SIGINT中断
ctrl+zsyscall.SIGTSTP停止
fgbgsyscall.SIGCONT继续

25. fmt 格式化输出库

25.1. Sprintf 格式化字符串

1) 自定义顺序格式化参数

1
2
3
func main() {
fmt.Println(fmt.Sprintf("%[2]d %[1]d %[1]d %[2]d", 10, 20)) // 20 10 10 20
}

26. math/bits 二进制相关

26.1. 统计二进制中1的个数

1
2
3
4
5
6
7
func main() {
fmt.Println(bits.OnesCount(1))
fmt.Println(bits.OnesCount(2))
fmt.Println(bits.OnesCount(3))
fmt.Println(bits.OnesCount(4))
fmt.Println(bits.OnesCount(5))
}

五、cgo

1. 多平台编译

1.1. 平台不同的flag

1
2
3
// #cgo CXXFLAGS: -D CGO -D FORCE_POSIX -I ${SRCDIR}/../../Core -std=c++17
// #cgo linux,amd64 LDFLAGS: -L./lib-linux-amd64 -lmmkv -lcore -lz -lpthread
// #cgo windows,amd64 LDFLAGS: -L./lib-windows-amd64 -lmmkv -lcore -lz -lpthread

2. pkg-config 自定义动态库路径

将 CGO 与 Pkg-Config 和 自定义动态库位置一起使用

六、go命令相关

1. go test 执行单测用例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
########## 指定目录 ##########
# 执行当前目录下所有单测文件
go test
# 执行当前目录下及子目录的所有单测文件
go test ./...
# 执行package的某个子目录单测文件
go test [package]/[dir]

########## 指定函数 ##########
# 执行特定函数
go test -run ^testFunc$
# 执行多个特定函数
go test -run ^(testFunc|testFunc1)$

########## 设定超时时间 ##########
go test -timeout 30s

########## 打印详细信息 ##########
# 默认成功不会打印fmt.Println等,加上-v就会打印
go test -v

2. go build 编译二进制

  • go build不区分平台,可以在任意系统上编译其他大多数系统的版本

2.1. windows 32bit和64bit

1
2
3
4
5
6
7
8
9
10
11
12
13
# 32位编译
go env -w GOOS=windows
go env -w GOARCH=386
go build
go env -u GOARCH
go env -u GOOS

# 64位编译
go env -w GOOS=windows
go env -w GOARCH=amd64
go build
go env -u GOARCH
go env -u GOOS

2.2. 全平台编译makefile示例

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
TARGET_NAME		= testaaa
BUILD_CMD = go build
OUTPUT = $(TARGET_NAME)_x86.exe $(TARGET_NAME)_x64.exe $(TARGET_NAME)_linux_amd64 $(TARGET_NAME)_linux_arm64 $(TARGET_NAME)_mac_amd64 $(TARGET_NAME)_mac_arm64

all: $(OUTPUT)
clean:
rm -f $(OUTPUT)

$(TARGET_NAME)_x86.exe: *.go
go env -w GOOS=windows
go env -w GOARCH=386
$(BUILD_CMD) -o $@
go env -u GOOS
go env -u GOARCH

$(TARGET_NAME)_x64.exe: *.go
go env -w GOOS=windows
go env -w GOARCH=amd64
$(BUILD_CMD) -o $@
go env -u GOOS
go env -u GOARCH

$(TARGET_NAME)_linux_amd64: *.go
go env -w GOOS=linux
go env -w GOARCH=amd64
$(BUILD_CMD) -o $@
go env -u GOOS
go env -u GOARCH

$(TARGET_NAME)_linux_arm64: *.go
go env -w GOOS=linux
go env -w GOARCH=arm64
$(BUILD_CMD) -o $@
go env -u GOOS
go env -u GOARCH

$(TARGET_NAME)_mac_amd64: *.go
go env -w GOOS=darwin
go env -w GOARCH=arm64
$(BUILD_CMD) -o $@
go env -u GOOS
go env -u GOARCH

$(TARGET_NAME)_mac_arm64: *.go
go env -w GOOS=darwin
go env -w GOARCH=arm64
$(BUILD_CMD) -o $@
go env -u GOOS
go env -u GOARCH

2.3. 编译给c调用的动态库

代码上

  • 必须有package main和main函数,但是可以什么都不做
  • 必须包含import "C"
  • 要导出的函数必须加上//export funcName,export前没有空格
  • 导出函数本身无所谓首字母大小写
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"
import "C"

// Rectangle define class
type Rectangle struct {
x int
y int
}

// Area define method
func (r *Rectangle) Area() int {
return r.x * r.y
}

//export test
func test() {
rec := Rectangle{
x: 14,
y: 34,
}
fmt.Println(rec.Area())
}

func main() {
}

编译

1
2
3
4
# 要先安装标准库,会生成目录到 /usr/lib/go/pkg/linux_[arch]_dynlink/
go install -buildmode=shared -linkshared std
# 编译C动态库
go build -buildmode=c-shared -linkshared -o libtest.so

结果

  • 会在当前目录生成一个静态库文件(libtest.so)一个头文件(libtest.h
  • strip后查看libtest.so,函数已经导出,依赖上述生成的目录下的libstd.so
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
=> strip libtest.so
=> readelf -Ws libtest.so

Symbol table '.dynsym' contains 33 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __stack_chk_fail@GLIBC_2.17 (2)
3: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTable
4: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable
5: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@GLIBC_2.17 (2)
6: 0000000000000000 0 FUNC GLOBAL DEFAULT UND runtime.morestack_noctxt
7: 0000000000000000 0 OBJECT GLOBAL DEFAULT UND type.int
8: 0000000000000000 0 OBJECT GLOBAL DEFAULT UND type.*os.File
9: 0000000000000000 0 OBJECT GLOBAL DEFAULT UND os.Stdout
10: 0000000000000000 0 OBJECT GLOBAL DEFAULT UND type.io.Writer
11: 0000000000000000 0 FUNC GLOBAL DEFAULT UND runtime.addmoduledata
12: 0000000000000000 0 FUNC GLOBAL DEFAULT UND os.(*File).Write
13: 0000000000000000 0 OBJECT GLOBAL DEFAULT UND go.link.abihash.libstd.so
14: 0000000000000000 0 FUNC GLOBAL DEFAULT UND runtime.unreachableMethod
15: 0000000000000000 0 FUNC GLOBAL DEFAULT UND runtime.convT64
16: 0000000000000000 0 FUNC GLOBAL DEFAULT UND _rt0_arm64_linux_lib
17: 0000000000000000 0 OBJECT GLOBAL DEFAULT UND runtime.lastmoduledatap
18: 0000000000000000 0 TLS GLOBAL DEFAULT UND runtime.tlsg
19: 0000000000000000 0 FUNC GLOBAL DEFAULT UND fmt.Fprintln
20: 0000000000000000 0 OBJECT GLOBAL DEFAULT UND __stack_chk_guard@GLIBC_2.17 (3)
21: 0000000000000000 0 FUNC GLOBAL DEFAULT UND _cgo_wait_runtime_init_done
22: 0000000000000000 0 FUNC GLOBAL DEFAULT UND crosscall2
23: 0000000000000000 0 FUNC GLOBAL DEFAULT UND _cgo_release_context
24: 000000000001f950 8 OBJECT GLOBAL DEFAULT 19 _rt0_arm64_linux_lib.ptr
25: 0000000000020238 0 NOTYPE GLOBAL DEFAULT 18 _edata
26: 0000000000020239 0 NOTYPE GLOBAL DEFAULT 18 _end
27: 0000000000001270 64 FUNC GLOBAL DEFAULT 13 _cgoexp_f70364e750d1_test
28: 00000000000012b0 144 FUNC GLOBAL DEFAULT 13 main.test
29: 0000000000001350 128 FUNC GLOBAL DEFAULT 13 test
30: 0000000000020238 0 NOTYPE GLOBAL DEFAULT 18 __bss_start
31: 000000000001f988 32 OBJECT GLOBAL DEFAULT 20 go.itab.*os.File,io.Writer
32: 000000000001f970 24 OBJECT GLOBAL DEFAULT 20 runtime.textsectionmap

# 可以看到依赖了go自己的标准库和libc的库
=> readelf -d libtest.so

Dynamic section at offset 0xfce8 contains 32 entries:
Tag Type Name/Value
0x0000000000000003 (PLTGOT) 0x1ff88
0x0000000000000002 (PLTRELSZ) 264 (bytes)
0x0000000000000017 (JMPREL) 0xf90
0x0000000000000014 (PLTREL) RELA
0x0000000000000007 (RELA) 0x948
0x0000000000000008 (RELASZ) 1608 (bytes)
0x0000000000000009 (RELAENT) 24 (bytes)
0x000000006ffffff9 (RELACOUNT) 55
0x0000000000000006 (SYMTAB) 0x288
0x000000000000000b (SYMENT) 24 (bytes)
0x0000000000000005 (STRTAB) 0x5a0
0x000000000000000a (STRSZ) 700 (bytes)
0x000000006ffffef5 (GNU_HASH) 0x860
0x0000000000000001 (NEEDED) Shared library: [libstd.so]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x0000000000000001 (NEEDED) Shared library: [ld-linux-aarch64.so.1]
0x000000000000000c (INIT) 0x1098
0x000000000000000d (FINI) 0x13d0
0x000000000000001a (FINI_ARRAY) 0x1f940
0x000000000000001c (FINI_ARRAYSZ) 8 (bytes)
0x0000000000000019 (INIT_ARRAY) 0x1f948
0x000000000000001b (INIT_ARRAYSZ) 24 (bytes)
0x000000000000001d (RUNPATH) Library runpath: [/usr/lib/go/pkg/linux_arm64_dynlink/]
0x0000000000000010 (SYMBOLIC) 0x0
0x000000000000001e (FLAGS) SYMBOLIC BIND_NOW
0x000000006ffffffb (FLAGS_1) Flags: NOW NODELETE
0x000000006ffffff0 (VERSYM) 0x8a8
0x000000006ffffffc (VERDEF) 0x8ec
0x000000006ffffffd (VERDEFNUM) 1
0x000000006ffffffe (VERNEED) 0x908
0x000000006fffffff (VERNEEDNUM) 2
0x0000000000000000 (NULL) 0x0

3. go env 设置环境变量

  • 设置的全局变量是全局生效的,重启都会生效
1
2
3
4
# 设置环境变量
go env -w xxx=xx
# 恢复环境变量到默认值
go env -u xxx

实例

  • 可以写到makefile中,跨平台的makefile
1
2
3
4
main.exe: main.go
go env -w GOOS=windows
go build
go env -u GOOS

4. go install 安装全局包

1
go install -v github.com/cweill/gotests/gotests@v1.6.0

4.1. 常用工具链接

  • gotest: github.com/cweill/gotests/gotests
  • go-outline: github.com/ramya-rao-a/go-outline
  • gopls: golang.org/x/tools/gopls
  • dlv: github.com/go-delve/delve/cmd/dlv

5. go generate 执行命令

  • 在go文件中定义的命令,调用go generate时会执行
  • 需要在main.go文件中如下定义,注释和go之间不能有空格,会执行generate后面的命令
  • 一般用于生成语言文件或错误码信息等可以用命令生成的文件
1
//go:generate gf pack -y i18n catalog/catalog.go

6. dlv 调试go程序

1
go install -v github.com/go-delve/delve/cmd/dlv@latest

6.1. dlv监听远程调试

1
2
3
4
# 监听6666端口,挂载16703进程号
# --api-version 设置version为2,默认是1,vscode的客户端会使用2
# --headless=true 只启动server调试,不清楚什么是headless,但是都加了。。。
dlv --listen=:6666 --headless=true --api-version=2 attach 16703
  • vscode中launch.json如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Connect to server",
"type": "go",
"request": "attach",
"mode": "remote",
"remotePath": "${workspaceFolder}",
"port": 6666,
"host": "172.22.231.55"
}
]
}

五、框架

1. 工程知识

1.1. go.mod

  • go.mod是go工程用来管理包使用的,类似于nodejs的package.json文件

1) 创建新工程

1. 先生成go.mod文件

1
2
# 初始化一个test工程
go mod init test

2) go mod 一些基本操作

1
2
3
4
# 添加工程依赖包,会加入到go.mod文件中的require中
go mod download github.com/mattn/go-sqlite3
# 根据go.mod文件处理依赖关系
go mod tidy
(1) vendor
1
2
# 同步依赖包到当前的vendor目录
go mod vendor

2. goframe

2.1. 安装配置

安装gf命令行工具

  • goframe安装需要一个gf命令行工具
1
2
# 下载安装gf
wget -O gf https://github.com/gogf/gf-cli/releases/download/v1.16.3/gf_linux_amd64 && chmod +x gf && ./gf install
  • 默认gf命令行工具不支持sqlite3,如果需要,上面安装完之后,自己编译一版sqlite3的gf工具
  • 确保有gcc编译环境
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# clone仓库
git clone https://github.com/gogf/gf-cli && cd gf-cli
# 设置环境变量
export CGO_ENABLED=1
# 将代码中的sqlite3依赖取消注释
vim command/gen/gen_dao.go
...
# 执行gf的编译命令,需要上述步骤先下载gf工具
# -a指定平台,可以编译下面几种平台
# darwin amd64,arm64
# freebsd 386,amd64,arm
# linux 386,amd64,arm,arm64,ppc64,ppc64le,mips,mipsle,mips64,mips64le
# netbsd 386,amd64,arm
# openbsd 386,amd64,arm
# windows 386,amd64
gf build main.go -a amd64 -s linux --cgo
# 安装
cp bin/linux_amd64/gf /usr/local/bin/gf

初始化

  • 如果需要使用sqlite3,需要在项目根目录执行
1
go mod download github.com/mattn/go-sqlite3

代码生成

  • 根据config.toml生成数据库代码,这个比较方便
1
gf gen dao -c config/config.toml

2.2. 开发知识点

2.2.1. orm时间维护功能

  • orm的数据库更新操作会自动更新updatedAt、createdAt、deletedAt等字段
  • 不想要更新字段需要加上Unscoped()
1
db.Table("user").Unscoped().Data(g.Map{"name" : "john guo"}).Where("name", "john").Update()

2.2.2. 前后端字段对应

  • gf中使用下面的方法可以将前端传入参数解析到struct的对应成员中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// app/model/xxx.go
type TaskApiCreateReq struct {
Content string `json:"content" v:"required|length:1,100#任务内容不能为空|任务内容应当在:min到:max之间"`
TaskType int `json:"taskType"`
}
// app/api/xxx.go
func (a *task) Create(r *ghttp.Request) {
var (
data *model.TaskApiCreateReq
)
if err := r.Parse(&data); err != nil {
response.JsonExit(r, 1, err.Error())
}
...
}
  • 解析规则,会把前端传入的字段去除除了字母和数字的所有特殊字符,然后不区分大小写比较
  • 下面的几个key都可以解析到上面的data.Content
1
2
3
4
5
6
{
"Content": "xxx",
"content": "xxx",
"con_tent": "xxx",
"conTent": "xxx"
}

六、好用的第三方库

1. github.com/google/uuid uuid

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

import (
"fmt"

"github.com/google/uuid"
)

func main() {
id := uuid.New()
fmt.Printf("%s %s\n", id, id.Version().String())
}

输出

1
ab8d63ef-f0c4-a01d-a6ef-5f2c3efff5af VERSION_4

2. github.com/makiuchi-d/gozxing 二维码解析和生成

  • github.com/tuotoo/qrcode没成功的,这个成功了
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 (
"bytes"
"fmt"
"image"
"image/png"
"io/ioutil"

"github.com/makiuchi-d/gozxing"
_ "github.com/makiuchi-d/gozxing/common"
"github.com/makiuchi-d/gozxing/qrcode"
)

func parseQrcode(pngPath string) string {
originByte, _ := ioutil.ReadFile(pngPath)
originBuffer := bytes.NewBuffer(originByte)
originImg, _, err := image.Decode(originBuffer)
if err != nil {
return err.Error()
}
fmt.Println(filename)

// 解析二维码返回结果
bmp, _ := gozxing.NewBinaryBitmapFromImage(originImg)
qrReader := qrcode.NewQRCodeReader()
result, err := qrReader.Decode(bmp, nil)
if err != nil {
return err.Error()
}
return result.GetText()
}

3. golang.org/x/mobile go开发android和ios

gomobile使用笔记

4. github.com/shirou/gopsutil/process 获取进程相关信息

4.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
// 获取进程对应的目录
func getProcessDir(name string) string {
// 获取所有进程id
pids, _ := process.Pids()
for _, pid := range pids {
p, _ := process.NewProcess(int32(pid))
tmp, _ := p.Name()
if tmp != name {
continue
}
fmt.Println(p.Name()) // 名称,一般是二进制名称
fmt.Println(p.Exe()) // 二进制路径
fmt.Println(p.Cwd()) // 二进制运行环境变量
fmt.Println(p.Cmdline()) // 二进制运行命令行参数
fmt.Println(p.Uids())
fmt.Println(p.Gids())
fmt.Println(p.Username())
fmt.Println(p.CreateTime())
fmt.Println(p.MemoryInfo())
fmt.Println(p.MemoryInfoEx())
fmt.Println(p.NumCtxSwitches())
fmt.Println(p.NumFDs())
fmt.Println(p.NumThreads())
fmt.Println(p.OpenFiles())
fmt.Println(p.Connections())
fmt.Println(p.IsRunning())
break
}
return ""
}

5. github.com/miekg/dns 构造dns包发送

5.1. 获取域名特定类型的解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func resolveDomain(domain string, server string, dnsType uint16) (r *dns.Msg, err error) {
cli := dns.Client{
Timeout: 5 * time.Second,
}
m := dns.Msg{}
m.SetQuestion(dns.Fqdn(domain), dnsType)
m.Id = dns.Id()
m.RecursionDesired = false

r, _, err = cli.Exchange(&m, server+":53")
if err != nil {
return
}
fmt.Println(r.String()) // 打印类似dig的输出
}

6. golang.org/x/text/encoding/simplifiedchinese 中文编码转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import "golang.org/x/text/encoding/simplifiedchinese"

func ConvertStr2GBK(str string) string {
//将utf-8编码的字符串转换为GBK编码
ret, err := simplifiedchinese.GBK.NewEncoder().String(str)
return ret //如果转换失败返回空字符串

//如果是[]byte格式的字符串,可以使用Bytes方法
b, err := simplifiedchinese.GBK.NewEncoder().Bytes([]byte(str))
return string(b)
}

func ConvertGBK2Str(gbkStr string) string {
//将GBK编码的字符串转换为utf-8编码
ret, err := simplifiedchinese.GBK.NewDecoder().String(gbkStr)
return ret //如果转换失败返回空字符串

//如果是[]byte格式的字符串,可以使用Bytes方法
b, err := simplifiedchinese.GBK.NewDecoder().Bytes([]byte(gbkStr))
return string(b)
}

7. github.com/google/gopacket

7.1. 构造ip包

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
import (
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
)

func main() {
// 构建destination ip
const (
IPv4 = "ip4"
Addr = "199.200.2.170"
Port = 12345
)

dstIP, _ := net.ResolveIPAddr(IPv4, Addr)
dstPort := layers.TCPPort(Port)

sIP, _ := net.ResolveIPAddr(IPv4, "2.0.0.1")
srcPort := layers.TCPPort(12345)

fmt.Printf("Local IP:PORT - %s:%d \n", sIP, srcPort)

// 构建IP包
ipPack := &layers.IPv4{
Version: 4,
SrcIP: sIP.IP,
DstIP: dstIP.IP,
TTL: 64,
Protocol: layers.IPProtocolTCP,
}

// 构建tcp报文段
tcpPack := &layers.TCP{
SrcPort: srcPort,
DstPort: dstPort,
Seq: 1205014776,
SYN: true,
}

if err := tcpPack.SetNetworkLayerForChecksum(ipPack); err != nil {
log.Fatalln(err)
}

serialBuf := gopacket.NewSerializeBuffer()
serialOpts := gopacket.SerializeOptions{
ComputeChecksums: true,
FixLengths: true,
}
if err := gopacket.SerializeLayers(serialBuf, serialOpts, ipPack, tcpPack); err != nil {
log.Fatalln(err)
}

// 打印ip包
fmt.Println(serialBuf.Bytes())
}

8. github.com/songgao/water linux虚拟网卡设备

8.1. 构造tun设备读写数据

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
func main() {
// 创建TUN设备
config := water.Config{
DeviceType: water.TUN,
PlatformSpecificParams: water.PlatformSpecificParams{
Name: "tun123",
},
}
ifce, err := water.New(config)
if err != nil {
log.Fatal(err)
}
fmt.Println("create tun", ifce.Name())

// 启用网卡
cmd := exec.Command("ifconfig", ifce.Name(), "up")
if err := cmd.Run(); err != nil {
log.Fatal(err)
}

// 接收数据包
go func() {
buf := make([]byte, 1500)
for {
n, err := ifce.Read(buf)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Received packet: % x\n", buf[:n])
}
}()

... // 构造数据包
// 发送数据包
fmt.Println(serialBuf.Bytes())
_, err = ifce.Write(serialBuf.Bytes())
if err != nil {
fmt.Println("Error sending packet:", err)
return
}

time.Sleep(1000 * time.Second)
}

9. github.com/smartystreets/goconvey/convey 单测辅助工具

  • 按照树的结构执行单测

9.1. 一般用法

1
2
3
4
5
6
7
8
9
10
11
12
func TestConvey(t *testing.T) {
Convey("测试1加上别的数", t, func() {
Convey("1+1=2", func() {
sum := 1 + 1
So(sum, ShouldEqual, 2)
})
Convey("1+2=3", func() {
sum := 1 + 2
So(sum, ShouldEqual, 3)
})
})
}

9.2. convey执行顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func TestConvey(t *testing.T) {
Convey("测试1加上别的数", t, func() {
i := 1
fmt.Println()
fmt.Print("set i to ", i)
Convey("1+1=2", func() {
i += 1
So(i, ShouldEqual, 2)
})
Convey("1+2=3", func() {
i += 2
So(i, ShouldEqual, 3)
})
fmt.Println()
fmt.Println("test done")
})
}
  • 实际输出
1
2
3
4
5
6
7
8
9
10
=== RUN   TestConvey

测试1加上别的数
set i to 1
1+1=2 ✔
test done

set i to 1
1+2=3 ✔
test done
  • 从上述代码可以得出结论,每一个Convey的出现,都是把父节点的函数复制了一份,只执行当前的Convey,兄弟Convey不执行
  • 所以每个父节点有几个子节点,其所在函数都会执行多少遍,每一遍只有一个Convey生效,其他的都是空
  • 上面代码可以理解为
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
func TestConvey(t *testing.T) {
// 测试1加上别的数
func() {
i := 1
fmt.Println()
fmt.Print("set i to ", i)
// 1+1=2
func() {
i += 1
So(i, ShouldEqual, 2)
}()

// 不执行 1+2=3

fmt.Println()
fmt.Println("test done")
}()

// 测试1加上别的数,第二个convey把父节点整个函数重新执行了一遍
func() {
i := 1
fmt.Println()
fmt.Print("set i to ", i)

// 不执行 1+1=2

// 1+2=3
func() {
i += 2
So(i, ShouldEqual, 3)
}()
fmt.Println()
fmt.Println("test done")
}()
}

测试集的实际效果

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
func TestConvey(t *testing.T) {
Convey("测试1加上别的数", t, func() {
tests := []struct {
name string
add int
want int
}{
{
name: "1+1=2",
add: 1,
want: 2,
},
{
name: "1+2=3",
add: 2,
want: 3,
},
{
name: "1+3=4",
add: 3,
want: 4,
},
}
for i, tt := range tests {
fmt.Println()
fmt.Print("in ", i, ", tt ", tt)
Convey(tt.name, func() {
So(1+tt.add, ShouldEqual, tt.want)
})
fmt.Println()
fmt.Print("out ", i, ", tt ", tt)
}
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
=== RUN   TestConvey

测试1加上别的数
in 0, tt {1+1=2 1 2}
1+1=2 ✔
out 0, tt {1+1=2 1 2}
in 1, tt {1+2=3 2 3}
out 1, tt {1+2=3 2 3}
in 2, tt {1+3=4 3 4}
out 2, tt {1+3=4 3 4}

in 0, tt {1+1=2 1 2}
out 0, tt {1+1=2 1 2}
in 1, tt {1+2=3 2 3}
1+2=3 ✔
out 1, tt {1+2=3 2 3}
in 2, tt {1+3=4 3 4}
out 2, tt {1+3=4 3 4}

in 0, tt {1+1=2 1 2}
out 0, tt {1+1=2 1 2}
in 1, tt {1+2=3 2 3}
out 1, tt {1+2=3 2 3}
in 2, tt {1+3=4 3 4}
1+3=4 ✔
out 2, tt {1+3=4 3 4}

10. 国际化库

10.1. github.com/mylukin/easy-i18n/i18n 轻量简单的国际化库

1) 准备工作

  • 在根目录建立locals目录,里面只能放json文件
  • 文件名只能是语言名称且只能是golang.org/x/text/language包中可以Make出来的名称
1
2
3
locales
├── en-US.json
└── zh-CN.json
  • 名字限制如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import (
"fmt"

"golang.org/x/text/language"
)

func main() {
name := "zh-CN"
tag := language.Make(name)
if tag.String() != name {
// 语言包不可用
fmt.Println("can't use language package", name)
return
}
fmt.Println("language package", name ,"is ok")
}
  • 也可以用系统内置的一部分
1
2
3
4
5
6
7
func main() {
fmt.Println(language.SimplifiedChinese.String()) // zh-Hans
fmt.Println(language.Chinese.String()) // zh
fmt.Println(language.TraditionalChinese.String()) // zh-Hant
fmt.Println(language.English.String()) // en
fmt.Println(language.AmericanEnglish.String()) // en-US
}
  • json文件内容如下
1
2
3
4
5
{
"hello": "hello world",
"test": "test",
"testFormat": "test %d"
}
  • 安装命令easy-i18n
1
go install github.com/mylukin/easy-i18n/easyi18n@latest
  • 执行命令将json生成go文件
1
easyi18n generate --pkg=catalog ./locales ./catalog/catalog.go
  • 然后对应的go文件直接包含即可
1
2
3
import (
_ "xtunnel/catalog"
)

2) 使用

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"

_ "test/catalog"

"github.com/mylukin/easy-i18n/i18n"
"golang.org/x/text/language"
)

func main() {
p := i18n.NewPrinter("zh-CN")
// 使用内置的则可以使用下面语句,不过zh-CN不能用内置的,内置的是zh-Hans
// p := i18n.NewPrinter(language.SimplifiedChinese)

// 直接打印
p.Printf("testFormat\r\n", 1)
// 翻译成字符串
fmt.Println(p.Sprintf("hello"), p.Sprintf("test"))
}

10.2. github.com/gogf/gf/i18n/gi18n 强大的gf框架中的语言库

1) 语言文件分离放置

  • 在项目根目录新建i18n目录,按照语言名称新建不同的目录,名字没有限制
  • i18n是默认的目录名,也可以自定义,自定义则在gi18n.New()设置到Options中就好
  • 里面可以放很多个文件,根据不同模块命名即可,文件可以是toml、yml、json、xml等
  • 目录结构如下
1
2
3
4
5
6
7
8
9
10
i18n
├── en-US
│ ├── test1.toml
│ └── test2.toml
├── hhhhh
│ ├── test1.toml
│ └── test2.toml
└── zh-CN
├── test1.toml
└── test2.toml
  • toml格式内容如下
1
2
3
# 这个是注释
test1 = "测试1"
testFormat = "测试格式 %d"
  • 使用如下,需要设置语言到ctx中,然后直接翻译成字符串
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import (
"context"
"fmt"

"github.com/gogf/gf/i18n/gi18n"
)

func main() {
g := gi18n.New()
// 语言名称设置到ctx中
ctx := gi18n.WithLanguage(context.TODO(), "hhhhh")

fmt.Println(g.T(ctx, "test1"))
fmt.Println(g.Tf(ctx, "testFormat", 1))
}

2) 语言文件打包到二进制

  • 使用gf命令将文件打包到go文件
1
2
3
4
# 直接打包保留i18n的目录
gf pack -y i18n catalog/catalog.go
# 将locales下文件打包到i18n目录(对于二进制来说)
gf pack -y locales -p=i18n catalog/catalog.go
  • 然后对应的go文件直接包含即可
  • 不过go自己会找当前工程目录下的i18n目录(绝对路径),所以要测试可以重命名此目录进行测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"context"
"fmt"

_ "test/catalog"

"github.com/gogf/gf/i18n/gi18n"
)

//go:generate gf pack -y i18n catalog/catalog.go
func main() {
g := gi18n.New()
// 语言名称设置到ctx中
ctx := gi18n.WithLanguage(context.TODO(), "hhhhh")

fmt.Println(g.T(ctx, "test1"))
fmt.Println(g.Tf(ctx, "testFormat", 1))
}

11. github.com/agiledragon/gomonkey hook函数很好的一个库

11.1 使用

踩坑

1) 直接go test不生效,vscode调试可以

  • 原因是直接跑编译器会自动优化代码,而此库不能对代码做优化,所以需要加-gcflags "all=-l"
  • 在vscode中点击run test则需要添加下面的配置到settings.json
1
2
3
4
{
...
"go.testFlags": [ "-gcflags=all=-l" ]
}

12. github.com/go-redis/redis/v7 redis操作库

12.1. 使用示例

1) 测试连接

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"

"github.com/go-redis/redis/v7"
)

func main() {
redisCli := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
// Password: "123456", // no password set
DB: 0, // use default DB
})

pong, err := redisCli.Ping().Result()
if err != nil {
fmt.Println("reis 连接失败:", pong, err)
return
}
fmt.Println("reis 连接成功:", pong)
}

2) 扫描key

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func main() {
...
var (
cursor uint64 = 0
keys []string
)
for {
keys, cursor, err = redisCli.Scan(cursor, "SESS#*", 1000).Result()
if err != nil {
fmt.Println(err)
break
}
fmt.Println(keys)
if cursor == 0 {
break
}
}
fmt.Println("redis 扫描结束")
}

3) 增删改

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
...
// 增和改是一样的
err := redisCli.Set(key, value, 0).Err()
if err != nil {
panic(err)
}
// 删
if err := redisCli.Del(key).Err(); err != nil {
fmt.Println("del key", key, "get err", err)
}
}

13. golang.org/x/sync/semaphore 信号量

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

import (
"context"
"fmt"
"math/rand"
"sync"
"time"

"golang.org/x/sync/semaphore"
)

func main() {
var lock sync.RWMutex
coMap := make(map[int]bool)

// 创建信号量,资源数为1000
s := semaphore.NewWeighted(10)
// 启100个协程竞争资源,同时只有10个协程拿到资源
for i := 0; i < 100; i++ {
go func(id int) {
for {
err := s.Acquire(context.Background(), 1)
if err != nil {
fmt.Println(err)
time.Sleep(time.Second)
continue
}

lock.Lock()
coMap[id] = true
lock.Unlock()

time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000)))

lock.Lock()
delete(coMap, id)
lock.Unlock()

s.Release(1)
}
}(i)
}

for {
time.Sleep(time.Second)
lock.RLock()
fmt.Println("currnet", coMap)
lock.RUnlock()
}
}

七、实战

1. 搭建简单http服务器

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

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

type retJson struct {
Code int `json:"code"`
LLLL string `json:"llll"`
AAAA string `json:"aaaa"`
Token string `json:"token"`
}

func smsServerHandler(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
fmt.Println(r.Form)
fmt.Println("path", r.URL.Path)
fmt.Println("scheme", r.URL.Scheme)

var ret retJson
ret.Code = 0
ret.LLLL = "test"
ret.AAAA = "stest"
ret.Token = "a454as1bbvd5g5s15155"
retBytes, _ := json.MarshalIndent(&ret, "", " ")
retStr := string(retBytes)
fmt.Println(retStr)

w.Header().Set("Content-Type", "application-json")
fmt.Fprintf(w, retStr)
}

func main() {
// 接口处理到函数
http.HandleFunc("/", smsServerHandler)
// 接口处理到目录,主要用于静态页面返回
http.Handle("/", http.FileServer(http.Dir("template")))
err := http.ListenAndServe(":7878", nil)
if err != nil {
log.Fatal("ListenAndServe failed, err: ", err)
}
}

1.2. 静态页面路径返回

最简单跟目录返回

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

import (
"log"
"net/http"
)

func main() {
// 接口处理到目录,主要用于静态页面返回
http.Handle("/", http.FileServer(http.Dir("template")))
err := http.ListenAndServe(":7878", nil)
if err != nil {
log.Fatal("ListenAndServe failed, err: ", 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
package main

import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
)

func rootHandler(w http.ResponseWriter, r *http.Request) {
// 访问根转发到template
if r.URL.Path == "/" {
http.Redirect(w, r, "/template/", http.StatusFound)
return
}
w.Write([]byte(r.URL.Path))
}

func main() {
// 返回界面,将 /template/ 裁掉访问目录
http.Handle("/template/", http.StripPrefix("/template/", http.FileServer(http.Dir("./template"))))
// 处理其他请求
http.HandleFunc("/", rootHandler)

log.Printf("start server: http://localhost:4567 ")
err := http.ListenAndServe(":4567", nil)
if err != nil {
log.Fatal("Listen port failed, err: ", err)
}
}

1.3. 上传文件

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"
"io"
"log"
"net/http"
"os"
)

func uploadHandler(w http.ResponseWriter, r *http.Request) {
// 获取表单的file字段
file, handler, err := r.FormFile("file")
if err != nil {
fmt.Println("upload failed, err:", err)
w.Write([]byte("upload success"))
return
}
defer file.Close()

// 本地创建文件
f, err := os.Create("./upload_dir/" + handler.Filename)
if err != nil {
panic(err)
}
defer f.Close()

// 文件保存
_, err = io.Copy(f, file)
if err != nil {
panic(err)
}
w.Write([]byte("upload success"))
}

func main() {
http.Handle("/", http.FileServer(http.Dir("template")))
// 设置接口处理文件传输
http.HandleFunc("/uploadfile", uploadHandler)
err := http.ListenAndServe(":4567", nil)
if err != nil {
log.Fatal("Listen port failed, err: ", err)
}
}

1.4. 302跳转

简单跳转

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

import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
)

func main() {
// 直接跳转
http.HandleFunc("/", http.RedirectHandler("http://1.1.1.1", http.StatusFound))

log.Printf("start server: http://localhost:4567 ")
err := http.ListenAndServe(":4567", nil)
if err != nil {
log.Fatal("Listen port failed, err: ", 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
package main

import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
)

func rootHandler(w http.ResponseWriter, r *http.Request) {
// 访问根转发到template
if r.URL.Path == "/" {
http.Redirect(w, r, "/template/", http.StatusFound)
return
}
w.Write([]byte(r.URL.Path))
}

func main() {
// 处理其他请求
http.HandleFunc("/", rootHandler)

log.Printf("start server: http://localhost:4567 ")
err := http.ListenAndServe(":4567", nil)
if err != nil {
log.Fatal("Listen port failed, err: ", err)
}
}

1.5. 代理转发

  • 定义tr忽略证书错误
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
package main

import (
"crypto/tls"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/http/httputil"
"net/url"
)

func rootHandler(w http.ResponseWriter, r *http.Request) {
// 访问根转发到template
if r.URL.Path == "/" {
http.Redirect(w, r, "/template/", http.StatusFound)
return
}

// 请求转发给另一个url
remote, err := url.Parse("https://1.1.1.1")
if err != nil {
log.Fatalln("serverAddr is invalid")
}
proxy := httputil.NewSingleHostReverseProxy(remote)

// 忽略证书错误
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
proxy.Transport = tr

proxy.ServeHTTP(w, r)
}

func main() {
// 处理其他请求
http.HandleFunc("/", rootHandler)

log.Printf("start server: http://localhost:4567 ")
err := http.ListenAndServe(":4567", nil)
if err != nil {
log.Fatal("Listen port failed, err: ", err)
}
}

1.4. 处理post的body数据

  • json数据存在body里面
    r.Body只能读取一次,读完就会关闭,需要再次读取需要加上下面注释说明的代码
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 (
"crypto/tls"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/http/httputil"
"net/url"
)

func rootHandler(w http.ResponseWriter, r *http.Request) {
var body []byte
if r.Body != nil {
body, err = ioutil.ReadAll(r.Body)
if err != nil {
fmt.Println(err)
return
}
}

// 如果body想要多次读取需要加上下面的语句,重新申请一个readerCloser
r.Body = ioutil.NopCloser(bytes.NewReader(body))
}

func main() {
// 处理其他请求
http.HandleFunc("/", rootHandler)

log.Printf("start server: http://localhost:4567 ")
err := http.ListenAndServe(":4567", nil)
if err != nil {
log.Fatal("Listen port failed, err: ", err)
}
}

八、算法和数据结构的实现

1. 数据结构

1.1. 队列

  • 可以使用list.List进行实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main() {
var test list.List
// 入队列
test.PushBack(1)
test.PushBack(2)
// 出队列
value := test.Front().Value
test.Remove(test.Front())
fmt.Println(value)
// 长度
fmt.Println(test.Len())
// 遍历
for iter := test.Front(); iter != nil; iter = iter.Next() {
fmt.Println(iter.Value)
}
}

1.2. 栈

1) list.List实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func main() {
var test list.List
// 入栈
test.PushFront(1)
test.PushFront(2)
test.PushFront(3)
// 出栈
value := test.Front().Value
test.Remove(test.Front())
fmt.Println(value)
// 长度
fmt.Println(test.Len())
// 从栈顶遍历
for iter := test.Front(); iter != nil; iter = iter.Next() {
fmt.Println(iter.Value)
}
}

2) slice实现

  • slice实现有个好处就是,入栈会动态扩容,出栈容量会保留
  • slice实现队列会导致重复内存申请
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func main() {
test := []int{}
// 入栈
test = append(test, 1)
test = append(test, 2)
test = append(test, 3)
// 出栈
value := test[len(test)-1]
test = test[:len(test)-1]
fmt.Println(value)
// 长度
fmt.Println(len(test))
// 从栈顶遍历
for i := len(test); i > 0; i-- {
fmt.Println(test[i-1])
}
}

1.3. 链表

  • 直接使用list.List就好,单向双向都支持

1.4. 大(小)根堆

  • 官方库有heap,可以进行此操作,但是需要自己实现几个接口
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
type bhp struct{ sort.IntSlice }

func (l *bhp) Less(i, j int) bool {
return l.IntSlice.Less(j, i)
}
func (l *bhp) Push(x interface{}) {
l.IntSlice = append(l.IntSlice, x.(int))
}
func (l *bhp) Pop() interface{} {
n := len(l.IntSlice)
x := l.IntSlice[n-1]
l.IntSlice = l.IntSlice[:n-1]
return x
}

func main() {
unorderList := []int{12, 10, 4, 2, 5, 23, 45, 3, 9, 2}
fmt.Println(unorderList)

// 初始化大根堆
b := bhp{unorderList}
heap.Init(&b)
// 堆排序,按照顺序一直将栈顶弹出,就为从大到小排序
for b.Len() > 0 {
fmt.Println(heap.Pop(&b))
}
}

1.5. 二叉树

1
2
3
4
5
type TreeNode struct {
value int
left *TreeNode
right *TreeNode
}

2. 算法

2.1. 全排列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func permutation(str []byte, index int, f func(str []byte)) {
if len(str) == index {
f(str)
return
}

// 不交换的场景
permutation(str, index+1, f)
// index对应位置向后交换
for i := index + 1; i < len(str); i++ {
str[i], str[index] = str[index], str[i]
permutation(str, index+1, f)
str[i], str[index] = str[index], str[i]
}
}

func main() {
permutation([]byte("abcd"), 0, func(str []byte) {
fmt.Println(string(str))
})
}

2.2. bfs 广度优先算法

1) 二叉树的广度优先遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type TreeNode struct {
value int
left *TreeNode
right *TreeNode
}

func bfs(root *TreeNode) {
var queue list.List
queue.PushBack(&root)
for queue.Len() > 0 {
node := queue.Front().Value.(*TreeNode)
queue.Remove(queue.Front())
if node.left != nil {
queue.PushBack(node.left)
}
if node.right != nil {
queue.PushBack(node.right)
}
fmt.Println(node.value) // 这里取值
}
}

2) 方格中找两点最短路径

  • 两格之间步数为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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
type pointT struct {
x int
y int
}

var (
// 按照上下左右的相对位置设定,用于后面方便找四周的点
kRoundPoints = [][]int{
{0, -1},
{0, 1},
{-1, 0},
{1, 0},
}
)

// 返回从src到dst的最短路径长度,带层间隔版本
func bfsFloor(src pointT, dst pointT, grid []string) int {
// 减小计算量,走过的路不再走,记录一下哪里走过了
seen := make([][]bool, len(grid))
for i := range seen {
seen[i] = make([]bool, len(grid[0]))
}
// 源地址记录走过了,注意x是第二维的坐标
seen[src.y][src.x] = true

// 使用层数作为步数
curDepth := 0
var queue list.List
// 插入源地址,作为第一层,使用nil作为层间隔
queue.PushBack(src)
queue.PushBack(nil)
// 队列一定含有一个层间隔,不在头就在尾,如果只剩一个层间隔,说明没路可走
for queue.Len() > 1 {
tmp := queue.Front().Value
queue.Remove(queue.Front())
if tmp == nil {
// 找到层间隔,说明当前层遍历完了,步数加一准备下一层
curDepth++
// 当前层遍历完,队列剩余的都是下一层,加入一个层间隔
queue.PushBack(nil)
continue
}

// 判断当前点是不是目标点,如果是,说明走到了,返回步数
tx, ty := tmp.(pointT).x, tmp.(pointT).y
if tx == dst.x && ty == dst.y {
return curDepth
}
// 不是目标点,从此点出发,向四周走一下
for i := range kRoundPoints {
px, py := tx+kRoundPoints[i][0], ty+kRoundPoints[i][1]
// 如果超出边界或者已经走过了或者碰到墙,就继续
if py < 0 || py >= len(grid) || px < 0 || px >= len(grid[0]) || seen[py][px] || grid[py][px] == '#' {
continue
}
// 这个点可以走,走上去,记录到队列中,作为下一层的起点
seen[py][px] = true
queue.PushBack(pointT{px, py})
}
}
return -1
}

type pointST struct {
x int
y int
step int
}

// 返回从src到dst的最短路径长度
func bfs(src pointST, dst pointST, grid []string) int {
// 减小计算量,走过的路不再走,记录一下哪里走过了
seen := make([][]bool, len(grid))
for i := range seen {
seen[i] = make([]bool, len(grid[0]))
}
// 源地址记录走过了,注意x是第二维的坐标
seen[src.y][src.x] = true

var queue list.List
// 插入源地址
queue.PushBack(src)
for queue.Len() > 0 {
tmp := queue.Front().Value.(pointST)
queue.Remove(queue.Front())

// 判断当前点是不是目标点,如果是,说明走到了,返回步数
if tmp.x == dst.x && tmp.y == dst.y {
return tmp.step
}
// 不是目标点,从此点出发,向四周走一下
for i := range kRoundPoints {
px, py := tmp.x+kRoundPoints[i][0], tmp.y+kRoundPoints[i][1]
// 如果超出边界或者已经走过了或者碰到墙,就继续
if py < 0 || py >= len(grid) || px < 0 || px >= len(grid[0]) || seen[py][px] || grid[py][px] == '#' {
continue
}
// 这个点可以走,走上去,记录到队列中,作为下一层的起点
seen[py][px] = true
queue.PushBack(pointST{px, py, tmp.step+1})
}
}
return -1
}

func main() {
/*
@ # . . *
. . . # .
# . . . .
*/
grid := []string{"@#..*", "...#.", "#...."}
// @ 到 * 的最短距离为6
fmt.Println(bfs(pointST{0, 0, 0}, pointST{4, 0, 0}, grid))
fmt.Println(bfsFloor(pointT{0, 0}, pointT{4, 0}, grid))
}

九、go.work 大工程开发利器

vscode只能当go.mod在根目录的情况下才能进行提示,而在大工程情况下,go.mod可能不止一个。使用go.work之后,可以解决这个问题

1
2
3
4
# 初始化go.work
go work init
# 添加一个目录
go work use path/to/project

踩坑记

1. 编译报错

1.1. method has pointer receiverd

  • 由于定义的接口多个方法的接受不一致导致
  • 部分方法实现使用了指针,部分使用了原始类型
  • golang编译器发现*T类型实现了接口,调用的时候传入的又是T就给报错了

2. 协程相关

2.1. 循环起协程

  • 迭代器会被更改,最终留下的是最后一个
  • 应该当参数传入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func main() {
var wg sync.WaitGroup
a := []int{1, 2, 3, 4, 5}
for i := range a {
wg.Add(2)
// 拿到的是最后一个
go func() {
defer wg.Done()
fmt.Println("wrong", i)
}()
// 这样就可以了
go func(i int) {
defer wg.Done()
fmt.Println("right", i)
}(i)
}
wg.Wait()
}