Go之封装Http请求和日志

小伙伴们,你们好呀!我是老寇!跟我一起学习封装Http请求和日志

Http请求

封装Http请求,直接使用 net/http 就行,主要是有两点需要注意,Https如何关闭校验客户端上传文件

Https如何关闭校验

go 复制代码
// 跳过TLS证书校验
client := &http.Client{
    Transport: &http.Transport{
       TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
    },
}

客户端上传文件

上传文件需遵循 RFC 1867 标准,因此,请求头 Content-Type设为 multipart/form-data;boundary=xxx

http.go

go 复制代码
import (
    "bytes"
    "crypto/tls"
    "errors"
    "io"
    "mime/multipart"
    "net/http"
)

func SendRequest(method, url string, param io.Reader, header map[string]string) (*http.Response, error) {
    request, err := http.NewRequest(method, url, param)
    if err != nil {
       return nil, errors.New("创建 Request 失败,错误信息:" + err.Error())
    }
    if header != nil {
       for k, v := range header {
          request.Header.Set(k, v)
       }
    }
    return sendHttpRequest(request)
}

func GetFormFile(fileName string, buf []byte) (io.Reader, string, error) {
    body := &bytes.Buffer{}
    writer := multipart.NewWriter(body)
    part, err := writer.CreateFormFile("file", fileName)
    if err != nil {
       return nil, "", errors.New("创建 FormFile 失败,错误信息:" + err.Error())
    }
    _, err = io.Copy(part, bytes.NewReader(buf))
    if err != nil {
       return nil, "", errors.New("复制字节数组失败,错误信息:" + err.Error())
    }
    err = writer.Close()
    if err != nil {
       return nil, "", errors.New("关闭 Writer 失败,错误信息:" + err.Error())
    }
    return body, writer.FormDataContentType(), nil
}

func SendRequestAndGetBody(method, url string, param io.Reader, header map[string]string) ([]byte, error) {
    response, err := SendRequest(method, url, param, header)
    if err != nil {
       return nil, err
    }
    body, err := io.ReadAll(response.Body)
    if err != nil {
       return nil, errors.New("读取响应失败,错误信息:" + err.Error())
    }
    defer response.Body.Close()
    return body, nil
}

func sendHttpRequest(request *http.Request) (*http.Response, error) {
    client := &http.Client{
       Transport: &http.Transport{
          TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
       },
    }
    response, err := client.Do(request)
    if err != nil {
       return nil, errors.New("发送 HTTP 请求失败,错误信息:" + err.Error())
    }
    return response, nil
}

http_test.go

go 复制代码
func Test_Http(t *testing.T) {
   // Get请求
    header = map[string]string{
        "Cookie": 'token=123',
    }
    _, _ = SendRequest(http.MethodGet, "https://localhost/api/test", nil ,header)
   
   // Post请求
   _, _ = SendRequest(http.MethodPost, "https://localhost/api/test", strings.NewReader("username=a&password=123") ,header)
   
   // Post上传文件【表单】
   // 这里便于演示,随便定义一个字节数组
    buf := []byte('123')
    formFile, contentType, err := core.GetFormFile("image.jpg", buf)
    if err != nil {
       fmt.println(err.Error())
       return
    }
    header = map[string]string{
        "Content-Type": contentType,
    }
    // 上传文件
    _, _ = SendRequest(http.MethodPost, "https://localhost/api/upload", formFile, header)
}

日志

日志对于一个项目来说是非常重要的,因此,很有必要根据业务进行挑选,go的常用日志包有gloglogruszap

维度 glog (Google) logrus (社区维护) zap (Uber)
项目背景 Kubernetes 等基础设施项目常用,轻量级 早期 Go 社区主流,已停止更新(仅维护) 高性能工业级实践,活跃维护
性能 中等(基于标准库优化) 较低(反射序列化,内存分配多) 极高(预分配内存、零反射,每秒百万级日志)
日志级别 4 级(Info/Warning/Error/Fatal) 6 级(Trace/Debug/Info/Warn/Error/Fatal) 7 级(Debug/Info/Warn/Error/Dpanic/Panic/Fatal)
结构化日志 ❌ 不支持(仅文本格式) ✅ 支持(WithFields 动态类型) ✅ 强类型(zap.String()),SugaredLogger 兼容非结构化
API 风格 类似标准库(如 glog.Info("msg") 链式调用(logrus.WithField().Info() 显式类型(zap.Info("msg", zap.String("k","v"))
日志轮转 ❌ 需手动实现或第三方库 ❌ 依赖插件(如 file-rotatelogs)2 ✅ 原生支持(集成 lumberjack
内存分配 较少 较多(反射+动态字段) 极少(预分配+非反射)
动态调级 ❌ 不支持 ✅ 需手动实现2 ✅ 原生支持(AtomicLevel
学习成本 低(类似标准库) 低(类似 fmt 中(结构化 API 稍复杂,SugaredLogger 简化)
适用场景 轻量级工具、K8s 生态兼容 旧项目维护、插件依赖型应用 高并发服务、云原生、性能敏感场景

考虑到网关设备,为了节约内存空间和极致的性能,我们采用zap封装,zap不支持日志滚动,所以,我们使用第三方组件 timberjack增强,支持日志滚动,日志保留天数,日志级别

log.go

go 复制代码
import (
    "errors"
    "github.com/DeRuina/timberjack"
    "go.uber.org/zap"
    "go.uber.org/zap/zapcore"
    "log"
    "time"
)

const (
    PROD = "prod"
)

type LogConfig struct {
    // 日志级别
    Level string `yaml:"level"`
    // 环境
    Profile string `yaml:"profile"`
    // 日志格式
    Pattern string `yaml:"pattern"`
    // 日志文件路径
    FilePath string `yaml:"file-path"`
    // 日志最大容量【单位M】
    MaxSize int `yaml:"max-size"`
    // 日志最大数量
    MaxBackups int `yaml:"max-backups"`
    // 日志保留时间【单位天】
    MaxAge int `yaml:"max-age"`
    // 是否压缩
    Compress bool `yaml:"compress"`
    // 本地时间,默认值:false(使用UTC)
    LocalTime bool `yaml:"local-time"`
    // json格式
    JsonFormat bool `yaml:"json-format"`
    // 转换频率
    RotationInterval time.Duration `yaml:"rotation-interval"`
    // 转换时间【分钟】
    RotateAtMinutes []int `yaml:"rotate-at-minutes"`
    // 日志时间格式
    BackupTimeFormat string `yaml:"backup-time-format"`
}

func (c *LogConfig) InitLogger() (*zap.Logger, error) {
    timberjackLogger := &timberjack.Logger{
       Filename:         c.FilePath,
       MaxSize:          c.MaxSize,
       MaxBackups:       c.MaxBackups,
       MaxAge:           c.MaxAge,
       Compress:         c.Compress,
       LocalTime:        c.LocalTime,
       RotationInterval: c.RotationInterval,
       RotateAtMinutes:  c.RotateAtMinutes,
       BackupTimeFormat: c.BackupTimeFormat,
    }
    log.SetOutput(timberjackLogger)
    defer timberjackLogger.Close()
    writeSyncer := zapcore.AddSync(timberjackLogger)
    // 配置日志级别
    levelConfig := zap.NewAtomicLevel()
    level, err := zapcore.ParseLevel(c.Level)
    if err != nil {
       return nil, errors.New("日志级别不存在,请重新配置,错误信息:" + err.Error())
    }
    levelConfig.SetLevel(level)
    // 配置环境
    var encoderConfig zapcore.EncoderConfig
    switch c.Profile {
    case PROD:
       encoderConfig = zap.NewProductionEncoderConfig()
    default:
       encoderConfig = zap.NewDevelopmentEncoderConfig()
    }
    // 设置时间格式
    encoderConfig.EncodeTime = c.customTimeEncoder
    var encoder zapcore.Encoder
    // Json格式
    if c.JsonFormat {
       encoder = zapcore.NewJSONEncoder(encoderConfig)
    } else {
       encoder = zapcore.NewConsoleEncoder(encoderConfig)
    }
    core := zapcore.NewCore(encoder, writeSyncer, levelConfig)
    return zap.New(core, zap.AddCaller()), nil
}

func (c *LogConfig) customTimeEncoder(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
    enc.AppendString(t.Format(c.Pattern))
}

application.yaml

yaml 复制代码
log:
  # 级别
  level: error
  # 环境
  profile: prod
  # 日志格式
  pattern: 2006-01-02 15:04:05.000
  # 日志文件路径
  file-path: test.log
  # 日志最大容量【单位M】
  max-size: 500
  # 日志最大数量
  max-backups: 10
  # 日志保留时间【单位天】
  max-age: 30
  # 是否压缩
  compress: true
  # 本地时间,默认值:false(使用UTC)
  local-time: true
  # json格式,true为json格式日志
  json-format: false
  # 转换频率
  rotation-interval: 24h
  # 转换时间【分钟】
  rotate-at-minutes:
    - 0
    - 15
    - 30
    - 45
  # 日志时间格式
  backup-time-format: 2006-01-02-15-04-05

config.go

go 复制代码
import (
    "errors"
    "gopkg.in/yaml.v3"
    "os"
)

type SystemConfig struct {
    Log LogConfig `yaml:"log"`
}

func GetSystemConfig(path string) (*SystemConfig, error) {
    data, err := os.ReadFile(path)
    if err != nil {
       return nil, errors.New("读取配置文件失败,错误信息:" + err.Error())
    }
    sysConfig := &SystemConfig{}
    err = yaml.Unmarshal(data, sysConfig)
    if err != nil {
       return nil, errors.New("配置文件反序列化失败,错误信息:" + err.Error())
    }
    return sysConfig, nil
}

log_test.go

go 复制代码
import (
    "testing"
)

func Test_Log(t *testing.T) {
    config, err := GetSystemConfig("application.yaml")
    if err != nil {
       t.Error(err.Error())
       return
    }
    logger, err := config.Log.InitLogger()
    if err != nil {
       t.Error(err.Error())
       return
    }
    logger.Info("信息")
    logger.Warn("警告")
    logger.Error("错误")
    t.Log("日志测试通过")
}

注意:部署到网关上面,日志无法打印,需要对日志增加权限和不能以sudo运行

shell 复制代码
# 增加日志权限
sudo chmod -R 7777 /home/a/app/logs
# 运行项目,不能以sudo运行项目,否则日志无法打印,这是我遇到的坑
nohup /home/a/app/log > /dev/null 2>&1 &

我是老寇,我们下次再见啦!

相关推荐
gopher_looklook5 分钟前
Go并发实战:singleflight 源码解读与二次封装
数据结构·后端·go
用户8338102512213 分钟前
我为什么做PmMock:让接口设计不再头疼
前端·后端
二闹20 分钟前
IService 和 BaseMapper:CRUD 操作的选择指南
后端
dylan_QAQ21 分钟前
【附录】Spring AOP 基础知识及应用
后端·spring
Java中文社群33 分钟前
抱歉!Java面试标准答案最不重要
java·后端·面试
鸭鸭鸭进京赶烤36 分钟前
EI检索-学术会议 | 人工智能、虚拟现实、可视化
人工智能·物联网·5g·信息可视化·云计算·vr·信号处理
jiguanghover44 分钟前
n8n 创建多维表格犯的错误
前端·后端
dylan_QAQ1 小时前
【附录】Spring 配置属性绑定 基础及应用
后端·spring
泡海椒1 小时前
jquick Path:让JSON数据处理像呼吸一样简单
后端
鹿鹿的布丁1 小时前
freeswitch+freeswitch+语音网关拨打电话
后端