WESEN / zh-CN wesen,追求事务的本质,学习也必有其规律性。用心把分享网络推广技巧和职场技能这件事,做好。 Thu, 04 Feb 2021 18:52:00 +0800 Thu, 04 Feb 2021 18:52:00 +0800 [go]通过重写type的MarshalJSON()方法,实现json编码时修改字段值 /post/119.html /post/119.html Thu, 04 Feb 2021 18:52:00 +0800 wesen 需求场景

现在有如下user模型:

type SysUser struct {
    ID       uint      `json:"-" gorm:"primarykey"`
    UUID     uuid.UUID `gorm:"notNull;unique;comment:UUID"`
    Username string    `gorm:"unique;notNull;comment:用户名"`
    Password string    `json:"-" gorm:"notNull;comment:登录密码"`
    NickName string    `gorm:"default:系统用户;comment:昵称"`
    Mobile   string    `gorm:"unique;notNull;comment:手机号"`
    Avatar   string    `gorm:"comment:头像"`
    Wechat   string    `gorm:"comment:微信号"`
}

现在考虑到用户隐私安全问题,需要在返回数据时,隐藏掉手机号码的中间几位(不是不需要Mobile这个字段),如何实现?
之前的方案是查库后,使用 fro循环修改。有没有其他方案?比如,类似php大多数框架支持的模型修改器?

实现思路

1.我们先定义一个类型别名Contact用于标识该字段是需要处理的联系方式字段

type Contact string

2.重写Contact类型的MarshalJSON()方法:

func (t *Contact) MarshalJSON() ([]byte, error) {
    s := string(*t)
    r := utils.HideMidStr(s, 4, "***") //处理隐藏号码的方法请自行实现
    return json.Marshal(r)
}

这样,Contact类型的字段在json编码映射时,就会调用MarshalJSON()方法,实现自定义转换。

使用方法

看大佬们的文章,一般建议模型struct和具体的业务struct区分开,遵循单一功能原则。那我们首先来新增一个struct

type SysUserResponse struct {
    UUID      uuid.UUID `json:"uuid"`      //UUID
    Username  string    `json:"userName"`  //用户名
    NickName  string    `json:"nickName"`  //昵称
    Mobile    Contact   `json:"mobile"`    //手机号
    Wechat    Contact   `json:"wechat"`    //微信
    Avatar    string    `json:"avatar"`    //头像
    CreatedAt time.Time `json:"createdAt"` //创建时间
    XX        string    `gorm:"-"`         //添加不是从模型获取的字段
}

注意:我们这里需要隐私保护的MobileWechat字段,类型用刚才定义的别名Contact
我们查询数据进行赋值时,直接这样使用:

db := DB.Model(&SysUser{})
var userList []*SysUserResponse
err = db.Find(&userList).Error
]]>
0 /post/119.html#comments /feed/post/119.html
[go]使用go-playground/validator实现请求参数验证并输出中文错误信息 /post/118.html /post/118.html Wed, 03 Feb 2021 17:40:00 +0800 wesen 实现目标

go-playground/validator原始的参数验证错误信息为英文字符串,很不友好。
在网上找到了这篇文章,解决了大部分问题。但是返回的错误信息没有分字段,且没有真实字段名,对前端来说不友好。
最后自己动手稍作改造,先看看最后实现的结果:
请求数据类型:

//PageInfo 分页请求数据结构
type PageInfo struct {
    Page     int `json:"page"  validate:"required,min=1" label:"页码"`
    PageSize int `json:"pageSize" validate:"required,max=100" label:"每页大小"`
}

错误请求参数响应json如下:

{
    "code": -1,
    "msg": "请求参数验证失败",
    "errors": {
        "Page": "页码为必填字段",
        "PageSize": "每页大小为必填字段"
    }
}

安装扩展包

github.com/go-playground/locales
github.com/go-playground/universal-translator
github.com/go-playground/validator/v10

添加validator服务包

validator.go

package validator

import (
    "os"
    "reflect"
    "fmt"

    "github.com/go-playground/locales/zh"
    translator "github.com/go-playground/universal-translator"
    "github.com/go-playground/validator/v10"
    zh_translations "github.com/go-playground/validator/v10/translations/zh"
)

//Validate 验证器
var Validate *validator.Validate
var trans translator.Translator

func init() {
    uni := translator.New(zh.New())
    trans, _ = uni.GetTranslator("zh")
    Validate = validator.New()
    //注册一个函数,获取struct tag里自定义的label作为字段名--重点1
    Validate.RegisterTagNameFunc(func(fld reflect.StructField) string {
        label := fld.Tag.Get("label")
        if label == "" {
            return fld.Name
        }
        return label
    })
    //注册翻译器
    err := zh_translations.RegisterDefaultTranslations(Validate, trans)
    if err != nil {
        fmt.Println(err.Error())
        os.Exit(0) //无法初始化验证器,退出应用
    }
}

//Translate 翻译工具
func Translate(err error, s interface{}) map[string]string {
    r := make(map[string]string)
    t := reflect.TypeOf(s).Elem()
    for _, err := range err.(validator.ValidationErrors) {
                //使用反射方法获取struct种的json标签作为key --重点2
        var k string
        if field, ok := t.FieldByName(err.StructField()); ok {
            k = field.Tag.Get("json")
        }
        if k == "" {
            k = err.StructField()
        }
        r[k] = err.Translate(trans)
    }
    return r
}

使用方法

//PageInfo 分页请求数据结构
type PageInfo struct {
    Page     int `json:"page"  validate:"required,min=1" label:"页码"`
    PageSize int `json:"pageSize" validate:"required,max=100" label:"每页大小"`
}

//Errors 错误响应
type Errors struct {
    Code   int         `json:"code"`
    Msg    string      `json:"msg"`
    Errors interface{} `json:"errors"`
}


…………
func Hello(c *gin.Context) {
    var pageInfo PageInfo 
    _ = c.ShouldBind(&pageInfo) //这里是gin的绑定参数方法,请改成自己的
    err := validator.Validate.Struct(pageInfo) //注意导入validator包
    if err != nil {
        r := validator.Translate(err,pageInfo)
        c.JSON(http.StatusBadRequest, Errors{
              Code:   -1,
              Msg:    "请求参数验证失败",
              Errors: r,
            })
        return
    }
    …………
}
]]>
0 /post/118.html#comments /feed/post/118.html
[go]使用swagger维护go项目的api文档(以gin框架为例) /post/117.html /post/117.html Sat, 30 Jan 2021 22:42:00 +0800 wesen 安装swag

如果开启了go module,建议不要在项目目录运行安装命令

 go get -u github.com/swaggo/swag/cmd/swag

安装完成后运行swag -v,查看版本号输出:swag version v1.X.0则安装成功

安装框架扩展包(以gin为例)

 go get -u github.com/swaggo/gin-swagger
 go get -u github.com/swaggo/files

初始化文档

swag init

配置文档路由

package main

import (
    "github.com/gin-gonic/gin"
    swaggerFiles "github.com/swaggo/files"
    ginSwagger "github.com/swaggo/gin-swagger"

    _ "XXX/docs" // !!!这里导入swag init 生成的包!!!!
)

// @title Swagger Example API
// @version 1.0
// @description This is a sample server Petstore server.
// @termsOfService http://swagger.io/terms/

// @contact.name API Support
// @contact.url http://www.swagger.io/support
// @contact.email support@swagger.io

// @license.name Apache 2.0
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html

// @host petstore.swagger.io
// @BasePath /v2
func main() {
    r := gin.New()

    r..GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))

    r.Run()
}

更新文档

再次执行swag init更新文档,运行go run main.go,访问localhost:8080/swagger/index.html即可查看文档

swagger注释文档书写格式:

以下示例中的object均为对应api的请求或响应struct,生成文档时会自动加载。

不带参数和Header的请求

示例接口:http://localhost/api/get
Go-Router:v2.GET("/api/get", api.GetApi)

// @Summary 接口描述
// @Tags 分类名称
// @Accept application/json
// @Success 200 object result.Response 返回值
// @Router /api/get [get]
func GetApi(c *gin.Context) {
    res := service.GetApi()
    c.JSON(200, res)
}

带Url参数请求

示例接口:http://localhost/api/get?userId=123
Go-Router:v2.GET("/api/get", api.GetApi)

// @Summary 接口描述
// @Tags 分类名称
// @Accept application/json
// @Param userId query integer true "用户ID"
// @Success 200 object result.Response 返回值
// @Router /api/get [get]
func GetApi(c *gin.Context) {
    userId:=c.Query("userId") //查询请求URL后面的参数
    res := service.GetApi(userId)
    c.JSON(200, res)
}

带Path参数请求

示例接口:http://localhost/api/get/123
Go-Router:v2.GET("/api/get/:userId", api.GetApi)

// @Summary 接口描述
// @Tags 分类名称
// @Accept application/json
// @Param userId path integer true "用户ID"
// @Success 200 object result.Response 返回值
// @Router /api/get/{userId} [get]
func GetApi(c *gin.Context) {
    userId:=c.Param("userId") //查询路径Path参数
    res := service.GetApi(userId)
    c.JSON(200, res)
}

带Token请求

示例接口:http://localhost/api/get/123
Go-Router:v2.GET("/api/get/:userId", api.GetApi)

// @Summary 接口描述
// @Tags 分类名称
// @Accept application/json
// @Param token header string true "登录信息"
// @Success 200 object result.Response 返回值
// @Router /api/get [get]
func GetApi(c *gin.Context) {
    userId,_ :=c.Get("tokenUserId") //从解析token获取用户Id
    res := service.GetApi(userId)
    c.JSON(200, res)
}

带Body参数跟Token请求

示例接口:http://localhost/api/post
Go-Router:v2.POST("/api/post", api.PostApi)

// @Summary 接口描述
// @Tags 分类名称
// @Accept application/json
// @Param token header string true "登录信息"
// @Param data body service.ReqData true "请求参数结构体"
// @Success 200 object result.Response 返回值
// @Router /api/post [ post ]
func PostApi(c *gin.Context) {
    var req service.ReqData
    err := c.BindJSON(&req)
    if err!=nil{
        c.JSON(200, result.ReturnFailMsg("获取参数失败"))
    }else {
        userId,_ :=c.Get("tokenUserId") //从解析token获取用户Id
        req.UserId = userId
        res := req.PostApi()
        c.JSON(200,res)
    }
}

带文件和Token请求

示例接口:http://localhost/api/file
Go-Router:v2.POST("/api/file", api.PostFile)

// @Summary 接口描述
// @Tags 分类名称
// @Accept application/json
// @Param token header string true "登录信息"
// @Param data formData file true "文件"
// @Success 200 object result.Response 返回值
// @Router /api/file [ post ]
func PostFile(c *gin.Context) {
    file, _ := c.FormFile("file")
    res := req.PostFile(file)
    c.JSON(200,res)
}
参考文章:Golang – Gin & Swaggo 使用方法
]]>
0 /post/117.html#comments /feed/post/117.html
[go]使用uber-go/zap提供日志服务并结合natefinch/lumberjack做日志分割 /post/116.html /post/116.html Sat, 30 Jan 2021 22:02:00 +0800 wesen 安装扩展包
go get -u go.uber.org/zap
go get -u github.com/natefinch/lumberjack

申明变量或增加配置

这里方便演示,直接申明了变量

var (
    level       = "debug"                 //日志级别
    director    = "logs/"                 //日志目录
    format      = "json"                  //日志格式,json/console可选
    encodeLevel = "LowercaseLevelEncoder" //编码器选择
)

检查并创建日志存储目录

//Zap 启动日志服务
func Zap() (Log *zap.Logger) {
    //检查日志目录,若不存在则创建
    if ok, _ := PathExists(director); !ok {
        fmt.Printf("create %v directory\n", director)
        error := os.Mkdir(director, os.ModePerm)
        if error != nil {
            fmt.Println(error)
        }
    }
    …………

    return Log 
}

……
//PathExists 判断目录是否存在
func PathExists(path string) (bool, error) {
    _, err := os.Stat(path)
    if err == nil {
        return true, nil
    }
    if os.IsNotExist(err) {
        return false, nil
    }
    return false, err
}

增加日志级别转化方法

//setLogLevel 设置日志级别
func getLogLevel() (lv zapcore.Level) {
    switch level {
    case "debug":
        lv = zap.DebugLevel
    case "info":
        lv = zap.InfoLevel
    case "warn":
        lv = zap.WarnLevel
    case "error":
        lv = zap.ErrorLevel
    case "dpanic":
        lv = zap.DPanicLevel
    case "fatal":
        lv = zap.FatalLevel
    default:
        lv = zap.InfoLevel
    }
    return
}

根据输出日志格式配置并获取编码器

// getEncoder 根据输出日志格式,获取编码器
func getEncoder() zapcore.Encoder {
    if format == "json" {
        return zapcore.NewJSONEncoder(getEncoderConfig())
    }
    return zapcore.NewConsoleEncoder(getEncoderConfig())
}

//getEncoderConfig 获取日志编码器配置
func getEncoderConfig() (config zapcore.EncoderConfig) {
    config = zapcore.EncoderConfig{
        MessageKey:     "message",
        LevelKey:       "level",
        TimeKey:        "time",
        NameKey:        "logger",
        CallerKey:      "caller",
        StacktraceKey:  "stacktrace",
        LineEnding:     zapcore.DefaultLineEnding,
        EncodeLevel:    zapcore.LowercaseLevelEncoder, //小写编码器
        EncodeTime:     customTimeEncoder,             //自定义时间格式
        EncodeDuration: zapcore.SecondsDurationEncoder,
        EncodeCaller:   zapcore.FullCallerEncoder,
    }
    switch {
    case encodeLevel == "LowercaseLevelEncoder": // 小写编码器(默认)
        config.EncodeLevel = zapcore.LowercaseLevelEncoder
    case encodeLevel == "LowercaseColorLevelEncoder": // 小写编码器带颜色
        config.EncodeLevel = zapcore.LowercaseColorLevelEncoder
    case encodeLevel == "CapitalLevelEncoder": // 大写编码器
        config.EncodeLevel = zapcore.CapitalLevelEncoder
    case encodeLevel == "CapitalColorLevelEncoder": // 大写编码器带颜色
        config.EncodeLevel = zapcore.CapitalColorLevelEncoder
    default:
        config.EncodeLevel = zapcore.LowercaseLevelEncoder
    }
    return config
}

//customTimeEncoder 自定义日志输出时间格式
func customTimeEncoder(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
    enc.AppendString(t.Format("2006/01/02 - 15:04:05.000"))
}

获取带日志分割的日志写入者

//getWriteSyncer 获取日志写入者
func getWriteSyncer() zapcore.WriteSyncer {
    // 日志分割
    f := path.Join(director, time.Now().Format("2006-01-02"+".log"))
    hook := &lumberjack.Logger{
        Filename:   f,    // 日志文件路径,默认 os.TempDir()
        MaxSize:    10,   // 每个日志文件保存10M,默认 100M
        MaxBackups: 30,   // 保留30个备份,默认不限
        MaxAge:     7,    // 保留7天,默认不限
        Compress:   true, // 是否压缩,默认不压缩
        LocalTime:  true, //使用计算机时间作为备份时间
    }

    //多端输出
    return zapcore.NewMultiWriteSyncer(zapcore.AddSync(os.Stdout), zapcore.AddSync(hook))
    //仅输出到日志文件
    //return zapcore.AddSync(hook)
}

生成日志服务及配置

//Zap 启动日志服务
func Zap() (Log *zap.Logger) {
    //检查日志目录,若不存在则创建
    if ok, _ := PathExists(director); !ok {
        fmt.Printf("create %v directory\n", director)
        error := os.Mkdir(director, os.ModePerm)
        if error != nil {
            fmt.Println(error)
        }
    }
    //设置日志级别
    lv := getLogLevel()
    //创建日志服务核心
    core := zapcore.NewCore(getEncoder(), getWriteSyncer(), lv)
    //获取logger实例
    if lv == zap.DebugLevel || lv == zap.ErrorLevel {
        // 开启文件及行号
        development := zap.Development()
        // 设置初始化字段,如:添加一个服务器名称
        filed := zap.Fields(zap.String("serviceName", "serviceName"))
        Log = zap.New(core, zap.AddStacktrace(lv), development, filed)
    } else {
        Log = zap.New(core)
    }
    // 开启开发模式,堆栈跟踪
    Log = Log.WithOptions(zap.AddCaller())
    //打印测试信息
    Log.Info("DefaultLogger init success")

    return
}

完整代码

package main

import (
    "fmt"
    "os"
    "path"
    "time"

    "github.com/natefinch/lumberjack"
    "go.uber.org/zap"
    "go.uber.org/zap/zapcore"
)

var (
    level       = "debug"                 //日志级别
    director    = "logs/"                 //日志目录
    format      = "json"                  //日志格式,json/console可选
    encodeLevel = "LowercaseLevelEncoder" //编码器选择
)

func main() {
    Log = Zap()
    Log.Info("success")
}

//Zap 启动日志服务
func Zap() (Log *zap.Logger) {
    //检查日志目录,若不存在则创建
    if ok, _ := PathExists(director); !ok {
        fmt.Printf("create %v directory\n", director)
        error := os.Mkdir(director, os.ModePerm)
        if error != nil {
            fmt.Println(error)
        }
    }
    //设置日志级别
    lv := getLogLevel()
    //创建日志服务核心
    core := zapcore.NewCore(getEncoder(), getWriteSyncer(), lv)
    //获取logger实例
    if lv == zap.DebugLevel || lv == zap.ErrorLevel {
        // 开启文件及行号
        development := zap.Development()
        // 设置初始化字段,如:添加一个服务器名称
        filed := zap.Fields(zap.String("serviceName", "serviceName"))
        Log = zap.New(core, zap.AddStacktrace(lv), development, filed)
    } else {
        Log = zap.New(core)
    }
    // 开启开发模式,堆栈跟踪
    Log = Log.WithOptions(zap.AddCaller())
    //打印测试信息
    Log.Info("DefaultLogger init success")

    return
}

//getWriteSyncer 获取日志写入者
func getWriteSyncer() zapcore.WriteSyncer {
    // 日志分割
    f := path.Join(director, time.Now().Format("2006-01-02"+".log"))
    hook := &lumberjack.Logger{
        Filename:   f,    // 日志文件路径,默认 os.TempDir()
        MaxSize:    10,   // 每个日志文件保存10M,默认 100M
        MaxBackups: 30,   // 保留30个备份,默认不限
        MaxAge:     7,    // 保留7天,默认不限
        Compress:   true, // 是否压缩,默认不压缩
        LocalTime:  true, //使用计算机时间作为备份时间
    }

    //多端输出
    return zapcore.NewMultiWriteSyncer(zapcore.AddSync(os.Stdout), zapcore.AddSync(hook))
    //仅输出到日志文件
    //return zapcore.AddSync(hook)
}

// getEncoder 根据输出日志格式,获取编码器
func getEncoder() zapcore.Encoder {
    if format == "json" {
        return zapcore.NewJSONEncoder(getEncoderConfig())
    }
    return zapcore.NewConsoleEncoder(getEncoderConfig())
}

//getEncoderConfig 获取日志编码器配置
func getEncoderConfig() (config zapcore.EncoderConfig) {
    config = zapcore.EncoderConfig{
        MessageKey:     "message",
        LevelKey:       "level",
        TimeKey:        "time",
        NameKey:        "logger",
        CallerKey:      "caller",
        StacktraceKey:  "stacktrace",
        LineEnding:     zapcore.DefaultLineEnding,
        EncodeLevel:    zapcore.LowercaseLevelEncoder, //小写编码器
        EncodeTime:     customTimeEncoder,             //自定义时间格式
        EncodeDuration: zapcore.SecondsDurationEncoder,
        EncodeCaller:   zapcore.FullCallerEncoder,
    }
    switch {
    case encodeLevel == "LowercaseLevelEncoder": // 小写编码器(默认)
        config.EncodeLevel = zapcore.LowercaseLevelEncoder
    case encodeLevel == "LowercaseColorLevelEncoder": // 小写编码器带颜色
        config.EncodeLevel = zapcore.LowercaseColorLevelEncoder
    case encodeLevel == "CapitalLevelEncoder": // 大写编码器
        config.EncodeLevel = zapcore.CapitalLevelEncoder
    case encodeLevel == "CapitalColorLevelEncoder": // 大写编码器带颜色
        config.EncodeLevel = zapcore.CapitalColorLevelEncoder
    default:
        config.EncodeLevel = zapcore.LowercaseLevelEncoder
    }
    return config
}

//customTimeEncoder 自定义日志输出时间格式
func customTimeEncoder(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
    enc.AppendString(t.Format("2006/01/02 - 15:04:05.000"))
}

//setLogLevel 设置日志级别
func getLogLevel() (lv zapcore.Level) {
    switch level {
    case "debug":
        lv = zap.DebugLevel
    case "info":
        lv = zap.InfoLevel
    case "warn":
        lv = zap.WarnLevel
    case "error":
        lv = zap.ErrorLevel
    case "dpanic":
        lv = zap.DPanicLevel
    case "fatal":
        lv = zap.FatalLevel
    default:
        lv = zap.InfoLevel
    }
    return
}

//PathExists 判断目录是否存在
func PathExists(path string) (bool, error) {
    _, err := os.Stat(path)
    if err == nil {
        return true, nil
    }
    if os.IsNotExist(err) {
        return false, nil
    }
    return false, err
}
]]>
0 /post/116.html#comments /feed/post/116.html
[go]使用viper管理go应用的配置项 /post/115.html /post/115.html Sat, 30 Jan 2021 18:28:00 +0800 wesen 一、安装viper:
go get github.com/spf13/viper

go get github.com/fsnotify/fsnotify //用于监控配置文件改变

二、创建配置文件:

在项目根目录(子目录亦可)创建配置文件,这里使用config.yaml,内容如下:

# web 服务器
server:
  address: ":8080" #运行端口
  run_mode: "debug" #运行模式`release`,`debug`

#文件存储
storage:
  driver: "local" #存储驱动,须在下面配置好
  local:
    path: "storage/public" #本地文件存储相对位置
    asset_prefix: "statics" #资源请求前缀
  ali_oss:
    oss_access_key: ""
    oss_secret_key: ""
    oss_endpoint: ""
    oss_bucket: ""
    oss_is_cname: ""

三、创建config包,结构如下:

config
  - config.go
  - server.go
  - storage.go

config.go

package config

//Config 配置总结构
type Config struct {
    Server   Server   `mapstructure:"server" json:"server" yaml:"server"`
    Storage  Storage  `mapstructure:"storage" json:"storage" yaml:"storage"`
}

server.go:

package config

//Server go服务配置
type Server struct {
    RunMode string `mapstructure:"run_mode" json:"run_mode" yaml:"run_mode"`
    Address string `mapstructure:"address" json:"address" yaml:"address"`
}

storage.go:

package config

//Storage 文件存储配置
type Storage struct {
    Driver string `mapstructure:"driver" json:"driver" yaml:"driver"`
    Local  Local  `mapstructure:"local" json:"local" yaml:"local"`
    AliOSS AliOSS `mapstructure:"ali_oss" json:"ali_oss" yaml:"ali_oss"`
}

//Local 本地存储配置
type Local struct {
    Path        string `mapstructure:"path" json:"path" yaml:"path"`
    AssetPrefix string `mapstructure:"asset_prefix" json:"asset_prefix" yaml:"asset_prefix"`
}

//AliOSS 阿里云oss
type AliOSS struct {
    OssAccessKey string `mapstructure:"oss_access_key" json:"oss_access_key" yaml:"oss_access_key"`
    OssSecretKey string `mapstructure:"oss_secret_key" json:"oss_secret_key" yaml:"oss_secret_key"`
    OssEndpoint  string `mapstructure:"oss_endpoint" json:"oss_endpoint" yaml:"oss_endpoint"`
    OssBucket    string `mapstructure:"oss_bucket" json:"oss_bucket" yaml:"oss_bucket"`
    OssIsCname   string `mapstructure:"oss_is_cname" json:"oss_is_cname" yaml:"oss_is_cname"`
}

四、初始化viper并加载配置

main.go:

package main

import (
  "xxx/config"
  "github.com/spf13/viper"
  "go.uber.org/zap"
)

//Cfg 全局配置项
Cfg config.Config
//VP 配置管理viper实例
VP *viper.Viper

func main() {
 VP = Viper();
}

//Viper 初始化全局配置项管理 Viper
func Viper() (v *viper.Viper) {
    v = viper.New()
    v.SetConfigFile("config.yaml")
    err := v.ReadInConfig()
    if err != nil {
        panic(fmt.Errorf("Fatal error config file: %s", err))
    }
        //监控配置文件的改变,并更细配置
    v.WatchConfig()
    v.OnConfigChange(func(e fsnotify.Event) {
        fmt.Println("config file changed:", e.Name)
        if err := v.Unmarshal(&Cfg); err != nil {
            fmt.Println(err)
        }
    })
    if err := v.Unmarshal(&Cfg); err != nil {
        fmt.Println(err)
    }
    return
}
]]>
0 /post/115.html#comments /feed/post/115.html
[go]捕获迭代变量陷进 /post/114.html /post/114.html Thu, 21 Jan 2021 12:18:57 +0800 wesen 本文摘选自《Go语言圣经(中文版)》

示例需求

考虑这样一个问题:你被要求首先创建一些目录,再将目录删除。

正确实现

var rmdirs []func()
for _, d := range tempDirs() {
    dir := d // 注意这里!!!
    os.MkdirAll(dir, 0755) // 同时创建上级目录
    rmdirs = append(rmdirs, func() {
        os.RemoveAll(dir)
    })
}
// ...do some work…
for _, rmdir := range rmdirs {
    rmdir() // clean up
}

你可能会感到困惑,为什么要在循环体中用循环变量d赋值一个新的局部变量,而不是像下面的代码一样直接使用循环变量dir?

错误实现

var rmdirs []func()
for _, dir := range tempDirs() {
    os.MkdirAll(dir, 0755)
    rmdirs = append(rmdirs, func() {
        os.RemoveAll(dir) // NOTE: incorrect!
    })
}

陷进分析

问题的原因在于循环变量的作用域。在上面的程序中,for循环语句引入了新的词法块,循环变量dir在这个词法块中被声明。在该循环中生成的所有函数值都共享相同的循环变量。
需要注意,函数值中记录的是循环变量的内存地址,而不是循环变量某一时刻的值。以dir为例,后续的迭代会不断更新dir的值,当删除操作执行时,for循环已完成,dir中存储的值等于最后一次迭代的值。这意味着,每次对os.RemoveAll的调用删除的都是相同的目录。

通常,为了解决这个问题,我们会引入一个与循环变量同名的局部变量,作为循环变量的副本。比如上面正确实现中的的变量dir := d,虽然这看起来很奇怪,但却很有用。

这个问题不仅存在基于range的循环,在下面的例子中,对循环变量i的使用也存在同样的问题:

var rmdirs []func()
dirs := tempDirs()
for i := 0; i < len(dirs); i++ {
    os.MkdirAll(dirs[i], 0755) // OK
    rmdirs = append(rmdirs, func() {
        os.RemoveAll(dirs[i]) // NOTE: incorrect!
    })
}

如果你使用go语句或者defer语句会经常遇到此类问题。这不是godefer本身导致的,而是因为它们都会等待循环结束后,再执行函数值

]]>
0 /post/114.html#comments /feed/post/114.html
[mysql]mysql中变量需要为数组的替代方案 /post/113.html /post/113.html Tue, 19 Jan 2021 13:17:53 +0800 wesen 需求描述

mysql自定义变量类型只有整数实数字符串null,在一些场景中,需要查出一些ids数组,后续的多条sql语句都需要使用这个ids数组做where in 查询。

实现思路

  1. 我们可以将SELECT id FROM table查到的id列表转化为字符串以存入用户变量,这里使用group_concat进行转换:
SET @ids= (SELECT group_concat(id) FROM users);
  1. 使用FIND_IN_SET代替IN做条件查询:
SELECT * FROM orders WHERE FIND_IN_SET(user_id,@ids);
]]>
0 /post/113.html#comments /feed/post/113.html
[go]常用go包集锦 /post/112.html /post/112.html Sun, 10 Jan 2021 13:37:00 +0800 wesen go学习中用到的包整理

第三方工具包

1.自动重载air:

go get -u github.com/cosmtrek/air

2.路由包gorilla/mux:

go get -u github.com/gorilla/mux

3.测试包stretchr/testify:

go get github.com/stretchr/testify

4.mysql驱动go-sql-driver/mysql:

go get github.com/go-sql-driver/mysql

5.ORM包:gorm.io/gorm

go get -u gorm.io/gorm
go get -u gorm.io/driver/mysql

6.配置库spf13/viper:

go get github.com/spf13/viper

7.会话管理gorilla/sessions:

go get -u github.com/gorilla/sessions

8.加解密crypto/bcrypt:

go get golang.org/x/crypto/bcrypt

9.日志包zap:

go get -u go.uber.org/zap

10.日志分割包github.com/natefinch/lumberjack

go get -u github.com/natefinch/lumberjack

11.热重启fvbock/endless:

go get github.com/fvbock/endless

12.API扩展swaggo/gin-swagger

go get -u github.com/swaggo/swag/cmd/swag
go get github.com/swaggo/gin-swagger
go get github.com/swaggo/files

]]>
0 /post/112.html#comments /feed/post/112.html
[go]初始化go项目 /post/111.html /post/111.html Sun, 10 Jan 2021 13:33:00 +0800 wesen 从零开始学go,项目所需的初始化配置都记录在这里

一、安装

1.下载安装包

养成习惯,从官网下载最新的安装包安装

cd ~ 
wget -c https://golang.google.cn/dl/go1.16.linux-amd64.tar.gz
sudo tar -C /usr/local -xzf go1.16.linux-amd64.tar.gz
rm go1.16.linux-amd64.tar.gz

2.添加环境变量并检查安装结果

vim ~/.bashrc
#配置go环境变量
export GOROOT=/usr/local/go
export PATH=$PATH:$GOROOT/bin
export GOPATH=$HOME/Applications/Go

保存退出

#更新
source ~/.bashrc
go version

3.启用Go modules并修改模块代理

Go modules通过配置GO111MODULE开启或关闭,默认auto,启用Go modules时,模块源使用国内资源。建议弃用$GOPATH,以便能在任意地方创建的 Go 项目,并实现依赖版本控制。
direct是用来告诉go get在获取源码包时先尝试https://goproxy.cn,如果遇到 404 等错误时,再尝试从源地址抓取

go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.cn,direct

4.创建项目并初始化

mkdir godemo
cd godemo
go mode init godemo

5.自动重载

使用air实现自动重载
安装:

go get -u github.com/cosmtrek/air

添加别名(mod 模式下可能无法访问air命令)vim ~/.bashrc,在最后添加alias air="$GOPATH/bin/air",保存后source ~/.bashrc更新.air -v查看安装结果
使用:

air
]]>
0 /post/111.html#comments /feed/post/111.html
[nginx]反向代理后,laravel获取用户真实ip /post/110.html /post/110.html Fri, 08 Jan 2021 16:21:19 +0800 wesen 需求描述

laravel+react前后端分离不同源项目,api接口被throttle斌率限制中间件拦截:

<?php

namespace App\Http;

use Illuminate\Foundation\Http\Kernel as HttpKernel;

class Kernel extends HttpKernel
{
     ……
    /**
     * The application's route middleware groups.
     *
     * @var array
     */
    protected $middlewareGroups = [
        ……
        'api' => [
            'throttle:60,1',
            'bindings',
            \Barryvdh\Cors\HandleCors::class,
        ],
       ……
    ];
    ……
}

通过nginx反向代理解决跨域问题后,laravel获取的用户ip,永远是服务器ip,导致大量访问时,api被频控。

解决方法

1.设置nginx反向代理请求头:

location ^~ /api/ {
        rewrite ^/api/(.*)$ /api/$1 break;
        include            uwsgi_params;
        proxy_method       POST;
        proxy_pass         127.0.0.1:8081;
        proxy_set_header   Host $host;
        proxy_set_header   X-real-ip $remote_addr;
        proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
    }

2.配置laravelApp\Http\Middleware\TrustProxies.php中间件,设置$proxies:

 protected $proxies = ['1.1.1.1'];//这里填受信代理服务器的ip
]]>
0 /post/110.html#comments /feed/post/110.html