业务代码
创建User表
package model
import (
"time"
"gorm.io/gorm"
)
type BaseModel struct {
ID int32 `gorm:"primarykey"`
CreatedAt time.Time `gorm:"column:add_time"`
UpdatedAt time.Time `gorm:"column:update_time"`
DeletedAt gorm.DeletedAt
IsDelete bool
}
type User struct {
BaseModel
Mobile string `gorm:"index:idx_mobile;unique;type:varchar(11);not null;comment:'手机号'"`
Password string `gorm:"type:varchar(100);not null;comment:'密码'"`
NickName string `gorm:"type:varchar(20);comment:'昵称'"`
Birthday *time.Time `gorm:"type:datetime;comment:'生日'"`
Gender string `gorm:"column:gender;default:male;type:varchar(6);comment:'性别'"`
Role int `gorm:"default:1;comment:'用户角色 1 是普通用户,2是管理员'"`
}
迁移User表
package main
import (
"log"
"os"
"time"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"gorm.io/gorm/schema"
"mxshop_srv/user_srv/model"
)
func main() {
dsn := "root:1230123@tcp(192.168.0.101:3306)/mxshop_srv?charset=utf8mb4&parseTime=True&loc=Local"
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer(日志输出的目标,前缀和日志包含的内容——译者注)
logger.Config{
SlowThreshold: time.Second, // 慢 SQL 阈值
LogLevel: logger.Info, // 日志级别
IgnoreRecordNotFoundError: true, // 忽略ErrRecordNotFound(记录未找到)错误
Colorful: false, // 禁用彩色打印
},
)
// 全局模式
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
// 取消表名复数
NamingStrategy: schema.NamingStrategy{
SingularTable: true,
},
// 开启数据库迁移控制台日志
Logger: newLogger,
})
if err != nil {
panic(err)
}
_ = db.AutoMigrate(&model.User{})
}
proto文件
syntax = "proto3";
option go_package=".;proto";
service User {
rpc GetUserList(PageInfo)returns(UserListResponse);
rpc GetUserByMobile(MobileRequest)returns(UserInfoResponse);
rpc GetUserById(IdRequest)returns(UserInfoResponse);
rpc CreateUser(CreateUserInfo)returns(UserInfoResponse);
rpc UpdateUser(UpdateUserInfo)returns(UpdateUserInfoResponse);
rpc ConfirmPassword(PasswordInfo)returns(PasswordResponse);
}
message PageInfo {
int32 pn = 1;
int32 pSize = 2;
}
message PasswordInfo{
string password = 1;
string hashPassword = 2;
}
message PasswordResponse{
bool success = 1;
}
message IdRequest{
uint32 id = 1;
}
message UpdateUserInfo {
uint32 id = 1;
string nickName = 2;
string gender = 3;
string birthday = 4;
}
message UpdateUserInfoResponse{
uint32 id = 1;
}
message MobileRequest{
string mobile =1;
}
message CreateUserInfo{
string nickname = 1;
string password = 2;
string mobile = 3;
}
message UserInfoResponse{
uint32 id = 1;
string password = 2;
string mobile = 3;
string nickName = 4;
uint32 birthday = 5;
string gender = 6;
int32 role = 7;
}
message UserListResponse{
int32 total = 1;
repeated UserInfoResponse data = 2;
}
protoc -I . user.proto --go_out=plugins=grpc:.
global
package global
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"gorm.io/gorm/schema"
"log"
"os"
"time"
)
var (
DB *gorm.DB
)
func init() {
dsn := "root:1230123@tcp(192.168.0.101:3306)/mxshop_srv?charset=utf8mb4&parseTime=True&loc=Local"
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer(日志输出的目标,前缀和日志包含的内容——译者注)
logger.Config{
SlowThreshold: time.Second, // 慢 SQL 阈值
LogLevel: logger.Info, // 日志级别
IgnoreRecordNotFoundError: true, // 忽略ErrRecordNotFound(记录未找到)错误
Colorful: false, // 禁用彩色打印
},
)
// 全局模式
var err error
DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
// 取消表名复数
NamingStrategy: schema.NamingStrategy{
SingularTable: true,
},
// 开启数据库迁移控制台日志
Logger: newLogger,
})
if err != nil {
panic(err)
}
}
user业务代码
package handler
import (
"context"
"gorm.io/gorm"
"mxshop_srv/user_srv/global"
"mxshop_srv/user_srv/model"
"mxshop_srv/user_srv/proto"
)
type UserService struct {
}
func (u *UserService) GetUserList(ctx context.Context, req *proto.PageInfo) (*proto.UserListResponse, error) {
var users []model.User
data := global.DB.Find(&users)
if data.Error != nil {
return nil, data.Error
}
rsp := &proto.UserListResponse{}
rsp.Total = int32(data.RowsAffected)
global.DB.Scopes(Paginate(int(req.Pn), int(req.PSize))).Find(&users)
for _, user := range users {
userInfoRes := modelToResponse(user)
rsp.Data = append(rsp.Data, &userInfoRes)
}
return rsp, nil
}
// 分页
func Paginate(page, pageSize int) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
if page == 0 {
page = 1
}
switch {
case pageSize > 100:
pageSize = 100
case pageSize <= 0:
pageSize = 10
}
offset := (page - 1) * pageSize
return db.Offset(offset).Limit(pageSize)
}
}
// 将每个User信息转化成grpc的 user信息
func modelToResponse(user model.User) proto.UserInfoResponse {
userInfoRsp := proto.UserInfoResponse{
Id: uint32(user.ID),
Mobile: user.Mobile,
NickName: user.NickName,
Gender: user.Gender,
Role: int32(user.Role),
}
// birthday在grpc中没有设置默认值,数据库有可能取出的数据是空值,这样会将一个nil赋值给grpc的birthday
// 在grpc中将一个字段设置为nil可能会抛出异常,所以此处将没有默认值的birthday进行特殊处理
if user.Birthday != nil {
userInfoRsp.Birthday = uint32(user.Birthday.Unix())
}
return userInfoRsp
}
根据用户ID和或者手机号查找用户
// 根据手机号查找用户
func (u *UserService) GetUserByMobile(ctx context.Context, req *proto.MobileRequest) (*proto.UserInfoResponse, error) {
var user model.User
result := global.DB.Where(&model.User{Mobile: req.Mobile}).First(&user)
if result.RowsAffected == 0 {
return nil, status.Error(codes.NotFound, "用户不存在")
}
if result.Error != nil {
return nil, result.Error
}
rsp := modelToResponse(user)
return &rsp, nil
}
//根据ID查找用户
func (u *UserService) GetUserById(ctx context.Context, req *proto.IdRequest) (*proto.UserInfoResponse, error) {
var user model.User
result := global.DB.First(&user, req.Id)
if result.RowsAffected == 0 {
return nil, status.Error(codes.NotFound, "用户不存在")
}
if result.Error != nil {
return nil, result.Error
}
rsp := modelToResponse(user)
return &rsp, nil
}
创建用户
// 新建用户
func (u *UserService) CreateUser(ctx context.Context, req *proto.CreateUserInfo) (*proto.UserInfoResponse, error) {
var user model.User
data := global.DB.First(&user, req.Mobile)
if data.RowsAffected == 1 {
return nil, status.Error(codes.AlreadyExists, "用户已存在")
}
user.Mobile = req.Mobile
user.NickName = req.Nickname
passwordHash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
user.Password = string(passwordHash)
rsp := modelToResponse(user)
return &rsp, nil
}
启动User服务
package main
import (
"flag"
"fmt"
"google.golang.org/grpc"
"mxshop_srv/user_srv/handler"
"mxshop_srv/user_srv/proto"
"net"
)
func main() {
IP := flag.String("ip", "0.0.0.0", "ip地址")
Port := flag.Int("port", 8080, "端口号")
flag.Parse()
fmt.Println("ip:", *IP)
fmt.Println("Port", *Port)
server := grpc.NewServer()
proto.RegisterUserServer(server, &handler.UserService{})
listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", *IP, *Port))
if err != nil {
panic("User服务监听失败:" + err.Error())
}
err = server.Serve(listener)
if err != nil {
panic("User服务启动失败:" + err.Error())
}
}
启动User客户端
package api
import (
"context"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"mxshop_api/proto"
"mxshop_api/response"
"mxshop_api/tools"
"net/http"
"time"
)
func GetUserList(ctx *gin.Context) {
zap.L().Debug("用户列表")
clientConn, err := grpc.Dial("127.0.0.1:8080", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
zap.S().Error("连接User客户端失败", err.Error())
}
userClient := proto.NewUserClient(clientConn)
rsp, err := userClient.GetUserList(context.Background(), &proto.PageInfo{
Pn: 0,
PSize: 0,
})
if err != nil {
zap.S().Error("查询用户列表失败")
tools.GrpcErrorToHttp(err, ctx)
return
}
result := make([]interface{}, 0)
for _, value := range rsp.Data {
user := response.UserResponse{
Id: value.Id,
Mobile: value.Mobile,
NickName: value.NickName,
Birthday: tools.JsonTime(time.Unix(int64(value.Birthday), 0)),
Gender: value.Gender,
Role: value.Role,
}
result = append(result, user)
}
ctx.JSON(http.StatusOK, result)
}
模块
分页
func Paginate(page, pageSize int) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
if page == 0 {
page = 1
}
switch {
case pageSize > 100:
pageSize = 100
case pageSize <= 0:
pageSize = 10
}
offset := (page - 1) * pageSize
return db.Offset(offset).Limit(pageSize)
}
}
grpc状态码
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
if result.RowsAffected == 0 {
return nil, status.Error(codes.NotFound, "用户不存在")
}
获取用户输入参数启动服务
IP := flag.String("ip", "0.0.0.0", "ip地址")
Port := flag.Int("port", 8080, "端口号")
flag.Parse()
fmt.Println("ip:", *IP)
fmt.Println("Port", *Port)
zap日志库
go get -u go.uber.org/zap
-
Zap提供了两种类型的日志记录器—Sugared Logger 和 Logger 。
-
在性能很好但不是很关键的上下文中,使用 SugaredLogger 。它比其他结构化日志记录包快4-10倍,并且支持结构化和printf风格的日志记录。
-
在每一微秒和每一次内存分配都很重要的上下文中,使用 Logger 。它甚至比 SugaredLogger 更快,内存分配次数也更少,但它只支持强类型的结构化日志记录。
Logger
- 可以通过调用
zap.NewProduction()/zap.NewDevelopment()来创建又给Logger
- 两种方式都可以,区别就是打印出来的格式不一样
NewDevelopment 是以 空格分开 的形式展示
NewProduction 使用的是 json格式 ,键值对的形式 展示出来
- 注意:默认情况下日志都会打印到应用程序的console界面。
SugaredLogger
- 这个就直接使用logger.Sugar()即可,啥使用都管用
- 他们基本上相同,唯一的不同的就是SugaredLogger可以用printf格式记录语句
例如:
sugarLogger.Infof("Success! statusCode = %s for URL %s", resp.Status, url)
自定义Logger
那么不想打印在终端怎么办呢,那我们只有自定义配置了
- 那我们只能使用
zap.New(…)方法来手动传递所有配置
- 我们可以看到需要一个zapcore.Core的参数,所以我们再进去看看
- 我们可以看到这个是一个接口,里面有个newCore的方法可以创建一个core。
- 源码里面可以明显的看到哈,只要我们给三个参数,就可以得到一个logger了,那么这三个参数分别表示上面呢?
Encoder : 编码器(如何写入日志)。我们将使用开箱即用的NewJSONEncoder(),并使用预先设置的ProductionEncoderConfig()。
// core 三个参数之 编码 func getEncoder() zapcore.Encoder { return zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()) }
WriteSyncer : 指定日志将写到哪里去。但是打开的类型不一样,文件打开的是io.writer类型,而我们需要的是WriteSyncer,所以我们使用zapcore.AddSync()函数来进行一个转换。
// core 三个参数之 路径
func getLogWriter() zapcore.WriteSyncer {
file,_ := os.Create("E:/test.log")
return zapcore.AddSync(file)
}
WriteSyncer : 指定日志将写到哪里去。但是打开的类型不一样,文件打开的是io.writer类型,而我们需要的是WriteSyncer,所以我们使用zapcore.AddSync()函数来进行一个转换。
// core 三个参数之 路径
func getLogWriter() zapcore.WriteSyncer {
file,_ := os.Create("E:/test.log")
return zapcore.AddSync(file)
}
LevelEnabler: 这个就是我们所需要打印的日志等级设置了,通过它来动态的保存日志,比如上线后我们error以下的日志就不打印了!
- 我们通过 *zapcore.**Level 来设置,里面都是封装好的日志等级
- 可以看下zapcore的源码哦
- 非常的ok!然后我们就可以创建一个logger了
var logger *zap.Logger
var sugarLogger *zap.SugaredLogger
func InitLogger() {
encoder := getEncoder()
writerSyncer := getLogWriter()
core := zapcore.NewCore(encoder,writerSyncer,zapcore.DebugLevel)
logger = zap.New(core)
sugarLogger = logger.Sugar()
}
func getEncoder() zapcore.Encoder {
return zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig())
}
func getLogWriter() zapcore.WriteSyncer {
file,_ := os.Create("E:/test.log")
return zapcore.AddSync(file)
}
- 我们来跑一个例子:
- 好用!!!只有存在文件里面才方便我们往后的查看呢!!!
将JSON Encoder更改为普通的Log Encoder
- 我们采用编码格式的时候,采用的json格式满,可以有的人习惯看空格呀,怎么办,那就换一个呗,
- 人家zap也是提供了的
// core 三个参数之 编码
func getEncoder() zapcore.Encoder {
return zapcore.NewConsoleEncoder(zap.NewProductionEncoderConfig())
}
- 非常的ok哈!!!
- 但是这个时间,,,还是有点不敬人意哈,所以我们好需要调整以下
编码配置优化
- 那么我们就需要对
encoderConfig进行一个自定义配置了
func getEncoder() zapcore.Encoder {
encoderConfig := zap.NewProductionEncoderConfig()
encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
return zapcore.NewConsoleEncoder(encoderConfig)
}
- 修改时间编码器
- 在日志文件中使用大写字母记录日志级别
- 是不是感觉又好很多了!
- 那么我们怎么来获取 调用的文件,函数名称,行号呢?
- 也很简单哈,我们再new一个zap 的时候加个
zap.AddCaller()即可!
logger := zap.New(core, zap.AddCaller())
- 来看看效果吧!
- 样子,我们基本上就可以进行一个很好的使用体验了哈!!!
- 你以为没了? 不,还有最重要的一点,
文件的切割,但是很可惜,zap没有这玩意,所以我们只有采用第三方库来实现拉!
使用Lumberjack进行日志切割归档
注意:Zap本身不支持切割归档日志文件
- 为了实现切割功能呢,我们采用第三方库
Lumberjack
Lumberjack的安装
- 老规矩哈,要是有依赖漏了就 go mod tidy 一下哈!!!
go get -u github.com/natefinch/lumberjack
zap logger中加入Lumberjack
- 要在zap中加入Lumberjack支持,我们需要修改WriteSyncer代码。我们将按照下面的代码修改getLogWriter()函数:
func getLogWriter() zapcore.WriteSyncer {
lumberJackLogger := &lumberjack.Logger{
Filename: "./test.log",
MaxSize: 10,
MaxBackups: 5,
MaxAge: 30,
Compress: false,
}
return zapcore.AddSync(lumberJackLogger)
}
- 分别表示上面意思呢? Lumberjack Logger采用以下属性作为输入:
| 属性 | 含义 |
|---|---|
| Filename | 日志文件的位置,也就是路径 |
| MaxSize | 在进行切割之前,日志文件的最大大小(以MB为单位) |
| MaxBackups | 保留旧文件的最大个数 |
| MaxAges | 保留旧文件的最大天数 |
| Compress | 是否压缩/归档旧文件 |
到这里我们的代码就完成了!!!
来看看总代码:
package main
import (
"github.com/natefinch/lumberjack"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"net/http"
)
var logger *zap.Logger
var sugarLogger *zap.SugaredLogger
func InitLogger() {
encoder := getEncoder()
writerSyncer := getLogWriter()
core := zapcore.NewCore(encoder,writerSyncer,zapcore.DebugLevel)
logger = zap.New(core,zap.AddCaller())
sugarLogger = logger.Sugar()
}
// core 三个参数之 编码
func getEncoder() zapcore.Encoder {
encoderConfig := zap.NewProductionEncoderConfig()
encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
return zapcore.NewConsoleEncoder(encoderConfig)
}
// core 三大核心之 路径
func getLogWriter() zapcore.WriteSyncer {
lumberJackLogger := &lumberjack.Logger{
Filename: "E:/test.log",
MaxSize: 10,
MaxBackups: 5,
MaxAge: 30,
Compress: false,
}
return zapcore.AddSync(lumberJackLogger)
}
func main() {
InitLogger()
defer logger.Sync()
simpleHttpGet("www.baidu.com")
simpleHttpGet("http://www.baidu.com")
}
func simpleHttpGet(url string) {
resp, err := http.Get(url)
if err != nil {
logger.Error(
"Error fetching url..",
zap.String("url", url),
zap.Error(err))
} else {
logger.Info("Success..",
zap.String("statusCode", resp.Status),
zap.String("url", url))
resp.Body.Close()
}
}
这里我们设置的是 日志文件每 5MB 会切割并且在当前目录下最多保存 5 个备份,并且会将旧文档保存30天。 到这里,我们就完成了zap日志程序集成到项目中了,还是很方便简单的哈! 至于测试数据,大家可以跑几个 goroutine来试试,把MaxSize调小一点,即可看到切分的效果哦!!
grpc状态码转换成http状态码
func GrpcErrorToHttp(err error, ctx *gin.Context) {
if err != nil {
if e, ok := status.FromError(err); ok {
switch e.Code() {
case codes.NotFound:
ctx.JSON(http.StatusNotFound, gin.H{
"msg": e.Message(),
})
case codes.Internal:
ctx.JSON(http.StatusInternalServerError, gin.H{
"msg": "内部错误",
})
case codes.InvalidArgument:
ctx.JSON(http.StatusBadRequest, gin.H{
"msg": "参数错误",
})
default:
ctx.JSON(http.StatusInternalServerError, gin.H{
"msg": "未知错误",
})
}
}
return
}
}
if err != nil {
zap.S().Error("查询用户列表失败")
tools.GrpcErrorToHttp(err, ctx)
return
}
time.Time类型处理
type JsonTime time.Time
func (j JsonTime) MarshalJSON() ([]byte, error) {
var stmp = fmt.Sprintf(""%s"", time.Time(j).Format("2006-01-02"))
return []byte(stmp), nil
}
for _, value := range rsp.Data {
user := response.UserResponse{
Id: value.Id,
Mobile: value.Mobile,
NickName: value.NickName,
Birthday: tools.JsonTime(time.Unix(int64(value.Birthday), 0)), // 标注
Gender: value.Gender,
Role: value.Role,
}
result = append(result, user)
}
type UserResponse struct {
Id uint32 `json:"id"`
Mobile string `json:"mobile"`
NickName string `json:"nickname"`
Birthday tools.JsonTime `json:"birthday"` // 标注
Gender string `json:"gender"`
Role int32 `json:"role"`
}
viper配置管理
name:'user-web'
user_srv:
host:'127.0.0.1'
port: 8080
package initialize
import (
"fmt"
"github.com/fsnotify/fsnotify"
"github.com/spf13/viper"
"go.uber.org/zap"
"mxshop_api/global"
)
func GetEnvInfo(env string) bool {
viper.AutomaticEnv()
return viper.GetBool(env)
}
func InitConfig() {
v := viper.New()
debug := GetEnvInfo("TEST_CONFIG")
v.AddConfigPath("../")
configFilePrefix := "config"
configFileName := fmt.Sprintf("%s-pro.yaml", configFilePrefix)
if debug {
configFileName = fmt.Sprintf("mxshop_api/%s-dev.yaml", configFilePrefix)
}
v.SetConfigFile(configFileName)
if err := v.ReadInConfig(); err != nil {
zap.S().Panic(err.Error())
}
if err := v.Unmarshal(&global.ServerConfig); err != nil {
zap.S().Panic(err.Error())
}
fmt.Println(global.ServerConfig)
fmt.Println("%V", v.Get("name"))
v.WatchConfig()
v.OnConfigChange(func(e fsnotify.Event) {
fmt.Println("config file change: ", e.Name)
_ = v.ReadInConfig()
_ = v.Unmarshal(&global.ServerConfig)
fmt.Println(global.ServerConfig)
})
}
gin多语言验证错误提示
package initialize
import (
"fmt"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/locales/en"
"github.com/go-playground/locales/zh"
ut "github.com/go-playground/universal-translator"
"mxshop_api/global"
"github.com/go-playground/validator/v10"
en_translation "github.com/go-playground/validator/v10/translations/en"
zh_translation "github.com/go-playground/validator/v10/translations/zh"
"go.uber.org/zap"
"reflect"
"strings"
)
func InitTrans(locale string) (err error) {
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
//注册一个获取json的tag的方法
v.RegisterTagNameFunc(func(fld reflect.StructField) string {
name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
if name == "-" {
return ""
}
return name
})
zhT := zh.New() //中文翻译器
enT := en.New() //英文翻译器
//第一个参数为备用语言环境,剩下了两个为必须支持的语言环境
uni := ut.New(enT, zhT, enT)
global.Trans, ok = uni.GetTranslator(locale)
if !ok {
return fmt.Errorf("uni.GetTranslator(%s)", locale)
}
switch locale {
case "en":
err = en_translation.RegisterDefaultTranslations(v, global.Trans)
case "zh":
err = zh_translation.RegisterDefaultTranslations(v, global.Trans)
default:
err = en_translation.RegisterDefaultTranslations(v, global.Trans)
}
if err != nil {
zap.S().Panic(err.Error())
}
}
return
}
func removeTopStruct(fields map[string]string) map[string]string {
rsp := map[string]string{}
for field, err := range fields {
rsp[field[strings.Index(field, ".")+1:]] = err
}
return rsp
}
func ValidatorError(ctx *gin.Context, err error) {
errs, ok := err.(validator.ValidationErrors)
if !ok {
ctx.JSON(http.StatusOK, gin.H{
"message": err.Error(),
})
}
ctx.JSON(http.StatusBadRequest, gin.H{
"error": removeTopStruct(errs.Translate(global.Trans)),
})
return
}
// 加载翻译器
if err := initialize.InitTrans("zh"); err != nil {
zap.S().Panic(err.Error())
}
gin自定义验证器
package initialize
import (
"github.com/gin-gonic/gin/binding"
ut "github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
"go.uber.org/zap"
"mxshop_api/global"
"regexp"
)
func ValidateMobile(fl validator.FieldLevel) bool {
mobile := fl.Field().String()
//使用正则表达式判断是否合法
ok, _ := regexp.MatchString(`^1([38][0-9]|14[579]|5[^4]|16[6]|7[1-35-8]|9[189])\d{8}$`, mobile)
if !ok {
return false
}
return true
}
//注册验证器
func InitValidator() {
//注册验证器
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
err := v.RegisterValidation("mobile", ValidateMobile)
err = v.RegisterTranslation("mobile", global.Trans, func(ut ut.Translator) error {
return ut.Add("mobile", "{0} 非法的手机号码!", true) // see universal-translator for details
}, func(ut ut.Translator, fe validator.FieldError) string {
t, _ := ut.T("mobile", fe.Field())
return t
})
if err != nil {
zap.S().Panic(err.Error())
}
}
}
JWT
包:
github.com/dgrijalva/jwt-go
声明结构体
type MyStandardClaims struct {
Username string `json:"username"`
jwt.StandardClaims
}
设置密钥
myKey := []byte(“qwertyuiop”)
创建结构体
ms := MyStandardClaims{
Username: "hzyy",
StandardClaims: jwt.StandardClaims{
ExpiresAt: time.Now().Unix() + 10,
Issuer: "233",
},
}
StandardClaims: jwt.StandardClaims{} 可以添加其他属性
创建token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, &ms)
jwt.SigningMethodHS256 加密方式
加密token
signedString, err := token.SignedString(myKey)
signedString 就是需要的jwt字符串了
解析token
claims, err := jwt.ParseWithClaims(signedString, &MyStandardClaims{}, func(token *jwt.Token) (interface{}, error) {
return myKey, nil
})
if err != nil {
fmt.Println(err)
}
fmt.Println(claims.Claims.(*MyStandardClaims).Username)
singnedString 为jwt字符串
&MyStandardClaims{} 为解析后的结构体
func(token *jwt.Token) 该函数的返回值为密钥
claims.Claims.(*MyStandardClaims).Username 断言为对应结构体,并获取对应属性的值
实例
package tools
import (
"github.com/dgrijalva/jwt-go"
"time"
)
const SECRET = "taoshihan"
type UserClaims struct {
Id uint `json:"id"`
Pid uint `json:"pid"`
Username string `json:"username"`
RoleId uint `json:"role_id"`
CreateTime time.Time `json:"create_time"`
jwt.StandardClaims
}
func MakeCliamsToken(obj UserClaims) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, obj)
tokenString, err := token.SignedString([]byte(SECRET))
return tokenString, err
}
func ParseCliamsToken(token string) (*UserClaims, error) {
tokenClaims, err := jwt.ParseWithClaims(token, &UserClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(SECRET), nil
})
if tokenClaims != nil {
if claims, ok := tokenClaims.Claims.(*UserClaims); ok && tokenClaims.Valid {
return claims, nil
}
}
return nil, err
}
跨域
func Cors() gin.HandlerFunc {
return func(context *gin.Context) {
method := context.Request.Method
context.Header("Access-Control-Allow-Origin", "*")
context.Header("Access-Control-Allow-Headers", "Content-Type,AccessToken,X-CSRF-Token, Authorization, Token,x-token")
context.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS")
context.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type")
context.Header("Access-Control-Allow-Credentials", "true")
if method == "OPTIONS" {
context.AbortWithStatus(http.StatusNoContent)
}
context.Next()
}
}
验证码
go get -u github.com/mojocn/base64Captcha
创建图像验证码
import "github.com/mojocn/base64Captcha"
func demoCodeCaptchaCreate() {
//config struct for digits
//数字验证码配置
var configD = base64Captcha.ConfigDigit{
Height: 80,
Width: 240,
MaxSkew: 0.7,
DotCount: 80,
CaptchaLen: 5,
}
//config struct for audio
//声音验证码配置
var configA = base64Captcha.ConfigAudio{
CaptchaLen: 6,
Language: "zh",
}
//config struct for Character
//字符,公式,验证码配置
var configC = base64Captcha.ConfigCharacter{
Height: 60,
Width: 240,
//const CaptchaModeNumber:数字,CaptchaModeAlphabet:字母,CaptchaModeArithmetic:算术,CaptchaModeNumberAlphabet:数字字母混合.
Mode: base64Captcha.CaptchaModeNumber,
ComplexOfNoiseText: base64Captcha.CaptchaComplexLower,
ComplexOfNoiseDot: base64Captcha.CaptchaComplexLower,
IsShowHollowLine: false,
IsShowNoiseDot: false,
IsShowNoiseText: false,
IsShowSlimeLine: false,
IsShowSineLine: false,
CaptchaLen: 6,
}
//create a audio captcha.
idKeyA, capA := base64Captcha.GenerateCaptcha("", configA)
//以base64编码
base64stringA := base64Captcha.CaptchaWriteToBase64Encoding(capA)
//create a characters captcha.
idKeyC, capC := base64Captcha.GenerateCaptcha("", configC)
//以base64编码
base64stringC := base64Captcha.CaptchaWriteToBase64Encoding(capC)
//create a digits captcha.
idKeyD, capD := base64Captcha.GenerateCaptcha("", configD)
//以base64编码
base64stringD := base64Captcha.CaptchaWriteToBase64Encoding(capD)
fmt.Println(idKeyA, base64stringA, "\n")
fmt.Println(idKeyC, base64stringC, "\n")
fmt.Println(idKeyD, base64stringD, "\n")
}
校验验证码
import "github.com/mojocn/base64Captcha"
func verfiyCaptcha(idkey,verifyValue string){
verifyResult := base64Captcha.VerifyCaptcha(idkey, verifyValue)
if verifyResult {
//success
} else {
//fail
}
}
获取可用端口
// 获取可用端口
func GetAvailablePort() (int, error) {
address, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:0", "0.0.0.0"))
if err != nil {
return 0, err
}
listener, err := net.ListenTCP("tcp", address)
if err != nil {
return 0, err
}
defer listener.Close()
return listener.Addr().(*net.TCPAddr).Port, nil
}
// 判断端口是否可以(未被占用)
func IsPortAvailable(port int) bool {
address := fmt.Sprintf("%s:%d", "0.0.0.0", port)
listener, err := net.Listen("tcp", address)
if err != nil {
log.Infof("port %s is taken: %s", address, err)
return false
}
defer listener.Close()
return true
}