小伙伴们,你们好呀!我是老寇!跟我一起学习封装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的常用日志包有glog
、logrus
和zap
维度 | 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 &
我是老寇,我们下次再见啦!