功能
- 多方法:get,post
- 并发安全:通过ctx上下文控制全局超时时间
- 调试日志:第三方zerolog包,实现日志双端输出,日志级别输出
- 自定义重试及超时
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
)
type APIClient struct {
Url string
Timeout int
Method string
Headers map[string]string
Retry int
Body string
}
func (c *APIClient) DoRequest(ctx context.Context) (*APIResponse, error) {
var lastError error
for i := 0; i < c.Retry; i++ {
if i > 0 {
time.Sleep(time.Duration(c.Retry) * time.Millisecond)
log.Info().Msgf("重试第 %d 次...", i)
}
if c.Method == "POST" {
if c.Body == "" {
log.Error().Msg("body不能为空")
return nil, fmt.Errorf("body不能为空")
}
payload := bytes.NewBuffer([]byte(c.Body))
req, err := http.NewRequestWithContext(ctx, c.Method, c.Url, payload)
if err != nil {
lastError = err
continue
}
if c.Headers != nil {
for k, v := range c.Headers {
req.Header.Set(k, v)
}
}
client := http.DefaultClient
resp, err := client.Do(req)
if err != nil {
lastError = err
continue
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
lastError = err
continue
}
return &APIResponse{
Status: resp.StatusCode,
Body: body,
Error: nil,
}, nil
}
if c.Method == "GET" {
if c.Body != "" {
c.Url = c.Url + "?" + c.Body
}
req, err := http.NewRequestWithContext(ctx, c.Method, c.Url, nil)
if err != nil {
lastError = err
continue
}
if c.Headers != nil {
for k, v := range c.Headers {
req.Header.Set(k, v)
}
}
client := http.DefaultClient
resp, err := client.Do(req)
if err != nil {
lastError = err
continue
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
lastError = err
continue
}
return &APIResponse{
Status: resp.StatusCode,
Body: body,
Error: nil,
}, nil
}
}
log.Error().Msgf("达到最大重试次数 %d,最后一次错误:%v", c.Retry, lastError)
return nil, fmt.Errorf("达到最大重试次数 %d,最后一次错误:%v", c.Retry, lastError)
}
type APIResponse struct {
Status int
Body []byte
Error error
}
func (c *APIResponse) ParseBody() (map[string]interface{}, error) {
var result map[string]interface{}
err := json.Unmarshal(c.Body, &result)
if err != nil {
return nil, err
}
return result, nil
}
func main() {
var inputurl, method, inputheaders, inputbodys, level string
var timeout, retry int
var outputConsole bool
rootCmd := &cobra.Command{
Use: "apiclient",
PreRun: func(cmd *cobra.Command, args []string) {
if inputurl == "" {
cmd.Help()
log.Error().Msg("url不能为空,请重新输入")
os.Exit(0)
}
},
Run: func(cmd *cobra.Command, args []string) {
logFile, err := os.OpenFile("apiclient.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
log.Error().Msgf("打开日志文件失败: %v", err)
}
defer logFile.Close()
initLogger(level, outputConsole, logFile)
client := APIClient{
Url: inputurl,
Timeout: timeout,
Method: method,
Retry: retry,
Body: inputbodys,
}
if inputheaders != "" {
client.Headers = parseHeaders(inputheaders)
}
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
defer cancel()
response, err := client.DoRequest(ctx)
if err != nil {
log.Error().Msgf("请求失败: %v", err)
os.Exit(1)
}
result, err := response.ParseBody()
if err != nil {
log.Error().Msgf("json格式化body失败: %v", err)
} else {
log.Info().Msgf("json格式化body成功: %v", result)
}
},
}
rootCmd.Flags().StringVarP(&inputurl, "url", "u", "", "api地址")
rootCmd.Flags().IntVarP(&timeout, "timeout", "t", 10, "超时时间,单位秒")
rootCmd.Flags().StringVarP(&method, "method", "m", "GET", "请求方法")
rootCmd.Flags().StringVarP(&inputheaders, "headers", "H", "", "请输入headers,格式为key:value,key:value")
rootCmd.Flags().IntVarP(&retry, "retry", "r", 3, "重试次数")
rootCmd.Flags().StringVarP(&inputbodys, "body", "b", "", "请输入body,格式为key=value&key=value")
rootCmd.Flags().StringVarP(&level, "level", "l", "info", "日志级别")
rootCmd.Flags().BoolVarP(&outputConsole, "console", "c", true, "是否输出到控制台")
if err := rootCmd.Execute(); err != nil {
log.Error().Msgf("执行命令失败: %v", err)
}
}
func parseHeaders(inputheaders string) map[string]string {
headers := make(map[string]string)
headerPairs := strings.Split(inputheaders, ",")
for _, header := range headerPairs {
parts := strings.Split(header, ":")
if len(parts) == 2 {
headers[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
}
}
return headers
}
func initLogger(level string, outputConsole bool, logFile *os.File) {
switch level {
case "info":
zerolog.SetGlobalLevel(zerolog.InfoLevel)
case "warn":
zerolog.SetGlobalLevel(zerolog.WarnLevel)
case "error":
zerolog.SetGlobalLevel(zerolog.ErrorLevel)
default:
zerolog.SetGlobalLevel(zerolog.InfoLevel)
}
zerolog.TimeFieldFormat = "2006-01-02 15:04:05"
const (
ColorReset = "\033[0m"
ColorRed = "\033[31m"
ColorYellow = "\033[33m"
ColorGreen = "\033[32m"
ColorBlue = "\033[34m"
)
writer := zerolog.ConsoleWriter{
Out: os.Stdout,
TimeFormat: "2006-01-02 15:04:05",
FormatLevel: func(i interface{}) string {
level := i.(string)
switch level {
case "info":
return ColorGreen + "[INFO]" + ColorReset
case "warn":
return ColorYellow + "[WARN]" + ColorReset
case "error":
return ColorRed + "[ERROR]" + ColorReset
case "debug":
return ColorBlue + "[DEBUG]" + ColorReset
default:
return "[" + level + "]"
}
},
FormatMessage: func(i interface{}) string {
return "- " + i.(string)
},
FormatTimestamp: func(i interface{}) string {
return "[" + i.(string) + "]"
},
}
multi := zerolog.MultiLevelWriter(writer, logFile)
if outputConsole {
log.Logger = zerolog.New(multi).With().Timestamp().Logger()
} else {
log.Logger = zerolog.New(logFile).With().Timestamp().Logger()
}
}
收获
- 第三方日志包zerolog的使用
- context.WithTimeout配合http.NewRequestWithContext精确控制超时