- [Golang](#golang) - [Get Started](#get-started) - [Enable dependency tracking](#enable-dependency-tracking) - [go mod init](#go-mod-init) - [go mod tidy](#go-mod-tidy) - [multi-module workspace](#multi-module-workspace) - [go work init](#go-work-init) - [go work use](#go-work-use) - [Gin框架构建restful api](#gin框架构建restful-api) - [向response中写入返回数据](#向response中写入返回数据) - [解析request中的数据](#解析request中的数据) - [将请求的endpoint注册到server中](#将请求的endpoint注册到server中) - [golang generic](#golang-generic) - [不使用泛型的代码编写](#不使用泛型的代码编写) - [使用泛型的代码编写](#使用泛型的代码编写) - [comparable](#comparable) - [泛型方法调用](#泛型方法调用) - [type constraint](#type-constraint) - [Fuzzing](#fuzzing) - [unit test编写](#unit-test编写) - [fuzz test编写](#fuzz-test编写) - [执行fuzz test](#执行fuzz-test) - [sync.Pool](#syncpool) - [sync.Pool使用示例](#syncpool使用示例) - [Pool和垃圾回收](#pool和垃圾回收) - [poolCleanup](#poolcleanup) - [allPools \& oldPools](#allpools--oldpools) - [Proc Pining](#proc-pining) - [per-P](#per-p) - [local \& localSize](#local--localsize) - [PinSlow](#pinslow) - [Pool Local](#pool-local) - [pool的Put/Get](#pool的putget) - [Put](#put) - [Get](#get) - [slow path](#slow-path) - [Sync.once](#synconce) - [use case](#use-case) - [reflect](#reflect) - [reflect basic](#reflect-basic) - [Type](#type) - [Kind](#kind) - [常用方法](#常用方法) - [NumField](#numfield) - [Field](#field) - [logrus](#logrus) - [import](#import) - [Log Level](#log-level) - [Redirect Output](#redirect-output) - [log on console](#log-on-console) - [log messages in log file](#log-messages-in-log-file) - [将日志同时输出到log file和console](#将日志同时输出到log-file和console) - [展示日志输出行数](#展示日志输出行数) - [logrus hook](#logrus-hook) - [JSONFormatter](#jsonformatter) - [设置TimeStampFormat格式](#设置timestampformat格式) - [自定义Formatter](#自定义formatter) - [syntax](#syntax) - [iota](#iota) # Golang ## Get Started ### Enable dependency tracking 当代码对其他module中包含的package进行了import时,在自己的module中来管理依赖。 自己的module通过`go.mod`文件来定义,`go.mod`文件中会track项目所需要的依赖。 #### go mod init `go mod init `命令会创建一个`go.mod`文件,其中``会是module path。 在实际开发中,module name通常是source code被保存的repository location,例如,`uuid`module的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格式来传递数据,首先,可定义业务实体: ```go // 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中写入数据, ```go // getAlbums responds with the list of all albums as JSON. func getAlbums(c *gin.Context) { c.IndentedJSON(http.StatusOK, albums) } ``` ### 解析request中的数据 可以通过`gin.Context`的`BindJSON`方法来将请求体中的数据解析到对象中。 ```go // 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: ```go 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 ### 不使用泛型的代码编写 如果不使用泛型,那么对于不同数值类型的求和,需要编写多个版本的代码,示例如下: ```go // 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`中执行的所有操作。 ```go // 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`,`|`符号代表取两种类型的并集,实际类型可以为两种类型中的任何一种。 ### 泛型方法调用 ```go // 指定类型参数 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的指定,那么传递给方法的任何类型参数都要实现该接口,包含该接口中所有的方法。 代码示例如下: ```go 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 如下为一个反转字符串的示例: ```go package fuzz func Reverse(str string) (ret string, err error) { bytes := []byte(str) for i, j := 0, len(bytes)-1;i 在编写fuzz test时,可以将unit test, fuzz test, benchmark都包含在同一个`_test.go`文件中 在编写fuzz test方法时,步骤如下 - fuzz test方法以`FuzzXxx`开头,和`TestXxx`类似 - 当调用`f.Add`时,会将参数作为seed添加 Fuzz test示例如下: ```go 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能够通过测试 ```bash go test ``` > 如果,只想执行指定的测试方法,可以指定`-run`参数,示例如下 > > ```bash > 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`选项。 > > 示例如下: > ```bash > go test -fuzz=Fuzz -fuzztime 30s > ``` fuzz test的输出结果示例如下: ```powershell 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数量通常会因发现新的代码路径快速增加,然后,会随着时间的推移逐渐减少 ## sync.Pool `sync.Pool`为golang标准库中的实现,用于降低allocation和减少垃圾回收。 ### sync.Pool使用示例 golang中`sync.Pool`使用示例如下: ```go 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 pool` - `victim pool` 调用Pool中的方法时,实际行为如下: - `Put`:调用Put方法时,会将对象添加到`local` - `Get`:调用Get时,首先会从`local`中查找,如果`local`中未找到,那么会从`victim`中查找,如果victim中仍然不存在,那么则是会调用`New` 在`sync.Pool`中,`local`被用作primary cache,victim则被用作victim cache。 #### poolCleanup poolCleanUp方法实现如下: ```go 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`,其实际结构如下: ```go 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-point`goroutine可以在`clean`状态被停止。故而,`抢占只能发生在safe-point`。 `proc pinning`会禁止抢占,在pinning后,P(物理线程)将会被独占,在`unpin`发生之前,goroutine会一直执行,并不会被停止,甚至不会被GC停止。`unpin之前,P无法被其他goroutine使用`。 一旦`pinned`后,execution flow在P上不会被中断,`这也意味着在访问threadlocal数据时无需加锁`。 如下是围绕Pinning的逻辑: ```go // 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都关联一个给定的P - `runtime_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的代码如下: ```go 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`结构如下: ```go // 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相关逻辑如下: ```go // 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的相关逻辑如下: ```go // 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实现如下: ```go 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 ## Sync.once sync.Once是golang中提供的工具包,确保指定的代码块在并发环境下只会被执行一次。 sync.Once为struct,其中只包含一个方法`Do(f func())`。sync.Once保证指定方法只会被执行一次,即使在多routine环境下被调用了多次。`并且,sync.Once方法是线程安全的`。 ### use case sync.Once的用例如下: - 对共享的资源进行初始化 - 设置单例 - 执行只需要运行一次的高开销计算 - 导入配置文件 sync.Once的使用示例如下: ```go var instance *singleton var once sync.Once func getInstance() *singleton { once.Do(func() { instance = &singleton{} }) return instance } ``` sync.Once实现逻辑如下: ```go type Once struct { // done indicates whether the action has been performed. // It is first in the struct because it is used in the hot path. // The hot path is inlined at every call site. // Placing done first allows more compact instructions on some architectures (amd64/386), // and fewer instructions (to calculate offset) on other architectures. done atomic.Uint32 m Mutex } func (o *Once) Do(f func()) { if o.done.Load() == 0 { // Outlined slow-path to allow inlining of the fast-path. o.doSlow(f) } } func (o *Once) doSlow(f func()) { o.m.Lock() defer o.m.Unlock() if o.done.Load() == 0 { defer o.done.Store(1) f() } } ``` ## reflect ### reflect basic go reflection基于`Values, Types, Kinds`,其相关类为`reflect.Value, reflect.Type, reflect.Kind`,获取方式如下: - `reflect.ValueOf(x interface{})` - `refelct.TypeOf(x interface{})` - `Type.kind()` #### Type type用于表示go中的类型,通过`reflect.TypeOf(x interface{})`来进行获取 ```go fmt.Println(reflect.TypeOf(Addr{})) // main.Addr fmt.Println(reflect.TypeOf(&Addr{})) // *main.Addr ``` #### Kind kind用于表示`类型`的数据类型,通过`.Kind`来进行获取: ```go fmt.Println(reflect.TypeOf(Addr{}).Kind()) // struct fmt.Println(reflect.TypeOf(&Addr{}).Kind()) // ptr ``` ### 常用方法 golang反射还提供了其他的一些常用方法。 #### NumField 该方法返回struct中的fields数量,如果类型参数其kind不为`reflect.struct`,那么其会发生`panic` ```go fmt.Println(reflect.TypeOf(Addr{}).NumField()) // 2 ``` #### Field 该方法允许通过下标来访问struct中的field。 ```go t := reflect.TypeOf(Addr{}) for i := 0; i < t.NumField(); i++ { fmt.Println(t.Field(i)) } ``` 上述输出为 ``` {FrmIp string 0 [0] false} {DstIp string 16 [1] false} ``` 根据反射遍历struct中的field,其示例如下: ```go var v interface{} = Addr{FrmIp: "1.2.3.4", DstIp: "7.8.9.10"} m := reflect.TypeOf(v).NumField() for i := 0; i < m; i++ { fn := reflect.TypeOf(v).Field(i).Name fv := reflect.ValueOf(v).Field(i).String() fmt.Println(fn, fv) } ``` ## logrus ### import logrus可以通过如下方式被引入: ```go import ( "github.com/sirupsen/logrus" ) ``` ### Log Level logrus提供了不同的日志级别,从低到高依次为: - Trace - Debug - Info - Warn - Error - Fatal - Panic 可以通过`logrus.SetLevel`来设置日志的输出级别 ```go logrus.SetLevel(logrus.TraceLevel) logrus.SetLevel(logrus.DebugLevel) logrus.SetLevel(logrus.InfoLevel) logrus.SetLevel(logrus.WarnLevel) logrus.SetLevel(logrus.ErrorLevel) logrus.SetLevel(logrus.FatalLevel) ``` > 当SetLevel将日志输出级别设置为指定级别时,日志只会输出该级别和该级别之上的内容。 > > 例如,当通过SetLevel将日志级别设置为Debug时,Trace级别的日志不会被输出 ### Redirect Output logrus支持三种方式来打印日志信息: - 将日志信息输出到console - 将日志信息输出到log file - 将日志信息输出到console和log file #### log on console 如果想要将日志输出到`os.Stdout`或`os.Stderr`,可以按如下方式进行配置 ```go package main import ( "os" log "github.com/sirupsen/logrus" ) func main() { // Output to stdout instead of the default stderr log.SetOutput(os.Stdout) // Only log the debug severity or above log.SetLevel(log.DebugLevel) log.Info("Info message") log.Warn("Warn message") log.Error("Error message") log.Fatal("Fatal message") } ``` #### log messages in log file 可以通过`logrus.SetOutput`配置将日志输出到文件中,示例如下: ```go package main import ( "github.com/sirupsen/logrus" "os" "path/filepath" ) func GetLogger(dir string, filename string) (logger *logrus.Logger, file *os.File, err error) { if err = os.MkdirAll(dir, 0750); err!=nil { return } logfile := filepath.Join(dir, filename) file, err = os.OpenFile(logfile, os.O_TRUNC|os.O_CREATE, 0640) if err != nil { return } logger = logrus.New() logger.SetLevel(logrus.InfoLevel) logger.SetOutput(file) logger.SetFormatter(&logrus.JSONFormatter{}) logger.SetReportCaller(true) return } func main() { logger, file, err := GetLogger("./log/", "app.log") if err!=nil { panic(any(err)) } defer file.Close() logger.Info("Ciallo~") } ``` #### 将日志同时输出到log file和console 如果想要将日志同时输出到log file和console,可以通过`io.MultiWrtier`来进行实现: ```go logger.SetOutput(io.MultiWriter(file, os.Stdout)) ``` ### 展示日志输出行数 如果在输出日志要,要同时输出打印日志代码的行数,可以通过如下设置 ```go // Only log the debug severity or above log.SetLevel(log.DebugLevel) ``` 其输出样式如下所示: ```go {"file":"D:/Workspace/GolangWorkspace/demo/logrus/logrus.go:33","func":"main.main","level":"info","msg":"Ciallo~","time":"2025-01-14T12:59:12+08:00"} ``` ### logrus hook 通过logrus hook,可以在输出日志时执行自定义逻辑。logrus hook需要实现`Hook`接口,该接口包含`Levels`和`Fire`方法: - `Levels`:触发该hook的log level - `Fire`:定义当hook被触发时的执行逻辑 logrus示例如下所示: ```go package main import ( "encoding/json" "errors" "fmt" "github.com/sirupsen/logrus" "io" "os" "path/filepath" ) type UploadESHook struct { } func (h *UploadESHook) Levels() []logrus.Level { return []logrus.Level{ logrus.InfoLevel, logrus.WarnLevel, logrus.ErrorLevel, logrus.FatalLevel, } } func (h *UploadESHook) Fire(e *logrus.Entry) error { jStr, err := e.String() m := map[string]any{} err = json.Unmarshal([]byte(jStr), &m) if err!=nil { return errors.New(err.Error()); } fmt.Printf("Uploading entry:\n %s to ES...\n", jStr) return nil } func GetLogger(dir string, filename string) (logger *logrus.Logger, file *os.File, err error) { if err = os.MkdirAll(dir, 0750); err!=nil { return } logfile := filepath.Join(dir, filename) file, err = os.OpenFile(logfile, os.O_TRUNC|os.O_CREATE, 0640) if err != nil { return } logger = logrus.New() logger.SetLevel(logrus.InfoLevel) logger.SetOutput(io.MultiWriter(file, os.Stdout)) logger.SetFormatter(&logrus.JSONFormatter{}) logger.SetReportCaller(true) logger.AddHook(&UploadESHook{}) return } func main() { logger, file, err := GetLogger("./log/", "app.log") if err!=nil { panic(any(err)) } defer file.Close() logger.Info("Ciallo~") } ``` ### JSONFormatter `logrus.JSONFormatter`会按照json格式来输出日志,可以通过如下方式进行配置: ```go logger.SetFormatter(&logrus.JSONFormatter{}) ``` #### 设置TimeStampFormat格式 可以通过如下代码设置日志输出时的TimeFormat格式: ```go logger.SetFormatter(&logrus.JSONFormatter{TimestampFormat: "2006-01-02 15:04:05.000"}) ``` ### 自定义Formatter 可以通过如下方式来自定义formatter: ```go package main import ( "fmt" "path/filepath" "runtime" "strings" "github.com/google/uuid" "github.com/sirupsen/logrus" ) type CustomTextFormatter struct { SessionID string } func (f *CustomTextFormatter) Format(entry *logrus.Entry) ([]byte, error) { // Get the file and line number where the log was called _, filename, line, _ := runtime.Caller(7) // Get the script name from the full file path scriptName := filepath.Base(filename) // Format the log message message := fmt.Sprintf("[%s] [%s] [%s] [%s:%d] %s\n", entry.Time.Format("2006-01-02 15:04:05"), // Date-time entry.Level.String(), // Log level f.SessionID, // Unique session ID scriptName, // Script name line, // Line number entry.Message, // Log message ) return []byte(message), nil } // Generate a unique session ID func generateSessionID() string { randomUUID := uuid.New() return strings.Replace(randomUUID.String(), "-", "", -1) } func main() { // Generate a new unique session ID sessionID := generateSessionID() // Create a new instance of the custom formatter customFormatter := &CustomTextFormatter{SessionID: sessionID} // Set the custom formatter as the formatter for the logger logrus.SetFormatter(customFormatter) // Now, log something logrus.Info("This is a custom-formatted log") } ``` ## syntax ### iota `iota`关键字代表连续的整数变量,`0, 1, 2`,每当`const`关键字出现时,其重置为0 其使用示例如下 ```go 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关键字也支持如下语法: ```go 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在`()`中声明多个常量时,如首个常量的类型被指定,则后续常量类型可省略,后续类型与首个被指定的类型保持一致。