Go最佳实践

来自NSQ

nsq的官方文档的Dsign中提到一个PPThttps://speakerdeck.com/snakes/nsq-nyc-golang-meetup, 里面有这样一段话

总结一下.

  1. don’t be afraid of sync package
    sync包里有
  • sync.Mutex(互斥锁,一读一写)
  • sync.RWMutex(读写锁,可以多读一写)
  • sync.Pool(对象池, 合理利用可以减少内存分配, 降低GC压力, 稍后写一篇博客说说)
  • sync.Once(并发控制. 适用于开几个goroutines去执行一个只执行一次的任务, 比如单例模式)
  • sync.Cond(并发控制, cond.Wait()阻塞至其他goroutie运行到cond.Signal())
  • sync.WaitGroup(并发控制. 通常用法 wg.Add增加任务数量 goroutie完成任务后执行wg.Done,任务数量减1 wg.Wait等待wg任务数量为0)
  1. goroutines are cheap not free
    这句话在其他地方也看过, go func()简单好用, 创建开销也很小, 但也是有开销的. 很多情况下开固定数量worker, 用channel传递数据, 效果会更好.
    go-apns2中的example是个非常好的例子.https://github.com/sideshow/apns2/blob/master/_example/channel/main.go

注意一个问题, go里面一个goroutine panic了, 会导致进程退出, 所以go func()时第一行带上

go func(){
defer func(){
if err:=recover(); err!=nil{
}
}()
}()

是安全的做法, worker channel法时类似

package main
import (
"fmt"
"log"
"time"
)
func main() {
ch := make(chan int, 10)
for i := 0; i < 2; i++ {
go worker(ch, i)
}
for i := 0; i < 3; i++ {
ch <- i
ch <- -1
}
time.Sleep(time.Second * 5)
}
func worker(ch <-chan int, goId int) {
defer func(ch <-chan int) {
if err := recover(); err != nil {
log.Printf("worker%d recover error:%s", goId, err)
go worker(ch, goId)
}
}(ch)
log.Printf("worker%d running", goId)
for data := range ch {
log.Printf("worker%d received data:%d", goId, data)
if data == -1 {
panic(fmt.Errorf("worker%d panic", goId))
}
}
}

fasthttp之所以快, 其中一个原因就是net/http是来一个连接就创建一个goroutie, 而fasthttp用了池复用了goroutines.

  1. watch your allocations (string() is costly, re-user buffers)
    go里面 []byte和string互转是会发生复制的, 开销明显, 如果代码里频繁互转, 考虑使用bytes.buffer 和 sync.Pool

  2. use anonymous structs for arbitrary JSON
    在写http api时, parse body这种事情, 如果只是纯粹取body里的json数据, 没必要单独定义结构体, 在函数里定义一个匿名结构体就好. var s struct { A int}

  3. no built-in per-request HTTP timeouts
    这是说要注意默认的httpClient没有超时

  4. synchronizing goroutine exit is hard - log each cleanup step in long-running goroutines
    同步化的goroutine一不小心就没有退出, 如果你写一个长期运行的服务, 用logger记录每一个goroutine的清理退出, 防止goroutine泄露

  5. select skips nil channels

select语句是会跳过nil的channels的. 因为在Go里往已经close掉的channel里发送数据是会panic的, 可以利用select语句.
附: channel操作导致panic的情况有: 关闭一个nil的channel, 关闭一个已经关闭的channel( j,ok:= <- ch, ok为false时代表ch已经关闭了), 往一个已经关闭的channel里发送数据(从已经关闭的channel里读数据是OK的, 如果这个channel是带缓冲的, 那么可以读到所有数据)

来自GO箴言

Python有import this的zen of Python, 想不到Go也有箴言
https://speakerdeck.com/ajstarks/go-proverbs

  1. 在go里, goroutines之间通信不要用共享内存的方式实现, 应该用channel来实现
  2. 并发不是并行
  3. channel是编排, mutexs是串行
  4. interface定义越多的方法, 抽象程度越低. Go提倡用接口组合的方式实现更大的接口
  5. 零值, 猜测这里说的是struct{}吧, struct{}是一个不占内存的空结构体, 在用map实现set, channel发送无额外意义的signal时能降低内存分配
  6. 提倡gofmt
  7. 一点点复制比一点点依赖好. 官方包里有时能见到一些复制的代码, 这是为了不互相依赖
  8. syscall每个平台实现不一样, 要加build tags
  9. cgo每个平台的lib不一样, 要加build tags
  10. Cgo不是go
  11. unsafe包不提供保障
  12. 简洁胜过高效
  13. error是值 可以用值的方式去处理错误: 传递, 比较
  14. 不用仅检查错误, 要优雅地处理
  15. 多花精力设计架构, 模块命名, 写详细的文档
  16. 写良好的文档给用户
  17. 对于普通错误, 应该用多值返回错误, 而不是手动panic

未知来源

  1. 写可重复使用的函数, 接收接口类型, 返回具体类型

Golang Github

https://github.com/golang/go/wiki/CodeReviewComments

分享到 评论

GO Logger 日志实践

分析一下用过的打印日志的log包

  1. Go标准库自带log, 这个log的func比较少, 没有区分level, 但足够简单, 有prefix功能, 可以设置flag来控制时间格式, caller的文件名和行数, 其它的标准包如 net/http database/sql 等也用了此包.
  2. 对自带的log进行包装, 加入level, 颜色. 如ngaut/log, 这个log star数并不多, 还是从最近很火的一个项目pingcap/tidb里看到的, 有点小清新的感觉, 但这个log可能只是为tidb使用的, 缺少自带log的一些方法, 导致没法使用在一些可以定制logger的第三方库如gorm中. 于是我fork了一下https://github.com/hanjm/log, 增加了一些方法, 以便可以给gorm用.
  3. 完全自己实现的log, 结构化输出, 通常是key=value或json, 有名的有logrus, zap等. 第一次看到logrus感觉美极了, 于是大量使用, 直到在关注tidb的时候收到带日志的issues邮件, 里面的日志带了caller, 感觉很有用, 于是去搜logrus的issue看有没有这个功能, 搜到了一个issuehttps://github.com/sirupsen/logrus/issues/63, 讨论了三年这个功能还没加上, 只好放弃美丽的logrus, 找到了替代品zap, zap的设计非常好, 定制性强. log是经常调用的代码, 每次调用不可避免地要进行内存分配, 分配次数和每次分配的内存大小将影响性能. 对log内容的处理也是一个涉及到性能的点, 像log.Printf参数是interface{}, logrus的field是map[stirng]interface{}, 打印interface{}只能靠reflect, Go是静态强类型语言, 用反射的开销比较大, 所以zap使用了手动指定类型的方式, 从zap提供的benchmark上开看, 性能提升还是蛮大的, 虽然相比logrus使用起来更麻烦, 但为了性能, 还是值得的.

总结

  1. 为了方便进行日志分析, 统一用json行日志, 这样用elk时可以免去定制正则表达式存储到elasticSearch的field中.
  2. net/http database/sql 及一些第三方包可能直接使用了标注库的log, 有个trick可以改变所有使用标准包log的行为, 通过log.SetOutput(w io.Writer)来改变位置, w是一个实现了Write(p []byte) (n int, err error)方法的io.Writer即可.
  3. runtime.Caller可以得到调用者的pc, 文件名, 文件行数, runtime.FuncForPC(pc).Name()可以得到pc所在的函数名, 对于debug非常有帮助. 但有一定性能开销, 所以方案是: 对于http server的access log, 没有必要使用带caller的日志, 而对于http api具体实现的函数内的log, 有必要记录caller, 而且光有文件名和行数还不够, 毕竟改了代码行数就变了, 而函数名一般不会变, 带上函数名会更直观.
  4. GitHub搜了一圈, 好多公司都会定制自己的log, 如tidb的ngaut/log, 七牛的qiniu/log, 饿了么的eleme/log, mailgun的mailgun/log, 是的, 我也造一个小轮子zaplog.zaplog是包装了zap, 带caller func name, 兼容logrus stdlog 的日志输出工具.
package zaplog
import (
"bytes"
"fmt"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"log"
"runtime"
"strings"
)
// CallerEncoder will add caller to log. format is "filename:lineNum:funcName", e.g:"zaplog/zaplog_test.go:15:zaplog.TestNewLogger"
func CallerEncoder(caller zapcore.EntryCaller, enc zapcore.PrimitiveArrayEncoder) {
enc.AppendString(strings.Join([]string{caller.TrimmedPath(), runtime.FuncForPC(caller.PC).Name()}, ":"))
}
func newLoggerConfig(debugLevel bool) (loggerConfig zap.Config) {
loggerConfig = zap.NewProductionConfig()
loggerConfig.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
loggerConfig.EncoderConfig.EncodeCaller = CallerEncoder
if debugLevel {
loggerConfig.Level = zap.NewAtomicLevelAt(zap.DebugLevel)
}
return
}
// NewCustomLoggers is a shortcut to get normal logger, noCallerLogger.
func NewCustomLoggers(debugLevel bool) (logger, noCallerLogger *zap.Logger) {
loggerConfig := newLoggerConfig(debugLevel)
logger, err := loggerConfig.Build()
if err != nil {
panic(err)
}
loggerConfig.DisableCaller = true
noCallerLogger, err = loggerConfig.Build()
if err != nil {
panic(err)
}
return
}
// NewLogger return a normal logger
func NewLogger(debugLevel bool) (logger *zap.Logger) {
loggerConfig := newLoggerConfig(debugLevel)
logger, err := loggerConfig.Build()
if err != nil {
panic(err)
}
return
}
// NewNoCallerLogger return a no caller key value, will be faster
func NewNoCallerLogger(debugLevel bool) (noCallerLogger *zap.Logger) {
loggerConfig := newLoggerConfig(debugLevel)
loggerConfig.DisableCaller = true
noCallerLogger, err := loggerConfig.Build()
if err != nil {
panic(err)
}
return
}
// CompatibleLogger is a logger which compatible to logrus/std log/prometheus.
// it implements Print() Println() Printf() Dbug() Debugln() Debugf() Info() Infoln() Infof() Warn() Warnln() Warnf()
// Error() Errorln() Errorf() Fatal() Fataln() Fatalf() Panic() Panicln() Panicf() With() WithField() WithFields()
type CompatibleLogger struct {
_log *zap.Logger
}
// NewCompatibleLogger return CompatibleLogger with caller field
func NewCompatibleLogger(debugLevel bool) *CompatibleLogger {
return &CompatibleLogger{NewLogger(debugLevel).WithOptions(zap.AddCallerSkip(1))}
}
// Print logs a message at level Info on the compatibleLogger.
func (l CompatibleLogger) Print(args ...interface{}) {
l._log.Info(fmt.Sprint(args...))
}
// Println logs a message at level Info on the compatibleLogger.
func (l CompatibleLogger) Println(args ...interface{}) {
l._log.Info(fmt.Sprint(args...))
}
// Printf logs a message at level Info on the compatibleLogger.
func (l CompatibleLogger) Printf(format string, args ...interface{}) {
l._log.Info(fmt.Sprintf(format, args...))
}
// Debug logs a message at level Debug on the compatibleLogger.
func (l CompatibleLogger) Debug(args ...interface{}) {
l._log.Debug(fmt.Sprint(args...))
}
// Debugln logs a message at level Debug on the compatibleLogger.
func (l CompatibleLogger) Debugln(args ...interface{}) {
l._log.Debug(fmt.Sprint(args...))
}
// Debugf logs a message at level Debug on the compatibleLogger.
func (l CompatibleLogger) Debugf(format string, args ...interface{}) {
l._log.Debug(fmt.Sprintf(format, args...))
}
// Info logs a message at level Info on the compatibleLogger.
func (l CompatibleLogger) Info(args ...interface{}) {
l._log.Info(fmt.Sprint(args...))
}
// Infoln logs a message at level Info on the compatibleLogger.
func (l CompatibleLogger) Infoln(args ...interface{}) {
l._log.Info(fmt.Sprint(args...))
}
// Infof logs a message at level Info on the compatibleLogger.
func (l CompatibleLogger) Infof(format string, args ...interface{}) {
l._log.Info(fmt.Sprintf(format, args...))
}
// Warn logs a message at level Warn on the compatibleLogger.
func (l CompatibleLogger) Warn(args ...interface{}) {
l._log.Warn(fmt.Sprint(args...))
}
// Warnln logs a message at level Warn on the compatibleLogger.
func (l CompatibleLogger) Warnln(args ...interface{}) {
l._log.Warn(fmt.Sprint(args...))
}
// Warnf logs a message at level Warn on the compatibleLogger.
func (l CompatibleLogger) Warnf(format string, args ...interface{}) {
l._log.Warn(fmt.Sprintf(format, args...))
}
// Error logs a message at level Error on the compatibleLogger.
func (l CompatibleLogger) Error(args ...interface{}) {
l._log.Error(fmt.Sprint(args...))
}
// Errorln logs a message at level Error on the compatibleLogger.
func (l CompatibleLogger) Errorln(args ...interface{}) {
l._log.Error(fmt.Sprint(args...))
}
// Errorf logs a message at level Error on the compatibleLogger.
func (l CompatibleLogger) Errorf(format string, args ...interface{}) {
l._log.Error(fmt.Sprintf(format, args...))
}
// Fatal logs a message at level Fatal on the compatibleLogger.
func (l CompatibleLogger) Fatal(args ...interface{}) {
l._log.Fatal(fmt.Sprint(args...))
}
// Fatalln logs a message at level Fatal on the compatibleLogger.
func (l CompatibleLogger) Fatalln(args ...interface{}) {
l._log.Fatal(fmt.Sprint(args...))
}
// Fatalf logs a message at level Fatal on the compatibleLogger.
func (l CompatibleLogger) Fatalf(format string, args ...interface{}) {
l._log.Fatal(fmt.Sprintf(format, args...))
}
// Panic logs a message at level Painc on the compatibleLogger.
func (l CompatibleLogger) Panic(args ...interface{}) {
l._log.Panic(fmt.Sprint(args...))
}
// Panicln logs a message at level Painc on the compatibleLogger.
func (l CompatibleLogger) Panicln(args ...interface{}) {
l._log.Panic(fmt.Sprint(args...))
}
// Panicf logs a message at level Painc on the compatibleLogger.
func (l CompatibleLogger) Panicf(format string, args ...interface{}) {
l._log.Panic(fmt.Sprintf(format, args...))
}
// With return a logger with an extra field.
func (l *CompatibleLogger) With(key string, value interface{}) *CompatibleLogger {
return &CompatibleLogger{l._log.With(zap.Any(key, value))}
}
// WithField return a logger with an extra field.
func (l *CompatibleLogger) WithField(key string, value interface{}) *CompatibleLogger {
return &CompatibleLogger{l._log.With(zap.Any(key, value))}
}
// WithFields return a logger with extra fields.
func (l *CompatibleLogger) WithFields(fields map[string]interface{}) *CompatibleLogger {
i := 0
var clog *CompatibleLogger
for k, v := range fields {
if i == 0 {
clog = l.WithField(k, v)
} else {
clog = clog.WithField(k, v)
}
i++
}
return clog
}
// FormatStdLog set the output of stand package log to zaplog
func FormatStdLog() {
log.SetFlags(log.Llongfile)
log.SetOutput(&logWriter{NewNoCallerLogger(false)})
}
type logWriter struct {
logger *zap.Logger
}
// Write implement io.Writer, as std log's output
func (w logWriter) Write(p []byte) (n int, err error) {
i := bytes.Index(p, []byte(":")) + 1
j := bytes.Index(p[i:], []byte(":")) + 1 + i
caller := bytes.TrimRight(p[:j], ":")
// find last index of /
i = bytes.LastIndex(caller, []byte("/"))
// find penultimate index of /
i = bytes.LastIndex(caller[:i], []byte("/"))
w.logger.Info("stdLog", zap.ByteString("caller", caller[i+1:]), zap.ByteString("log", bytes.TrimSpace(p[j:])))
return len(p), nil
}
分享到 评论