26 KiB
Golang
Get Started
Enable dependency tracking
当代码对其他module中包含的package进行了import时,在自己的module中来管理依赖。
自己的module通过go.mod文件来定义,go.mod文件中会track项目所需要的依赖。
go mod init
go mod init <module-name>命令会创建一个go.mod文件,其中<module-name>会是module path。
在实际开发中,module name通常是source code被保存的repository location,例如,uuidmodule的module name为github.com/google/uuid。
go mod tidy
go mod tidy命令会根据import添加缺失的module并且移除未使用的module。
multi-module workspace
示例目录结构如下所示:
- workspace
- workspace/hello
- workspace/example/hello
go work init
在本示例中,为了创建多module的workspace,可以执行go work init ./hello,其会创建go.work文件,并将./hello目录下的module包含到go.work文件中。
go.work内容如下:
go 1.18
use ./hello
go work use
通过go work use ./example/hello命令,会将./example/hello中的module加入到go.work文件中。
go.work内容如下:
go 1.18
use (
./hello
./example/hello
)
go work use [-r] [dir]命令行为如下:
- 如果指定目录存在,会为
dir向go.work文件中添加一条use指令 - 如果指定目录不存在,会删除
go.work文件中关于目录的use指令
Gin框架构建restful api
在构建resultful api时,通常都会通过json格式来传递数据,首先,可定义业务实体:
// album represents data about a record album.
type album struct {
ID string `json:"id"`
Title string `json:"title"`
Artist string `json:"artist"`
Price float64 `json:"price"`
}
向response中写入返回数据
可以通过调用gin.Context的IndentedJSON方法来向response中写入数据,
// getAlbums responds with the list of all albums as JSON.
func getAlbums(c *gin.Context) {
c.IndentedJSON(http.StatusOK, albums)
}
解析request中的数据
可以通过gin.Context的BindJSON方法来将请求体中的数据解析到对象中。
// postAlbums adds an album from JSON received in the request body.
func postAlbums(c *gin.Context) {
var newAlbum album
// Call BindJSON to bind the received JSON to
// newAlbum.
if err := c.BindJSON(&newAlbum); err != nil {
return
}
// Add the new album to the slice.
albums = append(albums, newAlbum)
c.IndentedJSON(http.StatusCreated, newAlbum)
}
将请求的endpoint注册到server中
可以将各个请求的处理handler注册到server中,并在指定端口上运行server:
func main() {
router := gin.Default()
router.GET("/albums", getAlbums)
router.POST("/albums", postAlbums)
router.Run("localhost:8080")
}
上述示例中,分别向/albums路径注册了GET和POST的处理handler,并在localhost:8080上对服务进行监听。
golang generic
不使用泛型的代码编写
如果不使用泛型,那么对于不同数值类型的求和,需要编写多个版本的代码,示例如下:
// SumInts adds together the values of m.
func SumInts(m map[string]int64) int64 {
var s int64
for _, v := range m {
s += v
}
return s
}
// SumFloats adds together the values of m.
func SumFloats(m map[string]float64) float64 {
var s float64
for _, v := range m {
s += v
}
return s
}
上述针对int64和float64的版本,编写了两个独立的函数
使用泛型的代码编写
对于泛型方法的编写,其相对普通方法多了类型参数,在对泛型方法进行调用时,可以传递类型参数和普通参数。
对于每个parameter type,其都有对应的type constraint。每个type constraint都制定了在调用泛型方法时,可以对parameter type指定哪些类型。
type parameter通常都带代表一系列类型的集合,但是在编译时type parameter则是代表由调用方传递的type argument类型。如果type argument类型不满足type constraint的要求,那么该代码则不会被成功编译。
type parameter必须要支持generic code中执行的所有操作。
// SumIntsOrFloats sums the values of map m. It supports both int64 and float64
// as types for map values.
func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V {
var s V
for _, v := range m {
s += v
}
return s
}
comparable
在上述示例中,K的constraint为comparable,go中预先声明了该constraint。comparable约束代表其可以类型可以通过==或!=符号来进行比较。
golang中要求key类型为comparable。
上述示例中,V的类型约束为float64 | int64,|符号代表取两种类型的并集,实际类型可以为两种类型中的任何一种。
泛型方法调用
// 指定类型参数
fmt.Printf("Generic Sums: %v and %v\n",
SumIntsOrFloats[string, int64](ints),
SumIntsOrFloats[string, float64](floats))
// 不指定类型参数
fmt.Printf("Generic Sums, type parameters inferred: %v and %v\n",
SumIntsOrFloats(ints),
SumIntsOrFloats(floats))
在调用golang的泛型方法时,可以省略类型参数,此时编译器会推断类型参数类型,但是对于部分场景,例如没有参数只有返回值的参数func returnObj[V any]() V,此时泛型类型无法被推断,只能手动指定。
type constraint
在golang中,支持将泛型约束声明为接口,并在多个地方重用该接口。通过将约束声明为接口,可以避免复杂泛型约束的重复声明。
可以将泛型constraint声明为接口,并且允许任何类型实现该接口,将接口用作type constraint的指定,那么传递给方法的任何类型参数都要实现该接口,包含该接口中所有的方法。
代码示例如下:
type Number interface {
int64 | float64
}
// SumNumbers sums the values of map m. It supports both integers
// and floats as map values.
func SumNumbers[K comparable, V Number](m map[K]V) V {
var s V
for _, v := range m {
s += v
}
return s
}
fmt.Printf("Generic Sums with Constraint: %v and %v\n",
SumNumbers(ints),
SumNumbers(floats))
Fuzzing
如下为一个反转字符串的示例:
package fuzz
func Reverse(str string) (ret string, err error) {
bytes := []byte(str)
for i, j := 0, len(bytes)-1;i<j; i, j = i+1, j-1 {
bytes[i], bytes[j] = bytes[j], bytes[i]
}
ret = string(bytes)
return
}
unit test编写
如果要对上述代码进行单元测试,可以手动在代码中指定测试用例,并对返回值进行断言判断。
如果想要针对golang文件创建unit test,可以按照如下步骤
- 为go文件创建对应的
_test.go文件,例如reverse.go文件,其对应单元测试文件为reverse_test.go - 为go文件中想要测试的方法,在
_test.go文件中创建对应的TestXxxx方法,例如对Reverse方法,可以创建名为TestReverse的测试方法
在完成上述步骤并且完成测试方法逻辑的编写后,可以调用go test命令执行单元测试。
示例如下所示:
package main
import (
"testing"
)
func TestReverse(t *testing.T) {
testcases := []struct {
in, want string
}{
{"Hello, world", "dlrow ,olleH"},
{" ", " "},
{"!12345", "54321!"},
}
for _, tc := range testcases {
rev := Reverse(tc.in)
if rev != tc.want {
t.Errorf("Reverse: %q, want %q", rev, tc.want)
}
}
}
fuzz test编写
在对功能进行测试时,相对于手动编码测试用例,fuzz能够自动生成用例,并可能触及到手动用例无法触及的边界用例。
但是,相较于unit text,fuzz无法预知传递的测试参数,也无法预测参数所对应的结果。
在编写fuzz test时,可以将unit test, fuzz test, benchmark都包含在同一个
_test.go文件中
在编写fuzz test方法时,步骤如下
- fuzz test方法以
FuzzXxx开头,和TestXxx类似 - 当调用
f.Add时,会将参数作为seed添加
Fuzz test示例如下:
func FuzzReverse(f *testing.F) {
testcases := []string{"Hello, world", " ", "!12345"}
for _, tc := range testcases {
f.Add(tc) // Use f.Add to provide a seed corpus
}
f.Fuzz(func(t *testing.T, orig string) {
rev := Reverse(orig)
doubleRev := Reverse(rev)
if orig != doubleRev {
t.Errorf("Before: %q, after: %q", orig, doubleRev)
}
if utf8.ValidString(orig) && !utf8.ValidString(rev) {
t.Errorf("Reverse produced invalid UTF-8 string %q", rev)
}
})
}
执行fuzz test
在编写完上述代码后,首先应该执行go test命令,确保作为seed被添加的cases能够通过测试
go test
如果,只想执行指定的测试方法,可以指定
-run参数,示例如下go test -run=FuzzReverse
在go test执行通过之后,应该执行go test -fuzz=Fuzz命令,其会将随机产生的输入作为测试。
如果存在随机生成的用例测试不通过,会将该用例写入到seed corups文件中,在下次go test命令被执行时,即使没有指定-fuzz选项,被写入文件中的用例也会被在此执行。
当通过
go test -fuzz=Fuzz执行fuzz test时,测试会一直进行,直到被Ctrl + C中断。如果要指定fuzz test的时间,可以指定
-fuzztime选项。示例如下:
go test -fuzz=Fuzz -fuzztime 30s
fuzz test的输出结果示例如下:
PS D:\CodeSpace\demo\fuzz> go test -fuzz=Fuzz -fuzztime 10s
fuzz: elapsed: 0s, gathering baseline coverage: 0/56 completed
fuzz: elapsed: 0s, gathering baseline coverage: 56/56 completed, now fuzzing with 32 workers
fuzz: elapsed: 3s, execs: 3028153 (1008398/sec), new interesting: 2 (total: 58)
fuzz: elapsed: 6s, execs: 6197524 (1057429/sec), new interesting: 2 (total: 58)
fuzz: elapsed: 9s, execs: 9423882 (1075420/sec), new interesting: 2 (total: 58)
fuzz: elapsed: 10s, execs: 10482150 (926639/sec), new interesting: 2 (total: 58)
PASS
ok git.kazusa.red/asahi/fuzz-demo 10.360s
new interesting
new interesting指会扩充code coverage的用例输入,在fuzz test刚开始时,new interesting数量通常会因发现新的代码路径快速增加,然后,会随着时间的推移逐渐减少
Go Sync
sync.Pool
sync.Pool为golang标准库中的实现,用于降低allocation和减少垃圾回收。
sync.Pool使用示例
golang中sync.Pool使用示例如下:
package main
import (
"fmt"
"sync"
)
type JobState int
const (
JobStateFresh JobState = iota
JobStateRunning
JobStateRecycled
)
type Job struct {
state JobState
}
func (j *Job) Run() {
switch j.state {
case JobStateRecycled:
fmt.Println("this job came from the pool")
case JobStateFresh:
fmt.Println("this job just got allocated")
}
j.state = JobStateRunning
}
func main() {
pool := &sync.Pool{
New: func() any {
return &Job{state: JobStateFresh}
},
}
// get a job from the pool
job := pool.Get().(*Job)
// run it
job.Run()
// put it back in the pool
job.state = JobStateRecycled
pool.Put(job)
}
Pool和垃圾回收
sync.Pool对象实际是由两部分组成:
local poolvictim pool
调用Pool中的方法时,实际行为如下:
Put:调用Put方法时,会将对象添加到localGet:调用Get时,首先会从local中查找,如果local中未找到,那么会从victim中查找,如果victim中仍然不存在,那么则是会调用New
在sync.Pool中,local被用作primary cache,victim则被用作victim cache。
poolCleanup
poolCleanUp方法实现如下:
func poolCleanup() {
// This function is called with the world stopped, at the beginning of a garbage collection.
// It must not allocate and probably should not call any runtime functions.
// Because the world is stopped, no pool user can be in a
// pinned section (in effect, this has all Ps pinned).
// Drop victim caches from all pools.
for _, p := range oldPools {
p.victim = nil
p.victimSize = 0
}
// Move primary cache to victim cache.
for _, p := range allPools {
p.victim = p.local
p.victimSize = p.localSize
p.local = nil
p.localSize = 0
}
// The pools with non-empty primary caches now have non-empty
// victim caches and no pools have primary caches.
oldPools, allPools = allPools, nil
}
var (
allPoolsMu Mutex
// allPools is the set of pools that have non-empty primary
// caches. Protected by either 1) allPoolsMu and pinning or 2)
// STW.
allPools []*Pool
// oldPools is the set of pools that may have non-empty victim
// caches. Protected by STW.
oldPools []*Pool
)
func init() {
runtime_registerPoolCleanup(poolCleanup)
}
allPools & oldPools
所有被实例化的sync.Pool对象,在修生变化时,都会将其自身注册到allPools静态变量中。
其中,allPools引用了所有local(primary cache)不为空的pool实例,而oldPools则引用了所有victim(victim cache)不为空的pool实例。
在init方法中,将poolCleanup注册到了runtime,在STW的上线文中,poolCleanup将会在垃圾回收之前被runtime调用。
poolCleanup方法逻辑比较简单,具体如下:
- 将
victim丢弃,并且将local转移到victim,最后将local置为空 - 将静态变量中
allPools的值转移到oldPools,并且将oldPools的值置为空
这代表如果pool中的对象如果长期未被访问,那么将会从pool中被淘汰。
poolCleanUp方法在STW时会被调用,第一次STW时,未使用对象会从local移动到victim,而第二次STW,则是会从victim中被丢弃,之后被后续的垃圾回收清理。
Proc Pining
关于sync.Pool,其实际结构如下:
type Pool struct {
noCopy noCopy
local unsafe.Pointer // local fixed-size per-P pool, actual type is [P]poolLocal
localSize uintptr // size of the local array
victim unsafe.Pointer // local from previous cycle
victimSize uintptr // size of victims array
// New optionally specifies a function to generate
// a value when Get would otherwise return nil.
// It may not be changed concurrently with calls to Get.
New func() any
}
per-P
关于调度的actor,其存在如下角色:
- goroutine:
G's - machines:
M's代表系统线程 - processor:
P's代表处理器物理线程
其中,goroutine由操作系统线程执行,而操作系统线程在执行时需要获取实际的cpu物理线程。
在gouroutine运行时,存在一些safe-point,在safe-pointgoroutine可以在clean状态被停止。故而,抢占只能发生在safe-point。
proc pinning会禁止抢占,在pinning后,P(物理线程)将会被独占,在unpin发生之前,goroutine会一直执行,并不会被停止,甚至不会被GC停止。unpin之前,P无法被其他goroutine使用。
一旦pinned后,execution flow在P上不会被中断,这也意味着在访问threadlocal数据时无需加锁。
如下是围绕Pinning的逻辑:
// pin pins the current goroutine to P, disables preemption and
// returns poolLocal pool for the P and the P's id.
// Caller must call runtime_procUnpin() when done with the pool.
func (p *Pool) pin() (*poolLocal, int) {
pid := runtime_procPin()
// In pinSlow we store to local and then to localSize, here we load in opposite order.
// Since we've disabled preemption, GC cannot happen in between.
// Thus here we must observe local at least as large localSize.
// We can observe a newer/larger local, it is fine (we must observe its zero-initialized-ness).
s := runtime_LoadAcquintptr(&p.localSize) // load-acquire
l := p.local // load-consume
if uintptr(pid) < s {
return indexLocal(l, pid), pid
}
return p.pinSlow()
}
func indexLocal(l unsafe.Pointer, i int) *poolLocal {
lp := unsafe.Pointer(uintptr(l) + uintptr(i)*unsafe.Sizeof(poolLocal{}))
return (*poolLocal)(lp)
}
local & localSize
local:local是一个由poolLocal对象组成c-style数组localSize:localSize是local数组的大小poolLocal: local数组中的每个poolLocal都关联一个给定的Pruntime_procPin:该方法会返回pin锁关联的processor id,processor id从0开始依次加1,直到GOMAXPROCS
分析上述indexLocal方法的逻辑,其根据processor id的值,计算了pid关联poolLocal对象地址的偏移量,并返回poolLocal对象的指针。这令我们可以并发安全的访问poolLocal对象而无需加锁,只需要pinned并且直接访问threadlocal变量。
PinSlow
pinSlow方法是针对pin的fallback方法,其代表我们针对local数组大小的假设是错误的,本次绑定的P其并没有对应的poolLocal。
代码进入到pinSlow有如下可能:
GOMAXPROCS被更新过,从而有了额外可用的P- 该pool对象是新创建的
pinSlow的代码如下:
func (p *Pool) pinSlow() (*poolLocal, int) {
// Retry under the mutex.
// Can not lock the mutex while pinned.
runtime_procUnpin()
allPoolsMu.Lock()
defer allPoolsMu.Unlock()
pid := runtime_procPin()
// poolCleanup won't be called while we are pinned.
s := p.localSize
l := p.local
if uintptr(pid) < s {
return indexLocal(l, pid), pid
}
if p.local == nil {
allPools = append(allPools, p)
}
// If GOMAXPROCS changes between GCs, we re-allocate the array and lose the old one.
size := runtime.GOMAXPROCS(0)
local := make([]poolLocal, size)
atomic.StorePointer(&p.local, unsafe.Pointer(&local[0])) // store-release
runtime_StoreReluintptr(&p.localSize, uintptr(size)) // store-release
return &local[pid], pid
}
当处于pinned状态时,无法获取针对allPools变量的锁,这样有可能会导致死锁。
如果在处于pinned状态的情况下获取锁,那么此时锁可能被其他goroutine持有,而持有锁的goroutine可能正在等待我们释放P
故而,在pinSlow中,首先unpin,然后获取锁,并且在获取锁之后重新进入pin状态
在重新进入pin状态并且获取到allPoolsMu的锁之后,首先会检测目前pid是否有关联的poolLocal对象,如果有,则直接返回,这通常在如下场景下发生:
- 在阻塞获取allPoolsMu锁时,其他goroutinue已经为我们扩充了local数组的大小
- 我们不再绑定在之前的P上了,我们可能绑定在另一个pid小于local数组大小的P上
如果目前pool对象其local数组为空,那么其会先将pool实例注册到allPools中,然后执行如下逻辑:
- 创建一个新的poolLocal slice,slice大小和GOMAXPROCS相同,并将新创建slice的头一个元素地址存储到
p.local中 - 将slice大小存储在
p.localSize中
Pool Local
poolLocal结构如下:
// Local per-P Pool appendix.
type poolLocalInternal struct {
private any // Can be used only by the respective P.
shared poolChain // Local P can pushHead/popHead; any P can popTail.
}
type poolLocal struct {
poolLocalInternal
// Prevents false sharing on widespread platforms with
// 128 mod (cache line size) = 0 .
pad [128 - unsafe.Sizeof(poolLocalInternal{})%128]byte
}
对于poolLocalIntenral中的poolChain,local P可以执行pushHead/popHead逻辑,而任何P都可以执行popTail逻辑
pool的Put/Get
Put
其中,Put相关逻辑如下:
// Put adds x to the pool.
func (p *Pool) Put(x any) {
if x == nil {
return
}
if race.Enabled {
if fastrandn(4) == 0 {
// Randomly drop x on floor.
return
}
race.ReleaseMerge(poolRaceAddr(x))
race.Disable()
}
l, _ := p.pin()
if l.private == nil {
l.private = x
} else {
l.shared.pushHead(x)
}
runtime_procUnpin()
if race.Enabled {
race.Enable()
}
}
其核心逻辑如下:
- pin,并获取poolLocal
- 如果poolLocal中private为空,将item放到private中
- 如果private不为空,将其放入shared中,LIFO
- 然后unpin
Get
Get的相关逻辑如下:
// Get selects an arbitrary item from the Pool, removes it from the
// Pool, and returns it to the caller.
// Get may choose to ignore the pool and treat it as empty.
// Callers should not assume any relation between values passed to Put and
// the values returned by Get.
//
// If Get would otherwise return nil and p.New is non-nil, Get returns
// the result of calling p.New.
func (p *Pool) Get() any {
if race.Enabled {
race.Disable()
}
l, pid := p.pin()
x := l.private
l.private = nil
if x == nil {
// Try to pop the head of the local shard. We prefer
// the head over the tail for temporal locality of
// reuse.
x, _ = l.shared.popHead()
if x == nil {
x = p.getSlow(pid)
}
}
runtime_procUnpin()
if race.Enabled {
race.Enable()
if x != nil {
race.Acquire(poolRaceAddr(x))
}
}
if x == nil && p.New != nil {
x = p.New()
}
return x
}
以下是pool的Get核心流程:
- pin, 并且获取poolLocal
- 将private清空,并且判断之前private是否有值,如果有值,将使用该值
- 如果private之前没有值,那么对shared执行pop操作,LIFO,如果pop操作获取的值不为空,使用该值
- 如果对shared执行LIFO pop操作的也为空,那么会执行slow path的getSlow方法
- 如果在getSlow仍然未获取到值的情况下,会调用
New方法来获取值
LIFO
对于poolLocal的shared队列,其使用的是LIFO,最后添加到队列的元素会被最先弹出。这代表我们希望使用最新分配的对象,旧分配的对象会随着
STW被逐渐淘汰。
slow path
在调用Get方法时,slow path仅当private和shared都为空时被触发,这代表当前threadlocal pool为空。
在触发slow path场景下,会尝试从其他P中窃取对象,如果在窃取仍然失败的场景下,才会去victim中进行查找。
Get方法中slow path实现如下:
func (p *Pool) getSlow(pid int) any {
// See the comment in pin regarding ordering of the loads.
size := runtime_LoadAcquintptr(&p.localSize) // load-acquire
locals := p.local // load-consume
// Try to steal one element from other procs.
for i := 0; i < int(size); i++ {
l := indexLocal(locals, (pid+i+1)%int(size))
if x, _ := l.shared.popTail(); x != nil {
return x
}
}
// Try the victim cache. We do this after attempting to steal
// from all primary caches because we want objects in the
// victim cache to age out if at all possible.
size = atomic.LoadUintptr(&p.victimSize)
if uintptr(pid) >= size {
return nil
}
locals = p.victim
l := indexLocal(locals, pid)
if x := l.private; x != nil {
l.private = nil
return x
}
for i := 0; i < int(size); i++ {
l := indexLocal(locals, (pid+i)%int(size))
if x, _ := l.shared.popTail(); x != nil {
return x
}
}
// Mark the victim cache as empty for future gets don't bother
// with it.
atomic.StoreUintptr(&p.victimSize, 0)
return nil
}
- 首先,会尝试对
pool.local数组中所有的poolLocal对象都调用popTail方法,如果任一方法返回值不为空,那么将会使用该返回的值。窃取操作会尝试窃取尾部的对象,这是最先被创建的对象。 - 如果在local中未能找到和窃取到对象,那么会从victim中进行查找
- 首先,获取victim中当前pid对象的poolLocal对象,检查poolLocal对象private是否不为空,如果不为空,使用该值并将victim.private清空
- 如果private为空,那么则对victim中所有P关联的poolLocal对象执行popTail操作,如果任何一个pop操作返回不为空,那么使用返回的对象
- 如果所有victim中的poolLocal对象都返回为空,那么会将victim中
p.victimSize标识为空,后续再次执行slow path时,如果感知到victimSize为空,那么便不会再次查找victim
syntax
iota
iota关键字代表连续的整数变量,0, 1, 2,每当const关键字出现时,其重置为0
其使用示例如下
package main
import "fmt"
const (
ZERO = iota
ONE
)
const TWO = iota
const (
THREE = iota
FOUR
)
func main() {
fmt.Println(ZERO) // 0
fmt.Println(ONE) // 1
fmt.Println(TWO) // 0
fmt.Println(THREE) // 0
fmt.Println(FOUR) // 1
}
另外,const关键字也支持如下语法:
package main
import (
"fmt"
"reflect"
)
type Shiro uint8
const (
ZERO Shiro = iota
ONE
)
const TWO Shiro = iota
const (
THREE Shiro = iota
FOUR
)
func main() {
fmt.Printf("ZERO %d, %s\n", ZERO, reflect.TypeOf(ZERO).Name())
fmt.Printf("ONE %d, %s\n", ONE, reflect.TypeOf(ONE).Name())
fmt.Printf("TWO %d, %s\n", TWO, reflect.TypeOf(TWO).Name())
fmt.Printf("THREE %d, %s\n", THREE, reflect.TypeOf(THREE).Name())
fmt.Printf("FOUR %d, %s\n", FOUR, reflect.TypeOf(FOUR).Name())
}
当const在()中声明多个常量时,如首个常量的类型被指定,则后续常量类型可省略,后续类型与首个被指定的类型保持一致。