【Go】使用 logrus 日志库

相比起 zap 日志库,logrus 性能会低一些,但在很多场景,并不需要太高性能,而 logrus 库的自定义能力较为强大,可以任意定义日志格式,较为方便。 package main import ( "context" "fmt" "github.com/sirupsen/logrus" "io" "os" "path/filepath" "runtime" "strings" ) const logPath = "./log.txt" var log = New() func New() *Logger { return &Logger{logger: logrus.New()} } type Logger struct { ctx context.Context logger *logrus.Logger } func (l *Logger) SetFormatter(formatter logrus.Formatter) { l.logger.SetFormatter(formatter) } func (l *Logger) SetOutput(output io.Writer) { l.logger.SetOutput(output) } func (l *Logger) SetLevel(level logrus.Level) { l.logger.SetLevel(level) } func (l *Logger) WithContext(ctx context.Context) *Logger { return &Logger{ctx: ctx, logger: l.logger} } func (l *Logger) Debug(format string, args ...any) { l.log(logrus.DebugLevel, format, args...) } func (l *Logger) Info(format string, args ...any) { l.log(logrus.InfoLevel, format, args...) } func (l *Logger) Warn(format string, args ...any) { l.log(logrus.WarnLevel, format, args...) } func (l *Logger) Error(format string, args ...any) { l.log(logrus.ErrorLevel, format, args...) } func (l *Logger) Fatal(format string, args ...any) { l.log(logrus.FatalLevel, format, args...) } func (l *Logger) log(level logrus.Level, format string, args ...any) { format = fmt.Sprintf("[%s] %s", GetCallerShort(3), format) l.logger.WithContext(l.ctx).Logf(level, format, args...) } type Formatter struct{} func (f *Formatter) Format(entry *logrus.Entry) ([]byte, error) { pid := os.Getpid() time := entry.Time.Format("2006-01-02T15:04:05.000000") level := strings.ToUpper(entry.Level.String()) traceId := GetTraceIdFromCtx(entry.Context) content := fmt.Sprintf("%s %s %d %s %s\n", time, level, pid, traceId, entry.Message) return []byte(content), nil } // GetCallerShort 获取调用点文件名 + 行号 func GetCallerShort(skip int) string { _, file, line, ok := runtime.Caller(skip + 1) if !ok { return "" } _, file = filepath.Split(file) return fmt.Sprintf("%s:%d", file, line) } // CtxSetTraceId 在 ctx 中设置 trace id func CtxSetTraceId(ctx context.Context) context.Context { return context.WithValue(ctx, "trace_id", "id1") } // GetTraceIdFromCtx 打印日志时,从 ctx 中获取 trace id 打印 func GetTraceIdFromCtx(ctx context.Context) string { if ctx == nil { return "-" } val := ctx.Value("trace_id") if traceId, ok := val.(string); ok { return fmt.Sprintf("trace-%s", traceId) } else { return "-" } } func init() { f, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o640) if err != nil { panic(err) } log.SetFormatter(&Formatter{}) log.SetOutput(io.MultiWriter(os.Stdout, f)) log.SetLevel(logrus.DebugLevel) } func main() { ctx := context.Background() ctx = CtxSetTraceId(ctx) log.WithContext(ctx).Info("main failed: %s", "detail") }

三月 13, 2024  |  326 字  |  总阅读

【Go】Gin 自定义参数校验和错误信息

Gin 使用 validator 库做数据校验,如下可以实现自定义校验、及自定义校验错误信息。 其中,自定义错误信息无法用于多层嵌套结构体,可能可以通过反射做到,但感觉在性能上很捉急。 代码 package main import ( "errors" "fmt" "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/binding" "github.com/go-playground/validator/v10" "net/http" "reflect" "strings" ) type Body struct { Name string `json:"name" binding:"oneof=vk vksir" err:"one of vk,vksir"` Age int `json:"age" binding:"BodyAgeValidate" err:"only 18"` } func BodyAgeValidate(f validator.FieldLevel) bool { value := f.Field().Int() if value != 18 { return false } return true } func GetValidateErr(obj any, rawErr error) error { validationErrs, ok := rawErr.(validator.ValidationErrors) if !ok { return rawErr } var errString []string for _, validationErr := range validationErrs { field, ok := reflect.TypeOf(obj).FieldByName(validationErr.Field()) if ok { if e := field.Tag.Get("err"); e != "" { errString = append(errString, fmt.Sprintf("%s: %s", validationErr.Namespace(), e)) continue } } errString = append(errString, validationErr.Error()) } return errors.New(strings.Join(errString, "\n")) } func ping(c *gin.Context) { b := Body{} if err := c.ShouldBindJSON(&b); err != nil { c.JSON(http.StatusBadRequest, gin.H{"detail": GetValidateErr(b, err).Error()}) return } c.JSON(http.StatusOK, gin.H{}) } func main() { e := gin.Default() if v, ok := binding.Validator.Engine().(*validator.Validate); ok { v.RegisterValidation("BodyAgeValidate", BodyAgeValidate) } e.GET("/ping", ping) e.Run("127.0.0.1:8080") } 运行 curl --location --request GET 'http://127.0.0.1:8080/ping' \ --header 'Content-Type: application/json' \ --data-raw '{ "name": "v", "age": 19 }' { "detail": "Body.Name: one of vk,vksir\nBody.Age: only 18" } 参考文档: ...

二月 8, 2023  |  203 字  |  总阅读

【Go】使用 zap 日志库

代码 loging\logging.go package logging import ( "go.uber.org/zap" "go.uber.org/zap/zapcore" "os" ) const logPath = "./log.txt" func SugaredLogger() *zap.SugaredLogger { return zap.S() } func init() { encoder := getEncoder() writeSyncer := getWriteSyncer() core := zapcore.NewCore(encoder, writeSyncer, zap.DebugLevel) logger := zap.New(core, zap.AddCaller(), zap.AddStacktrace(zap.ErrorLevel)) zap.ReplaceGlobals(logger) } func getEncoder() zapcore.Encoder { encoderConfig := zap.NewProductionEncoderConfig() encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder return zapcore.NewConsoleEncoder(encoderConfig) } func getWriteSyncer() zapcore.WriteSyncer { f, err := os.OpenFile(logPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) if err != nil { panic(err) } return zapcore.NewMultiWriteSyncer(zapcore.AddSync(f), zapcore.AddSync(os.Stdout)) } 使用 main.go package main import ( "GoCode/logging" ) var log = logging.SugaredLogger() func main() { log.Info("Hello zap") } 2023-02-06T01:26:29.514+0800 INFO GoCode/main.go:11 Hello zap

二月 8, 2023  |  102 字  |  总阅读

【Go】UDP 报文转发

【背景】 如 Wireguard 等工作在三层的 VPN,不会主动转发 UDP 广播报文,但能转发指定 IP 的 UDP 报文。 现有一程序,仅在 PC 主网卡(192.168.1.106)上发送从 14001 端口到 14001 端口的 UDP 广播报文。这种报文一是不会使用虚拟网卡(10.0.0.3)发送,二是就算使用虚拟网卡发送,该种虚拟网卡(Wireguard)也无法发送 UDP 广播报文。 想将报文捕获,使用虚拟网卡(10.0.0.3)指定目的 IP (10.0.0.2)进行转发。 【语言】Go 开发 主要用到 Google 的三个库: github.com/google/gopacket github.com/google/gopacket/layers github.com/google/gopacket/pcap 进行捕获和解析报文。其中,捕获报文在 Windows 上使用 ncap(Wireshark 底层也使用这个),在 Linux 上使用 libpcap(tcpdump 底层使用这个)。 代码不多,如下: package main import ( "context" "errors" "flag" "fmt" "github.com/google/gopacket" "github.com/google/gopacket/layers" "github.com/google/gopacket/pcap" "log" "net" "os" "os/signal" "strings" ) var packetChan chan *Packet type UDP struct { SrcIP net.IP DstIP net.IP SrcPort int DstPort int Content []byte } func (u *UDP) Send() error { laddr := &net.UDPAddr{IP: u.SrcIP, Port: u.SrcPort} raddr := &net.UDPAddr{IP: u.DstIP, Port: u.DstPort} if conn, err := net.DialUDP("udp", laddr, raddr); err != nil { return err } else { defer conn.Close() if _, err := conn.Write(u.Content); err != nil { return err } else { return nil } } } type Packet struct { Ethernet layers.Ethernet IP4 layers.IPv4 TCP layers.TCP UDP layers.UDP Payload gopacket.Payload Decoded []gopacket.LayerType } // NewPacket 解析报文 func NewPacket(packetData []byte) (*Packet, error) { var p Packet parser := gopacket.NewDecodingLayerParser(layers.LayerTypeEthernet, &p.Ethernet, &p.IP4, &p.TCP, &p.UDP, &p.Payload) if err := parser.DecodeLayers(packetData, &p.Decoded); err != nil { return nil, err } else { return &p, nil } } // String 显示报文信息 // 用于打印。 func (p *Packet) String() string { var info []string for _, layerType := range p.Decoded { switch layerType { case layers.LayerTypeEthernet: info = append(info, p.Ethernet.SrcMAC.String()+" > "+p.Ethernet.DstMAC.String(), "Ethernet Type: "+p.Ethernet.EthernetType.String(), ) case layers.LayerTypeIPv4: info = append(info, p.IP4.SrcIP.String()+" > "+p.IP4.DstIP.String(), "Protocol: "+p.IP4.Protocol.String(), ) case layers.LayerTypeTCP: info = append(info, p.TCP.SrcPort.String()+" > "+p.TCP.DstPort.String(), ) case layers.LayerTypeUDP: info = append(info, p.UDP.SrcPort.String()+" > "+p.UDP.DstPort.String(), ) case gopacket.LayerTypePayload: info = append(info, "Content: "+string(p.Payload.LayerContents()), ) } } return strings.Join(info, " | ") } // findDevByIp 通过设备 IP 查找设备 // 在 Linux 中,设备名很容易获取,如 “eth0”; 但在 Windows 中则较难, 因而有此函数。 func findDevByIp(ip net.IP) (*pcap.Interface, error) { devices, err := pcap.FindAllDevs() if err != nil { return nil, err } for _, device := range devices { for _, address := range device.Addresses { if address.IP.Equal(ip) { return &device, nil } } } return nil, errors.New("find device failed by ip") } // capture 捕获报文,解析并放入 chan func capture(dev *pcap.Interface, filter string) { if h, err := pcap.OpenLive(dev.Name, 4096, true, -1); err != nil { log.Panicln(err) } else if err := h.SetBPFFilter(filter); err != nil { log.Panicln(err) } else { defer h.Close() for { if packetData, _, err := h.ReadPacketData(); err != nil { log.Panicln(err) } else if p, err := NewPacket(packetData); err != nil { log.Panicln(err) } else { fmt.Println(p.String()) packetChan <- p } } } } // redirect 转发报文 // 指定源 IP 和目的 IP,端口不变。 func redirect(ctx context.Context, srcIP, dstIP net.IP) { for { select { case <-ctx.Done(): return case p := <-packetChan: u := UDP{ SrcIP: srcIP, DstIP: dstIP, SrcPort: int(p.UDP.SrcPort), DstPort: int(p.UDP.DstPort), Content: p.Payload.LayerContents(), } if err := u.Send(); err != nil { log.Printf("send p failed: %+v", u) } } } } func Run(ctx context.Context, devIp, filter, srcIp, dstIp string) { dev, err := findDevByIp(net.ParseIP(devIp)) if err != nil { log.Panicf("find device by ip failed: ip=%s, err=%s", devIp, err) } packetChan = make(chan *Packet, 64) go capture(dev, filter) redirect(ctx, net.ParseIP(srcIp), net.ParseIP(dstIp)) } func main() { var ( devIP = flag.String("d", "192.168.1.106", "Device IP.") filter = flag.String("f", "port 14001", "BPF filter expression.") srcIP = flag.String("src-ip", "10.0.1.3", "Redirect src IP.") dstIP = flag.String("dst-ip", "10.0.1.2", "Redirect dst IP.") ) flag.Parse() ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) defer stop() Run(ctx, *devIP, *filter, *srcIP, *dstIP) } 编译后使用命令如下: ...

七月 10, 2022  |  649 字  |  总阅读

【Go】利用 signal + context 实现优雅退出

package main import ( "context" "fmt" "os" "os/signal" "sync" "time" ) var wg sync.WaitGroup func worker(name string, ctx context.Context, t time.Duration) { fmt.Println(name, ": enter worker") select { case <-ctx.Done(): fmt.Println(name, ": worker context cancel, exit") case <-time.After(t): fmt.Println(name, ": worker process complete, exit") } wg.Done() } func main() { ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) defer stop() wg.Add(2) go worker("worker1", ctx, time.Second*2) go worker("worker2", ctx, time.Second*4) wg.Wait() fmt.Println("exit main") }

七月 10, 2022  |  70 字  |  总阅读

【Go】利用 reflect 实现结构体设置默认值

【Go】利用 reflect 实现结构体设置默认值 写 API 时经常会需要结构体中某个参数拥有默认值。但如 Gin 只有 ShouldBindQuery 这种 form 类型支持设置默认值,常用的 ShouldBindJSON 这种 json 类型却不支持,很奇怪。 Gin 中 bind 结构体设置默认值 package main import ( "fmt" "github.com/gin-gonic/gin" ) type FormData struct { Name string `form:"name,default=vksir"` Age int `form:"age,default=18"` } type JsonData struct { Name string `json:"name,default=vksir"` Age int `json:"age,default=18"` } func main() { e := gin.Default() e.GET("/", func(c *gin.Context) { var fd FormData c.ShouldBindQuery(&fd) var jd JsonData c.ShouldBindJSON(&jd) fmt.Printf("FormData: %+v\n", fd) fmt.Printf("JsonData:%+v\n", jd) }) e.Run() } 请求会打印如下结果: FormData: {Name:vksir Age:18} JsonData:{Name: Age:0} 也就是说 JsonData 的默认值没有生效,如果看源码也可以发现 ShouldBindJSON 是没有设置默认值的动作的。 简单实现结构体设置默认值 编辑 structutil/struct.go: package structutil import ( "reflect" "strconv" ) func SetDefault(v any) error { typeOf := reflect.TypeOf(v).Elem() valueOf := reflect.ValueOf(v).Elem() return subSetDefault(typeOf, valueOf) } func subSetDefault(typeOf reflect.Type, valueOf reflect.Value) error { for i := 0; i < typeOf.NumField(); i++ { tField := typeOf.Field(i) vField := valueOf.Field(i) switch tField.Type.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: defaultVal, ok := tField.Tag.Lookup("default") if !ok { continue } defaultValInt, err := strconv.ParseInt(defaultVal, 10, 64) if err != nil { return err } vField.SetInt(defaultValInt) case reflect.String: defaultVal, ok := tField.Tag.Lookup("default") if !ok { continue } vField.SetString(defaultVal) case reflect.Struct: err := subSetDefault(tField.Type, vField) if err != nil { return err } } } return nil } 简单使用: ...

六月 4, 2022  |  263 字  |  总阅读

【Go】实现分级日志

话不多说直接上代码: package log import ( "errors" "fmt" "log" "os" ) const ( LevelDebug = (1 + iota) * 10 LevelInfo LevelWaring LevelError LevelCritical ) var debugLogger *log.Logger var debugFileLogger *log.Logger var infoLogger *log.Logger var infoFileLogger *log.Logger var warningLogger *log.Logger var warningFileLogger *log.Logger var errorLogger *log.Logger var errorFileLogger *log.Logger var criticalLogger *log.Logger var criticalFileLogger *log.Logger var flag = log.Ldate | log.Ltime | log.Lshortfile | log.Lmsgprefix var logLevel = LevelInfo func SetLevel(level int) error { if level != LevelDebug && level != LevelInfo && level != LevelWaring && level != LevelError && level != LevelCritical { return errors.New(fmt.Sprintf("invalid level: %d", level)) } else { logLevel = level return nil } } func AddFileOutput(filePath string) error { logWriter, err := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0655) if err != nil { return err } else { debugFileLogger = log.New(logWriter, "[DEBUG] ", flag) infoFileLogger = log.New(logWriter, "[INFO] ", flag) warningFileLogger = log.New(logWriter, "[WARNING] ", flag) errorFileLogger = log.New(logWriter, "[ERROR] ", flag) criticalFileLogger = log.New(logWriter, "[CRITICAL] ", flag) return nil } } func Debug(format string, v ...any) { logWithLevel(debugLogger, LevelDebug, format, v) logWithLevel(debugFileLogger, LevelDebug, format, v) } func Info(format string, v ...any) { logWithLevel(infoLogger, LevelDebug, format, v) logWithLevel(infoFileLogger, LevelDebug, format, v) } func Waring(format string, v ...any) { logWithLevel(warningLogger, LevelDebug, format, v) logWithLevel(warningFileLogger, LevelDebug, format, v) } func Error(format string, v ...any) { logWithLevel(errorLogger, LevelDebug, format, v) logWithLevel(errorFileLogger, LevelDebug, format, v) } func Critical(format string, v ...any) { logWithLevel(criticalLogger, LevelDebug, format, v) logWithLevel(criticalFileLogger, LevelDebug, format, v) } func init() { debugLogger = log.New(os.Stderr, "[DEBUG] ", flag) infoLogger = log.New(os.Stderr, "[INFO] ", flag) warningLogger = log.New(os.Stderr, "[WARNING] ", flag) errorLogger = log.New(os.Stderr, "[ERROR] ", flag) criticalLogger = log.New(os.Stderr, "[CRITICAL] ", flag) } func logWithLevel(logger *log.Logger, level int, format string, v []any) { if logger != nil && logLevel >= level { logger.Printf(format, v...) } } 简单使用如下: ...

五月 30, 2022  |  317 字  |  总阅读

【Go】实现 requests 库

粗略实现了一下 requests 库。但后来想想,也必要自己造轮子,还是 resty 香! package requests import ( "encoding/json" "errors" "fmt" "io" "log" "net/http" "net/url" "reflect" "strings" ) type Client struct { Url string Headers map[string][]string Params map[string][]string Content string Data interface{} Json interface{} } type Response struct { Status string StatusCode int Body string } func (c Client) newRequest(method string) (req *http.Request, err error) { // url if reflect.ValueOf(c.Url).IsZero() { err = errors.New(fmt.Sprintf("url is needed: client=%+v", c)) return } u, err := url.ParseRequestURI(c.Url) if err != nil { return } // params if !reflect.ValueOf(c.Params).IsNil() { var params url.Values = c.Params u.RawQuery = params.Encode() } // body, headers body := "" if reflect.ValueOf(c.Headers).IsNil() { c.Headers = make(map[string][]string) } if !reflect.ValueOf(c.Content).IsZero() { body = c.Content } else if !reflect.ValueOf(c.Data).IsValid() { c.Headers["Content-Type"] = []string{"application/x-www-form-urlencoded"} var bodyBytes []byte bodyBytes, err = json.Marshal(c.Json) if err != nil { return } body = string(bodyBytes) } else if !reflect.ValueOf(c.Json).IsValid() { c.Headers["Content-Type"] = []string{"application/json"} var bodyBytes []byte bodyBytes, err = json.Marshal(c.Json) if err != nil { return } body = string(bodyBytes) } req, err = http.NewRequest(method, u.String(), strings.NewReader(body)) if err != nil { return } req.Header = c.Headers return } func (c Client) parseResponse(resp *http.Response) (response *Response, err error) { defer func(Body io.ReadCloser) { err := Body.Close() if err != nil { log.Printf("close response body failed: err=%s", err) } }(resp.Body) body, err := io.ReadAll(resp.Body) if err != nil { return } return &Response{ resp.Status, resp.StatusCode, string(body), }, nil } func (c Client) Request(method string) (response *Response, err error) { req, err := c.newRequest(method) if err != nil { return } client := http.Client{} resp, err := client.Do(req) if err != nil { return } return c.parseResponse(resp) } func (c Client) Get() (response *Response, err error) { return c.Request("GET") } func (c Client) Post() (response *Response, err error) { return c.Request("POST") }

四月 23, 2022  |  295 字  |  总阅读