References

Install

下载安装

rm -rf /usr/local/go && tar -C /usr/local -xzf go1.20.2.linux-amd64.tar.gz

添加环境变量

export GOPATH=/data/username/go # 修改 GOPATH,默认是 $HOME/go
export GO111MODULE=on # 默认开启 go module
export PATH=$PATH:/usr/local/go/bin:$GOPATH/bin # 添加环境变量

查看环境变量

go env

Go Modules

开启go modules

go env -w GO111MODULE="auto"

# 如果无法访问proxy.golang.org,需要修改proxy
go env -w GOPROXY=https://goproxy.cn,direct

# 公司proxy:https://goproxy.woa.com
export GOPROXY="https://yizhenchen:vm4UIuF1@goproxy.woa.com,direct"  
export GOPRIVATE="" 
export GOSUMDB="sum.woa.com+643d7a06+Ac5f5VOC4N8NUXdmhbm8pZSXIWfhek5JSmWdWrq7pLX4"
export no_proxy=$no_proxy",goproxy.woa.com"

使用

# 初始化包
go mod init example/user/hello

# build and install
go install example/user/hello

# 自动检测依赖包
go mod tidy

配置境内源

go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.cn,direct

Go Tools

Go Tools - Documentation

# 帮助文档
go help run|get|mod

Godoc

natefinch - godocgo

godoc -http=:6060

visit http://{ip}:6060/pkg/{package-name} to view documents

Build

编译

go build -v

交叉编译

env GOOS=linux GOARCH=arm64 go build -v

Run

运行

go run main.go

指定核数运行

GOMAXPROCS=1 ./your_program

或者在代码中设置

package main

import (
	"runtime"
)

func main() {
    runtime.GOMAXPROCS(1)
}

即使设置了 GOMAXPROCS 为 1,操作系统的调度器仍然可能在不同的核心之间迁移程序的执行线程

要严格控制程序只在一个特定的 CPU 核心上运行,需要设置进程的 CPU 亲和性(CPU affinity)

# 将程序绑定到 CPU 0 上
taskset -c 0 ./your_program

Test

learn go with tests

假设待测代码被放在 hello.go 文件中

package main

import "fmt"

func Hello(name string) string {
	return "Hello, " + name
}
func main() {
	s := Hello("world")
	fmt.Println(s)
}

新建一个测试文件,以 _test.go 结尾,即 hello_test.go

对要测试的函数前面加上 Test 前缀,参数传入 *testing.T

package main

import "testing"

func TestHello(t *testing.T) {
	got := Hello("Chris")
	want := "Hello, Chris"

	if got != want {
		t.Errorf("got '%q' want '%q'", got, want)
	}
}

在一个 Test 函数中,可以通过 t.Run() 创建多个子测试

func TestHello(t *testing.T) {

    t.Run("saying hello to people", func(t *testing.T) {
        got := Hello("Chris")
        want := "Hello, Chris"

        if got != want {
            t.Errorf("got '%q' want '%q'", got, want)
        }
    })

    t.Run("say hello world when an empty string is supplied", func(t *testing.T) {
        got := Hello("")
        want := "Hello, World"

        if got != want {
            t.Errorf("got '%q' want '%q'", got, want)
        }
    })

}

通过 t.Helper() 创建辅助函数,这里将断言重构为函数

func TestHello(t *testing.T) {

    assertCorrectMessage := func(t *testing.T, got, want string) {
        t.Helper()
        if got != want {
            t.Errorf("got '%q' want '%q'", got, want)
        }
    }

    t.Run("saying hello to people", func(t *testing.T) {
        got := Hello("Chris")
        want := "Hello, Chris"
        assertCorrectMessage(t, got, want)
    })

    t.Run("empty string defaults to 'world'", func(t *testing.T) {
        got := Hello("")
        want := "Hello, World"
        assertCorrectMessage(t, got, want)
    })

}

添加示例,这些示例在运行测试时会检查输出是否与 // Output 中的值一致。示例会被展示到自动文档中

func ExampleAdd() {
	sum := Add(1, 5)
	fmt.Println(sum)
	// Output: 6
}

由于数组不能直接比较,如果测试需要比较数组可以用反射

func TestSumAllTails(t *testing.T) {

    checkSums := func(t *testing.T, got, want []int) {
        if !reflect.DeepEqual(got, want) {
            t.Errorf("got %v want %v", got, want)
        }
    }

    t.Run("make the sums of tails of", func(t *testing.T) {
        got := SumAllTails([]int{1, 2}, []int{0, 9})
        want := []int{2, 9}
        checkSums(t, got, want)
    })

    t.Run("safely sum empty slices", func(t *testing.T) {
        got := SumAllTails([]int{}, []int{3, 4, 5})
        want := []int{0, 9}
        checkSums(t, got, want)
    })

}

运行测试

帮助文档

# help
go help test

假设项目结构如下

myproject/
├── main.go
└── mypackage/
    ├── mypackage.go
    └── mypackage_test.go

运行测试

# 运行项目所有测试
go test .

# 运行某个子 package 中所有测试
go test ./mypackage

# 运行 mypackage 目录下所有包含 `TestMyFunction` 名称的 Test 函数
go test -run=TestMyFunction ./mypackage

计算测试覆盖率

go test -cover

Benchmark

Practical Go Lessons - Benchmarks

编写基准测试(benchmarks),代码会运行 b.N 次,并测量需要多长时间

func BenchmarkRepeat(b *testing.B) {
	for i := 0; i < b.N; i++ {
		Repeat("a")
	}
}

运行基准测试

# 运行所有 Benchmark 函数
go test -bench=.

# 运行所有名称包含 Repeat 的 Benchmark 函数
go test -bench=Repeat

# run benchmarks with memory statistics
go test -bench=. -benchmem
# output `64 B/op` means each operation (on average) allocated 64 bytes
# output `2 allocs/op` means there were 2 memory allocations per operation

# 指定 benchmark 运行时间
go test -bench=. -benchtime=5s

# 指定 CPU 数量运行
go test -bench=. -cpu=1,2,4 # 分别以 1、2、4 核 CPU 运行基准测试

How to read benchmark results:

go_benchmark_result

Architecture

Golang 整洁架构实践

Go Blueprint

go_clean_architecture.jpg

Project structure

├── adapter // Adapter层,适配各种框架及协议的接入,比如:Gin,tRPC,Echo,Fiber 等
├── application // App层,处理Adapter层适配过后与框架、协议等无关的业务逻辑
│   ├── consumer //(可选)处理外部消息,比如来自消息队列的事件消费
│   ├── dto // App层的数据传输对象,外层到达App层的数据,从App层出发到外层的数据都通过DTO传播
│   ├── executor // 处理请求,包括command和query
│   └── scheduler //(可选)处理定时任务,比如Cron格式的定时Job
├── domain // Domain层,最核心最纯粹的业务实体及其规则的抽象定义
│   ├── gateway // 领域网关,model的核心逻辑以Interface形式在此定义,交由Infra层去实现
│   └── model // 领域模型实体
├── infrastructure // Infra层,各种外部依赖,组件的衔接,以及domain/gateway的具体实现
│   ├── cache //(可选)内层所需缓存的实现,可以是Redis,Memcached等
│   ├── client //(可选)各种中间件client的初始化
│   ├── config // 配置实现
│   ├── database //(可选)内层所需持久化的实现,可以是MySQL,MongoDB,Neo4j等
│   ├── distlock //(可选)内层所需分布式锁的实现,可以基于Redis,ZooKeeper,etcd等
│   ├── log // 日志实现,在此接入第三方日志库,避免对内层的污染
│   ├── mq //(可选)内层所需消息队列的实现,可以是Kafka,RabbitMQ,Pulsar等
│   ├── node //(可选)服务节点一致性协调控制实现,可以基于ZooKeeper,etcd等
│   └── rpc //(可选)广义上第三方服务的访问实现,可以通过HTTP,gRPC,tRPC等
└── pkg // 各层可共享的公共组件代码

go_timeline_diagram.png

Code snippets

string

修改字符串中的一个字符

str := "hello"
c := []byte(str)
c[0] = 'c'
s2 := string(c) // s2 == "cello"

获取字符串的子串

substr := str[n:m]

遍历一个字符串

// gives only the bytes:
for i:=0; i<len(str); i++ {
    fmt.Println(str[i])
}
// gives the Unicode characters:
for index, unicodeChar := range str {
	fmt.Println(index, unicodeChar) //  
}

获取一个字符串的字节数

len(str)

获取一个字符串的字符数

len([]rune(str))
// or
utf8.RuneCountInString(str)

连接字符串

// 使用 `+=`
str1 := "Hello " 
str2 := "World!"
str1 += str2 //str1 == "Hello World!"

// 使用 `bytes.Buffer`,当字符串数目特别多时推荐使用这种方式,而非 `+=`
var buffer bytes.Buffer
buffer.WriteString(str1)
buffer.WriteString(str2)
s := buffer.String()

// 使用`strings.Join()`
s := strings.Join([]string{str1, str2}, " ")

array/slice

创建

arr1 := new([len]type)

slice1 := make([]type, len)

初始化

arr1 := [...]type{i1, i2, i3, i4, i5}
arrKeyValue := [len]type{i1: val1, i2: val2}
var slice1 []type = arr1[start:end]

切片的最后一个元素

line = line[:len(line)-1]

遍历一个数组/切片

for i:=0; i < len(arr); i++ {
    fmt.Println(arr[i])
}
for ix, value := range arr {
    fmt.Println(value)
}

复制切片,注意创建目标切片时容量一定要大于源切片,否则无法复制

source := []int{1, 2, 3}
target := make([]int, len(source)) 
copy(target, source)

map

创建

map1 := make(map[keytype]valuetype)

初始化

map1 := map[string]int{"one": 1, "two": 2}

遍历

for key, value := range map1 {
    fmt.Println(key, value)
}

检测键是否存在

val1, ok = map1[key1]

if val1, ok = map1[key1]; ok {
    fmt.Println(val1)
}

删除一个键

delete(map1, key1)

struct

创建

type Person struct {
    Name string
    Age int
}
p := new(Person)

初始化

p := &Person{"Chris", 10}
// or
p := &Person{
    Name: "Chris",
    Age: 10,
}

通常情况下,为每个结构体定义一个构建函数,并推荐使用构建函数初始化结构体

func NewPerson(name string, age int) *Person {
    return &Person{name, age} 
}

p := NewPerson("Chris", 10)

interface

检测一个值 v 是否实现了接口 Stringer

if v, ok := v.(Stringer); ok {
    fmt.Printf("implements String(): %s\n", v.String())
}

使用接口实现类型分类

switch x.(type) {
case bool:
    fmt.Printf("param #%d is a bool\n", i)
case float64:
    fmt.Printf("param #%d is a float64\n", i)
case int, int64:
    fmt.Printf("param #%d is an int\n", i)
case nil:
    fmt.Printf("param #%d is nil\n", i)
case string:
    fmt.Printf("param #%d is a string\n", i)
default:
    fmt.Printf("param #%d’s type is unknown\n", i)
}

function

闭包

func() {
    fmt.Println("I'm a closure")
}()

捕捉 panic

func protect(g func()) {
    defer func() {
        log.Println("done")
        // Println executes normally even if there is a panic
        if x := recover(); x != nil {
            log.Printf("run time panic: %v", x)
        }
    }()
    log.Println("start")
    g()
}

goroutine

遍历一个通道

for v := range ch {
    // do something with v
}

阻塞主程序直到协程完成

ch := make(chan int) // Allocate a channel.
// Start something in a goroutine; when it completes, signal on the channel.
go func() {
    // doSomething
    ch <- 1 // Send a signal; value does not matter.
}()
doSomethingElseForAWhile()
<-ch // Wait for goroutine to finish; discard sent value.

通道的工厂模板

func pump() chan int {
    ch := make(chan int)
    go func() {
        for i := 0; ; i++ {
            ch <- i
        }
    }()
    return ch
}

通道迭代器模式

func (c *container) Iter () <- chan item {
    ch := make(chan item)
    go func () {
        for i:= 0; i < c.Len(); i++{	// or use a for-range loop
            ch <- c.items[i]
        }
    } ()
    return ch
}

限制同时处理的请求数

package main

const MAXREQS = 50
var sem = make(chan int, MAXREQS)

type Request struct {
	a, b   int
	replyc chan int
}

func process(r *Request) {
	// do something
}

func handle(r *Request) {
	sem <- 1 // doesn't matter what we put in it
	process(r)
	<-sem // one empty place in the buffer: the next request can start
}

func server(service chan *Request) {
	for {
		request := <-service
		go handle(request)
	}
}

func main() {
	service := make(chan *Request)
	go server(service)
}

在多核CPU上实现并行计算

func DoAll(){
    sem := make(chan int, NCPU) // Buffering optional but sensible
    for i := 0; i < NCPU; i++ {
        go DoPart(sem)
    }
    // Drain the channel sem, waiting for NCPU tasks to complete
    for i := 0; i < NCPU; i++ {
        <-sem // wait for one task to complete
    }
    // All done.
}

func DoPart(sem chan int) {
    // do the part of the computation
    sem <-1 // signal that this piece is done
}

func main() {
    runtime.GOMAXPROCS(NCPU) // runtime.GOMAXPROCS = NCPU
    DoAll()
}

终止一个协程

runtime.Goexit()

通知协程退出

func worker(exitChan chan bool) {
	for {
		select {
		case <-exitChan:
			fmt.Println("Worker exiting.")
			return
		default:
			fmt.Println("Working...")
			time.Sleep(1 * time.Second)
		}
	}
}

func RunCancel() {
	exitChan := make(chan bool)
	go worker(exitChan)

	time.Sleep(3 * time.Second) // 模拟做了一些工作
	exitChan <- true            // 通知协程退出
	time.Sleep(1 * time.Second) // 给协程时间退出
}

使用 context 通知协程退出

func workerWithCtx(ctx context.Context) {
	for {
		select {
		case <-ctx.Done():
			fmt.Println("Worker exiting.")
			return
		default:
			fmt.Println("Working...")
			time.Sleep(1 * time.Second)
		}
	}
}

func RunCancelWithCtx() {
	ctx, cancel := context.WithCancel(context.Background())
	go workerWithCtx(ctx)

	time.Sleep(3 * time.Second) // 模拟做了一些工作
	cancel()                    // 通知协程退出
	time.Sleep(1 * time.Second) // 给协程时间退出
}

使用输入通道和输出通道代替锁

func Worker(in, out chan *Task) {
    for {
        t := <-in
        process(t)
        out <- t
    }
}

超时模板

timeout := make(chan bool, 1)
go func() {
    time.Sleep(1e9) // one second  
    timeout <- true
}()
select {
    case <- ch:
    // a read from ch has occurred
    case <- timeout:
    // the read from ch has timed out
}

取消耗时很长的同步调用

ch := make(chan error, 1)
go func() { ch <- client.Call("Service.Method", args, &reply) } ()
select {
case resp := <-ch
    // use resp and reply
case <- time.After(timeoutNs): 
    // call timed out
    break
}

// time.After 会创建一个 Timer,在触发前不会被 GC 回收
// 如果会在很多个协程启用,不能这样写,可能会有内存泄露    

delay := time.NewTimer(3 * time.Minute)
defer delay.Stop()
for {
    delay.Reset(3 * time.Minute)
    select {
    case resp := <-ch:
        // ...
    case <- delay.C:
        // time out
    }
}

file

打开一个文件并读取

file, err := os.Open("input.dat")
    if err != nil {
        fmt.Printf("An error occurred on opening the inputfile\n" +
            "Does the file exist?\n" +
            "Have you got acces to it?\n")
        return
    }
    defer file.Close()
    iReader := bufio.NewReader(file)
    for {
        str, err := iReader.ReadString('\n')
        if err != nil {
            return // error or EOF
        }
        fmt.Printf("The input was: %s", str)
    }
}

exit

在程序出错时终止程序

if err != nil {
   fmt.Printf("Program stopping with error %v", err)
   os.Exit(1)
}
// or
if err != nil { 
	panic("ERROR occurred: " + err.Error())
}

Debug

go get问题

不能安装github包

因为go get是基于git的方式获取仓库的,然后默认用的是https的,被拒绝了,我们需要换成ssh的

git config --global url.git@github.com:.insteadOf https://github.com/

证书过期

# 加上 -insecure 参数,deprecated
go get -insecure https://git.code.oa.com/tpstelemetry/cgroups

# 设置 GOINSECURE 环境变量
export GOINSECURE="git.code.oa.com/*"

需要输入帐密

go get disables the “terminal prompt” by default. This can be changed by setting an environment variable of git:

env GIT_TERMINAL_PROMPT=1 go mod tidy

找不到外部包(<1.11)

example.go:3:8: no required module provides package XXX: go.mod file not found in current directory or any parent directory; see 'go help modules'

  • go get ./...
  • 要把项目建到$GOPATH/src/下,否则会报 go get: no install location for directory XXX outside GOPATH

Don’t split the main package

不然编译器会报错

查看运行的Go程序

go get -u github.com/google/gops
gops

VSCode自动删除未引用包

settings.json中加上

"[go]": {
  "editor.codeActionsOnSave": {
    "source.organizeImports": false
  }
}