1232 lines
38 KiB
Markdown
1232 lines
38 KiB
Markdown
- [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)
|
||
- [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 text,fuzz无法预知传递的测试参数,也无法预测参数所对应的结果。
|
||
|
||
> 在编写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")
|
||
}
|
||
```
|
||
|
||
## 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 clock,monotonic clock在拱廊内部使用,用于衡量时间差异。monotonic用于补偿`程序运行时对computer system clock的日期和时间造成的潜在修改`。
|
||
|
||
通过使用monotonic clock,如果在两次调用`time.Now`间隔5min的场景下,即使在调用的5分钟间隙内认为修改系统时间,两次调用`time.Now`返回值的差额仍然为5min。
|
||
|
||
|
||
|
||
## 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在`()`中声明多个常量时,如首个常量的类型被指定,则后续常量类型可省略,后续类型与首个被指定的类型保持一致。
|
||
|