Golang

25 注入外部参数 阅读更多

用 -ldflags 参数注入了的外部参数到go的变量当中。 main.go package main import ( "fmt" ) var ( AppName string // 应用名称 AppVersion string // 应用版本 BuildVersion string // 编译版本 BuildTime string // 编译时间 GitRevision string // Git版本 GitBranch string // Git分支 GoVersion string // Golang信息 ) func main() { Version() // 你的业务代码入口 } // Version 版本信息 func Version() { fmt.Printf("App Name:\t%s\n", AppName) fmt.Printf("App Version:\t%s\n", AppVersion) fmt.Printf("Build version:\t%s\n", BuildVersion) fmt.Printf("Build time:\t%s\n", BuildTime) fmt.Printf("Git revision:\t%s\n", GitRevision) fmt.Printf("Git branch:\t%s\n", GitBranch) fmt.Printf("Golang Version: %s\n", GoVersion) } build.sh #!/bin/bash set -e PROJECT_NAME="app-api" BINARY="app-api" OUTPUT_DIR=output GOOS=$(go env GOOS) APP_NAME=${PROJECT_NAME} APP_VERSION=$(git log -1 --oneline) BUILD_VERSION=$(git log -1 --oneline) BUILD_TIME=$(date "+%FT%T%z") GIT_REVISION=$(git rev-parse --short HEAD) GIT_BRANCH=$(git name-rev --name-only HEAD) GO_VERSION=$(go version) CGO_ENABLED=0 go build -a -installsuffix cgo -v -mod=vendor \ -ldflags "-s -X 'main.AppName=${APP_NAME}' \ -X 'main.AppVersion=${APP_VERSION}' \ -X 'main.BuildVersion=${BUILD_VERSION}' \ -X 'main.BuildTime=${BUILD_TIME}' \ -X 'main.GitRevision=${GIT_REVISION}' \ -X 'main.GitBranch=${GIT_BRANCH}' \ -X 'main.GoVersion=${GO_VERSION}'" \ -o ${OUTPUT_DIR}/${BINARY} cmd/${BINARY}.go 效果 App Name: app-api App Version: v2.0.1 Build version: 84d4ffb verdor Build time: 2019-08-06T09:58:48+0800 Git revision: 84d4ffb Git branch: master Golang Version: go version go1.12.2 linux/amd64 2019-07-24 10:53:34.732 11516: http server started listening on [:20000]

24 Goroutine退出、泄露 阅读更多

Go中,goroutine是否结束执行(退出)是由其自身决定,其他goroutine只能通过消息传递的方式通知其关闭,而并不能在外部强制结束一个正在执行的goroutine。 有一种特殊情况会导致正在运行的goroutine会因为其他goroutine的结束而终止,即main函数退出。 常见goroutine退出方式 main函数结束 package main import ( "fmt" "time" ) func main() { go func() { time.Sleep(time.Second) fmt.Println("goroutine exit") }() fmt.Println("main exit") } // output main exit 如上所示,程序未等goroutine执行完毕,即随着main函数的退出而停止执行。 context通知退出 package main import ( "context" "fmt" "time" ) func main() { ctx, cancel := context.WithCancel(context.Background()) go func(ctx context.Context) { num := 0 for { select { case <-ctx.Done(): fmt.Println("goroutine exit") return case <-time.After(time.Second): num++ fmt.Printf("goroutine wait times: %d\n", num) } } }(ctx) time.Sleep(time.Second * 3) cancel() time.Sleep(time.Second) fmt.Println("main exit") } // output goroutine wait times: 1 goroutine wait times: 2 goroutine exit main exit panic异常退出 package main import ( "fmt" "os" "time" ) func main() { go func() { defer func() { if err := recover(); err != nil { fmt.Printf("goroutine exit by panic: %v\n", err) } }() _, err := os.Open("notExistFile.txt") if err != nil { panic(err) } fmt.Println("goroutine exit naturally") }() time.Sleep(time.Second) fmt.Println("main exit") } // output goroutine exit by panic: open notExistFile.txt: no such file or directory main exit 上面自定义函数中defer函数使用了recover来捕获panic,当panic发生时可使goroutine拿回控制权,确保程序不会将panic传递到goroutine调用栈顶部后引起崩溃。 执行完毕退出 package main import ( "fmt" "time" ) func main() { go func() { for i := 0; i < 10000; i++ { // TODO: do some thing } fmt.Println("goroutine exit") }() time.Sleep(time.Second) fmt.Println("main exit") } // output goroutine exit main exit goroutine里的任务执行完毕,即结束。 goroutine泄露 如果启动了一个goroutine,但并没有按照预期的一样退出,等到程序结束,此goroutine才结束,这种情况就是 goroutine 泄露。 当 goroutine 泄露发生时,该 goroutine 的栈一直被占用而不能释放,goroutine 里的函数在堆上申请的空间也不能被垃圾回收器回收。这样,在程序运行期间,内存占用持续升高,可用内存越来也少,最终将导致系统崩溃。 大多数情况下,引起goroutine泄露的原因有两类: channel阻塞 goroutine陷入死循环 channel阻塞 // 从channel中读取,但是没有向channel中写入 package main import ( "fmt" "runtime" "time" ) func main() { go func() { c := make(chan int) go func() { <-c }() time.Sleep(time.Second * 2) fmt.Println("goroutine exit") }() c := time.Tick(time.Second) for range c { fmt.Printf("goroutine [nums]: %d\n", runtime.NumGoroutine()) } } // output goroutine [nums]: 3 goroutine exit goroutine [nums]: 3 goroutine [nums]: 2 goroutine [nums]: 2 goroutine [nums]: 2 ... // 向已满的channel中写入,但是没有读取 package main import ( "flag" "fmt" "runtime" "time" ) var size = flag.Int("c", 0, "define channel size") func main() { flag.Parse() go func(size int) { c := make(chan int, size) go func() { <-c }() go func() { for i := 0; i < 10; i++ { c <- i } }() fmt.Println("goroutine exit") }(*size) c := time.Tick(time.Second) for range c { fmt.Printf("goroutine [nums]: %d\n", runtime.NumGoroutine()) } } // output go run main.go -c 2 goroutine exit goroutine [nums]: 2 goroutine [nums]: 2 goroutine [nums]: 2 ... go run main.go -c 11 goroutine exit goroutine [nums]: 1 goroutine [nums]: 1 goroutine [nums]: 1 ... 死循环 当代码里循环的退出条件不可达时,会令该goroutine进入死循环中,进而导致资源一直无法释放,引起泄露。 在实际项目中,往往死循环会发生在一些后台的常驻服务中。 goroutine泄露的预防和检测 预防 在创建goroutine时,就应该知道goroutine啥时能结束。 channel引起的goroutine泄露问题,主要是看在channel阻塞goroutine时,该goroutine的阻塞是正常的,还是可能会导致goroutine永远没有机会执行(极大可能会造成协程泄露)。 channel的实际使用中,常用的两种模型:生产者-消费者模型;master-worker模型。一般的解决方案是:当主线程结束时,告知worker goroutine,worker goroutine得到通知后,进行清理工作然后退出;为每个worker任务制定超时,当超时触发,返回给master超时信息,并结束该worker goroutine。 实现循环语句时必须清晰地知道退出循环的条件,避免死循环。 检测 Go提供的pprof工具。 利用runtime.NumGoroutine接口,实时查看程序中运行的goroutine数。 开源三方profiling库,如:gops或goleak。

23 Interface重要性 阅读更多

场景与接口定义 在线商城,需要在Go后台提供存储与查询产品的服务,既实现一个负责保存和检索产品的存储库。 productrepo/ └── api.go 0 directories, 1 file 创建一个productrepo包和一个api.go文件,该API应该暴露出存储库里所有的产品方法。 // api.go package productrepo type ProductRepository interface { StoreProduct(name string, id int) FindProductByID(id int) } 在productrepo包下,定义了ProductRepository接口,它代表的就是存储库。该接口中定义两个方法: StoreProduct()方法用于存储产品信息 FindProductByID()方法通过产品ID查找产品信息 接口实现 存储库接口定义完成后,就需要有实体对象去实现该接口。 productrepo/ ├── api.go └── mock.go 0 directories, 2 files 在productrepo包下,新建mock.go文件,定义mockProductRepo对象。 // mock.go package productrepo import "fmt" // 实现接口的空实体对象 type mockProductRepo struct {} func (m mockProductRepo) StoreProduct(name string, id int) { fmt.Println("mocking the StoreProduct func") } func (m mockProductRepo) FindProductByID(id int) { fmt.Println("mocking the FindProductByID func") } 在示例代码中mock出ProductRepository接口所需的方法。 然后在api.go中增加New()方法,它返回的一个实现了ProductRepository接口的对象。 // api.go func New() ProductRepository { return mockProductRepo{} } 为什么使用接口 对于已经定义的ProductRepository接口,可以有多种对象去实现它。 对于小型的个人项目来说可以不要接口,直接创建一个实体对象。 在复杂的实际应用项目中,通常会有很多种存储对象: 使用本地MySQL存储, 连接到云数据库(例如阿里云、谷歌云和腾讯云等)存储。 不同的数据库存储都需要实现ProductRepository接口定义的StoreProduct()方法和FindProductByID()方法。 以本地MySQL存储库为例,它要管理产品对象,需要实现ProductRepository接口。 productrepo/ ├── api.go ├── mock.go └── mysql.go 0 directories, 3 files 在productrepo包下,新建mysql.go文件,定义了mysqlProductRepo对象并实现接口方法。 // mysql.go package productrepo import "fmt" type mysqlProductRepo struct { } func (m mysqlProductRepo) StoreProduct(name string, id int) { fmt.Println("mysqlProductRepo: mocking the StoreProduct func") // In a real world project you would query a MySQL database here. } func (m mysqlProductRepo) FindProductByID(id int) { fmt.Println("mysqlProductRepo: mocking the FindProductByID func") // In a real world project you would query a MySQL database here. } 相似地,当项目中同时需要把产品信息存储到云端时,以阿里云为例。 productrepo/ ├── aliyun.go ├── api.go ├── mock.go └── mysql.go 0 directories, 4 files 在productrepo包下,新建aliyun.go文件,定义了aliCloudProductRepo对象并实现接口方法。 // aliyun.go package productrepo import "fmt" type aliCloudProductRepo struct { } func (m aliCloudProductRepo) StoreProduct(name string, id int) { fmt.Println("aliCloudProductRepo: mocking the StoreProduct func") // In a real world project you would query an ali Cloud database here. } func (m aliCloudProductRepo) FindProductByID(id int) { fmt.Println("aliCloudProductRepo: mocking the FindProductByID func") // In a real world project you would query an ali Cloud database here. } 此时,更新api.go中定义的New()方法。 // api.go func New(environment string) ProductRepository { switch environment { case "aliCloud": return aliCloudProductRepo{} case "local-mysql": return mysqlProductRepo{} } return mockProductRepo{} } 通过将环境变量environment传递给New()函数,它将基于该环境值返回ProductRepository接口的正确实现对象。 . ├── go.mod ├── main.go └── productrepo ├── aliyun.go ├── api.go ├── mock.go └── mysql.go 1 directory, 6 files 定义程序入口main.go文件以及main函数。 // main.go package main import "productrepo" func main() { env := "aliCloud" repo := productrepo.New(env) repo.StoreProduct("HuaWei mate 40", 105) } 通过使用productrepo.New()方法基于环境值来获取ProductRepository接口对象。 如果需要切换产品存储库,则只需要使用对应的env值调用productrepo.New()方法即可。 如果不使用接口 需要为每个对象增加初始化方法 // mysql.go func NewMysqlProductRepo() *mysqlProductRepo { return &mysqlProductRepo{} } //aliyun.go func NewAliCloudProductRepo() *aliCloudProductRepo{ return &aliCloudProductRepo{} } // mock.go func NewMockProductRepo() *mockProductRepo { return &mockProductRepo{} } 调用对象处产生大量重复代码 // main.go package main import "productrepo" func main() { env := "aliCloud" switch env { case "aliCloud": repo := productrepo.NewAliCloudProductRepo() repo.StoreProduct("HuaWei mate 40", 105) // the more function to do, the more code is repeated. case "local-mysql": repo := productrepo.NewMysqlProductRepo() repo.StoreProduct("HuaWei mate 40", 105) // the more function to do, the more code is repeated. default: repo := productrepo.NewMockProductRepo() repo.StoreProduct("HuaWei mate 40", 105) // the more function to do, the more code is repeated. } } 在项目演进过程中,会迭代很多存储库对象,而通过ProductRepository接口,可以轻松地实现扩展,而不必反复编写相同逻辑的代码。 总结 开发中,常常提到要功能模块化,上面示例:通过接口为载体,一类服务就是一个接口,实现接口即服务。

22 for range 技巧 阅读更多

0.1. 技巧 0.1.1. 示例一 0.1.2. 示例二 0.2. 更多 0.3. for-range与goroutine 0.3.1. 问题代码 0.3.2. 原因 0.3.3. 解决方案 0.1. 技巧 在for range开始之前,就先获取slice的大小,在后面的迭代中不会改变 在for range开始之前,就先声明两个全局变量index和value 0.1.1. 示例一 func main() { v := []int{1, 2, 3} for i := range v { v = append(v, i) } } 先初始化了一个内容为1、2、3的slice 然后遍历这个slice 然后给这个切片追加元素 随着遍历的进行,数组v也在逐渐增大,但是for循环并不会死循环。只会遍历三次,v的结果是[0, 1, 2]。原因就在于for range实现的时候用到了语法糖。 对于切片的for range,它的底层代码就是: // for_temp := range // len_temp := len(for_temp) // for index_temp = 0; index_temp < len_temp; index_temp++ { // value_temp = for_temp[index_temp] // index = index_temp // value = value_temp // original body // } 第二行,在遍历之前就获取切片的长度len_temp := len(for_temp),遍历的次数不会随着切片的变化而变化。 0.1.2. 示例二 func main() { slice := []int{0, 1, 2, 3} myMap := make(map[int]*int) for index, value := range slice { fmt.Println(&index, &value) myMap[index] = &value } fmt.Println("=====new map=====") for k, v := range myMap { fmt.Printf("%d => %d\n", k, *v) } } // 输出 0xc0000140e0 0xc0000140e8 0xc0000140e0 0xc0000140e8 0xc0000140e0 0xc0000140e8 0xc0000140e0 0xc0000140e8 =====new map===== 0 => 3 1 => 3 2 => 3 3 => 3 循环切片时,index和value这两个变量的地址在一开始是就分配好,之后一直没变过,只是被赋予的值不断变化。 myMap[index] = &value语句把value变量的地址保存到myMap中,for range迭代结束后,map的值存储的都是value变量在for range 开始时申请的内存地址,所以他们的值都是最后一次赋予value变量的值3。 理解技巧:for index, value := range slice其实是在开始之前先声明了两个全局变量,而不是在每次循环中声明局部变量(临时变量),这样也是更为合理的操作。 0.2. 更多 map: // Lower a for range over a map. // The loop we generate: // var hiter map_iteration_struct // for mapiterinit(type, range, &hiter); hiter.key != nil; mapiternext(&hiter) { // index_temp = *hiter.key // value_temp = *hiter.val // index = index_temp // value = value_temp // original body // } channel: // Lower a for range over a channel. // The loop we generate: // for { // index_temp, ok_temp = <-range // if !ok_temp { // break // } // index = index_temp // original body // } array: // Lower a for range over an array. // The loop we generate: // len_temp := len(range) // range_temp := range // for index_temp = 0; index_temp < len_temp; index_temp++ { // value_temp = range_temp[index_temp] // index = index_temp // value = value_temp // original body // } string: // Lower a for range over a string. // The loop we generate: // len_temp := len(range) // var next_index_temp int // for index_temp = 0; index_temp < len_temp; index_temp = next_index_temp { // value_temp = rune(range[index_temp]) // if value_temp < utf8.RuneSelf { // next_index_temp = index_temp + 1 // } else { // value_temp, next_index_temp = decoderune(range, index_temp) // } // index = index_temp // value = value_temp // // original body // } 0.3. for-range与goroutine 0.3.1. 问题代码 package main import ( "fmt" "sync" ) func mockSendToServer(url string) { fmt.Printf("server url: %s\n", url) } func main() { urls := []string{"0.0.0.0:5000", "0.0.0.0:6000", "0.0.0.0:7000"} wg := sync.WaitGroup{} for _, url := range urls { wg.Add(1) go func() { defer wg.Done() mockSendToServer(url) }() } wg.Wait() } // output $ go run main.go server url: 0.0.0.0:7000 server url: 0.0.0.0:7000 server url: 0.0.0.0:7000 0.3.2. 原因 goroutine的启动需要准备时间。 当主goroutine中的for循环逻辑已经走完并阻塞于wg.Wait()一段时间后,go func的goroutine才启动准备(准备资源,挂载M线程等)完毕。 此时url局部变量中的值是最后一次for循环的url的内容,三个goroutine准备完毕开始启动读取url局部变量时都读取到同样的内容,因此就造成了上面的bug。 0.3.3. 解决方案 package main import ( "fmt" "sync" ) func mockSendToServer(url string) { fmt.Printf("server url: %s\n", url) } func main() { urls := []string{"0.0.0.0:5000", "0.0.0.0:6000", "0.0.0.0:7000"} wg := sync.WaitGroup{} for _, url := range urls { wg.Add(1) go func(url string) { defer wg.Done() mockSendToServer(url) }(url) } wg.Wait() } 将每次遍历的url所指向值,通过函数入参,作为数据资源赋予给go func,这样不管goroutine启动会有多耗时,其url已经作为goroutine的私有数据保存,后续运行就用上了正确的url,那么,上文bug也相应解除。

21 Channel技巧 阅读更多

赢者为王模式 赢者为王模式:核心思想就是同时开几个协程做同样的事情,谁先搞定,就用谁的结果。在Go语言的channel支持下,很容易实现这种并发方式。 例子 假设把同一份资源,存储在网络上的5个服务器上(镜像、备份等),现在需要获取这个资源,可以同时开5个协程,访问这5个服务器上的资源,谁先获取到,就用谁的,这样就可以最快速度获取,排除掉网络慢的服务器。 func main() { txtResult := make(chan string, 5) go func() {txtResult <- getTxt("res1.flysnow.org")}() go func() {txtResult <- getTxt("res2.flysnow.org")}() go func() {txtResult <- getTxt("res3.flysnow.org")}() go func() {txtResult <- getTxt("res4.flysnow.org")}() go func() {txtResult <- getTxt("res5.flysnow.org")}() println(<-txtResult) } // 模拟函数 func getTxt(host string) string{ //省略网络访问逻辑,直接返回模拟结果 //http.Get(host+"/1.txt") return host+":模拟结果" } 这种并发模式适合多个协程做同一件事情,只要有一个协程干成了就OK了。这种模式的优点主要有两个: 最大程度减少耗时 提高成功率 最终成功模式 最终成功模式:核心思想是同时并发的从10个文件中成功读取任意5个文件,可以开启5个协程,也可以开启3个,但是必须成功读取5个才算成功,否则就是失败。 两种常见实现思路 先并发获取,存放起来,然后再一个个判断是否获取成功,如果有的没有成功再重新获取,注意获取的文件不能重复。这种方式是取到结果后进行判断是否成功,然后根据情况再决定是否重新获取,要去重,要判断,业务逻辑比较复杂。 并发的时候就保证成功,里面可能是个for循环,直到成功为止,然后再返回结果。这种思路缺陷也很明显,如果这个文件损坏,那么就会一直死循环下去,要避免死循环,就要加上重试次数。 更优的实现思路 使用多个协程,但是发现如果有文件读取不成功,会通过channel的方式标记,换一个文件读取。因为一共10个文件,这个不行,换一个,不能在一个文件上等死,只要成功读取5个就可以了。 实现代码如下: // Read reads from readers in parallel. Returns p.dataBlocks number of bufs. func (p *parallelReader) Read(dst [][]byte) ([][]byte, error) { newBuf := dst //省略不太相关代码 var newBufLK sync.RWMutex //省略无关 //channel开始创建,要发挥作用了。这里记住几个数字: //readTriggerCh大小是10,p.dataBlocks大小是5 readTriggerCh := make(chan bool, len(p.readers)) for i := 0; i < p.dataBlocks; i++ { // Setup read triggers for p.dataBlocks number of reads so that it reads in parallel. readTriggerCh <- true } healRequired := int32(0) // Atomic bool flag. readerIndex := 0 var wg sync.WaitGroup // readTrigger 为 true, 意味着需要用disk.ReadAt() 读取下一个数据 // readTrigger 为 false, 意味着读取成功了,不再需要读取 for readTrigger := range readTriggerCh { newBufLK.RLock() canDecode := p.canDecode(newBuf) newBufLK.RUnlock() //判断是否有5个成功的,如果有,退出for循环 if canDecode { break } //读取次数上限,不能大于10 if readerIndex == len(p.readers) { break } //成功了,退出本次读取 if !readTrigger { continue } wg.Add(1) //并发读取数据 go func(i int) { defer wg.Done() //省略不太相关代码 _, err := rr.ReadAt(p.buf[bufIdx], p.offset) if err != nil { //省略不太相关代码 // 失败了,标记为true,触发下一个读取. readTriggerCh <- true return } newBufLK.Lock() newBuf[bufIdx] = p.buf[bufIdx] newBufLK.Unlock() // 成功了,标记为false,不再读取 readTriggerCh <- false }(readerIndex) //控制次数,同时用来作为索引获取和存储数据 readerIndex++ } wg.Wait() //最终结果判断,如果OK了就正确返回,如果有失败的,返回error信息。 if p.canDecode(newBuf) { p.offset += p.shardSize if healRequired != 0 { return newBuf, errHealRequired } return newBuf, nil } return nil, errErasureReadQuorum } 前提是从10个数据里读取任意5个。 初始化的chan大小是10,但是通过for循环只存放了5个true 然后对chan循环读取数据,如果是true就开启go协程获取数据,如果是false就终止这次循环 当前在这之前还会判断下是否已经成功获取了5个,如果是的话,直接跳出整个for循环 通过readerIndex每次尝试获取一个数据,如果成功塞一个false到chan中,如果失败则塞个true 这样不成功的readerIndex不再尝试读取,失败了就通过true标记尝试读取下一个readerIndex 通过chan这种巧妙的方式不断循环,直到成功读取5个,或者把10个数据都读一遍为止 最终再基于是否成功读取到5个数据,做最终的判断,是返回成功数据,还是错误 利用channel来做标记和循环取数据,是一种非常好的方式,简化了代码逻辑,整体看起来非常清晰。

20 读取文件 阅读更多

0.1. 打开文件的方式 0.2. 判断文件大小的方式 0.2.1. 打开文件后把内容全部读一遍并统计字节数 0.2.2. ioutil.ReadFile 0.2.3. File.State 0.2.4. os.Stat os.Stat的其他用途 0.1. 打开文件的方式 func readFile(fileName string) error { f, err := os.Open(fileName) defer file.Close() // 不要忘记关闭文件 if err != nil { fmt.Println("cannot able to read the file", err) return } return nil } 打开文件后有两个选择: 逐行读取文件:减少内存负担,IO时间花销大 立即将整个文件读取到内存中:消耗更所内存,IO时间小 使用Golang中的bufio.NewReader()将整个文件加载到内存 buffer与cache的区别: buffer(缓冲)是为了提高内存和硬盘(或其他I/0设备)之间的数据交换的速度而设计的。 cache(缓存)是为了提高CPU和内存之间的数据交换速度而设计,也就是平常见到的一级缓存、二级缓存、三级缓存。 func readFile(fileName string) error { f, err := os.Open(fileName) defer f.Close() // 不要忘记关闭文件 if err != nil { fmt.Println("cannot able to read the file", err) return err } r := bufio.NewReader(f) for { buf := make([]byte, 4*1024) // 每个块的大小 n, err := r.Read(buf) // 加载块到缓冲(buffer)中 buf = buf[:n] if n == 0 { if err == nil { fmt.Println(err) break } if err == io.EOF { break } return err } } return nil } 将上面的代码进行优化,使用goroutine对多个块同时处理。 完整代码在这里 package main import ( "bufio" "fmt" "io" "math" "os" "strings" "sync" "time" ) func main() { s := time.Now() args := os.Args[1:] if len(args) != 6 { // for format LogExtractor.exe -f "From Time" -t "To Time" -i "Log file directory location" fmt.Println("Please give proper command line arguments") return } startTimeArg := args[1] finishTimeArg := args[3] fileName := args[5] file, err := os.Open(fileName) ; if err != nil { fmt.Println("cannot able to read the file", err) return } defer file.Close() // close after checking err queryStartTime, err := time.Parse("2006-01-02T15:04:05.0000Z", startTimeArg) if err != nil { fmt.Println("Could not able to parse the start time", startTimeArg) return } queryFinishTime, err := time.Parse("2006-01-02T15:04:05.0000Z", finishTimeArg) if err != nil { fmt.Println("Could not able to parse the finish time", finishTimeArg) return } fileStat, err := file.Stat() if err != nil { fmt.Println("Could not able to get the file stat") return } fileSize := fileStat.Size() offset := fileSize - 1 lastLineSize := 0 for { b := make([]byte, 1) n, err := file.ReadAt(b, offset) if err != nil { fmt.Println("Error reading file ", err) break } char := string(b[0]) if char == "\n" { break } offset-- lastLineSize += n } lastLine := make([]byte, lastLineSize) _, err = file.ReadAt(lastLine, offset+1) if err != nil { fmt.Println("Could not able to read last line with offset", offset, "and last line size", lastLineSize) return } logSlice := strings.SplitN(string(lastLine), ",", 2) logCreationTimeString := logSlice[0] lastLogCreationTime, err := time.Parse("2006-01-02T15:04:05.0000Z", logCreationTimeString) if err != nil { fmt.Println("can not able to parse time : ", err) } if lastLogCreationTime.After(queryStartTime) && lastLogCreationTime.Before(queryFinishTime) { Process(file, queryStartTime, queryFinishTime) } fmt.Println("\nTime taken - ", time.Since(s)) } func Process(f *os.File, start time.Time, end time.Time) error { linesPool := sync.Pool{New: func() interface{} { lines := make([]byte, 250*1024) return lines }} stringPool := sync.Pool{New: func() interface{} { lines := "" return lines }} r := bufio.NewReader(f) var wg sync.WaitGroup for { buf := linesPool.Get().([]byte) n, err := r.Read(buf) buf = buf[:n] if n == 0 { if err != nil { fmt.Println(err) break } if err == io.EOF { break } return err } nextUntilNewline, err := r.ReadBytes('\n') if err != io.EOF { buf = append(buf, nextUntilNewline...) } wg.Add(1) go func() { ProcessChunk(buf, &linesPool, &stringPool, start, end) wg.Done() }() } wg.Wait() return nil } func ProcessChunk(chunk []byte, linesPool *sync.Pool, stringPool *sync.Pool, start time.Time, end time.Time) { var wg2 sync.WaitGroup logs := stringPool.Get().(string) logs = string(chunk) linesPool.Put(chunk) logsSlice := strings.Split(logs, "\n") stringPool.Put(logs) chunkSize := 300 n := len(logsSlice) noOfThread := n / chunkSize if n%chunkSize != 0 { noOfThread++ } for i := 0; i < (noOfThread); i++ { wg2.Add(1) go func(s int, e int) { defer wg2.Done() // to avoid deadlocks for i := s; i < e; i++ { text := logsSlice[i] if len(text) == 0 { continue } logSlice := strings.SplitN(text, ",", 2) logCreationTimeString := logSlice[0] logCreationTime, err := time.Parse("2006-01-02T15:04:05.0000Z", logCreationTimeString) if err != nil { fmt.Printf("\n Could not able to parse the time :%s for log : %v", logCreationTimeString, text) return } if logCreationTime.After(start) && logCreationTime.Before(end) { // fmt.Println(text) } } }(i*chunkSize, int(math.Min(float64((i+1)*chunkSize), float64(len(logsSlice))))) } wg2.Wait() logsSlice = nil } 0.2. 判断文件大小的方式 0.2.1. 打开文件后把内容全部读一遍并统计字节数 func main() { file,err:=os.Open("water") if err ==nil { sum := 0 buf:=make([]byte,2014) for { n,err:=file.Read(buf) sum+=n if err==io.EOF { break } } fmt.Println("file size is ",sum) } } 通过for循环读取文件的字节内容,然后算出文件的大小,效率低,代码量大。 0.2.2. ioutil.ReadFile func main() { content,err:=ioutil.ReadFile("water") if err == nil { fmt.Println("file size is ",len(content)) } } 使用ioutil包的ReadFile来代替,直接获得文件的内容,进而计算出文件的大小。 看看ReadFile的具体实现: // ReadFile reads the file named by filename and returns the contents. // A successful call returns err == nil, not err == EOF. Because ReadFile // reads the whole file, it does not treat an EOF from Read as an error // to be reported. func ReadFile(filename string) ([]byte, error) { f, err := os.Open(filename) if err != nil { return nil, err } defer f.Close() // It's a good but not certain bet that FileInfo will tell us exactly how much to // read, so let's try it but be prepared for the answer to be wrong. var n int64 = bytes.MinRead if fi, err := f.Stat(); err == nil { // As initial capacity for readAll, use Size + a little extra in case Size // is zero, and to avoid another allocation after Read has filled the // buffer. The readAll call will read into its allocated internal buffer // cheaply. If the size was wrong, we'll either waste some space off the end // or reallocate as needed, but in the overwhelmingly common case we'll get // it just right. if size := fi.Size() + bytes.MinRead; size > n { n = size } } return readAll(f, n) } 发现里面用了file的Stat()方法。 0.2.3. File.State func main() { file,err:=os.Open("water") if err == nil { fi,_:=file.Stat() fmt.Println("file size is ",fi.Size()) } } 看一下file的State的实现: // Stat returns the FileInfo structure describing file. // If there is an error, it will be of type *PathError. func (f *File) Stat() (FileInfo, error) { if f == nil { return nil, ErrInvalid } var fs fileStat err := f.pfd.Fstat(&fs.sys) if err != nil { return nil, &PathError{"stat", f.name, err} } fillFileStatFromSys(&fs, f.name) return &fs, nil } 0.2.4. os.Stat func main() { fi,err:=os.Stat("water") if err ==nil { fmt.Println("file size is ",fi.Size(),err) } } 看一下os的Stat的实现: // Stat returns a FileInfo describing the named file. // If there is an error, it will be of type *PathError. func Stat(name string) (FileInfo, error) { testlog.Stat(name) return statNolog(name) } os.Stat的其他用途 // 获取文件信息 func main() { fi,err:=os.Stat("water") if err ==nil { fmt.Println("name:",fi.Name()) fmt.Println("size:",fi.Size()) fmt.Println("is dir:",fi.IsDir()) fmt.Println("mode::",fi.Mode()) fmt.Println("modTime:",fi.ModTime()) } } // 判断文件是否存在 func main() { _,err:=os.Stat(".") if err ==nil { fmt.Println("file exist") }else if os.IsNotExist(err){ fmt.Println("file not exist") }else{ fmt.Println(err) } }

19 开启pprof 阅读更多

开启pprof pprof 功能有两种开启方式,对应两种包: net/http/pprof :使用在 web 服务器的场景 runtime/pprof :使用在非服务器应用程序的场景 这两个本质上是一致的,net/http/pporf 也只是在 runtime/pprof 上的一层 web 封装,通过程序的HTTP运行时提供服务,使用pprof可视化工具性能分析数据并提供期望的输出格式。 web方式 只要导入这个包来注册它的HTTP处理程序就可以了,处理路径都是以/debug/pprof/开头的。要使用pprof主要在程序中导入import _ "net/http/pprof" 如果应用程序不启动http服务器,那么需要导入net/http和log包,如下所示: go func() { log.Println(http.ListenAndServe("localhost:6060", nil)) }() 如果不使用DefaultServeMux,那么在路由器上注册使用的处理程序即可。 非web方式 这种通常用于程序调优的场景,程序只是一个应用程序,跑一次就结束,想找到瓶颈点,那么通常会使用到这个方式。 // cpu pprof 文件路径 f, err := os.Create("cpufile.pprof") if err != nil { log.Fatal(err) } // 开启 cpu pprof pprof.StartCPUProfile(f) defer pprof.StopCPUProfile() 调用pprof 查看所有可用的概要信息,在浏览器其中打开http://localhost:6060/debug/pprof/,这里的端口根据实际运行情况确定。 案例 调用 查看堆信息 go tool pprof http://localhost:6060/debug/pprof/heap 查看30秒内CPU信息 go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30 查看goroutine的阻塞信息(在程序中调用runtime.SetBlockProfileRate) go tool pprof http://localhost:6060/debug/pprof/block 手收集5秒内执行的跟踪信息 wget http://localhost:6060/debug/pprof/trace?seconds=5 查看锁的持有者(在程序中调用runtime.SetMutexProfileFraction) go tool pprof http://localhost:6060/debug/pprof/mutex 更多 了解更多,可查看golang官方博客的文章,点击这里。

18 缓冲问题 阅读更多

缓冲方式 涉及到不同的输出终端,自然涉及到一个问题:缓冲。 在C语言中基于流的 I/O 提供了 3 种缓冲: 全缓冲 直到缓冲区被填满,才调用系统 I/O 函数。 对于读操作来说,直到读入内容的字节数等于缓冲区大小或者文件已经到达结尾,才进行实际的 I/O 操作,将外存文件内容读入缓冲区 对于写操作来说,直到缓冲区被填满,才进行实际的 I/O 操作,缓冲区内容写到外存文件中 磁盘文件通常是全缓冲的。 行缓冲 直到遇到换行符 ‘\n’,才调用系统 I/O 库函数。 对于读操作来说,遇到换行符 ‘\n’ 才进行 I/O 操作,将所读内容读入缓冲区 对于写操作来说,遇到换行符 ‘\n’ 才进行 I/O 操作,将缓冲区内容写到外存中 由于缓冲区的大小是有限的,所以当缓冲区被填满时,即使没有遇到换行符‘\n’,也同样会进行实际的 I/O 操作。 标准输入 stdin 和标准输出 stdout 默认都是行缓冲的。 无缓冲 没有缓冲区,数据会立即读入或者输出到外存文件和设备上。 标准错误 stderr 是无缓冲的,这样保证错误提示和输出能够及时反馈给用户,供用户排除错误。 go中的缓冲方式 官方回答,go中os.Stdin,os.Stdout和os.Stderr都是无缓冲的,因为它的类型实际上是 *os.File,很显然 *os.File 是无缓冲的。 Go 中如果需要缓冲,请使用 bufio 包,特殊的需求,可以基于它进行扩展。

17 错误处理 阅读更多

0.1. 好处 0.2. 最佳实践 0.2.1. 创建可行的错误链 0.2.2. 附加堆栈跟踪信息 0.2.2.1. 向错误添加上下文信息 0.3. 建议 // 我觉得这个处理挺好的 if err != nil { return err } 0.1. 好处 如果以标准方式处理 Go 中的错误,将获得以下好处: 没有隐藏的控制流 没有意外的未捕获异常日志在终端疯狂输出(除了由于 panic 导致的实际程序崩溃) 可以完全控制代码中的错误,可以选择处理,返回和执行任何其他操作 func f() (value, error) 的语法不仅易于向新手讲解,而且在任何 Go 项目中都可确保一致性。 注意:Go 的错误语法不会强迫处理程序可能抛出的每个错误。 Go 只是提供了一种模式,以确保你认为错误对于程序流至关重要,没有其他更多要求。 应用程序结束时,如果发生错误,并且使用 err != nil 来发现它 如果应用程序不对其执行任何操作,则可能会遇到麻烦(Go 不会自动保存任何信息) if err := criticalDatabaseOperation(); err != nil { // 仅仅是记录错误日志,而没有返回或停止控制流(这样不好!) log.Printf("Something went wrong in the DB: %v", err) // 在这一行我们应该输入`return`! } if err := saveUser(user); err != nil { return fmt.Errorf("Could not save user: %w", err) } 0.2. 最佳实践 0.2.1. 创建可行的错误链 if err != nil 模式的优势在于,通过错误链能方便遍历程序层次结构,直到发生错误的地方。 例如,由程序的 main 函数处理的常见 Go 错误可能如下所示: [2020-07-05-9:00] ERROR: Could not create user: could not check if user already exists in DB: could not establish database connection: no internet connection 从:也能看出,这是四个函数调用发错误返回,可以快速定位到no internet connection导致了这个错误的发生。 0.2.2. 附加堆栈跟踪信息 github.com/pkg/errors提供了将堆栈跟踪附加到函数的功能。 Github仓库地址 参考文档地址 0.2.2.1. 向错误添加上下文信息 errors.Wrap函数返回一个新错误,这个新错误通过在调用Wrap的点记录堆栈跟踪以及所提供的消息来将上下文添加到原始错误中。例如: _, err := ioutil.ReadAll(r) if err != nil { return errors.Wrap(err, "read failed") } 它将打印出堆栈跟踪以及您通过代码创建的易于理解的错误链。如果可以,我想总结一下我想到的有关在 Go 中编写符合 0.3. 建议 Go 习惯的错误处理的最重要建议: 当您的错误需要服务开发人员时,请添加堆栈跟踪 对返回的错误进行处理,不要只是将它们冒出来(返回),记录下来,然后忘记它们 保持您的错误链明确 当编写 Go 代码时,错误处理是永远不会担心的一件事,因为错误本身是编写的每个函数的核心内容之一,从而使我能够完全控制我如何安全,可读且负责任地处理它们。

16 容器运行注意事项 阅读更多

0.1. 问题描述 0.2. 会造成什么后果 0.3. 解决方法 0.1. 问题描述 在Go语言中,Go scheduler的P数量非常重要,因为它会极大地影响Go在运行时的表现。在目前的Go语言中,P的数量默认是系统的CPU核数。 在容器化的环境中,Go程序所获取的CPU核数是错误的,它所获取的是宿主机的CPU核数。 即使容器和宿主机的CPU核数是共享的,但在集群中会针对每个Pod分配指定的核数,因此实际上需要的是Pod的核数,而不是宿主机的CPU核数。 0.2. 会造成什么后果 Go运行时调度模型,要求M必须与P进行绑定,然后才能不断地在M上循环寻找可运行的G来执行相应的任务。 注意,M必须与P进行绑定,其绑定的这个P,要求必须是空闲状态。 在容器化的部署环境中,P的数量由于被“错误”设置,因此拥有大量空闲的P。可以这样理解,只要有足够多的M,那么P就可以都被绑定。 这时又产生了另外一个问题,M的数量是会不断增加。在程序运行过程中,由于产生了网络I/O阻塞,导致M会随着程序的不断执行而不断增加。最终导致Go程序的延迟加大,程序响应缓慢。 0.3. 解决方法 产生这个问题的本质原因是Go程序没有正确地获得所期望的CPU核数(应当获取具体分配给Pod的配额),因此解决方案有两种: 结合部署情况,主动设置正确的GOMAXPROCS核数 通过cgroup信息,读取容器内的正确GOMAXPROCS核数 目前,Go尚没有非常完美的办法来解决这个问题,因此这里推荐使用Uber公司推出的uber-go/maxprocs开源库,它会在Go程序运行时根据cgroup的挂载信息来修改GOMAXPROCS核数,并基于一定规则选择一个最合适的数值。 使用方式如下: package automaxprocs_test // Importing automaxprocs automatically adjusts GOMAXPROCS. import _ "go.uber.org/automaxprocs" // To render a whole-file example, we need a package-level declaration. var _ = "" func Example() {} 只需在Go程序启动时进行引用即可,如果有特殊的需求,那么主动设置GOMAXPROCS也是可以的。

15 uintptr与unsafe.Pointer 阅读更多

0.1. uintptr 0.2. unsafe.Pointer 0.3. 区别 Golang有两个无类型的指针: uintptr unsafe.Pointer 这两者可以互相转换。 0.1. uintptr uintptr 是整数,不是引用。 将 Pointer 转换为 uintptr 会创建一个没有指针语义的整数值。 即使 uintptr 持有某个对象的地址,如果对象移动,垃圾收集器并不会更新 uintptr 的值,uintptr 也无法阻止该对象被回收。 0.2. unsafe.Pointer unsafe包中type Pointer的说明如下: Pointer表示指向任意类型的指针。Pointer类型有四种特殊操作,而其他类型则没有: 任何类型的指针值都可以转换为Pointer Pointer可以转换为任何类型的指针值 可以将uintptr转换为Pointer 可以将Pointer转换为uintptr Pointer允许程序绕过类型系统并读取任意的内存地址。使用时要格外小心。 尽管 unsafe.Pointer 是通用指针,但 Go 垃圾收集器知道它们指向 Go 对象;换句话说,它们是真正的 Go指针。 垃圾收集器使用Pointer来防止活动对象被回收并发现更多活动对象(如果unsafe.Pointer指向的对象自身持有指针)。 0.3. 区别 因此,对 unsafe.Pointer 的合法操作上的许多限制归结为“在任何时候,它们都必须指向真正的 Go 对象”。如果创建的 unsafe.Pointer 并不符合,即使很短的时间,Go 垃圾收集器也可能会在该时刻扫描,然后由于发现了无效的 Go 指针而崩溃。 相比之下,uintptr 只是一个数字。这种特殊的垃圾收集魔法机制并不适用于 uintptr 所“引用”的对象,因为它仅仅是一个数字,一个 uintptr 不会引用任何东西。 反过来,这导致在将 unsafe.Pointer 转换为 uintptr,对其进行操作然后再将其转回的各种方式上存在许多微妙的限制。 基本要求是以这种方式进行操作,使编译器和运行时可以屏蔽不安全的指针的临时非指针性,使其免受垃圾收集器的干扰,因此这种临时转换对于垃圾收集将是原子的。 从 Go 1.8 开始,即使当时没有运行垃圾回收,所有 Go 指针必须始终有效(包括 unsafe.Pointer)。如果在变量或字段中存储了无效的指针,则仅通过将字段更新为包括 nil 在内的完全有效的值即可使代码崩溃。

14 条件断点 阅读更多

目前,Golang支持的调试器有: GDB:Golang最早支持的调试工具,对Golang专有特性缺少很好的支持 LLDB:macOS推荐的标准调试工具,对Golang专有特性缺少很好的支持 Delve:专门为Golang设计开发的调试工具,本身由Golang开发,对Windows平台也提供同样的支持 Golang中的条件断点需要使用delve来实现。 安装Delve go get github.com/go-delve/delve/cmd/dlv sugoi@sugoi:~$ dlv version Delve Debugger Version: 1.4.0 Build: $Id: 67422e6f7148fa1efa0eac1423ab5594b223d93b $ Delve介绍 sugoi@sugoi:~$ dlv help Delve is a source level debugger for Go programs. Delve enables you to interact with your program by controlling the execution of the process, evaluating variables, and providing information of thread / goroutine state, CPU register state and more. The goal of this tool is to provide a simple yet powerful interface for debugging Go programs. Pass flags to the program you are debugging using `--`, for example: `dlv exec ./hello -- server --config conf/config.toml` Usage: dlv [command] Available Commands: attach Attach to running process and begin debugging. 可以用来对一个正在运行的进行进行调试. connect Connect to a headless debug server. 连接到headless调试器. core Examine a core dump. 用来调试core文件. debug Compile and begin debugging main package in current directory, or the package specified. 在当前包或者指定的包编译并debug程序. exec Execute a precompiled binary, and begin a debug session. 如果已经编译好了二进制,可以用该命令启动调试. help Help about any command 帮助命令. run Deprecated command. Use 'debug' instead. test Compile test binary and begin debugging program. 可以用来测试自己编写的测试源码文件. trace Compile and begin tracing program. 编译并跟踪程序. version Prints version. 输出版本信息 Flags: --accept-multiclient Allows a headless server to accept multiple client connections. --api-version int Selects API version when headless. (default 1) --backend string Backend selection (see 'dlv help backend'). (default "default") --build-flags string Build flags, to be passed to the compiler. --check-go-version Checks that the version of Go in use is compatible with Delve. (default true) --headless Run debug server only, in headless mode. --init string Init file, executed by the terminal client. -l, --listen string Debugging server listen address. (default "127.0.0.1:0") --log Enable debugging server logging. --log-dest string Writes logs to the specified file or file descriptor (see 'dlv help log'). --log-output string Comma separated list of components that should produce debug output (see 'dlv help log') --only-same-user Only connections from the same user that started this instance of Delve are allowed to connect. (default true) --wd string Working directory for running the program. (default ".") Additional help topics: dlv backend Help about the --backend flag. dlv log Help about logging flags. Use "dlv [command] --help" for more information about a command. 这里需要用到的是debug子命令。 dlv debug介绍 进入项目目录,输入dlv debug这是一个交互式的界面,输入help命令查看调试过程中可以执行的操作: sugoi@sugoi:~/go/src/awesomeProject/learndelve$ dlv debug Type 'help' for list of commands. (dlv) help The following commands are available: args ------------------------ Print function arguments. break (alias: b) ------------ Sets a breakpoint. breakpoints (alias: bp) ----- Print out info for active breakpoints. call ------------------------ Resumes process, injecting a function call (EXPERIMENTAL!!!) clear ----------------------- Deletes breakpoint. clearall -------------------- Deletes multiple breakpoints. condition (alias: cond) ----- Set breakpoint condition. config ---------------------- Changes configuration parameters. continue (alias: c) --------- Run until breakpoint or program termination. deferred -------------------- Executes command in the context of a deferred call. disassemble (alias: disass) - Disassembler. down ------------------------ Move the current frame down. edit (alias: ed) ------------ Open where you are in $DELVE_EDITOR or $EDITOR exit (alias: quit | q) ------ Exit the debugger. frame ----------------------- Set the current frame, or execute command on a different frame. funcs ----------------------- Print list of functions. goroutine (alias: gr) ------- Shows or changes current goroutine goroutines (alias: grs) ----- List program goroutines. help (alias: h) ------------- Prints the help message. libraries ------------------- List loaded dynamic libraries list (alias: ls | l) -------- Show source code. locals ---------------------- Print local variables. next (alias: n) ------------- Step over to next source line. on -------------------------- Executes a command when a breakpoint is hit. print (alias: p) ------------ Evaluate an expression. regs ------------------------ Print contents of CPU registers. restart (alias: r) ---------- Restart process. set ------------------------- Changes the value of a variable. source ---------------------- Executes a file containing a list of delve commands sources --------------------- Print list of source files. stack (alias: bt) ----------- Print stack trace. step (alias: s) ------------- Single step through program. step-instruction (alias: si) Single step a single cpu instruction. stepout (alias: so) --------- Step out of the current function. thread (alias: tr) ---------- Switch to the specified thread. threads --------------------- Print out info for every traced thread. trace (alias: t) ------------ Set tracepoint. types ----------------------- Print list of types up -------------------------- Move the current frame up. vars ------------------------ Print package variables. whatis ---------------------- Prints type of an expression. Type help followed by a command for full documentation. 其实有缩写的都是比较常用的。 常用的子命令包括: 命令 缩写 说明 示例 break b 设置断点 break [文件名:行数] condition cond 设置断点的条件 condition [断点名称/编号] [判断条件](如i==3) breakpoints bp 显示已经设置的断点 breakpoints clear 删除断点 bp显示的断点有名称,clear [断点名称] continue c 让程序运行到下一个断点处 continue next n 单步执行 next step s 进入某个函数内部,无法进入goroutine中 step(在函数入口出执行) stepout so 退出当前函数,回到进入点 so print p 打印变量的值 print goroutine gr 显示当前go协程或切换go协程 goroutine [协程编号] goroutines grs 显示全部go协程 goroutines restart r 重新运行 上次设置的断点依然有效 args 输出函数参数 locals 输出函数局部变量 条件断点 主函数代码如下: package main import "fmt" func main() { nums := make([]int, 5) for i := 0; i < len(nums); i++ { nums[i] = i * i } fmt.Println(nums) } Delve内部为panic()异常函数设置了断点。 (dlv) bp Breakpoint runtime-fatal-throw at 0x433f20 for runtime.fatalthrow() /opt/go/src/runtime/panic.go:1158 (0) Breakpoint unrecovered-panic at 0x433f90 for runtime.fatalpanic() /opt/go/src/runtime/panic.go:1185 (0) print runtime.curg._panic.arg 在主函数入口处设置断点,输入c运行代码。 (dlv) b main.main Breakpoint 1 set at 0x4ad768 for main.main() ./main.go:5 (dlv) bp Breakpoint runtime-fatal-throw at 0x433f20 for runtime.fatalthrow() /opt/go/src/runtime/panic.go:1158 (0) Breakpoint unrecovered-panic at 0x433f90 for runtime.fatalpanic() /opt/go/src/runtime/panic.go:1185 (0) print runtime.curg._panic.arg Breakpoint 1 at 0x4ad768 for main.main() ./main.go:5 (0) (dlv) c > main.main() ./main.go:5 (hits goroutine(1):1 total:1) (PC: 0x4ad768) 1: package main 2: 3: import "fmt" 4: => 5: func main() { 6: nums := make([]int, 5) 7: for i := 0; i < len(nums); i++ { 8: nums[i] = i * i 9: } 10: fmt.Println(nums) (dlv) 组合使用break和condition来创建条件断点。 (dlv) b main.go:8 Breakpoint 2 set at 0x4ad7bd for main.main() ./main.go:7 (dlv) cond 2 i==3 (dlv) bp Breakpoint runtime-fatal-throw at 0x433f20 for runtime.fatalthrow() /opt/go/src/runtime/panic.go:1158 (0) Breakpoint unrecovered-panic at 0x433f90 for runtime.fatalpanic() /opt/go/src/runtime/panic.go:1185 (0) print runtime.curg._panic.arg Breakpoint 1 at 0x4ad768 for main.main() ./main.go:5 (1) Breakpoint 2 at 0x4ad7bd for main.main() ./main.go:8 (0) cond i == 3 使用continue运行到断点2,使用locals查看当前变量,条件断点生效。 (dlv) c > main.main() ./main.go:8 (hits goroutine(1):1 total:1) (PC: 0x4ad7db) 3: import "fmt" 4: 5: func main() { 6: nums := make([]int, 5) 7: for i := 0; i < len(nums); i++ { => 8: nums[i] = i * i 9: } 10: fmt.Println(nums) 11: } (dlv) locals nums = []int len: 5, cap: 5, [...] i = 3 查看其他信息包括协程额函数栈等。 (dlv) stack 0 0x00000000004ad7db in main.main at ./main.go:8 1 0x0000000000436438 in runtime.main at /opt/go/src/runtime/proc.go:203 2 0x0000000000463df1 in runtime.goexit at /opt/go/src/runtime/asm_amd64.s:1373 (dlv) goroutines * Goroutine 1 - User: ./main.go:8 main.main (0x4ad7db) (thread 197086) Goroutine 2 - User: /opt/go/src/runtime/proc.go:305 runtime.gopark (0x4367eb) Goroutine 3 - User: /opt/go/src/runtime/proc.go:305 runtime.gopark (0x4367eb) Goroutine 4 - User: /opt/go/src/runtime/proc.go:305 runtime.gopark (0x4367eb) Goroutine 5 - User: /opt/go/src/runtime/proc.go:305 runtime.gopark (0x4367eb) [5 goroutines] (dlv) goroutine Thread 197086 at ./main.go:8 Goroutine 1: Runtime: ./main.go:8 main.main (0x4ad7db) User: ./main.go:8 main.main (0x4ad7db) Go: /opt/go/src/runtime/asm_amd64.s:220 runtime.rt0_go (0x461d64) Start: /opt/go/src/runtime/proc.go:113 runtime.main (0x436270) (dlv)quit

13 函数设置默认值 阅读更多

想到设置函数默认值,第一反映是如下的设计思路: 提供一个初始化函数,所有待设置默认值的字段都做为参数,如果不需要的时候传该类型的零值(把复杂度暴露给函数调用者) 所有待设置默认值的字段封装为结构体做为初始化函数中的一个参数(把复杂度暴露给函数调用者) 提供多个初始化函数,针对每个场景都进行内部默认值设置 这几个思路可行,但是不够优雅。当所有待设置默认值的字段发生变化时,上面三个思路都要修改源代码,才能适应变化,有没有更好的方法,参看gRPC的实现。 把gRPC中函数设置默认值部分单独拿出来看一下: package main import ( "context" "time" ) // 待设置默认值的字段,封装为一个内部结构体 type dialOptions struct { insecure bool timeout time.Duration } // 用户需要创建的客户端连接,其中包含上述待设置默认值的字段 type ClientConn struct { ctx context.Context authority string dopts dialOptions } // 创建一个接口把所有待设置默认值的字段都封装起来 type DialOption interface { apply(options *dialOptions) } // 待设置默认值字段的初始默认值 func defaultDialOptions() dialOptions { return dialOptions{ insecure: false, timeout: 0, } } // 用于创建客户端连接的函数,其中包含待设置默认值的字段 func DialContext(ctx context.Context, target string, opts ...DialOption) (conn *ClientConn, err error) { cc := &ClientConn{ ctx: ctx, authority: target, dopts: defaultDialOptions(), csMgr: &connectivityStateManager{}, } for _, opt := range opts { opt.apply(&cc.dopts) } return cc, nil } // 实现DialOption接口 type EmptyDialOption struct{} func (e EmptyDialOption) apply(options *dialOptions) {} // DialOption接口的具体实现 // 重点1:结构体中的函数对象的参数是待设置默认值的字段组成的结构体 type funcDialOption struct { do func(options *dialOptions) } // 实现DialOption接口的apply方法 func (f *funcDialOption) apply(options *dialOptions) { f.do(options) } // 创建一个funcDialOption结构体,传入一个dialOptions为参数的匿名函数 func newFuncDialOption(do func(options *dialOptions)) *funcDialOption { return &funcDialOption{do: do} } // 暴露给用户,用于修改待设置默认值字段的值 func WithInsecure() DialOption { return newFuncDialOption(func(options *dialOptions) { options.insecure = true }) } func WithTime(duration time.Duration) DialOption { return newFuncDialOption(func(options *dialOptions) { options.timeout = duration }) } // 用户调用过程 func main() { opts := []DialOption{ WithInsecure(), //返回的funcDialOption对象实现了DialOption接口 WithTime(1000), } DialContext(context.Background(), "", opts...) } 总结一下: 把待设置默认值的字段封装在一个结构体中,并且将字段都私有化 定义一个接口类型,这个接口提供一个方法,该方法的参数是1中封装的结构体的指针类型(用于修改结构体的内部值) 定义一个函数类型,该函数类型与接口类型中方法拥有一样的参数(划重点),在上面发gRPC中没有创建函数类型而是直接使用了匿名函数 定义一个结构体,并且实现2中的接口类型(接口中方法的具体实现) 创建函数(使用with+字段名)的命名方式,封装待设置默认值的字段的修改方法

12 Slice作为参数的坑 阅读更多

关于数组和切片,golang官方博客有文章详细说明,点击这里。其实这里说的已经很清楚了,论好好阅读官方说明的重要性。 问题 package main import ( "fmt" ) func main() { slice := []int{0, 1, 2, 3} fmt.Printf("slice: %v slice addr %p \n", slice, &slice) // slice: [0 1 2 3] slice addr 0xc00000c080 ret := changeSlice(slice) fmt.Printf("slice: %v slice addr %p | ret: %v ret addr %p \n", slice, &slice, ret, &ret) // slice: [0 111 2 3] slice addr 0xc00000c080 | ret: [0 111 2 3] ret addr 0xc00000c0c0 res := appendSlice(slice) fmt.Printf("slice: %v slice addr %p | res: %v ret addr %p \n", slice, &slice, res, &res) // slice: [0 111 2 3] slice addr 0xc00000c080 | res: [0 111 2 3 1] ret addr 0xc00000c120 } func changeSlice(s []int) []int { s[1] = 111 return s } func appendSlice(s []int) []int { s = append(s, 1) return s } 从上面代码和输出结果(注释部分)可以看出: changeSlice()函数对外部slice生效了 appendSlcie()函数对外部没有生效 分析 值传递和引用传递 golang中只有值传递,所有的引用传递都是直接把对应的指针拷贝过去了,所以修改能直接在原对象生效。 slice 很多地方都说slice是引用类型(这是相对于slice底层的数组而言的),其实slice是一个结构体类型(也就是值类型)。 type slice struct { array unsafe.Pointer len int cap int } 为啥changeSlice生效了 因为,slice是一个结构体且参数传递是值传递,所以changeSlice()函数中的s是slice的一个副本,所以changeSlice()函数的返回值ret的地址与slice不同,他们是内存中的两个对象。 在slice中array是一个指针,指向底层数组的开头,所以在changeSlice()函数中s[1] = 111是对底层数组的修改。那么在main()函数中不论是读取slice还是读取ret,他们都指向同一个底层数组,所以看起来就是changeSlice()函数修改了传入的切片对象的原始值。 为啥appendSlice没有生效 根据上面的分析,在appendSlice()函数中的append()操作是作用在res上而不是slice上。 让appendSlice生效 因为slice其实是一个结构体而不是一个引用。要让appendSlice生效,只要传入引用就可以,代码修改如下: res := appendSlice(&slice) func appendSlice(s *[]int) *[]int { *s = append(*s, 1) return s }

11 减小二进制文件 阅读更多

upx UPX是一种免费的,可移植的,可扩展的高性能可执行打包程序,适用于多种可执行格式。 可用于压缩可执行文件,具体使用方式如下所示: sugoi@sugoi:~$ upx -h Ultimate Packer for eXecutables Copyright (C) 1996 - 2020 UPX 3.96 Markus Oberhumer, Laszlo Molnar & John Reiser Jan 23rd 2020 Usage: upx [-123456789dlthVL] [-qvfk] [-o file] file.. Commands: -1 compress faster -9 compress better --best compress best (can be slow for big files) -d decompress -l list compressed file -t test compressed file -V display version number -h give this help -L display software license Options: -q be quiet -v be verbose -oFILE write output to 'FILE' -f force compression of suspicious files --no-color, --mono, --color, --no-progress change look Compression tuning options: --brute try all available compression methods & filters [slow] --ultra-brute try even more compression variants [very slow] Backup options: -k, --backup keep backup files --no-backup no backup files [default] Overlay options: --overlay=copy copy any extra data attached to the file [default] --overlay=strip strip any extra data attached to the file [DANGEROUS] --overlay=skip don't compress a file with an overlay Options for djgpp2/coff: --coff produce COFF output [default: EXE] Options for dos/com: --8086 make compressed com work on any 8086 Options for dos/exe: --8086 make compressed exe work on any 8086 --no-reloc put no relocations in to the exe header Options for dos/sys: --8086 make compressed sys work on any 8086 Options for ps1/exe: --8-bit uses 8 bit size compression [default: 32 bit] --8mib-ram 8 megabyte memory limit [default: 2 MiB] --boot-only disables client/host transfer compatibility --no-align don't align to 2048 bytes [enables: --console-run] Options for watcom/le: --le produce LE output [default: EXE] Options for win32/pe, win64/pe, rtm32/pe & arm/pe: --compress-exports=0 do not compress the export section --compress-exports=1 compress the export section [default] --compress-icons=0 do not compress any icons --compress-icons=1 compress all but the first icon --compress-icons=2 compress all but the first icon directory [default] --compress-icons=3 compress all icons --compress-resources=0 do not compress any resources at all --keep-resource=list do not compress resources specified by list --strip-relocs=0 do not strip relocations --strip-relocs=1 strip relocations [default] Options for linux/elf: --preserve-build-id copy .gnu.note.build-id to compressed output file.. executables to (de)compress This version supports: amd64-darwin.dylib dylib/amd64 amd64-darwin.macho macho/amd64 amd64-linux.elf linux/amd64 amd64-linux.kernel.vmlinux vmlinux/amd64 amd64-win64.pe win64/pe arm-darwin.macho macho/arm arm-linux.elf linux/arm arm-linux.kernel.vmlinux vmlinux/arm arm-linux.kernel.vmlinuz vmlinuz/arm arm-wince.pe arm/pe arm64-darwin.macho macho/arm64 arm64-linux.elf linux/arm64 armeb-linux.elf linux/armeb armeb-linux.kernel.vmlinux vmlinux/armeb fat-darwin.macho macho/fat i086-dos16.com dos/com i086-dos16.exe dos/exe i086-dos16.sys dos/sys i386-bsd.elf.execve bsd.exec/i386 i386-darwin.macho macho/i386 i386-dos32.djgpp2.coff djgpp2/coff i386-dos32.tmt.adam tmt/adam i386-dos32.watcom.le watcom/le i386-freebsd.elf freebsd/i386 i386-linux.elf linux/i386 i386-linux.elf.execve linux.exec/i386 i386-linux.elf.shell linux.sh/i386 i386-linux.kernel.bvmlinuz bvmlinuz/i386 i386-linux.kernel.vmlinux vmlinux/i386 i386-linux.kernel.vmlinuz vmlinuz/i386 i386-netbsd.elf netbsd/i386 i386-openbsd.elf openbsd/i386 i386-win32.pe win32/pe m68k-atari.tos atari/tos mips-linux.elf linux/mips mipsel-linux.elf linux/mipsel mipsel.r3000-ps1 ps1/exe powerpc-darwin.macho macho/ppc32 powerpc-linux.elf linux/ppc32 powerpc-linux.kernel.vmlinux vmlinux/ppc32 powerpc64-linux.elf linux/ppc64 powerpc64le-darwin.macho macho/ppc64le powerpc64le-linux.elf linux/ppc64le powerpc64le-linux.kernel.vmlinux vmlinux/ppc64le UPX comes with ABSOLUTELY NO WARRANTY; for details visit https://upx.github.io 链接器标记 编译 Go 程序时使用链接器标记(linker flags)来减小输出文件大小,但是,同时使用 -w 和 -s 标记会带来叠加的问题,而不是叠加的效果。 -w 和 -s 标志通常用在 App 链接阶段和 Go 编译阶段与 -ldflags 指令结合使用。 -w:删除编译后的二进制文件中的DWARF(一种可以包含在二进制文件中的调试数据格式)信息 -s:不仅删除了调试信息(DWARF),同时还删除了指定的符号表(包含了局部变量、全局变量和函数名等信息)和字符串表 因此,如果要删除调试信息,使用-w,如果要删除符号和字符串表,使用-s。 反面教材:go build -ldflags="-w -s" .

10 Goquery踩坑 阅读更多

// package import "github.com/PuerkitoBio/goquery" // go mod require github.com/PuerkitoBio/goquery v1.5.1 API文档在pkg.go.dev点这里,Github仓库在这里。 goquery在Github上近9k的star,golang著名的爬虫框架colly也用的它。 goquery简介 goquery实现了与jQuery类似的功能,包括使用可链接语法操纵和查询HTML文档。 它为Go语言带来了一种类似于jQuery的语法和函数。它基于Go的net/html包和CSS Selector库cascadia。由于net/html解析器返回的是节点,而不是功能完整的DOM树,因此jQuery的有状态操作函数(例如height(),css(),detach())已被省去。 net/html解析器读取UTF-8编码的文件(Go默认处理的就是UTF-8编码的文件),所以要确保被操作的源文档是UTF-8编码的HTML文件。有关如何执行此操作的各种选项,可查看Github仓库的Wiki。 在语法上,它尽可能接近jQuery,并在可能的情况下使用相同的方法名称,并且具有熟悉而类似的可链接接口。 jQuery是一个广受欢迎的库,因此,参照和遵循它的API来编写类似的HTML操作库会更好,这也是Go语言的精神(如fmt包的实现),即使jQuery有些方法看起来不那么直观(如index()等)。 注意:goquery以来net/html库,因此需要Go1.1+以上版本。 根据方法的不同类型分类到不同的文件中,三个点(...)表示该方法可以重载(overloads)。 array.go(数组类位置选择操作):Eq(),First(),Get(),Index...(),Last(),Slice() expend.go(扩充选择的集合):Add...(),AndSelf(),Union()是AddSelection()的别名 filter.go(过滤选择的集合):End(),Filter...(),Has...(),Intersection()是FilterSelection()的别名,Not...() iteration.go(遍历节点):Each(),EachWithBreak(),Map() manipulation.go(修改HTML):After...(),Append...(),Before...(),Clone(),Empty(),Prepend...(),Remove...(),ReplaceWith...(),Unwrap(),Wrap...(),WrapAll...(),WrapInner...() property.go(检查并获取节点属性值):Attr*(), RemoveAttr(), SetAttr(),AddClass(), HasClass(), RemoveClass(), ToggleClass(),Html(),Length(),Size()是Length()的别名,Text() query.go(判断节点身份):Contains(),Is...() traversal.go(遍历HTML文档树):,Children...(),Contents(),Find...(),Next...(),Parent[s]...(),Prev...(),Siblings...() type.go(goquery公开的类型):Document,Selection,Matcher utilities.go(辅助函数,而不是* Selection的方法,这不是jQuery的一部分):NodeName,OuterHtml 了解了具体的功能和提供的函数,具体就是在调用上面函数的时候提供CSS选择器作为参数。 CSS选择器 分类 类别 描述 语法 例子 基本选择器 通用选择器 选择所有元素 * `ns 基本选择器 类型选择器 按照给定的节点名称,选择所有匹配的元素 elementname input 匹配任何 <input> 元素 基本选择器 类选择器 按照给定的 class 属性的值,选择所有匹配的元素 .classname .index 匹配任何 class 属性中含有 "index" 的元素 基本选择器 ID选择器 按照 id 属性选择一个与之匹配的元素。需要注意的是,一个文档中,每个 ID 属性都应当是唯一的 #idname #toc 匹配 ID 为 "toc" 的元素 基本选择器 属性选择器 按照给定的属性,选择所有匹配的元素 [attr] [attr=value] [attr~=value] `[attr =value][attr^=value][attr$=value][attr*=value]` 分组选择器 选择器列表 ,将不同的选择器组合在一起,它选择所有能被列表中的任意一个选择器选中的节点 A,B div, span 会同时匹配 <span> 元素和 <div> 元素 组合选择器 后代组合器 空格组合器选择前一个元素的后代节点 A B div span 匹配所有位于任意 <div> 元素之内的 <span> 元素 组合选择器 直接子代组合器 >组合器选择前一个元素的直接子代的节点 A > B ul > li 匹配直接嵌套在 <ul> 元素内的所有 <li> 元素 组合选择器 一般兄弟组合器 ~组合器选择兄弟元素,即后一个节点在前一个节点后面的任意位置,并且共享同一个父节点 A ~ B p ~ span 匹配同一父元素下,<p> 元素后的所有 <span> 元素 组合选择器 紧邻兄弟组合器 +组合器选择相邻元素,即后一个元素紧跟在前一个之后,并且共享同一个父节点 A + B h2 + p 会匹配所有紧邻在 <h2> 元素后的 <p> 元素 组合选择器 列组合器 ` `组合器选择属于某个表格行的节点 伪选择器 伪类 :伪选择器支持按照未被包含在文档树中的状态信息来选择元素 a:visited 匹配所有曾被访问过的 <a> 元素 伪选择器 伪元素 ::伪选择器用于表示无法用HTML语义表达的实体 p::first-line 匹配所有 <p> 元素的第一行 goquery使用样例 大部分都和上面的一样,比较特殊的列在下面。 选择器 说明 Find(“div[lang]") 筛选含有lang属性的div元素 Find(“div[lang=zh]") 筛选lang属性为zh的div元素 Find(“div[lang!=zh]") 筛选lang属性不等于zh的div元素 Find(“div[lang¦=zh]") 筛选lang属性为zh或者zh-开头的div元素 Find(“div[lang*=zh]") 筛选lang属性包含zh这个字符串的div元素 Find(“div[lang~=zh]") 筛选lang属性包含zh这个单词的div元素,单词以空格分开的 Find(“div[lang$=zh]") 筛选lang属性以zh结尾的div元素,区分大小写 Find(“div[lang^=zh]") 筛选lang属性以zh开头的div元素,区分大小写 下面的操作在选择器选出的内容中再进行过滤。 类别 描述 语法 例子 内容过滤器 筛选出的元素要包含指定的文本 Find(":contains(text)") Find("div:contains(DIV2)"),选择出的div元素要包含DIV2文本Find(":empty),选出的元素都不能有子元素Find("span:has(div)"),选出包含div的span元素,与has类似的contains :first-child/:last-child 选出其父元素的第一个子元素 Find(":first-child") Find("div:first-child") :first-of-type/:last-of-type 选出其父元素的第一个该类型子元素 Find(":first-of-type") Find("div:first-of-type") :nth-child(n)/:nth-last-child(n) 选出其父元素的第n个子元素 Find(":nth-child(n)") Find("div:nth-child(3)") :nth-of-type(n)/:nth-last-of-type(n) 选出其父元素的第n个该类型子元素 Find(":nth-of-type(n)") Find("div:nth-of-type(3)") :only-child 选出其父元素中只有该元素(数量唯一)的子元素 Find(":only-child") Find("div:only-child") :only-of-type 选出其父元素中只有该类型元素(类型唯一)的子元素 Find(":only-of-type") Find("div:only-of-type")

09 项目布局 阅读更多

如何优雅的组织Go代码,更好的编写Go项目。 标准项目布局,Github的样例仓库点这里。 Go项目标准布局 这是Go应用程序项目的基础布局。这不是Go核心开发团队定义的官方标准;无论是在经典项目还是在新兴的项目中,这都是Go生态系统中一组常见的项目布局模式。这其中有一些模式比另外的一些更受欢迎。它通过几个支撑目录为任何足够大规模的实际应用程序提供一些增强功能。 如果你正准备学习Go、正在构建PoC项目或编写玩具项目,那么按照这个项目进行布局就大材小用了。从一些正真简单是的事情开始(一个main.go文件就足够了)。随着项目的增长,确保代码结构的合理是非常重要的,否则最终会出现很多隐藏的依赖关系和全局状态而到这项目代码混乱。当一个项目多人同时进行时,就更需要有清晰的结构,此时引入一种通用的项目包/标准库管理工具就显得尤为重要。当你维护一个开源项目或者有其他项目导入了你的代码,那么有一个私有的包(如internal)就很重要了。克隆这个项目,保留你项目中需要的部分,并删除其他部分。通常来说不需要也没必要使用这个项目中的全部内容。因为,从没有在一个单一的项目中使用本项目中定义的全部模式,甚至如vendor模式。 Go 1.14 Go Modules已经可以用于生产环境。没有什么特殊原因的话,请使用Go Modules,使用它之后,你就在也不用担心$GOPATH的配置和项目实际的存放位置,项目想放在哪里就放在哪里。本项目中go.mod文件的内容假设你的项目是托管在GitHub上的,当然这不是必选项,因为Module中的路径可以是任意的值,一般Module路径的第一部分中应该包含一个点(最新版的Go中不再强制要求这一点,如果使用的是稍微旧一些的版本,那么可能遇到编译失败的问题)。了解更多请看Issues 37554和 32819。 本项目布局有意设计的更通用一些,而不会尝试去引入一些特定的Go包结构。 这是社区共同的努力。如果发现了一种新的模式或者项目中已经存在的某些模式需要更新是,请新建一个issue。 如果需要一些关于命名、格式化或者样式方面的帮助,请先运行gofmt和golint。另外,请务必阅读以下Go代码样式指南和建议: https://talks.golang.org/2014/names.slide https://golang.org/doc/effective_go.html#names https://blog.golang.org/package-names https://github.com/golang/go/wiki/CodeReviewComments Style guideline for Go packages (rakyll/JBD) 更多背景信息请查看Go Project Layout。 有关命名和项目包组织样式以及其他代码结构的更多推荐文章: GopherCon EU 2018: Peter Bourgon - Best Practices for Industrial Programming GopherCon Russia 2018: Ashley McNamara + Brian Ketelsen - Go best practices GopherCon 2017: Edward Muller - Go Anti-Patterns GopherCon 2018: Kat Zien - How Do You Structure Your Go Apps Go目录 /cmd 项目主要的应用程序。 对于每个应用程序来说这个目录的名字应该和项目可执行文件的名字匹配(例如,/cmd/myapp)。 不要在这个目录中放太多的代码。如果目录中的代码可以被其他项目导入并使用,那么应该把他们放在/pkg目录。如果目录中的代码不可重用,或者不希望被他人使用,应该将代码放在/internal目录。显示的表明意图比较好! 通常来说,项目都应该拥有一个小的main函数,并在main函数中导入或者调用/internal和/pkg目录中的代码。 更多详情,请看/cmd目录中的例子。 /internal 私有的应用程序代码库。这些是不希望被其他人导入的代码。请注意:这种模式是Go编译器强制执行的。详细内容情况Go 1.4的release notes。再次注意,在项目的目录树中的任意位置都可以有internal目录,而不仅仅是在顶级目录中。 可以在内部代码包中添加一些额外的结构,来分隔共享和非共享的内部代码。这不是必选项(尤其是在小项目中),但是有一个直观的包用途是很棒的。应用程序实际的代码可以放在/internal/app目录(如,internal/app/myapp),而应用程序的共享代码放在/internal/pkg目录(如,internal/pkg/myprivlib)中。 /pkg 这里存放外部应用策划给你需可以是使用的库代码(如,/pkg/mypubliclib)。其他项目将会导入这些库来保证项目可以正常运行,所以在将代码放在这里前,一定要三四而行。请注意,internal目录是一个更好的选择来确保项目私有代码不会被其他人导入,因为这是Go强制执行的。使用/pkg目录来明确表示代码可以被其他人安全的导入仍然是一个好方式。Travis Jeffery撰写的关于 I’ll take pkg over internal 文章很好地概述了pkg和inernal目录以及何时使用它们。 当根目录中包含很多非Go组件和目录时,将Go代码组织在一个地方有助于更人欧冠难以的运行多种不同的Go工具(在如下的文章中都有提到:2018年GopherCon Best Practices for Industrial Programming,Kat Zien - How Do You Structure Your Go Apps ,Golab 2018 Massimiliano Pippi - Project layout patterns in Go)。 点击查看/pkg就能看到那些使用这个布局模式的流行Go代码仓库。这是一种常见的布局模式,但未被普遍接受,并且Go社区中的某些人不推荐这样做。 如果项目确实很小并且嵌套的层次并不会带来多少价值(除非你就是想用它),那么就不要使用它。请仔细思考这种情况,当项目变得很大,并且根目录中包含的内容相当繁杂(尤其是有很多非Go的组件)。 /vendor 应用程序的依赖关系(通过手动或者使用喜欢的依赖管理工具,如新增的内置Go Modules特性)。执行go mod vendor命令将会在项目中创建/vendor目录,注意,如果使用的不是Go 1.14版本,在执行go build进行编译时,需要添加-mod=vendor命令行选项,因为它不是默认选项。 构建库文件时,不要提交应用程序依赖项。 请注意,从1.13开始,Go也启动了模块代理特性(使用https://proxy.golang.org作为默认的模块代理服务器)。点击这里阅读有关它的更多信息,来了解它是否符合所需要求和约束。如果Go Module满足需要,那么就不需要vendor目录。 服务端应用程序的目录 /api OpenAPI/Swagger规范,JSON模式文件,协议定义文件。 更多样例查看/api目录。 Web应用程序的目录 /web Web应用程序特定的组件:静态Web资源,服务器端模板和单页应用(Single-Page App,SPA)。 通用应用程序的目录 /configs 配置文件模板或默认配置。 将confd或者consul-template文件放在这里。 /init 系统初始化(systemd、upstart、sysv)和进程管理(runit、supervisord)配置。 /scripts 用于执行各种构建,安装,分析等操作的脚本。 这些脚本使根级别的Makefile变得更小更简单(例如 https://github.com/hashicorp/terraform/blob/master/Makefile)。 更多样例查看/scripts。 /build 打包和持续集成。 将云(AMI),容器(Docker),操作系统(deb,rpm,pkg)软件包配置和脚本放在/build/package目录中。 将CI(travis、circle、drone)配置文件和就脚本放在build/ci目录中。请注意,有一些CI工具(如,travis CI)对于配置文件的位置有严格的要求。尝试将配置文件放在/build/ci目录,然后链接到CI工具想要的位置。 /deployments IaaS,PaaS,系统和容器编排部署配置和模板(docker-compose,kubernetes/helm,mesos,terraform,bosh)。请注意,在某些存储库中(尤其是使用kubernetes部署的应用程序),该目录的名字是/deploy。 /test 外部测试应用程序和测试数据。随时根据需要构建/test目录。对于较大的项目,有一个数据子目录更好一些。例如,如果需要Go忽略目录中的内容,则可以使用/test/data或/test/testdata这样的目录名字。请注意,Go还将忽略以“.”或“_”开头的目录或文件,因此可以更具灵活性的来命名测试数据目录。 更多样例查看/test。 其他 /docs 设计和用户文档(除了godoc生成的文档)。 更多样例查看/docs。 /tools 此项目的支持工具。请注意,这些工具可以从/pkg和/internal目录导入代码。 更多样例查看/tools。 / examples 应用程序或公共库的示例。 更多样例查看/examples。 /third_party 外部辅助工具,fork的代码和其他第三方工具(例如Swagger UI)。 /githooks Git的钩子。 /assets 项目中使用的其他资源(图像,Logo等)。 /website 如果不使用Github pages,则在这里放置项目的网站数据。 更多样例查看/website。 不应该出现的目录 /src 有一些Go项目确实包含src文件夹,但通常只有在开发者是从Java(这是Java中一个通用的模式)转过来的情况下才会有。如果可以的话请不要使用这种Java模式。你肯定不希望你的Go代码和项目看起来向Java。 不要将项目级别的/src目录与Go用于其工作空间的/src目录混淆,就像How to Write Go Code中描述的那样。$GOPATH环境变量指向当前的工作空间(默认情况下指向非Windows系统中的$HOME/go)。此工作空间包括顶级/pkg,/bin和/src目录。实际的项目最终变成/src下的子目录,因此,如果项目中有/src目录,则项目路径将会变成:/some/path/to/workspace/src/your_project/ src/your_code.go。请注意,使用Go 1.11,可以将项目放在GOPATH之外,但这并不意味着使用此布局模式是个好主意。 徽章 Go Report Card:它将使用gofmt,vet,gocyclo,golint,ineffassign,license和mispell扫描项目中的代码。将github.com/golang-standards/project-layout替换为你的项目的引用。 GoDoc:它将提供GoDoc生成的文档的在线版本。更改链接以指向你的项目。 Release:它将显示你项目的最新版本号。更改github链接以指向你的项目。 注意 WIP项目是一个自以为是的项目模板其中带有sample/reusable配置、脚本和代码。

08 系统调用 阅读更多

进程间通信 应用场景 数据传输 :一个进程需要将它的数据发送给另一个进程,发送的数据量在一个字节到几兆字节之间。 共享数据 :多个进程想要操作共享数据,一个进程对共享数据的修改,别的进程应该立刻看到。 通知事件 :一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。 资源共享 :多个进程之间共享同样的资源。为了作到这一点,需要内核提供锁和同步机制。 进程控制 :有些进程希望完全控制另一个进程的执行(如 Debug 进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。 实现方式 管道 (pipe),管道包括三种: 普通管道 PIPE: 通常有两种限制, 一是单工, 只能单向传输; 二是只能在父子或者兄弟进程间使用. 流管道 s_pipe: 去除了第一种限制, 为半双工,只能在父子或兄弟进程间使用,可以双向传输. 命名管道 name_pipe:去除了第二种限制, 可以在许多并不相关的进程之间进行通讯. 信号量 (semophore)是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。 消息队列 (message queue)是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。 信号 (signal)是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。 共享内存 (shared memory)就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。 套接字 (socket)是一种进程间通信机制,与其他通信机制不同的是,它可用于不同机器间的进程通信。 标准库 syscall包,一个用于底层操作系统原语的接口,其中定义了所有的系统调用看这里。 os/signal包,封装信号实现对输入信号的访问。 标准定义 在POSIX.1-1990标准中定义的信号列表 信号 值 动作 说明 SIGHUP 1 Term 终端控制进程结束(终端连接断开) SIGINT 2 Term 用户发送INTR字符(Ctrl+C)触发 SIGQUIT 3 Core 用户发送QUIT字符(Ctrl+/)触发 SIGILL 4 Core 非法指令(程序错误、试图执行数据段、栈溢出等) SIGABRT 6 Core 调用abort函数触发 SIGFPE 8 Core 算术运行错误(浮点运算错误、除数为零等) SIGKILL 9 Term 无条件结束程序(不能被捕获、阻塞或忽略) SIGSEGV 11 Core 无效内存引用(试图访问不属于自己的内存空间、对只读内存空间进行写操作) SIGPIPE 13 Term 消息管道损坏(FIFO/Socket通信时,管道未打开而进行写操作) SIGALRM 14 Term 时钟定时信号 SIGTERM 15 Term 结束程序(可以被捕获、阻塞或忽略) SIGUSR1 30,10,16 Term 用户保留 SIGUSR2 31,12,17 Term 用户保留 SIGCHLD 20,17,18 Ign 子进程结束(由父进程接收) SIGCONT 19,18,25 Cont 继续执行已经停止的进程(不能被阻塞) SIGSTOP 17,19,23 Stop 停止进程(不能被捕获、阻塞或忽略) SIGTSTP 18,20,24 Stop 停止进程(可以被捕获、阻塞或忽略) SIGTTIN 21,21,26 Stop 后台程序从终端中读取数据时触发 SIGTTOU 22,22,27 Stop 后台程序向终端中写数据时触发 在SUSv2和POSIX.1-2001标准中的信号列表: 信号 值 动作 说明 SIGTRAP 5 Core Trap指令触发(如断点,在调试器中使用) SIGBUS 0,7,10 Core 非法地址(内存地址对齐错误) SIGPOLL Term Pollable event (Sys V). Synonym for SIGIO SIGPROF 27,27,29 Term 性能时钟信号(包含系统调用时间和进程占用CPU的时间) SIGSYS 12,31,12 Core 无效的系统调用(SVr4) SIGURG 16,23,21 Ign 有紧急数据到达Socket(4.2BSD) SIGVTALRM 26,26,28 Term 虚拟时钟信号(进程占用CPU的时间)(4.2BSD) SIGXCPU 24,24,30 Core 超过CPU时间资源限制(4.2BSD) SIGXFSZ 25,25,31 Core 超过文件大小资源限制(4.2BSD) Linux kill命令 sugoi@sugoi:~$ kill --help kill: kill [-s 信号声明 | -n 信号编号 | -信号声明] 进程号 | 任务声明 ... 或 kill -l [信号声明] 向一个任务发送一个信号。 向以 PID 进程号或者 JOBSPEC 任务声明指定的进程发送一个以 SIGSPEC 信号声明或 SIGNUM 信号编号命名的信号。如果没有指定 SIGSPEC 或 SIGNUM,那么假定发送 SIGTERM 信号。 选项: -s sig SIG 是信号名称 -n sig SIG 是信号编号 -l 列出信号名称;如果参数后跟 `-l'则被假设为信号编号, 而相应的信号名称会被列出 Kill 成为 shell 内建有两个理由:它允许使用任务编号而不是进程号, 并且在可以创建的进程数上限达到是允许进程被杀死。 退出状态: 返回成功,除非使用了无效的选项或者有错误发生。 sugoi@sugoi:~$ kill -l 1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM 16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP 21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ 26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR 31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3 38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8 43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13 48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12 53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7 58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2 63) SIGRTMAX-1 64) SIGRTMAX kill与 kill -9 获取PID的方式,执行这些命令ps/ps/pidof/pstree/top。 kill pid的作用是向进程号为pid的进程发送SIGTERM(这是kill默认发送的信号),该信号是一个结束进程的信号且可以被应用程序捕获。若应用程序没有捕获并响应该信号的逻辑代码,则该信号的默认动作是kill掉进程。这是终止指定进程的推荐做法。 kill -9 pid则是向进程号为pid的进程发送 SIGKILL(该信号的编号为9),SIGKILL信号既不能被应用程序捕获,也不能被阻塞或忽略,其动作是立即结束指定进程。所以,应用程序根本无法“感知”SIGKILL信号,在完全无准备的情况下,被收到SIGKILL信号的操作系统给终止,在这种“暴力”情况下,应用程序完全没有释放当前占用资源的机会。 事实上,SIGKILL信号是直接发给init进程的,它收到该信号后,负责终止pid指定的进程。 在某些情况下(如进程已经hang死,无法响应正常信号),就可以使用 kill -9 来结束进程。 系统调用的应用 应用程序优雅退出 Linux Server端的应用程序经常会长时间运行,在运行过程中,可能申请了很多系统资源,也可能保存了很多状态,在这些场景下,我们希望进程在退出前,可以释放资源或将当前状态dump到磁盘上或打印一些重要的日志,也就是希望进程优雅退出(exit gracefully)。 Go中对信号的处理主要使用os/signal包中的两个方法: notify方法用来监听收到的信号,func Notify(c chan<- os.Signal, sig ...os.Signal) stop方法用来取消监听,func Stop(c chan<- os.Signal) os包中对syscall包中的几个常用信号进行了封装,如下: var ( Interrupt Signal = syscall.SIGINT Kill Signal = syscall.SIGKILL )package main import ( "fmt" "os" "os/signal" ) func main() { s := &http.Server{ Addr: ":8080", Handler: myHandler, ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second, MaxHeaderBytes: 1 << 20, } log.Fatal(s.ListenAndServe()) c := make(chan os.Signal) // 设置发送信号通知的通道。如果在发送信号时还没有准备好接收信号,则必 使用缓冲通道,否则可能会丢失信号 signal.Notify(c) // 不将任何信号传递给通知意味着将所有信号发送到通道 signal.Notify(c, os.Interrupt) // 监听Interrupt即syscall.SIGINT信号,用户发送INTR字 (Ctrl+C)触发 fmt.Println("启动了程序") s := <-c // 阻塞直到收到信号 fmt.Println("收到信号:", s) ctx := context.Background() log.Println("shut:",s.Shutdown(ctx)) } 优雅的退出一般就是根据需要监听多个信号。 应用程序热更新 在服务器不停机状态下,对正常访问流程不造成干扰和影响的程序升级方式。主要指的是服务程序的热更新,并不是 APP 和操作系统的 HotFix 技术。 互联网服务都追求服务高可用,即能提供 7x24 不间断服务,升级代码影响用户是不可以接受的。一般都追求 SLA 达标 99.99%,即一年故障时间不能超过 1 小时,而几乎每天都会进行代码升级,因此必须采用热更新。后端服务往往都是用集群承担大规模流量,代码升级往往涉及很多台机器,人工升级肯定是不现实的,需要自动化的升级方式。 热更新的目标 不关闭现有连接(正在运行中的程序) 新的进程启动并替代旧进程 新的进程接管新的连接 连接要随时响应用户的请求 当用户仍在请求旧进程时要保持连接 新用户应请求新进程,不可以出现拒绝请求的情况 Nginx热更新原理 Nginx进程及控制信息: Master: 监控worker进程:CHLD 管理worker进程:TREM,INT,QUIT,HUP,USER1,USER2,WINCH worker:TREM,INT,QUIT,HUP,USER1,WINCH nginx命令行: reload:HUP reopen:USER1 stop:TERM quit:QUIT 热更新流程: 向master进程发生HUP信息(reload命令) master进程校验配置语法是否正确 master进程打开新的监听端口 master进程用新配置启动新的woker子进程 master进程向老worker子进程发生QUIT信号 老worker进程关闭监听句柄,处理完当前连接后结束进程 golang热更新 主动流量调度:API网关+CI/CD:发布时自动摘除机器,等待程序处理完现有请求再做发布处理,或者使用 SLB 或者 LVS 手动切换流量。 程序优雅重启:保证在重启的时候 listen socket FD(文件描述符) 依然可以接受请求,只不过切换新老进程 kubernetes滚动升级 标准库函数优雅关闭 func (srv *Server) Shutdown(ctx context.Context) error

07 时间戳 阅读更多

通常我们说的时间戳,就是指格林威治时间(GMT)1970 年 01 月 01 日 00 时 00 分 00 秒起至现在的总秒数。 1970-01-01是根据Unix操作系统的诞生时间(1971-01-01,纪元时间)便于记忆得来的。 每增加一秒钟,时间戳就变化一下,最开始的设计是每1/60秒就变化一下。 时间戳的上限 32位操作系统,能表示的最大整数是2^31-1,即2147483647单位是秒,换算成年是68年,很快就要不够用了。 64位操作系统,能表示的最大整数是2^63-1,即9223372036854775807单位是纳秒,换算成年是292.471208677536年目前看来还行。 package main import ( "fmt" "time" ) func main() { now := time.Now() // 获取当前时间,2020-05-14 10:31:30.856251261 +0800 CST m=+0.000062967 timeStamp := now.Unix() // 获取时间戳,即从纪元时间到现在的秒数,1589423490 fmt.Println(now) fmt.Println("timeStamp:", timeStamp) fmt.Println("year:", timeStamp/60/60/24/365) // 简单计算一下,50年,没毛病 } 因此,如果时间设置的不正确,默认的时间零点为纪元时间(1970-01-01 00:00:00 +0000 UTC),但是Go语言却是0001-01-01 00:00:00 +0000 UTC,暂时不清楚这样的设计意图。 在Go语言中最小的时间单位是纳米,time包中也定义了这些常量,如下: type Duration int64 const ( Nanosecond Duration = 1 Microsecond = 1000 * Nanosecond Millisecond = 1000 * Microsecond Second = 1000 * Millisecond Minute = 60 * Second Hour = 60 * Minute ) 时区的概念 wikipedia 协调世界时(英语:Coordinated Universal Time,法语:Temps Universel Coordonné,简称UTC)是最主要的世界时间标准,其以原子时秒长为基础,在时刻上尽量接近于格林威治标准时间。 格林尼治平均时间(英语:Greenwich Mean Time,GMT)是指位于英国伦敦郊区的皇家格林尼治天文台当地的平太阳时,因为本初子午线被定义为通过那里的经线。 UTC-12(IDLW — 国际换日线) UTC-11 (MIT — 中途岛标准时间) UTC-10(HST — 夏威夷-阿留申标准时间) UTC-9:30(MSIT — 马克萨斯群岛标准时间) UTC-9(AKST — 阿拉斯加标准时间) UTC-8(PST — 太平洋标准时间A) UTC-7(MST — 北美山区标准时间) UTC-6(CST — 北美中部标准时间) UTC-5(EST — 北美东部标准时间) UTC-4(AST — 大西洋标准时间) UTC-3:30(NST — 纽芬兰岛标准时间) UTC-3(SAT — 南美标准时间) UTC-2 UTC-1(CVT — 佛得角标准时间) UTC(WET — 欧洲西部时区,GMT - 格林威治标准时间) UTC+1(CET — 欧洲中部时区) UTC+2(EET — 欧洲东部时区) UTC+3(MSK — 莫斯科时区) UTC+3:30(IRT — 伊朗标准时间) UTC+4(META — 中东时区A) UTC+4:30(AFT — 阿富汗标准时间) UTC+5(METB — 中东时区B) UTC+5:30(IDT — 印度标准时间) UTC+5:45(NPT — 尼泊尔标准时间) UTC+6(BHT — 孟加拉标准时间) UTC+6:30(MRT — 缅甸标准时间) UTC+7(IST — 中南半岛标准时间) UTC+8(EAT — 东亚标准时间/中国标准时间(BJT))我们在这里 UTC+9(FET — 远东标准时间) UTC+9:30(ACST — 澳大利亚中部标准时间) UTC+10(AEST — 澳大利亚东部标准时间) UTC+10:30(FAST — 澳大利亚远东标准时间) UTC+11(VTT — 瓦努阿图标准时间) UTC+11:30(NFT — 诺福克岛标准时间) UTC+12(PSTB — 太平洋标准时间B) UTC+12:45(CIT — 查塔姆群岛标准时间) UTC+13(PSTC — 太平洋标准时间C) UTC+14(PSTD — 太平洋标准时间D) 来张世界时区地图看看,更加直观一点。

06 string2time 阅读更多

const shortForm = "2006-01-02" parse, err := time.Parse(shortForm, tmp) 这里的shortForm的内容不能任意定义,Golang中这些数字都是有特殊函义的,见下面列表: 单位 可选值 月份 1,01,Jan,January 日  2,02,_2 时  3,03,15,PM,pm,AM,am 分  4,04 秒  5,05 年  06,2006 周几 Mon,Monday 时区时差表示 -07,-0700,Z0700,Z07:00,-07:00,MST 时区字母缩写 MST 看一看time包中定义的常量,就能理解啦,这些是预定义的布局,用于Time.Format和time.Parse。布局中使用的参考时间是特定时间:Mon Jan 2 15:04:05 MST 2006,换成Unix时间是1136239445 const ( ANSIC = "Mon Jan _2 15:04:05 2006" UnixDate = "Mon Jan _2 15:04:05 MST 2006" RubyDate = "Mon Jan 02 15:04:05 -0700 2006" RFC822 = "02 Jan 06 15:04 MST" RFC822Z = "02 Jan 06 15:04 -0700" // RFC822 with numeric zone RFC850 = "Monday, 02-Jan-06 15:04:05 MST" RFC1123 = "Mon, 02 Jan 2006 15:04:05 MST" RFC1123Z = "Mon, 02 Jan 2006 15:04:05 -0700" // RFC1123 with numeric zone RFC3339 = "2006-01-02T15:04:05Z07:00" RFC3339Nano = "2006-01-02T15:04:05.999999999Z07:00" Kitchen = "3:04PM" // Handy time stamps. Stamp = "Jan _2 15:04:05" StampMilli = "Jan _2 15:04:05.000" StampMicro = "Jan _2 15:04:05.000000" StampNano = "Jan _2 15:04:05.000000000" )

05 编写命令行工具 阅读更多

使用这个cobra这个库。 cobra 中有个重要的概念,分别是: commands:代表行为 arguments :命令行参数(或者称为位置参数) flags:flags 代表对行为的改变(也就是我们常说的命令行选项) 执行命令行程序时的一般格式为: APPNAME COMMAND ARG --FLAG arguments 首先来搞清楚命令行参数(arguments)与命令行选项的区别(flags/options)。 以常见的 ls 命令来说,其命令行的格式为: ls [OPTION]... [FILE]… # OPTION 对应本文中介绍的 flags,以 - 或 -- 开头 # FILE 则被称为参数(arguments)或位置参数 一般的规则是参数在所有选项的后面,上面的 … 表示可以指定多个选项和多个参数。 cobra 默认提供了一些验证方法: ExactArgs(int) : 必须有 N 个位置参数,否则报错 ExactValidArgs(int) :必须有 N 个位置参数,且都在命令的 ValidArgs 字段中,否则报错 MaximumNArgs(int) : 如果位置参数超过 N 个将报错 MinimumNArgs(int) : 至少要有 N 个位置参数,否则报错 RangeArgs(min, max) : 如果位置参数的个数不在区间 min 和 max 之中,报错 比如要让 Command cmdTimes 至少有一个位置参数,可以这样初始化它: var cmdTimes = &cobra.Command{ Use: … Short: … Long: … Args: cobra.MinimumNArgs(1), Run: … } flags 选项(flags)用来控制 Command 的具体行为。根据选项的作用范围,可以把选项分为两类: persistent local persistent 对于 persistent 类型的选项,既可以设置给该 Command,又可以设置给该 Command 的子 Command。对于一些全局性的选项,比较适合设置为 persistent 类型,比如控制输出的 verbose 选项: var Verbose bool rootCmd.PersistentFlags().BoolVarP(&Verbose, "verbose", "v", false, "verbose output") local local 类型的选项只能设置给指定的 Command,比如下面定义的 source 选项: var Source string rootCmd.Flags().StringVarP(&Source, "source", "s", "", "Source directory to read from") // 该选项不能指定给 rootCmd 之外的其它 Command。 默认情况下的选项都是可选的,但一些用例要求用户必须设置某些选项,这种情况 cobra 也是支持的,通过 Command 的 MarkFlagRequired 方法标记该选项即可: var Name string rootCmd.Flags().StringVarP(&Name, "name", "n", "", "user name (required)") rootCmd.MarkFlagRequired("name")

04 http POST 请求 阅读更多

通过HTTP POST方法传输数据的时候,有两种不同的方式需要区分。 form Data:url的参数形式 payload(有效负载):json串 不同形式的数据传输,需要对应不同的请求头。 Content-Type: application/x-www-form-urlencoded POST /some-path HTTP/1.1 Content-Type: application/x-www-form-urlencoded foo=bar&name=John Content-Type: application/json POST /some-path HTTP/1.1 Content-Type: application/json { "foo" : "bar", "name" : "John" } RFC的定义 如果不受请求方法和响应状态码的限制,则HTTP消息可以传输payload。payload由header字段形式的元数据和消息主体中的八位组序列形式的数据组成,在对任何传输编码进行解码之后。 HTTP中的“有效负载”始终是某些资源的部分或完整表示。 我们对payload使用单独的术语,因为某些消息仅包含关联的表示的header字段(例如,对HEAD的响应)或仅表示的某些部分(例如206状态代码)。 专门定义payload而不是相关表示的HTTP header字段称为“payload header 字段”。 payload header 字段由HTTP/1.1定义。 仅当存在消息body时,payload body 才出现在消息中。 payload body 是通过对可能已应用以确保安全正确地传输消息的任何传输编码进行解码而从消息主体获得的。

03 error处理 阅读更多

问题 报错信息如下: http: named cookie not present 引起问题的代码: cookie, err := r.Cookie("user") if err != nil { log.Fatal(err) } 问题的现象就是,浏览器在发送http请求的时候,在Request中没有我们需要的Cookie:user=XXX,因此代码执行log.Fatal(err),然后退出了。 开始以为是Cookie的问题,其实是golang错误处理的理解不到位。 分析 Fatal():等价于Print()然后会执行os.Exit()调用。 也就是说,上面的程序执行过程中遇到err就会直接打印日志信息,并退出了。 Panic():等价于Println()然后会执行内置函数panic()。 同样的,panic之后程序也会退出,不过会打印出更详细的信息。但是panic可以用recover来避免程序崩溃。 关于,panic、recover、defer的具体说明和用法,参考这里。 大部分情况下,都希望记录并打印异常,但是程序不崩溃继续运行。 所以上面的代码可以这样修改: defer func() { // p!=nil 判断确实发生了panic if p := recover(); p != nil { log.Printf("panic: %s\n", p) } }() cookie, err := r.Cookie("user") if err != nil { panic(err) } 另外 其实有很多比官方log库更好用的一些记录日志的库,比如go-kit的log库。

02 Module 阅读更多

1. 多版本依赖冲突问题 修改项目中的go.mod文件,将冲突的版本进行指定替换,如下所示: module example go 1.13 require( github.com/ugorji/go v1.1.7 // indirect ... ) replace ( github.com/ugorji/go v1.1.4 => github.com/ugorji/go/codec v0.0.0-20190204201341-e444a5086c43 ) 2. 导入本地模块问题 本地项目目录结构如下,在一个项目中有两个modules,根据两个go.mod文件的位置即可确定,分别是mod-a和mod-b。 mod-a/ ├── mod-b │ ├── go.mod │ ├── go.sum │ ├── pkg │ └── main.go ├── go.mod ├── go.sum └── main.go 现在要在mod-a中导入mod-b,那么只需要修改mod-a的go.mod文件即可,具体如下: module mod-a go 1.13 require ( mod-b v0.0.0-00010101000000-000000000000 ... ) replace ( mod-b => ./mod-b )

01 编译 阅读更多

交叉、动态、静态 问题一 默认情况下,golang的编译是动态编译,通过环境变量CGO_ENABLED控制,默认开启cgo,允许在Go代码中调用C代码,如下所示: go env ... GCCGO="gccgo" CC="gcc" # 就是这个,默认开启 CGO_ENABLED="1" CGO_CFLAGS="-g -O2" CGO_CPPFLAGS="" CGO_CXXFLAGS="-g -O2" CGO_FFLAGS="-g -O2" CGO_LDFLAGS="-g -O2" GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0 \ -fdebug-prefix-map=/tmp/go-build071200252=/tmp/go-build -gno-record-gcc-switches" ... 动态编译完成后的二进制文件,放在Alpine基础镜像中运行报错如下,但是放在ubuntu基础镜像中可以运行: standard_init_linux.go:211: exec user process caused "no such file or directory" 因为,在制作Alpine的时候,是基于musl libc和busybox构建的,导致动态依赖的二进制文件在运行时找不到依赖的文件。 问题一解决方案 使用动态编译后运行在大基础镜像中,即包含动态调用的C库的基础镜像 使用静态编译后运行在小基础镜像中 注意,有的情况下,可能会出现不能静态编译的依赖包,如libpcap这个库。 问题二 使用CGO_ENABLED=1 go build -a -ldflags '-extldflags "-static"' .编译web应用后,无法在scratch和Alpine基础镜像中运行,但是可以直接在开发环境的服务器上运行。 具体表现为,容器启动后直接退出,Exited (127) xxx ago。 问题二解决方案 使用CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .,编译后的web应用可以正常运行。 也就是说,动态编译静态链接不行,直接静态编译就可以。所以,静态编译主要用于有内置库的情况下,这样可以是二进制文件在更小的镜像中运行;动态编译静态链接主要用于没有内置库的情况下,将动态依赖的C库一起编译进来,然后将二进制文件放在更小的镜像中运行。 编译参数说明 参数-a,强制重新编译,不利用缓存或已编译好的部分文件,所有包都是最新的代码重新编译和关联 参数-installsuffix,在软件包安装的目录中增加后缀标识,以保持输出与默认版本分开(如果使用 -race 标识,则后缀就会默认设置为 -race 标识,用于区别 race 和普通的版本) 参数-o,指定编译后的可执行文件名称 参数GOOS,用于标识(声明)程序构建环境的目标操作系统。 参数GOARCH,用于标识(声明)程序构建环境的目标计算机架构。 参数GOHOSTOS,用于标识(声明)程序运行环境的目标操作系统。 参数GOHOSTARCH,用于标识(声明)程序运行环境的目标计算机架构。 参数CGO_ENABLED,用于标识(声明)cgo工具是否可用。 存在交叉编译的情况时,cgo 工具是不可用的。在标准go命令的上下文环境中,交叉编译意味着程序构建环境的目标计算架构的标识与程序运行环境的目标计算架构的标识不同,或者程序构建环境的目标操作系统的标识与程序运行环境的目标操作系统的标识不同。 在制作容器的时候,特别是最求镜像足够小的时候,比如下面的使用scratch来构建,这样必然编译环境和运行环境两者之间的计算机架构和运行环境标识不一致。 那么就要进行交叉编译,而交叉编译不支持 cgo,因此这里要禁用掉它,即必须进行静态编译,关闭 cgo 后,在构建过程中会忽略 cgo 并静态链接所有内置依赖库,而开启 cgo 后,方式将转为动态链接,即动态编译。 常见列表: 操作系统 GOOS GOARCH Windows32 windows 386 Windows64 windows amd64 OSX32 darwin 386 OSX64 darwin amd64 Linux32 linux 386 Linux64 linux amd64 scratch 明确为空的镜像,尤其适合构建“从零开始”的镜像。 FROMscratch 在构建基础镜像(例如debian和busybox)或超小型镜像(仅包含一个二进制文件以及它所需要的任何内容(例如hello-world))的上下文中,此镜像最有用。 从Docker1.5.0开始,DOckerfile中的FROM scratch将不进行任何操作,不会在生成的镜像中创建一个额外的层。 创建一个基础镜像:从文看出: 使用Docker保留的最小镜像scratch作为构建容器的启动,使用scratch表示在Dockerfile文件中的下一条指令将会作为生成镜像中的第一个文件系统层。 虽然在DockerHub中有scratch,但是不能拉取、运行或者将任何其他镜像贴标签为scratch。相反可以在Dockerfile中引用它,如下例子。 FROMscratchCOPY hello /CMD ["/hello"] 注意 scratch 镜像几乎不包含任何东西,不支持环境变量,也没有shell命令。 因此,基于 scratch 的镜像通过 ADD 指令进行添加,以此绕过目录创建。 分析总结 一切要从golang的可移植性(交叉编译)说起。说到一门编程语言可移植性,一般从下面两个方面考量: 语言自身被移植到不同平台的容易程度; 通过这种语言编译出来的应用程序对平台的适应性。 # 查看当前版本支持的操作系统 go tool dist list 得益于golang独立实现了runtime,移植起来相对容易,如下图所示。 runtime是支撑程序运行的基础。如C语言的运行时libc是目前主流操作系统上应用最普遍的运行时,通常以动态链接库的形式(比如:/lib/x86_64-linux-gnu/libc.so.6)随着系统一并发布,它的功能大致有如下几个: 提供基础库函数调用,比如:strncpy(操作字符串); 封装syscall,它是操作系统提供的API口,当用户层进行系统调用时,代码会trap(陷入)到内核层面执行,并提供同语言的库函数调用,比如:malloc(动态分配内存); 提供程序启动入口函数,比如:linux下的__libc_start_main。 libc等c runtime lib是很早以前就已经实现的了,甚至有些老旧的libc还是单线程的。一些从事c/c++开发多年的程序员早年估计都有过这样的经历:那就是链接runtime库时甚至需要选择链接支持多线程的库还是只支持单线程的库。除此之外,c runtime的版本也参差不齐。这样的c runtime状况完全不能满足golang自身的需求;另外Go的目标之一是原生支持并发,并使用goroutine模型,c runtime对此是无能为力的,因为c runtime本身是基于线程模型的。综合以上因素,golang自己实现了runtime,并封装了syscall,为不同平台上的go user level代码提供封装完成的、统一的go标准库;同时Go runtime实现了对goroutine模型的支持。 独立实现的go runtime层将Go user-level code与OS syscall解耦,把Go porting到一个新平台时,将runtime与新平台的syscall对接即可(当然porting工作不仅仅只有这些);同时,runtime层的实现基本摆脱了Go程序对libc的依赖,这样静态编译的Go程序具有很好的平台适应性。比如:一个compiled for linux amd64的Go程序可以很好的运行于不同linux发行版(centos、ubuntu)下。 静态编译 go程序在编译时会将go runtime一起编译为二进制文件,因此编译完成后的二进制文件比较大。可以使用ldd (print shared object dependencies)或nm (list symbols from object files)工具查看文件的外部动态库依赖。 在Docker化的今天,经常需要静态编译一个Go程序,以便放在Docker容器中。即使没有引用其它的第三方包,只是在程序中使用了标准库net,也会发现编译后的程序依赖glibc,这时候需要glibc-static库并且静态链接。 # 编译指令,关键是CGO_ENABLED=0表示关闭 CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main . ldd main 不是动态可执行文件 Go将所有运行需要的函数代码都放到了hellogo中,这就是所谓的“静态链接”。但是,并不是所有情况下,Go都能够不依赖外部动态库。 因为有一些库在编写的时候就依赖的外部动态库,使用cgo调用C语言实现的代码,因此在代码中import这些库,最终编译的二进制文件也必要有外部动态依赖,即设置CGO_ENABLE=0将会导致编译报错,报错如下所示。 CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main . github.com/google/gopacket/pcap ../../../go/pkg/mod/github.com/google/gopacket@v1.1.17/pcap/pcap.go:30:22: undefined: pcapErrorNotActivated ../../../go/pkg/mod/github.com/google/gopacket@v1.1.17/pcap/pcap.go:52:17: undefined: pcapTPtr ../../../go/pkg/mod/github.com/google/gopacket@v1.1.17/pcap/pcap.go:64:10: undefined: pcapPkthdr ../../../go/pkg/mod/github.com/google/gopacket@v1.1.17/pcap/pcap.go:102:7: undefined: pcapBpfProgram ../../../go/pkg/mod/github.com/google/gopacket@v1.1.17/pcap/pcap.go:103:7: undefined: pcapPkthdr ../../../go/pkg/mod/github.com/google/gopacket@v1.1.17/pcap/pcap.go:261:33: undefined: pcapErrorActivated ../../../go/pkg/mod/github.com/google/gopacket@v1.1.17/pcap/pcap.go:262:33: undefined: pcapWarningPromisc ../../../go/pkg/mod/github.com/google/gopacket@v1.1.17/pcap/pcap.go:263:33: undefined: pcapErrorNoSuchDevice ../../../go/pkg/mod/github.com/google/gopacket@v1.1.17/pcap/pcap.go:264:33: undefined: pcapErrorDenied ../../../go/pkg/mod/github.com/google/gopacket@v1.1.17/pcap/pcap.go:265:33: undefined: pcapErrorNotUp ../../../go/pkg/mod/github.com/google/gopacket@v1.1.17/pcap/pcap.go:265:33: too many errors 动态编译静态链接 编译命令如下: CGO_ENABLED=1 go build -a -ldflags '-extldflags "-static"' . 在Go中将-ldflags与go build命令一起使用,以在构建时将动态信息插入二进制文件中,而无需修改源代码。在此标志中,ld代表链接程序,该程序将已编译源代码的不同部分链接到最终二进制文件中。 ldflags代表链接器标志。它向底层Go工具链链接器cmd/link传递了一个标志,该标志使得在构建时从命令行更改导入的软件包的值。 来看看go tool link的参数介绍 go tool link ... -X definition add string value definition of the form importpath.name=value -extldflags flags pass flags to external linker ... 实现原理: 将所有生成的.o文件都打到一个.o文件中 将其交给外部的链接器,比如gcc/clang去做最终链接处理 参数中传入-ldflags 'extldflags "-static"',那么gcc/clang将会去做静态链接,将.o中undefined的符号都替换为真正的代码 通过-linkmode=external来强制cmd/link采用external linker。 下面的例子将会强制编译为静态链接的二进制文件。 go build -a -ldflags '-extldflags "-static"' . # sidecar /tmp/go-link-136417324/000021.o:在函数‘mygetgrouplist’中: /opt/go/src/os/user/getgrouplist_unix.go:16: 警告: \ Using 'getgrouplist' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking /tmp/go-link-136417324/000020.o:在函数‘mygetgrgid_r’中: /opt/go/src/os/user/cgo_lookup_unix.go:38: 警告: \ Using 'getgrgid_r' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking /tmp/go-link-136417324/000020.o:在函数‘mygetgrnam_r’中: /opt/go/src/os/user/cgo_lookup_unix.go:43: 警告: \ Using 'getgrnam_r' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking /tmp/go-link-136417324/000020.o:在函数‘mygetpwnam_r’中: /opt/go/src/os/user/cgo_lookup_unix.go:33: 警告: \ Using 'getpwnam_r' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking /tmp/go-link-136417324/000020.o:在函数‘mygetpwuid_r’中: /opt/go/src/os/user/cgo_lookup_unix.go:28: 警告: \ Using 'getpwuid_r' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking /tmp/go-link-136417324/000006.o:在函数‘_cgo_26061493d47f_C2func_getaddrinfo’中: /tmp/go-build/cgo-gcc-prolog:58: 警告: \ Using 'getaddrinfo' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking /usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/libpcap.a(nametoaddr.o):在函数‘pcap_nametoaddr’中:(.text+0x5): 警告: \ Using 'gethostbyname' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking /usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/libpcap.a(nametoaddr.o):在函数‘pcap_nametonetaddr’中:(.text+0xc5): 警告: \ Using 'getnetbyname' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking /usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/libpcap.a(nametoaddr.o):在函数‘pcap_nametoproto’中:(.text+0x305): 警告: \ Using 'getprotobyname' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking /usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/libpcap.a(nametoaddr.o):在函数‘pcap_nametoport’中:(.text+0xfb): 警告: \ Using 'getservbyname' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking ldd sidecar 不是动态可执行文件 动态编译 默认golang设置CGO_ENABLED=1。 go build . ldd sidecar linux-vdso.so.1 (0x00007ffda0bb6000) libpcap.so.0.8 => /usr/lib/x86_64-linux-gnu/libpcap.so.0.8 (0x00007fcba7fb5000) libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fcba7d96000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fcba79a5000) /lib64/ld-linux-x86-64.so.2 (0x00007fcba81f6000) 扩展-Linux分析文件 二进制指每天运行的可执行文件,从命令行工具到成熟的应用程序都是。Linux 提供了一套丰富的工具,让分析二进制文件变得轻而易举。 file 用途:帮助确定文件类型。 二进制文件、库文件、ASCII 文本文件、视频文件、图片文件、PDF、数据文件等各种文件类型 sugoi@sugoi:~/Documents/Golang-Guide$ file /bin/ls \ /bin/ls: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, \ BuildID[sha1]=2f15ad836be3339dec0e2e6a3c637e08e48aacbd, for GNU/Linux 3.2.0, stripped sugoi@sugoi:~/Documents/Golang-Guide$ file /etc/passwd /etc/passwd: ASCII text ldd 用途:打印共享对象依赖关系。 在开发软件的时候,如打印输出或从标准输入/打开的文件中读取等是大多数软件都需要的。所有这些被抽象成一组通用的函数,被放在一个叫 libc 或 glibc 的库中。 对动态链接的二进制文件运行ldd命令会显示出所有依赖库和它们的路径。 sugoi@sugoi:~/Documents/Golang-Guide$ ldd /bin/ls linux-vdso.so.1 (0x00007ffd119f3000) libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 (0x00007f9ca976e000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f9ca957c000) libpcre2-8.so.0 => /usr/lib/x86_64-linux-gnu/libpcre2-8.so.0 (0x00007f9ca94ec000) libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f9ca94e6000) /lib64/ld-linux-x86-64.so.2 (0x00007f9ca97d2000) libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f9ca94c3000) ltrace 用途:库调用跟踪器具。 使用 ldd 命令找到可执行程序所依赖的库。然而,一个库可以包含数百个函数。ltrace 命令可以显示运行时从库中调用的所有函数。 如下,可以看到被调用的函数名称,以及传递给该函数的参数,在输出的最右边看到这些函数返回的内容。 $ ltrace ls __libc_start_main(0x4028c0, 1, 0x7ffd94023b88, 0x412950 <unfinished ...> strrchr("ls", '/') = nil setlocale(LC_ALL, "") = "en_US.UTF-8" bindtextdomain("coreutils", "/usr/share/locale") = "/usr/share/locale" textdomain("coreutils") = "coreutils" __cxa_atexit(0x40a930, 0, 0, 0x736c6974756572) = 0 isatty(1) = 1 getenv("QUOTING_STYLE") = nil getenv("COLUMNS") = nil ioctl(1, 21523, 0x7ffd94023a50) = 0 << snip >> fflush(0x7ff7baae61c0) = 0 fclose(0x7ff7baae61c0) = 0 +++ exited (status 0) +++ $ 直接执行,没有这样的输出,很奇怪了。 hexdump 用途:以 ASCII、十进制、十六进制或八进制显示文件内容。 $ hexdump -C /bin/ls | head 00000000 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 |.ELF............| 00000010 02 00 3e 00 01 00 00 00 d4 42 40 00 00 00 00 00 |..>......B@.....| 00000020 40 00 00 00 00 00 00 00 f0 c3 01 00 00 00 00 00 |@...............| 00000030 00 00 00 00 40 00 38 00 09 00 40 00 1f 00 1e 00 |....@.8...@.....| 00000040 06 00 00 00 05 00 00 00 40 00 00 00 00 00 00 00 |........@.......| 00000050 40 00 40 00 00 00 00 00 40 00 40 00 00 00 00 00 |@.@.....@.@.....| 00000060 f8 01 00 00 00 00 00 00 f8 01 00 00 00 00 00 00 |................| 00000070 08 00 00 00 00 00 00 00 03 00 00 00 04 00 00 00 |................| 00000080 38 02 00 00 00 00 00 00 38 02 40 00 00 00 00 00 |8.......8.@.....| 00000090 38 02 40 00 00 00 00 00 1c 00 00 00 00 00 00 00 |8.@.............| $ strings 用途:打印文件中的可打印字符的字符串。 在开发软件的时候,各种文本/ASCII 信息会被添加到其中,比如打印信息、调试信息、帮助信息、错误等。只要这些信息都存在于二进制文件中,就可以用 strings 命令将其转储到屏幕上。 sugoi@sugoi:~/Documents/TUO/tuoctl$ strings /bin/ls | head /lib64/ld-linux-x86-64.so.2 .j<c~ MB#F- libselinux.so.1 _ITM_deregisterTMCloneTable __gmon_start__ _ITM_registerTMCloneTable fgetfilecon freecon lgetfilecon readelf 用途:显示有关ELF文件的信息。 上面使用file命令的时候,有输出ELF这种文件类型。 ELF( 可执行和可链接文件格式(Executable and Linkable File Format))是可执行文件或二进制文件的主流格式,不仅是 Linux 系统,也是各种 UNIX 系统的主流文件格式。 在使用 readelf 命令时,有一份实际的 ELF 规范的参考是非常有用的。 sugoi@sugoi:~/Documents/TUO/tuoctl$ readelf -h /bin/ls ELF 头: Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 类别: ELF64 数据: 2 补码,小端序 (little endian) Version: 1 (current) OS/ABI: UNIX - System V ABI 版本: 0 类型: DYN (共享目标文件) 系统架构: Advanced Micro Devices X86-64 版本: 0x1 入口点地址: 0x67d0 程序头起点: 64 (bytes into file) Start of section headers: 140224 (bytes into file) 标志: 0x0 Size of this header: 64 (bytes) Size of program headers: 56 (bytes) Number of program headers: 13 Size of section headers: 64 (bytes) Number of section headers: 30 Section header string table index: 29 objdump 用途:从对象文件中显示信息。 二进制文件是通过源码创建的,这些源码会通过编译器进行编译。编译器会生成相对于源代码的机器语言指令,然后由 CPU 执行特定的任务。这些机器语言代码可以通过被称为汇编语言的助记词来解读。汇编语言是一组指令,它可以帮助你理解由程序所进行并最终在 CPU 上执行的操作。 objdump 实用程序读取二进制或可执行文件,并将汇编语言指令转储到屏幕上。汇编语言知识对于理解 objdump 命令的输出至关重要。 请记住:汇编语言是特定于体系结构的。 sugoi@sugoi:~/Documents/Golang-Guide$ objdump -d /bin/ls | head /bin/ls: 文件格式 elf64-x86-64 Disassembly of section .init: 0000000000004000 <.init>: 4000: f3 0f 1e fa endbr64 4004: 48 83 ec 08 sub $0x8,%rsp 4008: 48 8b 05 c9 ef 01 00 mov 0x1efc9(%rip),%rax # 22fd8 <__gmon_start__> strace 用途:跟踪系统调用和信号。 strace 工具不是追踪调用的库,而是追踪系统调用。系统调用是与内核对接来完成工作的。 举个例子,如果你想把一些东西打印到屏幕上,会使用标准库 libc 中的 printf 或 puts 函数;但是,在底层,最终会有一个名为 write 的系统调用来实际把东西打印到屏幕上。 sugoi@sugoi:~/Documents/Golang-Guide$ strace /bin/ls execve("/bin/ls", ["/bin/ls"], 0x7ffea0244060 /* 67 vars */) = 0 brk(NULL) = 0x561686ceb000 arch_prctl(0x3001 /* ARCH_??? */, 0x7ffc8cfa1fb0) = -1 EINVAL (无效的参数) access("/etc/ld.so.preload", R_OK) = -1 ENOENT (没有那个文件或目录) openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3 fstat(3, {st_mode=S_IFREG|0644, st_size=72366, ...}) = 0 mmap(NULL, 72366, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f468d381000 close(3) = 0 openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libselinux.so.1", O_RDONLY|O_CLOEXEC) = 3 read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0@p\0\0\0\0\0\0"..., 832) = 832 fstat(3, {st_mode=S_IFREG|0644, st_size=163200, ...}) = 0 mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f468d37f000 mmap(NULL, 174600, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f468d354000 mprotect(0x7f468d35a000, 135168, PROT_NONE) = 0 mmap(0x7f468d35a000, 102400, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x6000) = 0x7f468d35a000 mmap(0x7f468d373000, 28672, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1f000) = 0x7f468d373000 mmap(0x7f468d37b000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x26000) = 0x7f468d37b000 mmap(0x7f468d37d000, 6664, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f468d37d000 close(3) = 0 openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3 read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\360q\2\0\0\0\0\0"..., 832) = 832 pread64(3, "\6\0\0\0\4\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0"..., 784, 64) = 784 pread64(3, "\4\0\0\0\20\0\0\0\5\0\0\0GNU\0\2\0\0\300\4\0\0\0\3\0\0\0\0\0\0\0", 32, 848) = 32 pread64(3, "\4\0\0\0\24\0\0\0\3\0\0\0GNU\0cBR\340\305\370\2609W\242\345)q\235A\1"..., 68, 880) = 68 fstat(3, {st_mode=S_IFREG|0755, st_size=2029224, ...}) = 0 pread64(3, "\6\0\0\0\4\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0"..., 784, 64) = 784 pread64(3, "\4\0\0\0\20\0\0\0\5\0\0\0GNU\0\2\0\0\300\4\0\0\0\3\0\0\0\0\0\0\0", 32, 848) = 32 pread64(3, "\4\0\0\0\24\0\0\0\3\0\0\0GNU\0cBR\340\305\370\2609W\242\345)q\235A\1"..., 68, 880) = 68 mmap(NULL, 2036952, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f468d162000 mprotect(0x7f468d187000, 1847296, PROT_NONE) = 0 mmap(0x7f468d187000, 1540096, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x25000) = 0x7f468d187000 mmap(0x7f468d2ff000, 303104, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x19d000) = 0x7f468d2ff000 mmap(0x7f468d34a000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1e7000) = 0x7f468d34a000 mmap(0x7f468d350000, 13528, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f468d350000 close(3) = 0 openat(AT_FDCWD, "/usr/lib/x86_64-linux-gnu/libpcre2-8.so.0", O_RDONLY|O_CLOEXEC) = 3 read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\340\"\0\0\0\0\0\0"..., 832) = 832 fstat(3, {st_mode=S_IFREG|0644, st_size=584392, ...}) = 0 mmap(NULL, 586536, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f468d0d2000 mmap(0x7f468d0d4000, 409600, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x2000) = 0x7f468d0d4000 mmap(0x7f468d138000, 163840, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x66000) = 0x7f468d138000 mmap(0x7f468d160000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x8d000) = 0x7f468d160000 close(3) = 0 openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libdl.so.2", O_RDONLY|O_CLOEXEC) = 3 read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0 \22\0\0\0\0\0\0"..., 832) = 832 fstat(3, {st_mode=S_IFREG|0644, st_size=18816, ...}) = 0 mmap(NULL, 20752, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f468d0cc000 mmap(0x7f468d0cd000, 8192, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1000) = 0x7f468d0cd000 mmap(0x7f468d0cf000, 4096, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x3000) = 0x7f468d0cf000 mmap(0x7f468d0d0000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x3000) = 0x7f468d0d0000 close(3) = 0 openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libpthread.so.0", O_RDONLY|O_CLOEXEC) = 3 read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\220\201\0\0\0\0\0\0"..., 832) = 832 pread64(3, "\4\0\0\0\24\0\0\0\3\0\0\0GNU\0w\\\273\377\370\24Ef`xg\200\260\263\264\0"..., 68, 824) = 68 fstat(3, {st_mode=S_IFREG|0755, st_size=157224, ...}) = 0 pread64(3, "\4\0\0\0\24\0\0\0\3\0\0\0GNU\0w\\\273\377\370\24Ef`xg\200\260\263\264\0"..., 68, 824) = 68 mmap(NULL, 140408, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f468d0a9000 mmap(0x7f468d0b0000, 69632, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x7000) = 0x7f468d0b0000 mmap(0x7f468d0c1000, 20480, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x18000) = 0x7f468d0c1000 mmap(0x7f468d0c6000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1c000) = 0x7f468d0c6000 mmap(0x7f468d0c8000, 13432, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f468d0c8000 close(3) = 0 mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f468d0a7000 arch_prctl(ARCH_SET_FS, 0x7f468d0a8400) = 0 mprotect(0x7f468d34a000, 12288, PROT_READ) = 0 mprotect(0x7f468d0c6000, 4096, PROT_READ) = 0 mprotect(0x7f468d0d0000, 4096, PROT_READ) = 0 mprotect(0x7f468d160000, 4096, PROT_READ) = 0 mprotect(0x7f468d37b000, 4096, PROT_READ) = 0 mprotect(0x561684dff000, 4096, PROT_READ) = 0 mprotect(0x7f468d3c0000, 4096, PROT_READ) = 0 munmap(0x7f468d381000, 72366) = 0 set_tid_address(0x7f468d0a86d0) = 66950 set_robust_list(0x7f468d0a86e0, 24) = 0 rt_sigaction(SIGRTMIN, {sa_handler=0x7f468d0b0bf0, sa_mask=[], sa_flags=SA_RESTORER|SA_SIGINFO, sa_restorer=0x7f468d0be3c0}, NULL, 8) = 0 rt_sigaction(SIGRT_1, {sa_handler=0x7f468d0b0c90, sa_mask=[], sa_flags=SA_RESTORER|SA_RESTART|SA_SIGINFO, sa_restorer=0x7f468d0be3c0}, NULL, 8) = 0 rt_sigprocmask(SIG_UNBLOCK, [RTMIN RT_1], NULL, 8) = 0 prlimit64(0, RLIMIT_STACK, NULL, {rlim_cur=8192*1024, rlim_max=RLIM64_INFINITY}) = 0 statfs("/sys/fs/selinux", 0x7ffc8cfa1f00) = -1 ENOENT (没有那个文件或目录) statfs("/selinux", 0x7ffc8cfa1f00) = -1 ENOENT (没有那个文件或目录) brk(NULL) = 0x561686ceb000 brk(0x561686d0c000) = 0x561686d0c000 openat(AT_FDCWD, "/proc/filesystems", O_RDONLY|O_CLOEXEC) = 3 fstat(3, {st_mode=S_IFREG|0444, st_size=0, ...}) = 0 read(3, "nodev\tsysfs\nnodev\ttmpfs\nnodev\tbd"..., 1024) = 385 read(3, "", 1024) = 0 close(3) = 0 access("/etc/selinux/config", F_OK) = -1 ENOENT (没有那个文件或目录) openat(AT_FDCWD, "/usr/lib/locale/locale-archive", O_RDONLY|O_CLOEXEC) = 3 fstat(3, {st_mode=S_IFREG|0644, st_size=8850624, ...}) = 0 mmap(NULL, 8850624, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f468c836000 close(3) = 0 ioctl(1, TCGETS, {B38400 opost isig icanon echo ...}) = 0 ioctl(1, TIOCGWINSZ, {ws_row=16, ws_col=186, ws_xpixel=0, ws_ypixel=0}) = 0 openat(AT_FDCWD, ".", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 3 fstat(3, {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 getdents64(3, /* 12 entries */, 32768) = 360 getdents64(3, /* 0 entries */, 32768) = 0 close(3) = 0 fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0x1), ...}) = 0 write(1, "archetypes config.yaml content"..., 80archetypes config.yaml content LICENSE README.md resources static themes ) = 80 close(1) = 0 close(2) = 0 exit_group(0) = ? +++ exited with 0 +++ nm 用途:列出对象文件中的符号。 如果使用的二进制文件没有被剥离,nm 命令将提供在编译过程中嵌入到二进制文件中的有价值的信息。nm 可以从二进制文件中识别变量和函数。在无法访问二进制文件的源代码时这很有用。 $ cat hello.c #include <stdio.h> int main() { printf("Hello world!"); return 0; } $ gcc -g hello.c -o hello $ file hello hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), \ dynamically linked (uses shared libs), for GNU/Linux 2.6.32, \ BuildID[sha1]=3de46c8efb98bce4ad525d3328121568ba3d8a5d, not stripped $ ./hello Hello world! $ nm hello | tail 0000000000600e20 d __JCR_END__ 0000000000600e20 d __JCR_LIST__ 00000000004005b0 T __libc_csu_fini 0000000000400540 T __libc_csu_init U __libc_start_main@@GLIBC_2.2.5 000000000040051d T main U printf@@GLIBC_2.2.5 0000000000400490 t register_tm_clones 0000000000400430 T _start 0000000000601030 D __TMC_END__ $ gdb 用途:GNU调试器。 不是所有的二进制文件中的东西都可以进行静态分析。唯一方法是在运行时环境,在任何给定的位置停止或暂停程序,并能够分析信息,然后再往下执行。 这就是调试器的作用,在 Linux 上,gdb 就是调试器的事实标准。它可以加载程序,在特定的地方设置断点,分析内存和 CPU 的寄存器,以及更多的功能。它是对上面工具的补充,可以做更多的运行时分析。 使用 gdb 加载一个程序,会看到 (gdb) 提示符。所有进一步的命令都将在这个 gdb 命令提示符中运行,直到退出。 $ gdb -q ./hello Reading symbols from /home/flash/hello...done. (gdb) break main Breakpoint 1 at 0x400521: file hello.c, line 4. (gdb) info break Num Type Disp Enb Address What 1 breakpoint keep y 0x0000000000400521 in main at hello.c:4 (gdb) run Starting program: /home/flash/./hello Breakpoint 1, main () at hello.c:4 4 printf("Hello world!"); Missing separate debuginfos, use: debuginfo-install glibc-2.17-260.el7_6.6.x86_64 (gdb) bt #0 main () at hello.c:4 (gdb) c Continuing. Hello world![Inferior 1 (process 29620) exited normally] (gdb) q $