Files
rikako-note/Golang/Golang Document.md
2025-01-18 20:37:27 +08:00

1785 lines
53 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

- [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)
- [golang time](#golang-time)
- [get current time](#get-current-time)
- [monotonic clock](#monotonic-clock)
- [获取年月日时分秒](#获取年月日时分秒)
- [time.Date](#timedate)
- [format time](#format-time)
- [使用预定义的time Format](#使用预定义的time-format)
- [字符串转日期/时间](#字符串转日期时间)
- [timeZone](#timezone)
- [ParseInLocation](#parseinlocation)
- [获取UTC时间](#获取utc时间)
- [获取Local时间](#获取local时间)
- [compare time](#compare-time)
- [Sub](#sub)
- [time.Duration](#timeduration)
- [Bufio](#bufio)
- [Buffered Reader](#buffered-reader)
- [Buffered Writer](#buffered-writer)
- [修改缓冲区大小](#修改缓冲区大小)
- [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 <module-name>`命令会创建一个`go.mod`文件,其中`<module-name>`会是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<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`命令执行单元测试。
示例如下所示:
```go
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示例如下
```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 cachevictim则被用作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 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的代码如下
```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 sliceslice大小和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中的poolChainlocal 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")
}
```
## golang time
### get current time
在golang中可以通过`time`package获取当前时间该包中提供了许多date和time相关的方法可以通过`time.Time`类型来表示特定的时间点。
当前时间可以通过`time.Now`方法来进行获取,示例如下:
```go
fmt.Println(time.Now())
```
其返回结果如下所示:
```
2025-01-16 12:55:07.748015 +0800 CST m=+0.000000001
```
#### monotonic clock
上述返回结果中,`m=`部分代表monotonic clockmonotonic clock在拱廊内部使用用于衡量时间差异。monotonic用于补偿`程序运行时对computer system clock的日期和时间造成的潜在修改`
通过使用monotonic clock如果在两次调用`time.Now`间隔5min的场景下即使在调用的5分钟间隙内认为修改系统时间两次调用`time.Now`返回值的差额仍然为5min。
#### 获取年月日时分秒
golang `time.Time`类提供多个方法用于获取时间中的各个部分,示例如下:
```go
...
func main() {
currentTime := time.Now()
fmt.Println("The time is", currentTime)
fmt.Println("The year is", currentTime.Year())
fmt.Println("The month is", currentTime.Month())
fmt.Println("The day is", currentTime.Day())
fmt.Println("The hour is", currentTime.Hour())
fmt.Println("The minute is", currentTime.Hour())
fmt.Println("The second is", currentTime.Second())
}
```
上述示例的运行结果为
```
The time is 2025-01-16 19:01:50.6266158 +0800 CST m=+0.000000001
The year is 2025
The month is January
The day is 16
The hour is 19
The minute is 19
The second is 50
```
其中,`time.Month()`方法的打印结果为字符串而不是数字,实际上`Month`方法的返回值为`time.Month`类型。
`time.Month`类型的定义如下:
```go
type Month int
```
#### time.Date
除了通过time.Now获取当前时间外time包还支持通过time.Date获取指定时间该方法会返回一个`time.Time`类型,使用示例如下:
```go
...
func main() {
theTime := time.Date(2021, 8, 15, 14, 30, 45, 100, time.Local)
fmt.Println("The time is", theTime)
}
```
`time.Date`方法接收参数如下
-
-
-
-
-
-
- `nanos`
- 该时间表示的时区
### format time
`time.Time`类型提供了`Format`方法该方法接收一个string类型的layoutlayout用于定义日期和时间如何展示。
在golang中指定time格式的方式和其他语言不同。在go中通过`表达指定时间的字符串`来定义日期时间格式。
例如在定义四位的年部分时,可以使用`2006`。根据该种格式表示方法,表达格式的字符串和实际输出的字符串相同。
> 在golang中日期和时间表示的特定时间点为`01/02 03:04:05PM '06 -0700`。
该指定时间各段的值依次递增,分别为
- month 01
- day of month 02
- hour 03`PM` (如果想要展示24小时制而不是12小时制可以使用15代替03)
- minute 04
- second 05
- year 06 `2006`
- time zone 07
示例如下所示:
```go
func main() {
fmt.Println(time.Date(1997, 10, 25, 16, 2, 28, 100, time.Local).Format("2006-01-02 15:04:05"))
}
```
代码运行输出如下:
```go
1997-10-25 16:02:28
```
如果想要使用12小时制可以按照如下示例
```go
func main() {
fmt.Println(time.Date(1997, 10, 25, 4, 2, 28, 100, time.Local).Format("2006-01-02 03:04:05 pm"))
fmt.Println(time.Date(1997, 10, 25, 16, 2, 28, 100, time.Local).Format("2006-01-02 03:04:05 pm"))
}
```
其输出为
```go
1997-10-25 04:02:28 am
1997-10-25 04:02:28 pm
```
#### 使用预定义的time Format
golang的time包中定义了`RFC 3339`的格式,`RFC 3339`定义了internet中被广泛使用的时间戳格式。
每个预定义的格式都是一个string类型字符串对于`RFC 3339`,time包中存在两种格式
- `time.RFC3339`: `2006-01-02T15:04:05Z07:00`
- `time.RFC3339Nano` : `2006-01-02T15:04:05.999999999Z07:00`
`time.RFC3339Nano`相比`time.RFC3339`其在格式中包含了nanos
### 字符串转日期/时间
golang提供了`time.Parse`方法将时间转化为`time.Time`类型。
`time.Parse`的使用示例如下所示:
```go
...
func main() {
timeString := "2021-08-15 02:30:45"
theTime, err := time.Parse("2006-01-02 03:04:05", timeString)
if err != nil {
fmt.Println("Could not parse time:", err)
}
fmt.Println("The time is", theTime)
fmt.Println(theTime.Format(time.RFC3339Nano))
}
```
time.Parse除了返回time.Time类型的值之外还返回一个error对象。
### timeZone
当formatLayout中不包含时区且被转换的日期字符串中也不包含时区时`time.Parse`方法在转换日期字符串时会使用默认的时区。
> #### 默认时区
> 默认时区为"+0000`时区也被称为UTC时区。
示例如下:
```go
t, err := time.Parse("2006-01-02 15:04:05", "2024-10-25 12:13:14")
if err != nil {
panic(any(err))
}
fmt.Println(t)
```
上述示例输出内容为
```
2024-10-25 12:13:14 +0000 UTC
```
其在未指定时区时默认使用UTC时区。
#### ParseInLocation
如果在转换未包含时区信息的字符串中,使用指定时区,可以调用`ParseInLocation`方法,示例如下
```go
var (
t time.Time
cst *time.Location
err error
)
cst, err = time.LoadLocation("Asia/Shanghai")
if err != nil {
panic(any(err))
}
t, err = time.ParseInLocation("2006-01-02 15:04:05", "2024-10-25 12:13:14", cst)
if err != nil {
panic(any(err))
}
fmt.Println(t)
```
#### 获取UTC时间
通过调用`time.UTC`方法可以将当前时间转换为UTC时间示例如下
```go
now := time.Now()
fmt.Println("current time is ", now)
fmt.Println("current utc time is ", now.UTC())
```
上述示例输出为
```
current time is 2025-01-17 13:46:20.6897373 +0800 CST m=+0.000000001
current utc time is 2025-01-17 05:46:20.6897373 +0000 UTC
```
可见将cst时区转化为utc时区时小时部分`-8`
#### 获取Local时间
相对的将UTC或其他时区的时间转化为本地时区的时间时可以调用`Local`方法
```go
var (
t time.Time
err error
)
t, err = time.ParseInLocation("2006-01-02 15:04:05", "2010-12-18 19:03:32", time.UTC)
if err != nil {
panic(any(err))
}
fmt.Println("utc time is ", t)
fmt.Println("local time is ", t.Local())
```
其输出结果如下:
```
utc time is 2010-12-18 19:03:32 +0000 UTC
local time is 2010-12-19 03:03:32 +0800 CST
```
### compare time
对于日期的比较golang提供了`Before`, `After`, `Equal`方法,
其使用示例如下:
```go
var (
t1 time.Time
t2 time.Time
err error
)
t1, err = time.ParseInLocation("2006-01-02 15:04:05", "2010-12-18 19:03:32", time.UTC)
if err != nil {
panic(any(err))
}
t2, err = time.ParseInLocation("2006-01-02 15:04:05", "2010-12-19 03:03:33", time.Local)
fmt.Println("t1 utc time is ", t1.UTC())
fmt.Println("t2 utc time is ", t2.UTC())
fmt.Println(t1.Equal(t2))
fmt.Println(t1.Before(t2))
fmt.Println(t1.After(t2))
```
其输出为
```
t1 utc time is 2010-12-18 19:03:32 +0000 UTC
t2 utc time is 2010-12-18 19:03:33 +0000 UTC
false
true
false
```
### Sub
除了通过上述方法进行比较外,针对两个`time.Time`类型还支持`Sub`方法,该方法会返回`time.Duration`类型的值。
`Sub`方法的使用示例如下:
```go
var (
t1 time.Time
t2 time.Time
err error
)
t1, err = time.ParseInLocation("2006-01-02 15:04:05", "2010-12-18 19:03:32", time.UTC)
if err != nil {
panic(any(err))
}
t2, err = time.ParseInLocation("2006-01-02 15:04:05", "2010-12-19 03:03:33", time.Local)
fmt.Println("t1 utc time is ", t1.UTC())
fmt.Println("t2 utc time is ", t2.UTC())
fmt.Println("t2 - t1 = ", t2.Sub(t1))
```
上述代码输出为
```
t1 utc time is 2010-12-18 19:03:32 +0000 UTC
t2 utc time is 2010-12-18 19:03:33 +0000 UTC
t2 - t1 = 1s
```
### time.Duration
在golang中`time.Duration`类型定义如下:
```go
type Duration int64
```
在time包中还定义了诸多`time.Duration`类型的常量,示例如下:
```go
const (
Nanosecond Duration = 1
Microsecond = 1000 * Nanosecond
Millisecond = 1000 * Microsecond
Second = 1000 * Millisecond
Minute = 60 * Second
Hour = 60 * Minute
)
```
如上所示,`time.Duration`类型的底层类型为int64并且最小单位为`nanos`
故而,使用`10 * time.Minute`, `3 * time.Hour`等表达式可以定义自己的time.Duration值。
对于`time.Time`类型,支持通过`Add`方法来对时间进行加减,`Add`方法接收一个`time.Duration`类型的参数,返回值为`time.Time`类型
```go
var (
t1 time.Time
err error
)
t1, err = time.ParseInLocation("2006-01-02 15:04:05", "2010-12-18 19:03:32", time.UTC)
if err != nil {
panic(any(err))
}
t2 := t1.Add(time.Hour * 24)
t3 := t1.Add(time.Hour * -24)
fmt.Println(t1)
fmt.Println(t2)
fmt.Println(t3)
```
上述示例的返回结果为
```
2010-12-18 19:03:32 +0000 UTC
2010-12-19 19:03:32 +0000 UTC
2010-12-17 19:03:32 +0000 UTC
```
## Bufio
`bufio`为golang中的一个包提供了带缓冲的读写功能。
### Buffered Reader
如果要创建一个buffered reader可以使用`bufio.NewReader`方法,该方法接受一个`io.Reader`作为参数,返回类型则是`*bufio.Reader`
常用的`bufio.NewReader`的入参为`os.File`, `strings.Reader`, `bytes.Buffer`
使用带缓冲区io和不带缓冲区io针对`10M`的文件,性能差别如下所示。
测试文件如下,大小为`10M`
```bash
$ ls -lh random.file
-rw-r--r-- 1 Asahi 197609 10M Jan 18 01:24 random.file
```
带缓冲和不带缓冲代码如下:
```go
func CountWithoutBuf(filename string, b byte) (cnt uint64) {
file, err := os.OpenFile(filename, os.O_RDONLY, 0)
if err != nil {
panic(any(err))
}
defer file.Close()
cnt = 0
bs := []byte{0}
for {
_,err = file.Read(bs)
if err == io.EOF {
break
} else if err != nil {
panic(any(err))
} else if bs[0] == b {
cnt++
}
}
return
}
func CountWithBuf(filename string, b byte) (cnt uint64) {
file, err := os.OpenFile(filename, os.O_RDONLY, 0)
if err != nil {
panic(any(err))
}
defer file.Close()
cnt = 0
bs := []byte{0}
br := bufio.NewReader(file)
for {
_,err = br.Read(bs)
if err == io.EOF {
break
} else if err != nil {
panic(any(err))
} else if bs[0] == b {
cnt++
}
}
return
}
```
benchmark测试代码如下
```go
func BenchmarkCountWithBuf(b *testing.B) {
CountWithBuf(FileName, B)
}
func BenchmarkCountWithoutBuf(b *testing.B) {
CountWithoutBuf(FileName, B)
}
```
执行benchmark命令
```bash
go test -bench=.*
```
测试结果返回如下:
```
goos: windows
goarch: amd64
pkg: git.kazusa.red/asahi/bufio
cpu: AMD Ryzen 9 7950X 16-Core Processor
BenchmarkCountWithBuf-32 1000000000 0.03209 ns/op
BenchmarkCountWithoutBuf-32 1 8937341700 ns/op
PASS
ok git.kazusa.red/asahi/bufio 9.254s
```
可知带缓冲场景下10M文件每次读取花费`0.03209 ns`,而不带缓冲按字节读取时每次读取花费`8.93s`
### Buffered Writer
类似于`bufio.NewReader`方法,可以通过`bufio.NewWriter`方法来创建一个buffered writer该方法接受`io.Writer`作为参数。
通常,可以将`os.File, stirngs.Builder, bytes.Buffer`作为参数传入。
在写入大小为50M的文件时其性能测试如下.
带缓冲和不带缓冲的代码实现
```go
func WriteToFileWithoutBuf(filename string, n uint32, generate func () []byte) (err error) {
file, err := os.OpenFile(filename, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0740)
if err != nil {
return
}
defer file.Close()
for i := 0; uint32(i) < n; i++ {
_, err = file.Write(generate())
if err != nil {
break
}
}
return
}
func WriteToFileWithBuf(filename string, n uint32, generate func () []byte) (err error) {
file, err := os.OpenFile(filename, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0740)
if err != nil {
return
}
defer file.Close()
w := bufio.NewWriter(file)
for i := 0; uint32(i) < n; i++ {
_, err = w.Write(generate())
if err != nil {
break
}
}
defer func(w *bufio.Writer) {
err := w.Flush()
if err != nil {
}
}(w)
return
}
```
benchmark代码如下所示
```go
func BenchmarkWriteToFileWithBuf(b *testing.B) {
if err := WriteToFileWithBuf(WriteFileName, 1024*1024*10, generateCharsSlice); err != nil {
panic(any(err))
}
}
func BenchmarkWriteToFileWithoutBuf(b *testing.B) {
if err := WriteToFileWithoutBuf(WriteFileName, 1024*1024*10, generateCharsSlice); err != nil {
panic(any(err))
}
}
func generateCharsSlice() []byte {
return []byte("asahi")
}
```
执行benmark命令进行测试
```bash
go test -bench=BenchmarkWriteToFile.*
```
测试结果如下所示:
```go
goos: windows
goarch: amd64
pkg: git.kazusa.red/asahi/bufio
cpu: AMD Ryzen 9 7950X 16-Core Processor
BenchmarkWriteToFileWithBuf-32 1000000000 0.1407 ns/op
BenchmarkWriteToFileWithoutBuf-32 1 13251963400 ns/op
PASS
ok git.kazusa.red/asahi/bufio 14.945s
```
易知当使用带缓冲版本时50M大小的文件写入事件为`0.1407ns`,而当使用不带缓冲的操作时,写入时间则需要花费`13s`
### 修改缓冲区大小
默认情况下,通过`NewReader`, `NewWriter`方法生成的buffered reader或writer其缓冲区大小默认为`4096`字节。
当想要修改缓冲区大小是,可以使用`NewReaderSize`方法,该方法额外接受一个定义缓冲区大小的参数。
代码如下
```go
func WriteToFileWithBufSize(filename string, n uint32, bufSize int, generate func() []byte) (err error) {
file, err := os.OpenFile(filename, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0740)
if err != nil {
return
}
defer file.Close()
w := bufio.NewWriterSize(file, bufSize)
for i := 0; uint32(i) < n; i++ {
_, err = w.Write(generate())
if err != nil {
break
}
}
defer func(w *bufio.Writer) {
err := w.Flush()
if err != nil {
}
}(w)
return
}
```
测试代码如下:
```go
func BenchmarkWriteToFileWith4KBuf(b *testing.B) {
if err := WriteToFileWithBufSize(WriteFileName, 1024*1024*10, 4096, generateCharsSlice); err != nil {
panic(any(err))
}
}
func BenchmarkWriteToFileWith128BytesBuf(b *testing.B) {
if err := WriteToFileWithBufSize(WriteFileName, 1024*1024*10, 128, generateCharsSlice); err != nil {
panic(any(err))
}
}
```
benchmark命令如下
```bash
go test -bench="BenchmarkWriteToFileWith(4K|128Bytes)Buf"
```
benchmark结果如下
```go
goos: windows
goarch: amd64
pkg: git.kazusa.red/asahi/bufio
cpu: AMD Ryzen 9 7950X 16-Core Processor
BenchmarkWriteToFileWith4KBuf-32 1000000000 0.1383 ns/op
BenchmarkWriteToFileWith128BytesBuf-32 1000000000 0.6549 ns/op
PASS
ok git.kazusa.red/asahi/bufio 26.042s
```
易知当读写50M文件时缓冲区大小为4096时每次写操作耗时`0.1383ns`而将缓冲区修改为128大小后每次写操作耗时增加到`0.65ns`
## 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在`()`中声明多个常量时,如首个常量的类型被指定,则后续常量类型可省略,后续类型与首个被指定的类型保持一致。