做者 | 杨成立(忘篱) 阿里巴巴高级技术专家html
关注“阿里巴巴云原生”公众号,回复 Go 便可查看清晰知识大图!linux
导读:从问题自己出发,不局限于 Go 语言,探讨服务器中经常遇到的问题,最后回到 Go 如何解决这些问题,为你们提供 Go 开发的关键技术指南。咱们将以系列文章的形式推出 《Go 开发的关键技术指南》,共有 4 篇文章,本文为第 2 篇。
在 C/C++ 中,git
若是能有一种方案,将指针和内存都管理起来,避免用户错误访问和释放,这样虽然浪费了一部分的 CPU,可是能够在快速变化的业务中避免这些头疼的问题。在现代的高级语言中,好比 Java、Python 和 JS 的异常,以及 Go 的 panic-recover 都是这种机制。github
毕竟,用一些 CPU 换得快速迭代中的不 Crash,怎么算都是划得来的。golang
Go 有 Defer, Panic, and Recover。其中 defer 通常用在资源释放或者捕获 panic。而 panic 是停止正常的执行流程,执行全部的 defer,返回调用函数继续 panic;主动调用 panic 函数,还有些运行时错误都会进入 panic 过程。最后 recover 是在 panic 时获取控制权,进入正常的执行逻辑。数据库
注意 recover 只有在 defer 函数中才有用,在 defer 的函数调用的函数中 recover 不起做用,以下实例代码不会 recover:编程
go package main import "fmt" func main() { f := func() { if r := recover(); r != nil { fmt.Println(r) } } defer func() { f() } () panic("ok") }
执行时依旧会 panic,结果以下:json
$ go run t.go panic: ok goroutine 1 [running]: main.main() /Users/winlin/temp/t.go:16 +0x6b exit status 2
有些状况是不能够被捕获,程序会自动退出,这种都是没法正常 recover。固然,通常的 panic 都是能捕获的,好比 Slice 越界、nil 指针、除零、写关闭的 chan。segmentfault
下面是 Slice 越界的例子,recover 能够捕获到:数组
go package main import ( "fmt" ) func main() { defer func() { if r := recover(); r != nil { fmt.Println(r) } }() b := []int{0, 1} fmt.Println("Hello, playground", b[2]) }
下面是 nil 指针被引用的例子,recover 能够捕获到:
go package main import ( "bytes" "fmt" ) func main() { defer func() { if r := recover(); r != nil { fmt.Println(r) } }() var b *bytes.Buffer fmt.Println("Hello, playground", b.Bytes()) }
下面是除零的例子,recover 能够捕获到:
go package main import ( "fmt" ) func main() { defer func() { if r := recover(); r != nil { fmt.Println(r) } }() var v int fmt.Println("Hello, playground", 1/v) }
下面是写关闭的 chan 的例子,recover 能够捕获到:
go package main import ( "fmt" ) func main() { defer func() { if r := recover(); r != nil { fmt.Println(r) } }() c := make(chan bool) close(c) c <- true }
通常 recover 后会判断是否 err,有可能须要处理特殊的 error,通常也须要打印日志或者告警,给一个 recover 的例子:
package main import ( "fmt" ) type Handler interface { Filter(err error, r interface{}) error } type Logger interface { Ef(format string, a ...interface{}) } // Handle panic by hdr, which filter the error. // Finally log err with logger. func HandlePanic(hdr Handler, logger Logger) error { return handlePanic(recover(), hdr, logger) } type hdrFunc func(err error, r interface{}) error func (v hdrFunc) Filter(err error, r interface{}) error { return v(err, r) } type loggerFunc func(format string, a ...interface{}) func (v loggerFunc) Ef(format string, a ...interface{}) { v(format, a...) } // Handle panic by hdr, which filter the error. // Finally log err with logger. func HandlePanicFunc(hdr func(err error, r interface{}) error, logger func(format string, a ...interface{}), ) error { var f Handler if hdr != nil { f = hdrFunc(hdr) } var l Logger if logger != nil { l = loggerFunc(logger) } return handlePanic(recover(), f, l) } func handlePanic(r interface{}, hdr Handler, logger Logger) error { if r != nil { err, ok := r.(error) if !ok { err = fmt.Errorf("r is %v", r) } if hdr != nil { err = hdr.Filter(err, r) } if err != nil && logger != nil { logger.Ef("panic err %+v", err) } return err } return nil } func main() { func() { defer HandlePanicFunc(nil, func(format string, a ...interface{}) { fmt.Println(fmt.Sprintf(format, a...)) }) panic("ok") }() logger := func(format string, a ...interface{}) { fmt.Println(fmt.Sprintf(format, a...)) } func() { defer HandlePanicFunc(nil, logger) panic("ok") }() }
对于库若是须要启动 goroutine,如何 recover 呢?
errChannel <- conn.Handshake()
;if err := recover(); err != nil && err != ErrAbortHandler {
;下面看看一些状况是没法捕获的,包括(不限于):
sync.Map
解决这个问题。Map 竞争写致使 panic 的实例代码以下:
go package main import ( "fmt" "time" ) func main() { m := map[string]int{} p := func() { defer func() { if r := recover(); r != nil { fmt.Println(r) } }() for { m["t"] = 0 } } go p() go p() time.Sleep(1 * time.Second) }
注意:若是编译时加了
-race
,其余竞争条件也会退出,通常用于死锁检测,但这会致使严重的性能问题,使用须要谨慎。
备注:通常标准库中经过
throw
抛出的错误都是没法 recover 的,搜索了下 Go1.11 一共有 690 个地方有调用 throw。
Go1.2 引入了能使用的最多线程数限制 ThreadLimit,若是超过了就 panic,这个 panic 是没法 recover 的。
fatal error: thread exhaustion runtime stack: runtime.throw(0x10b60fd, 0x11) /usr/local/Cellar/go/1.8.3/libexec/src/runtime/panic.go:596 +0x95 runtime.mstart() /usr/local/Cellar/go/1.8.3/libexec/src/runtime/proc.go:1132
默认是 1 万个物理线程,咱们能够调用 runtime
的 debug.SetMaxThreads
设置最大线程数。
SetMaxThreads sets the maximum number of operating system threads that the Go program can use. If it attempts to use more than this many, the program crashes. SetMaxThreads returns the previous setting. The initial setting is 10,000 threads.
用这个函数设置程序能使用的最大系统线程数,若是超过了程序就 crash,返回的是以前设置的值,默认是 1 万个线程。
The limit controls the number of operating system threads, not the number of goroutines. A Go program creates a new thread only when a goroutine is ready to run but all the existing threads are blocked in system calls, cgo calls, or are locked to other goroutines due to use of runtime.LockOSThread.
注意限制的并非 goroutine 的数目,而是使用的系统线程的限制。goroutine 启动时,并不老是新开系统线程,只有当目前全部的物理线程都阻塞在系统调用、cgo 调用,或者显示有调用 runtime.LockOSThread
时。
SetMaxThreads is useful mainly for limiting the damage done by programs that create an unbounded number of threads. The idea is to take down the program before it takes down the operating system.
这个是最后的防护措施,能够在程序干死系统前把有问题的程序干掉。
举一个简单的例子,限制使用 10 个线程,而后用 runtime.LockOSThread
来绑定 goroutine 到系统线程,能够看到没有建立 10 个 goroutine 就退出了(runtime 也须要使用线程)。参考下面的例子 Playground: ThreadLimit:
go package main import ( "fmt" "runtime" "runtime/debug" "sync" "time" ) func main() { nv := 10 ov := debug.SetMaxThreads(nv) fmt.Println(fmt.Sprintf("Change max threads %d=>%d", ov, nv)) var wg sync.WaitGroup c := make(chan bool, 0) for i := 0; i < 10; i++ { fmt.Println(fmt.Sprintf("Start goroutine #%v", i)) wg.Add(1) go func() { c <- true defer wg.Done() runtime.LockOSThread() time.Sleep(10 * time.Second) fmt.Println("Goroutine quit") }() <- c fmt.Println(fmt.Sprintf("Start goroutine #%v ok", i)) } fmt.Println("Wait for all goroutines about 10s...") wg.Wait() fmt.Println("All goroutines done") }
运行结果以下:
bash Change max threads 10000=>10 Start goroutine #0 Start goroutine #0 ok ...... Start goroutine #6 Start goroutine #6 ok Start goroutine #7 runtime: program exceeds 10-thread limit fatal error: thread exhaustion runtime stack: runtime.throw(0xffdef, 0x11) /usr/local/go/src/runtime/panic.go:616 +0x100 runtime.checkmcount() /usr/local/go/src/runtime/proc.go:542 +0x100 ...... /usr/local/go/src/runtime/proc.go:1830 +0x40 runtime.startm(0x1040e000, 0x1040e000) /usr/local/go/src/runtime/proc.go:2002 +0x180
从此次运行能够看出,限制可用的物理线程为 10 个,其中系统占用了 3 个物理线程,user-level 可运行 7 个线程,开启第 8 个线程时就崩溃了。
注意这个运行结果在不一样的 go 版本是不一样的,好比 Go1.8 有时候启动 4 到 5 个 goroutine 就会崩溃。
并且加 recover 也没法恢复,参考下面的实例代码。
可见这个机制是最后的防护,不能突破的底线。咱们在线上服务时,曾经由于 block 的 goroutine 过多,致使触发了这个机制。
go package main import ( "fmt" "runtime" "runtime/debug" "sync" "time" ) func main() { defer func() { if r := recover(); r != nil { fmt.Println("main recover is", r) } } () nv := 10 ov := debug.SetMaxThreads(nv) fmt.Println(fmt.Sprintf("Change max threads %d=>%d", ov, nv)) var wg sync.WaitGroup c := make(chan bool, 0) for i := 0; i < 10; i++ { fmt.Println(fmt.Sprintf("Start goroutine #%v", i)) wg.Add(1) go func() { c <- true defer func() { if r := recover(); r != nil { fmt.Println("main recover is", r) } } () defer wg.Done() runtime.LockOSThread() time.Sleep(10 * time.Second) fmt.Println("Goroutine quit") }() <- c fmt.Println(fmt.Sprintf("Start goroutine #%v ok", i)) } fmt.Println("Wait for all goroutines about 10s...") wg.Wait() fmt.Println("All goroutines done") }
如何避免程序超过线程限制被干掉?通常可能阻塞在 system call,那么何时会阻塞?还有,GOMAXPROCS 又有什么做用呢?
The GOMAXPROCS variable limits the number of operating system threads that can execute user-level Go code simultaneously. There is no limit to the number of threads that can be blocked in system calls on behalf of Go code; those do not count against the GOMAXPROCS limit. This package's GOMAXPROCS function queries and changes the limit.
GOMAXPROCS sets the maximum number of CPUs that can be executing simultaneously and returns the previous setting. If n < 1, it does not change the current setting. The number of logical CPUs on the local machine can be queried with NumCPU. This call will go away when the scheduler improves.
可见 GOMAXPROCS 只是设置 user-level 并行执行的线程数,也就是真正执行的线程数 。实际上若是物理线程阻塞在 system calls,会开启更多的物理线程。关于这个参数的说明,文章《Number of threads used by goroutine》解释得很清楚:
There is no direct correlation. Threads used by your app may be less than, equal to or more than 10.
So if your application does not start any new goroutines, threads count will be less than 10.
If your app starts many goroutines (>10) where none is blocking (e.g. in system calls), 10 operating system threads will execute your goroutines simultaneously.
If your app starts many goroutines where many (>10) are blocked in system calls, more than 10 OS threads will be spawned (but only at most 10 will be executing user-level Go code).
设置 GOMAXPROCS 为 10:若是开启的 goroutine 小于 10 个,那么物理线程也小于 10 个。若是有不少 goroutines,可是没有阻塞在 system calls,那么只有 10 个线程会并行执行。若是有不少 goroutines 同时超过 10 个阻塞在 system calls,那么超过 10 个物理线程会被建立,可是只有 10 个活跃的线程执行 user-level 代码。
那么何时会阻塞在 system blocking 呢?例子《Why does it not create many threads when many goroutines are blocked in writing》解释很清楚,虽然设置了 GOMAXPROCS 为 1,但实际上仍是开启了 12 个线程,每一个 goroutine 一个物理线程,具体执行下面的代码 Writing Large Block:
go package main import ( "io/ioutil" "os" "runtime" "strconv" "sync" ) func main() { runtime.GOMAXPROCS(1) data := make([]byte, 128*1024*1024) var wg sync.WaitGroup for i := 0; i < 10; i++ { wg.Add(1) go func(n int) { defer wg.Done() for { ioutil.WriteFile("testxxx"+strconv.Itoa(n), []byte(data), os.ModePerm) } }(i) } wg.Wait() }
运行结果以下:
Mac chengli.ycl$ time go run t.go real 1m44.679s user 0m0.230s sys 0m53.474s
虽然 GOMAXPROCS 设置为 1,实际上建立了 12 个物理线程。
有大量的时间是在 sys 上面,也就是 system calls。
So I think the syscalls were exiting too quickly in your original test to show the effect you were expecting.
Effective Go 中的解释:
Goroutines are multiplexed onto multiple OS threads so if one should block, such as while waiting for I/O, others continue to run. Their design hides many of the complexities of thread creation and management.
因而可知,若是程序出现由于超过线程限制而崩溃,那么能够在出现瓶颈时,用 linux 工具查看系统调用的统计,看哪些系统调用致使建立了过多的线程。
错误处理是现实中常常碰到的、难以处理好的问题,下面会从下面几个方面探讨错误处理:
咱们总会遇到非预期的非正常状况,有一种是符合预期的,好比函数返回 error 并处理,这种叫作能够预见到的错误,还有一种是预见不到的好比除零、空指针、数组越界等叫作 panic,panic 的处理主要参考 Defer, Panic, and Recover。
错误处理的模型通常有两种,通常是错误码模型好比 C/C++ 和 Go,还有异常模型好比 Java 和 C#。Go 没有选择异常模型,由于错误码比异常更有优点,参考文章《Cleaner, more elegant, and wrong》以及《Cleaner, more elegant, and harder to recognize》。
看下面的代码:
try { AccessDatabase accessDb = new AccessDatabase(); accessDb.GenerateDatabase(); } catch (Exception e) { // Inspect caught exception } public void GenerateDatabase() { CreatePhysicalDatabase(); CreateTables(); CreateIndexes(); }
这段代码的错误处理有不少问题,好比若是 CreateIndexes
抛出异常,会致使数据库和表不会删除,形成脏数据。从代码编写者和维护者的角度看这两个模型,会比较清楚:
错误处理不容易作好,要说容易那说明作错了;要把错误处理写对了,基于错误码模型虽然很难,但比异常模型简单。
若是使用错误码模型,很是容易就能看出错误处理没有写对,也能很容易知道作得好很差;要知道是否作得很是好,错误码模型也不太容易。若是使用异常模型,不管作的好很差都很难知道,并且也很难知道怎么作好。
Go 官方的 error 介绍,简单一句话就是返回错误对象的方式,参考《Error handling and Go》,解释了 error 是什么?如何判断具体的错误?以及显式返回错误的好处。
文中举的例子就是打开文件错误:
func Open(name string) (file *File, err error)
Go 能够返回多个值,最后一个通常是 error,咱们须要检查和处理这个错误,这就是 Go 的错误处理的官方介绍:
if err := Open("src.txt"); err != nil { // Handle err }
看起来很是简单的错误处理,有什么难的呢?稍等,在 Go2 的草案中,提到的三个点 Error Handling、Error Values和 Generics 泛型,两个点都是错误处理的,这说明了 Go1 中对于错误是有改进的地方。
再详细看下 Go2 的草案,错误处理:Error Handling 中,主要描述了发生错误时的重复代码,以及不能便捷处理错误的状况。好比草案中举的这个例子 No Error Handling: CopyFile,没有作任何错误处理:
package main import ( "fmt" "io" "os" ) func CopyFile(src, dst string) error { r, _ := os.Open(src) defer r.Close() w, _ := os.Create(dst) io.Copy(w, r) w.Close() return nil } func main() { fmt.Println(CopyFile("src.txt", "dst.txt")) }
还有草案中这个例子 Not Nice and still Wrong: CopyFile,错误处理是特别啰嗦,并且比较明显有问题:
package main import ( "fmt" "io" "os" ) func CopyFile(src, dst string) error { r, err := os.Open(src) if err != nil { return err } defer r.Close() w, err := os.Create(dst) if err != nil { return err } defer w.Close() if _, err := io.Copy(w, r); err != nil { return err } if err := w.Close(); err != nil { return err } return nil } func main() { fmt.Println(CopyFile("src.txt", "dst.txt")) }
当 io.Copy
或 w.Close
出现错误时,目标文件其实是有问题,那应该须要删除 dst 文件的。并且须要给出错误时的信息,好比是哪一个文件,不能直接返回 err。因此 Go 中正确的错误处理,应该是这个例子 Good: CopyFile,虽然啰嗦繁琐不简洁:
package main import ( "fmt" "io" "os" ) func CopyFile(src, dst string) error { r, err := os.Open(src) if err != nil { return fmt.Errorf("copy %s %s: %v", src, dst, err) } defer r.Close() w, err := os.Create(dst) if err != nil { return fmt.Errorf("copy %s %s: %v", src, dst, err) } if _, err := io.Copy(w, r); err != nil { w.Close() os.Remove(dst) return fmt.Errorf("copy %s %s: %v", src, dst, err) } if err := w.Close(); err != nil { os.Remove(dst) return fmt.Errorf("copy %s %s: %v", src, dst, err) } return nil } func main() { fmt.Println(CopyFile("src.txt", "dst.txt")) }
具体应该如何简洁的处理错误,能够读 Error Handling,大体是引入关键字 handle 和 check,因为本文重点侧重 Go1 如何错误处理,就不展开分享了。
明显上面每次都返回的 fmt.Errorf
信息也是不够的,因此 Go2 还对于错误的值有提案,参考 Error Values。大规模程序应该面向错误编程和测试,同时错误应该包含足够的信息。
Go1 中判断 error 具体是什么错误,有如下几种办法:
io.EOF
这个全局变量,那么能够直接比较是不是这个错误;os.IsNotExist
判断是不是指定错误;error.Error()
返回的字符串匹配,看是不是某个错误。在复杂程序中,有用的错误须要包含调用链的信息。例如,考虑一次数据库写,可能调用了 RPC,RPC 调用了域名解析,最终是没有权限读 /etc/resolve.conf
文件,那么给出下面的调用链会很是有用:
write users database: call myserver.Method: \ dial myserver:3333: open /etc/resolv.conf: permission denied
因为 Go1 的错误值没有完整的解决这个问题,才致使出现很是多的错误处理的库,好比:
errors.Is
、errors.As
和 errors.Match
;%+v
来格式化错误的额外信息好比堆栈;pkg/errors
的 Cause 返回最底层的错误不一样,它只反馈错误链的下一个错误;Go1.13 改进了 errors,参考以下实例代码:
package main import ( "errors" "fmt" "io" ) func foo() error { return fmt.Errorf("read err: %w", io.EOF) } func bar() error { if err := foo(); err != nil { return fmt.Errorf("foo err: %w", err) } return nil } func main() { if err := bar(); err != nil { fmt.Printf("err: %+v\n", err) fmt.Printf("unwrap: %+v\n", errors.Unwrap(err)) fmt.Printf("unwrap of unwrap: %+v\n", errors.Unwrap(errors.Unwrap(err))) fmt.Printf("err is io.EOF? %v\n", errors.Is(err, io.EOF)) } }
运行结果以下:
err: foo err: read err: EOF unwrap: read err: EOF unwrap of unwrap: EOF err is io.EOF? true
从上面的例子能够看出:
errors.Is
能判断出是不是最里面的那个 error。另外,错误处理每每和 log 是容易混为一谈的,由于遇到错误通常会打日志,特别是在 C/C 中返回错误码通常都会打日志记录下,有时候还会记录一个全局的错误码好比 linux 的 errno,而这种习惯,致使 error 和 log 混淆形成比较大的困扰。考虑之前写了一个 C 的服务器,出现错误时会在每一层打印日志,因此就会造成堆栈式的错误日志,便于排查问题,若是只有一个错误,不知道调用上下文,排查会很困难:
avc decode avc_packet_type failed. ret=3001 Codec parse video failed, ret=3001 origin hub error, ret=3001
这种比只打印一条日志 origin hub error, ret=3001
要好,可是还不够好:
改进后的错误日志变成了在底层返回,而不在底层打印在调用层打印,有调用链和堆栈,有线程切换的 ID 信息,也有文件的行数:
Error processing video, code=3001 : origin hub : codec parser : avc decoder [100] video_avc_demux() at [srs_kernel_codec.cpp:676] [100] on_video() at [srs_app_source.cpp:1076] [101] on_video_imp() at [srs_app_source:2357]
从 Go2 的描述来讲,实际上这个错误处理也尚未考虑完备。从实际开发来讲,已经比较实用了。
总结下 Go 的 error,错误处理应该注意如下几点:
推荐用 github.com/pkg/errors 这个错误处理的库,基本上是够用的,参考 Refine: CopyFile,能够看到 CopyFile 中低级重复的代码已经比较少了:
package main import ( "fmt" "github.com/pkg/errors" "io" "os" ) func CopyFile(src, dst string) error { r, err := os.Open(src) if err != nil { return errors.Wrap(err, "open source") } defer r.Close() w, err := os.Create(dst) if err != nil { return errors.Wrap(err, "create dest") } nn, err := io.Copy(w, r) if err != nil { w.Close() os.Remove(dst) return errors.Wrap(err, "copy body") } if err := w.Close(); err != nil { os.Remove(dst) return errors.Wrapf(err, "close dest, nn=%v", nn) } return nil } func LoadSystem() error { src, dst := "src.txt", "dst.txt" if err := CopyFile(src, dst); err != nil { return errors.WithMessage(err, fmt.Sprintf("load src=%v, dst=%v", src, dst)) } // Do other jobs. return nil } func main() { if err := LoadSystem(); err != nil { fmt.Printf("err %+v\n", err) } }
改写的函数中,用errors.Wrap
和errors.Wrapf
代替了fmt.Errorf
,咱们不记录 src 和 dst 的值,由于在上层会记录这个值(参考下面的代码),而只记录咱们这个函数产生的数据,好比nn
。
import "github.com/pkg/errors" func LoadSystem() error { src, dst := "src.txt", "dst.txt" if err := CopyFile(src, dst); err != nil { return errors.WithMessage(err, fmt.Sprintf("load src=%v, dst=%v", src, dst)) } // Do other jobs. return nil }
在这个上层函数中,咱们用的是errors.WithMessage
添加了这一层的错误信息,包括src
和dst
,因此CopyFile
里面就不用重复记录这两个数据了。同时咱们也没有打印日志,只是返回了带完整信息的错误。
func main() { if err := LoadSystem(); err != nil { fmt.Printf("err %+v\n", err) } }
在顶层调用时,咱们拿到错误,能够决定是打印仍是忽略仍是送监控系统。
好比咱们在调用层打印错误,使用 %+v
打印详细的错误,有完整的信息:
err open src.txt: no such file or directory open source main.CopyFile /Users/winlin/t.go:13 main.LoadSystem /Users/winlin/t.go:39 main.main /Users/winlin/t.go:49 runtime.main /usr/local/Cellar/go/1.8.3/libexec/src/runtime/proc.go:185 runtime.goexit /usr/local/Cellar/go/1.8.3/libexec/src/runtime/asm_amd64.s:2197 load src=src.txt, dst=dst.txt
可是这个库也有些小毛病:
CopyFile
中仍是有可能会有重复的信息,仍是 Go2 的 handle
和 check
方案是最终解决;Wrap
,有时候是调用 WithMessage
(不须要加堆栈时),这个真是很是很差用的地方(这个咱们已经修改了库,能够所有使用 Wrap 不用 WithMessage,会去掉重复的堆栈)。一直在码代码,对日志的理解老是不断在变,大体分为几个阶段:
Note: 推荐阅读 Kafka 对于 Log 的定义,广义日志是能够理解的消息, The Log: What every software engineer should know about real-time data's unifying abstraction。
考虑一个服务,处理不一样的链接请求:
package main import ( "context" "fmt" "log" "math/rand" "os" "time" ) type Connection struct { url string logger *log.Logger } func (v *Connection) Process(ctx context.Context) { go checkRequest(ctx, v.url) duration := time.Duration(rand.Int()%1500) * time.Millisecond time.Sleep(duration) v.logger.Println("Process connection ok") } func checkRequest(ctx context.Context, url string) { duration := time.Duration(rand.Int()%1500) * time.Millisecond time.Sleep(duration) logger.Println("Check request ok") } var logger *log.Logger func main() { ctx := context.Background() rand.Seed(time.Now().UnixNano()) logger = log.New(os.Stdout, "", log.LstdFlags) for i := 0; i < 5; i++ { go func(url string) { connecton := &Connection{} connecton.url = url connecton.logger = logger connecton.Process(ctx) }(fmt.Sprintf("url #%v", i)) } time.Sleep(3 * time.Second) }
这个日志的主要问题,就是有了和没有差很少,啥也看不出来,常量太多变量太少,缺失了太多的信息。看起来这是个简单问题,却常常容易犯这种问题,须要咱们在打印每一个日志时,须要思考这个日志比较完善的信息是什么。上面程序输出的日志以下:
2019/11/21 17:08:04 Check request ok 2019/11/21 17:08:04 Check request ok 2019/11/21 17:08:04 Check request ok 2019/11/21 17:08:04 Process connection ok 2019/11/21 17:08:05 Process connection ok 2019/11/21 17:08:05 Check request ok 2019/11/21 17:08:05 Process connection ok 2019/11/21 17:08:05 Check request ok 2019/11/21 17:08:05 Process connection ok 2019/11/21 17:08:05 Process connection ok
若是完善下上下文信息,代码能够改为这样:
type Connection struct { url string logger *log.Logger } func (v *Connection) Process(ctx context.Context) { go checkRequest(ctx, v.url) duration := time.Duration(rand.Int()%1500) * time.Millisecond time.Sleep(duration) v.logger.Println(fmt.Sprintf("Process connection ok, url=%v, duration=%v", v.url, duration)) } func checkRequest(ctx context.Context, url string) { duration := time.Duration(rand.Int()%1500) * time.Millisecond time.Sleep(duration) logger.Println(fmt.Sprintf("Check request ok, url=%v, duration=%v", url, duration)) }
输出的日志以下:
2019/11/21 17:11:35 Check request ok, url=url #3, duration=32ms 2019/11/21 17:11:35 Check request ok, url=url #0, duration=226ms 2019/11/21 17:11:35 Process connection ok, url=url #0, duration=255ms 2019/11/21 17:11:35 Check request ok, url=url #4, duration=396ms 2019/11/21 17:11:35 Check request ok, url=url #2, duration=449ms 2019/11/21 17:11:35 Process connection ok, url=url #2, duration=780ms 2019/11/21 17:11:35 Check request ok, url=url #1, duration=1.01s 2019/11/21 17:11:36 Process connection ok, url=url #4, duration=1.099s 2019/11/21 17:11:36 Process connection ok, url=url #3, duration=1.207s 2019/11/21 17:11:36 Process connection ok, url=url #1, duration=1.257s
完善日志信息后,对于服务器特有的一个问题,就是如何关联上下文,常见的上下文包括:
以上面的代码为例,能够用请求 URL 来做为上下文。
package main import ( "context" "fmt" "log" "math/rand" "os" "time" ) type Connection struct { url string logger *log.Logger } func (v *Connection) Process(ctx context.Context) { go checkRequest(ctx, v.url) duration := time.Duration(rand.Int()%1500) * time.Millisecond time.Sleep(duration) v.logger.Println(fmt.Sprintf("Process connection ok, duration=%v", duration)) } func checkRequest(ctx context.Context, url string) { duration := time.Duration(rand.Int()%1500) * time.Millisecond time.Sleep(duration) logger.Println(fmt.Sprintf("Check request ok, url=%v, duration=%v", url, duration)) } var logger *log.Logger func main() { ctx := context.Background() rand.Seed(time.Now().UnixNano()) logger = log.New(os.Stdout, "", log.LstdFlags) for i := 0; i < 5; i++ { go func(url string) { connecton := &Connection{} connecton.url = url connecton.logger = log.New(os.Stdout, fmt.Sprintf("[CONN %v] ", url), log.LstdFlags) connecton.Process(ctx) }(fmt.Sprintf("url #%v", i)) } time.Sleep(3 * time.Second) }
运行结果以下所示:
[CONN url #2] 2019/11/21 17:19:28 Process connection ok, duration=39ms 2019/11/21 17:19:28 Check request ok, url=url #0, duration=149ms 2019/11/21 17:19:28 Check request ok, url=url #1, duration=255ms [CONN url #3] 2019/11/21 17:19:28 Process connection ok, duration=409ms 2019/11/21 17:19:28 Check request ok, url=url #2, duration=408ms [CONN url #1] 2019/11/21 17:19:29 Process connection ok, duration=594ms 2019/11/21 17:19:29 Check request ok, url=url #4, duration=615ms [CONN url #0] 2019/11/21 17:19:29 Process connection ok, duration=727ms 2019/11/21 17:19:29 Check request ok, url=url #3, duration=1.105s [CONN url #4] 2019/11/21 17:19:29 Process connection ok, duration=1.289s
若是须要查链接 2 的日志,能够 grep 这个 url #2
关键字:
Mac:gogogo chengli.ycl$ grep 'url #2' t.log [CONN url #2] 2019/11/21 17:21:43 Process connection ok, duration=682ms 2019/11/21 17:21:43 Check request ok, url=url #2, duration=998ms
然鹅,仍是发现有很多问题:
解决办法包括:
SetOutput(io.Writer)
将日志送给它处理就能够了。固然,这要求函数传参时须要带 context.Context,通常在本身的应用程序中能够要求这么作,凡是打日志的地方要带 context。对于库,通常能够不打日志,而返回带堆栈的复杂错误的方式,参考 Errors 错误处理部分。
本书亮点
“ 阿里巴巴云原生关注微服务、Serverless、容器、Service Mesh 等技术领域、聚焦云原生流行技术趋势、云原生大规模的落地实践,作最懂云原生开发者的技术圈。”