Files
rikako-note/Golang/Golang Document.md
2025-01-14 13:03:09 +08:00

33 KiB
Raw Blame History

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]命令行为如下:

  • 如果指定目录存在,会为dirgo.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.ContextIndentedJSON方法来向response中写入数据

// getAlbums responds with the list of all albums as JSON.
func getAlbums(c *gin.Context) {
    c.IndentedJSON(http.StatusOK, albums)
}

解析request中的数据

可以通过gin.ContextBindJSON方法来将请求体中的数据解析到对象中。

// 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为comparablego中预先声明了该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 textfuzz无法预知传递的测试参数也无法预测参数所对应的结果。

在编写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数量通常会因发现新的代码路径快速增加然后会随着时间的推移逐渐减少

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 pool
  • victim pool

调用Pool中的方法时实际行为如下

  • Put调用Put方法时会将对象添加到local
  • Get调用Get时首先会从local中查找,如果local中未找到,那么会从victim中查找如果victim中仍然不存在那么则是会调用New

sync.Pool中,local被用作primary cachevictim则被用作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其存在如下角色

  • goroutineG's
  • machinesM's代表系统线程
  • processorP's代表处理器物理线程

其中,goroutine由操作系统线程执行而操作系统线程在执行时需要获取实际的cpu物理线程。

在gouroutine运行时存在一些safe-point,在safe-pointgoroutine可以在clean状态被停止。故而,抢占只能发生在safe-point

proc pinning会禁止抢占在pinning后P物理线程将会被独占unpin发生之前goroutine会一直执行并不会被停止甚至不会被GC停止。unpin之前P无法被其他goroutine使用

一旦pinnedexecution 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

  • locallocal是一个由poolLocal对象组成c-style数组
  • localSizelocalSize是local数组的大小
  • poolLocal: local数组中的每个poolLocal都关联一个给定的P
  • runtime_procPin:该方法会返回pin锁关联的processor idprocessor 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 sliceslice大小和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中的poolChainlocal 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

Sync.once

sync.Once是golang中提供的工具包确保指定的代码块在并发环境下只会被执行一次。

sync.Once为struct其中只包含一个方法Do(f func())。sync.Once保证指定方法只会被执行一次即使在多routine环境下被调用了多次。并且sync.Once方法是线程安全的

use case

sync.Once的用例如下

  • 对共享的资源进行初始化
  • 设置单例
  • 执行只需要运行一次的高开销计算
  • 导入配置文件

sync.Once的使用示例如下

var instance *singleton
var once sync.Once

func getInstance() *singleton {
    once.Do(func() {
        instance = &singleton{}
    })
    return instance
}

sync.Once实现逻辑如下

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{})来进行获取

fmt.Println(reflect.TypeOf(Addr{})) // main.Addr
fmt.Println(reflect.TypeOf(&Addr{})) // *main.Addr

Kind

kind用于表示类型的数据类型,通过.Kind来进行获取:

fmt.Println(reflect.TypeOf(Addr{}).Kind())  // struct
fmt.Println(reflect.TypeOf(&Addr{}).Kind())  // ptr

常用方法

golang反射还提供了其他的一些常用方法。

NumField

该方法返回struct中的fields数量如果类型参数其kind不为reflect.struct,那么其会发生panic

fmt.Println(reflect.TypeOf(Addr{}).NumField()) // 2

Field

该方法允许通过下标来访问struct中的field。

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其示例如下

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可以通过如下方式被引入

import (
	"github.com/sirupsen/logrus"
)

Log Level

logrus提供了不同的日志级别从低到高依次为:

  • Trace
  • Debug
  • Info
  • Warn
  • Error
  • Fatal
  • Panic

可以通过logrus.SetLevel来设置日志的输出级别

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.Stdoutos.Stderr,可以按如下方式进行配置

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配置将日志输出到文件中,示例如下:

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来进行实现:

logger.SetOutput(io.MultiWriter(file, os.Stdout))

展示日志输出行数

如果在输出日志要,要同时输出打印日志代码的行数,可以通过如下设置

// Only log the debug severity or above
log.SetLevel(log.DebugLevel)

其输出样式如下所示:

{"file":"D:/Workspace/GolangWorkspace/demo/logrus/logrus.go:33","func":"main.main","level":"info","msg":"Ciallo~","time":"2025-01-14T12:59:12+08:00"}

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在()中声明多个常量时,如首个常量的类型被指定,则后续常量类型可省略,后续类型与首个被指定的类型保持一致。