数据库。。。。。。mysql
缓存。。。。。。。redis
MQ。。。。。。。 rabbitmq
路由框架。。。。。gin
ORM框架。。 。 。gorm
日志框架。。。。。logrus
项目完整代码–github
由于本篇文章篇幅偏多,代码会直接贴上来,细节地方不会特别标出,
建议按照博客内容进行实操(不建议直接复制),便会发现实际开发中会遇到的一些细节问题是如何处理的
golang
//检查go安装完成
go version
//查看go配置
go env
//安装完后会自动添加环境变量 GOPATH 默认在c盘 一般需要更换目录
例:
> e:
> mkdir GoPath
修改环境变量 GOPATH -- E:\GoPath
// go的很多库都需要代理访问 配置代理
> go env -w GOPROXY=https://goproxy.cn,direct
> go env -w GO111MODULE=auto
>
开发工具_goland
//创建目录
mkdir pro
cd pro
//初始化项目
go mod init pro
mkdir common 存放数据库 redis等
mkdir config 存放配置文件 application.yml
mkdir controller controller
mkdir logs 日志文件
mkdir middleware 中间件
mkdir model 实体类
mkdir rabbitmq mq
mkdir response 响应
mkdir service service
mkdir task 定时任务
mkdir util 工具类
type nul>main.go 入门文件(必需)
type nul>router.go 路由
type nul>sql.sql 存放sql语句
完成后打开项目 目录结构如下
准备接口
/controller/UserController.go
package controller
import (
"github.com/gin-gonic/gin"
"net/http"
"strconv"
)
func Test(c *gin.Context) {
//获取参数 a b
a := c.Query("a")
println(a)
c.JSON(http.StatusOK, gin.H{
"code": 200,
"data": a,
})
}
导包
鼠标放到需要导包的地方 点击sync导包等待下载即可
路由配置
编辑router.go文件
package main
import (
"github.com/gin-gonic/gin"
"pro/controller"
)
func StartRouter(r *gin.Engine) *gin.Engine {
//用户
user := r.Group("/user")
{
user.GET("/tes", controller.Test)
}
return r
}
入口文件配置
编辑main.go
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
//路由配置
r := gin.Default()
StartRouter(r)
panic(r.Run())
}
启动测试
点击main函数前的箭头 run 启动项目
浏览器访问 http://localhost:8080/user/test?a=12
测试结果:{"code":200,"data":"12"}
服务默认启动使用的端口是8080,实际开发中可能需要自定义,或者配置在yml内
这里直接丢进yml
创建配置文件
config/application.yml
profiles:
mode: dev
# mode: tes
config/application-dev.yml
name: 开发环境
server:
port: 1016
读取配置文件
修改 main.go
import (
"github.com/gin-gonic/gin"
"github.com/spf13/viper"
)
func main() {
//初始化配置文件
InitConfig()
//路由配置
r := gin.Default()
StartRouter(r)
//端口配置
port := viper.GetString("server.port")
//项目启动
panic(r.Run(":" + port))
}
func InitConfig() {
//先读取 application.yml 文件内 判断要使用哪一个环境 再读取对应配置的内容
viper.SetConfigName("application")
viper.SetConfigType("yml")
//可以添加多个搜索路径 第一个找不到会找后面的
viper.AddConfigPath("./config")
//linux路径
viper.AddConfigPath("/opt/config")
//读取内容
err := viper.ReadInConfig()
if err != nil {
panic(err)
}
mode := viper.GetString("profiles.mode")
switch mode {
case "dev":
viper.SetConfigName("application-dev")
case "tes":
viper.SetConfigName("application-tes")
case "pro":
viper.SetConfigName("application-pro")
}
//读取内容
err = viper.ReadInConfig()
if err != nil {
panic(err)
}
println("***********************************************")
println("配置文件读取完成, 当前运行环境为: ", viper.GetString("name"))
println("***********************************************")
}
测试
重新启动项目可以看到输出
***********************************************
配置文件读取完成, 当前运行环境为: 开发环境
***********************************************
[GIN-debug] Listening and serving HTTP on :1016
CREATE TABLE t_user
(
id int NOT NULL AUTO_INCREMENT,
username varchar(255) DEFAULT NULL COMMENT '用户名',
password varchar(255) DEFAULT NULL COMMENT '密码',
tel varchar(20) DEFAULT NULL COMMENT '手机号',
created_at datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_at datetime DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
deleted_at datetime DEFAULT NULL COMMENT '删除时间',
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT '用户表';
配置
/common/Database.go
yml文件中自行添加对应内容 此处略
package common
import (
"fmt"
_ "github.com/go-sql-driver/mysql"
"github.com/jinzhu/gorm"
"github.com/spf13/viper"
"net/url"
)
var DB *gorm.DB
func InitDB() *gorm.DB {
driverName := viper.GetString("datasource.driverName")
host := viper.GetString("datasource.host")
port := viper.GetString("datasource.port")
database := viper.GetString("datasource.database")
username := viper.GetString("datasource.username")
password := viper.GetString("datasource.password")
charset := viper.GetString("datasource.charset")
loc := viper.GetString("datasource.loc")
args := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=%s&parseTime=true&loc=%s",
username,
password,
host,
port,
database,
charset,
url.QueryEscape(loc))
db, err := gorm.Open(driverName, args)
if err != nil {
panic("failed to connect database, err: " + err.Error())
}
//禁用复数表
db.SingularTable(true)
//打印sql
db.LogMode(true)
// gorm - v1.x
// 指定表前缀,修改默认表名
gorm.DefaultTableNameHandler = func(db *gorm.DB, defaultTableName string) string {
return "t_" + defaultTableName
}
DB = db
return db
}
func GetDB() *gorm.DB {
return DB
}
func main() {
//初始化配置文件
InitConfig()
//初始化数据库
db := common.InitDB()
defer db.Close()
//路由配置
r := gin.Default()
StartRouter(r)
//端口配置
port := viper.GetString("server.port")
//项目启动
panic(r.Run(":" + port))
}
model
model/User.go
package model
import "github.com/jinzhu/gorm"
type User struct {
//model内已经包含id,创建、修改、删除时间, 并且会在对应的操作时自动插入/更新时间
gorm.Model
Username string
Password string
Tel string
}
CRUD
controller/UserController.go
package controller
import (
"github.com/gin-gonic/gin"
"net/http"
"pro/model"
"pro/service"
"strconv"
)
func SaveUser(c *gin.Context) {
//获取参数 a b
username := c.Query("username")
id, _ := strconv.Atoi(c.Query("id"))
user := model.User{
Username: username,
}
service.Saveuser(uint(id), &user)
c.JSON(http.StatusOK, gin.H{
"code": 200,
"data": user,
})
}
func DelUser(c *gin.Context) {
//获取参数 a b
id, _ := strconv.Atoi(c.Query("id"))
println(id)
service.DelUser(uint(id))
c.JSON(http.StatusOK, gin.H{
"code": 200,
"data": nil,
})
}
func GetUser(c *gin.Context) {
//获取参数 a b
id, _ := strconv.Atoi(c.Query("id"))
println(id)
var user model.User
service.GetUser(uint(id), &user)
c.JSON(http.StatusOK, gin.H{
"code": 200,
"data": user,
})
}
service/UserService.go
package service
import (
"pro/common"
"pro/model"
)
func Saveuser(id uint, user *model.User) {
db := common.GetDB()
if id > 0 {
db.Model(&model.User{}).Where("id = ?", id).Update(user)
} else {
db.Create(user)
}
}
func DelUser(id uint) {
db := common.GetDB()
db.Delete(&model.User{}, id)
}
func GetUser(id uint, user *model.User) {
db := common.GetDB()
db.Where("id = ?", id).Find(user)
}
路由
router.go
package main
import (
"github.com/gin-gonic/gin"
"pro/controller"
)
func StartRouter(r *gin.Engine) *gin.Engine {
//用户
user := r.Group("/user")
{
user.GET("/save", controller.SaveUser)
user.GET("/del", controller.DelUser)
user.GET("/get", controller.GetUser)
}
return r
}
测试
新增
http://localhost:1016/user/save?username=aaa
{“code”:200,“data”:{“ID”:3,“CreatedAt”:“2023-02-13T10:40:26.2750692+08:00”,“UpdatedAt”:“2023-02-13T10:40:26.2750692+08:00”,“DeletedAt”:null,“Username”:“aaa”,“Password”:“”,“Tel”:“”}}
编辑
http://localhost:1016/user/save?username=abc&id=3
{“code”:200,“data”:{“ID”:0,“CreatedAt”:“0001-01-01T00:00:00Z”,“UpdatedAt”:“0001-01-01T00:00:00Z”,“DeletedAt”:null,“Username”:“abc”,“Password”:“”,“Tel”:“”}}
查询
http://localhost:1016/user/get?id=3
{“code”:200,“data”:{“ID”:0,“CreatedAt”:“0001-01-01T00:00:00Z”,“UpdatedAt”:“0001-01-01T00:00:00Z”,“DeletedAt”:null,“Username”:“abc”,“Password”:“”,“Tel”:“”}}
删除
http://localhost:1016/user/del?id=3
{“code”:200,“data”:null}
删除后再次查询
{“code”:200,“data”:{“ID”:0,“CreatedAt”:“0001-01-01T00:00:00Z”,“UpdatedAt”:“0001-01-01T00:00:00Z”,“DeletedAt”:null,“Username”:“”,“Password”:“”,“Tel”:“”}}
建表
有了user表, 这里再建一个余额表 方便测试
CREATE TABLE t_balance
(
id int NOT NULL AUTO_INCREMENT,
user_id int DEFAULT 0 COMMENT 'uid',
balance int DEFAULT 0 COMMENT '余额',
created_at datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_at datetime DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
deleted_at datetime DEFAULT NULL COMMENT '删除时间',
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT '余额表';
/model/Balance.go
package model
import "github.com/jinzhu/gorm"
type Balance struct {
//model内已经包含id,创建、修改、删除时间, 并且会在对应的操作时自动插入/更新时间
gorm.Model
UserId uint
Balance uint
}
模拟场景
以 SaveUser 接口为基础,假设当接口入参没有传id时,需要创建用户,并给用户添加1000余额
逻辑层修改
/service/UserService.go
func Saveuser(id uint, user *model.User) {
db := common.GetDB()
if id > 0 {
db.Model(&model.User{}).Where("id = ?", id).Update(user)
} else {
//开启事务
tx := db.Begin()
defer func() {
//如果存在 数据库操作异常 回滚事务
if tx.Error != nil {
println("tx存在异常")
middleware.Err(tx.Error.Error())
tx.Rollback()
return
}
//如果出现运行时异常 回滚事务
//若无异常 提交事务
if r := recover(); r != nil {
println("recover存在异常")
tx.Rollback()
} else {
println("无异常")
tx.Commit()
}
}()
tx = tx.Create(user)
tx = tx.Create(&model.Balance{UserId: user.ID, Balance: 1000})
}
}
测试
postman测试
http://localhost:1016/user/save
{
"username":"asd"
}
运行成功,数据库添加了两条数据
t_balance
1 5 1000 2023-02-13 15:00:44 2023-02-13 15:00:44
t_user
5 asd 2023-02-13 15:00:44 2023-02-13 15:00:44
加点bug
修改逻辑层, 两次操作间加个bug
tx = tx.Create(user)
for i := 0; i < 1; i++ {
println(1 / i)
}
tx = tx.Create(&model.Balance{UserId: user.ID, Balance: 1000})
测试
我们的gorm开启了打印sql 可以看到控制台是执行了一次insert后输出
recover存在异常
数据库并没有新增数据
说明我们的事务再遇到异常时成功回滚了
换一种bug
删除刚才的for循环,修改 model/Balance.go
对应的修改一下Saveuser内的字段名
type Balance struct {
//model内已经包含id,创建、修改、删除时间, 并且会在对应的操作时自动插入/更新时间
gorm.Model
UserId uint
Balanc uint
}
tx = tx.Create(user)
tx = tx.Create(&model.Balance{UserId: user.ID, Balanc: 1000})
再次测试
同样做了一次insert 然后报错 字段balanc未定义
tx存在异常
[Error 1054: Unknown column 'balanc' in 'field list']
提取事务处理
很明显,这不够优雅
Saveuser内 defer 篇幅过长
项目中也不可能只有这一个地方需要事务,那肯定要提取出来
/service/BaseService.go
package service
import (
"github.com/jinzhu/gorm"
"golang/middleware"
)
package service
import (
"github.com/jinzhu/gorm"
"pro/middleware"
)
// AutoTransaction 自动处理事务
func AutoTransaction(tx *gorm.DB) {
//如果存在数据库操作异常 回滚事务
if tx.Error != nil {
middleware.Err(tx.Error.Error())
tx.Rollback()
return
}
//如果出现运行时异常 回滚事务
//若无异常 提交事务
if r := recover(); r != nil {
tx.Rollback()
} else {
tx.Commit()
}
}
Saveuser
func Saveuser(id uint, user *model.User) {
db := common.GetDB()
if id > 0 {
db.Model(&model.User{}).Where("id = ?", id).Update(user)
} else {
//开启事务
tx := db.Begin()
defer func() {
AutoTransaction(tx)
}()
tx = tx.Create(user)
tx = tx.Create(&model.Balance{UserId: user.ID, Balanc: 1000})
}
}
响应类
每个接口无论何种情况下都要做出对应的响应内容,对响应基本结构做一个统一规范,这样更加优雅
response/response.go
package response
import (
"github.com/gin-gonic/gin"
"net/http"
)
func Response(ctx *gin.Context, httpStatus int, code int, data any, msg string) {
ctx.JSON(httpStatus, gin.H{"code": code, "data": data, "msg": msg})
}
func Success(ctx *gin.Context, data any) {
Response(ctx, http.StatusOK, 200, data, "success")
}
func Fail(ctx *gin.Context, msg string) {
Response(ctx, http.StatusOK, 400, nil, msg)
}
使用
接口中的
c.JSON(http.StatusOK, gin.H{
"code": 200,
"data": user,
})
更新为
response.Success(c, user)
问题发现
通过上面的crud发现 ,接口内没有try catch ,如果代码执行出现问题,会出现500错误/不正常的返回,这似乎不对
做个实验,在GetUser 内加入以下代码
for i := 0; i < 1; i++ {
println(1 / i)
}
重启服务后调用接口,会发现控制台飘红,接口无法正常返回
大部分语言的异常处理方式是在每个接口内进行异常捕捉,
golang的 defer 函数和闭包机制 使得golang可以对异常进行统一处理
异常处理中间件
middleware/RecoveryMiddleware.go
package middleware
import (
"github.com/gin-gonic/gin"
"net/http"
)
func RecoveryMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
println(fmt.Sprintf("recovery异常:%s", err))
response.Fail(c, "服务异常")
//终止请求执行
c.Abort()
}
}()
}
}
启用中间件
main入口方法内启用
func main() {
//初始化配置文件
InitConfig()
//初始化数据库
db := common.InitDB()
defer db.Close()
//路由配置
r := gin.Default()
r.Use(middleware.RecoveryMiddleware())
StartRouter(r)
//端口配置
port := viper.GetString("server.port")
//项目启动
panic(r.Run(":" + port))
}
测试
http://localhost:1016/user/get?id=3
{"code":400,"data":null,"msg":"服务异常"}
控制台输出
recovery异常:runtime error: integer divide by zero
为了方便测试,上面代码使用get请求,而实际开发中post请求居多,观察控制台,gin框架帮我们打印了请求url、请求方式、响应状态、
但如果通过post请求是看不到入参的,这很不方便!!
开发中我习惯通过json入参,便以此为例
package middleware
import (
"bytes"
"encoding/json"
"fmt"
"github.com/gin-gonic/gin"
"io"
"strings"
)
//请求参数处理
func RequestParamsMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
if strings.ToLower(c.Request.Header.Get("Content-Type")) == "application/json" {
//读取全部入参
var params map[string]any
json.NewDecoder(c.Request.Body).Decode(¶ms)
//遍历入参 set存储 便于单一方式取参
for key := range params {
c.Set(key, params[key])
}
b, _ := json.Marshal(params)
//存取完整参数 便于打印请求参数
c.Set("reqParam", params)
println(fmt.Sprintf("reqParam: %s", string(b)))
//参数重新放回body 便于绑定取参
c.Request.Body = io.NopCloser(bytes.NewBuffer(b))
}
}
}
启用中间件
main方法内
r.Use(middleware.RequestParamsMiddleware())
接口接收参数调整
以 SaveUser 为例
注意:
1.postman入参类型和此处接收的类型要一直 如果传入字符串 这里用其他类型接收是拿不到的
2.数字类型接收时用float64**
func SaveUser(c *gin.Context) {
//获取参数 a b
username := c.GetString("username")
id := uint(c.GetFloat64("id"))
//验证此处有没有接收到参数
println("username", username)
println("id", id)
user := model.User{
Username: username,
}
service.Saveuser(id, &user)
response.Success(c, user)
}
func StartRouter(r *gin.Engine) *gin.Engine {
//用户
user := r.Group("/user")
{
user.POST("/save", controller.SaveUser)
user.POST("/del", controller.DelUser)
user.POST("/get", controller.GetUser)
}
return r
}
postman测试
post请求 http://localhost:1016/user/save
body → raw→ json
{
"username":"asd",
"id": 1
}
控制台输出:
reqParam: {"id":1,"username":"asd"}
username asd
id 1
gin帮我们打印了接口,我们自己打印了参数,但好像还差点意思,应该把接口和参数放在一起更方便查看,
另外项目少不了输出日志文件以便发现问题和排查问题,这里使用 logrus 框架
package middleware
import (
"bytes"
"encoding/json"
"fmt"
"github.com/gin-gonic/gin"
rotatelogs "github.com/lestrrat-go/file-rotatelogs"
"github.com/mattn/go-colorable"
"github.com/rifflock/lfshook"
"github.com/sirupsen/logrus"
"io"
"os"
"time"
)
// 实例化
var InfoLog *logrus.Logger
var ErrLog *logrus.Logger
func init() {
infoName := "info.log"
InfoLog = initLog(infoName)
errName := "err.log"
ErrLog = initLog(errName)
}
func initLog(fileName string) *logrus.Logger {
fileName = "./logs/" + fileName
// 写入文件
var f *os.File
var err error
//判断日志文件是否存在,不存在则创建,否则就直接打开
if _, err := os.Stat(fileName); os.IsNotExist(err) {
f, err = os.Create(fileName)
} else {
f, err = os.OpenFile(fileName, os.O_APPEND|os.O_WRONLY, os.ModeAppend)
}
if err != nil {
fmt.Println("open log file failed")
}
//初始化
log := logrus.New()
//设置日志级别
log.SetLevel(logrus.InfoLevel)
log.SetFormatter(&logrus.TextFormatter{
ForceColors: true, //色彩启用
FullTimestamp: true, //时间戳
//DisableLevelTruncation: true, //禁用截断
PadLevelText: true, //宽度相同
})
//同时输出到控制台和文件
writers := []io.Writer{
f,
colorable.NewColorableStdout(),
}
fileAndStdoutWriter := io.MultiWriter(writers...)
//设置输出
log.SetOutput(fileAndStdoutWriter)
//log.SetOutput(f)
// 设置 rotatelogs
logWriter, err := rotatelogs.New(
// 分割后的文件名称
fileName+".%Y%m%d.log",
// 生成软链,指向最新日志文件
rotatelogs.WithLinkName(fileName),
// 设置最大保存时间(7天)
rotatelogs.WithMaxAge(7*24*time.Hour),
// 设置日志切割时间间隔(1天)
rotatelogs.WithRotationTime(24*time.Hour),
)
writeMap := lfshook.WriterMap{
logrus.InfoLevel: logWriter,
logrus.FatalLevel: logWriter,
logrus.DebugLevel: logWriter,
logrus.WarnLevel: logWriter,
logrus.ErrorLevel: logWriter,
logrus.PanicLevel: logWriter,
}
log.AddHook(lfshook.NewHook(writeMap, &logrus.JSONFormatter{
TimestampFormat: "2006-01-02 15:04:05",
}))
//log.AddHook(lfshook.NewHook(writeMap, &logrus.TextFormatter{
// ForceColors: true,
// FullTimestamp: true,
// DisableLevelTruncation: true,
//}))
return log
}
// 日志
func LogMiddle() gin.HandlerFunc {
return func(c *gin.Context) {
bodyLogWriter := &bodyLogWriter{body: bytes.NewBufferString(""), ResponseWriter: c.Writer}
c.Writer = bodyLogWriter
//开始时间
startTime := time.Now()
//处理请求
c.Next()
//结束时间
endTime := time.Now()
// 执行时间
latencyTime := endTime.Sub(startTime)
//请求方式
reqMethod := c.Request.Method
//请求路由
reqUrl := c.Request.RequestURI
//状态码
//statusCode := c.Writer.Status()
//请求ip
//clientIP := c.ClientIP()
//请求参数
reqParam, _ := c.Get("reqParam")
//响应数据
responseBody := bodyLogWriter.body.String()
var responseData interface{}
if responseBody != "" {
res := Result{}
err := json.Unmarshal([]byte(responseBody), &res)
if err == nil {
responseData = res.Data
}
}
// 日志格式
InfoLog.WithFields(logrus.Fields{
//"status_code": statusCode,
//"client_ip": clientIP,
"req_uri": reqUrl,
"req_method": reqMethod,
"req_param": reqParam,
"resp": responseData,
"latency_time": latencyTime,
}).Info()
}
}
func Err(obj ...any) {
ErrLog.Error(obj)
}
func Info(obj ...any) {
InfoLog.Info(obj)
}
type bodyLogWriter struct {
gin.ResponseWriter
body *bytes.Buffer
}
type Result struct {
Code int `json:"code"`
Data any `json:"data"`
Error string `json:"error"`
}
func (w bodyLogWriter) Write(b []byte) (int, error) {
w.body.Write(b)
return w.ResponseWriter.Write(b)
}
func (w bodyLogWriter) WriteString(s string) (int, error) {
w.body.WriteString(s)
return w.ResponseWriter.WriteString(s)
}
r.Use(middleware.LogMiddle())
老规矩 试一下
logs目录生成了日志文件!
:什么? 文件有乱码???
:那你要不要颜色 中间件里颜色关了就好了!!
:什么? 日志好丑???
:那我也没办法,你找到好看的告诉我一下,我也换!
跨域中间件
/middleware/CORSMiddleware.go
package middleware
import (
"github.com/gin-gonic/gin"
"net/http"
)
// 跨域
func CORSMiddleware() gin.HandlerFunc {
return func(ctx *gin.Context) {
ctx.Writer.Header().Set("Access-Control-Allow-Origin", "*")
ctx.Writer.Header().Set("Access-Control-Allow-Methods", "*")
ctx.Writer.Header().Set("Access-Control-Allow-Headers", "*")
ctx.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
if ctx.Request.Method == http.MethodOptions {
ctx.AbortWithStatus(200)
} else {
ctx.Next()
}
}
}
启用中间件
main方法内的代码越来越多了, 这不够优雅!!
func main() {
//初始化配置文件
InitConfig()
//初始化数据库
db := common.InitDB()
defer db.Close()
//路由配置
r := Router()
//端口配置
port := viper.GetString("server.port")
//项目启动
panic(r.Run(":" + port))
}
func Router() *gin.Engine {
r := gin.Default()
r.Use(
//异常处理
middleware.RecoveryMiddleware(),
//跨域
middleware.CORSMiddleware(),
//请求参数处理
middleware.RequestParamsMiddleware(),
//日志
middleware.LogMiddle(),
)
return StartRouter(r)
}
配置
/common/Redis.go
package common
import (
"context"
"fmt"
"github.com/go-redis/redis/v8"
"github.com/spf13/viper"
"time"
)
var ctx = context.Background()
var client *redis.Client
func InitRedis() {
host := viper.GetString("redis.host")
port := viper.GetString("redis.port")
password := viper.GetString("redis.password")
conn := redis.NewClient(&redis.Options{
Addr: fmt.Sprintf("%s:%s", host, port),
Password: password, // no password set
DB: 0, // use default DB
})
client = conn
}
func Set(key string, value string) {
err := client.Set(ctx, key, value, 0).Err()
if err != nil {
panic(err)
}
}
// SetWithTime seconds 秒后过期
func SetWithTime(key string, value string, seconds uint64) {
err := client.SetEX(ctx, key, value, time.Duration(seconds)*time.Second).Err()
if err != nil {
panic(err)
}
}
// SetNx 保存成功返回true key已存在返回false
func SetNx(key, value string, seconds uint) bool {
return client.SetNX(ctx, key, value, time.Duration(seconds)*time.Second).Val()
}
func Get(key string) string {
value, _ := client.Get(ctx, key).Result()
return value
}
func Del(key string) {
client.Del(ctx, key)
}
func main() {
//初始化配置文件
InitConfig()
//初始化数据库
db := common.InitDB()
defer db.Close()
//初始化redis
common.InitRedis()
//路由配置
r := Router()
//端口配置
port := viper.GetString("server.port")
//项目启动
panic(r.Run(":" + port))
}
测试
接口是不可能写接口的,写了接口还要配置路由,直接改写好的
func GetUser(c *gin.Context) {
common.Set("test", "123")
println("test:::" + common.Get("test"))
common.SetWithTime("test", "456", 2)
println("test:::" + common.Get("test"))
time.Sleep(3 * time.Second)
println("test:::" + common.Get("test"))
//获取参数 a b
id, _ := strconv.Atoi(c.Query("id"))
var user model.User
service.GetUser(uint(id), &user)
response.Success(c, user)
}
控制台输出
test:::123
test:::456
test:::
这里直接用的延时mq,用习惯了。。需要即时消费,只需要把延时时间设为很小就好了
配置
/rabbitmq/MqConfig.go
package rabbitmq
import (
"fmt"
"github.com/spf13/viper"
"github.com/streadway/amqp"
"log"
"net/url"
"pro/middleware"
"time"
)
var conn *amqp.Connection
var ch *amqp.Channel
var err error
const (
DelayExchange = "delayExchange"
Queue1 = "queue1"
)
func InitMq() {
host := viper.GetString("rabbitmq.host")
port := viper.GetString("rabbitmq.port")
username := viper.GetString("rabbitmq.username")
password := viper.GetString("rabbitmq.password")
dsn := fmt.Sprintf("amqp://%s:%s@%s:%s/",
username,
url.QueryEscape(password),
host,
port)
conn, err = amqp.Dial(dsn)
//ch = conn
if err != nil {
panic(err)
}
ch, err = conn.Channel()
if err != nil {
panic(err)
}
//申明交换机
err := ch.ExchangeDeclare(DelayExchange, "x-delayed-message",
//交换机持久化
false,
false,
false,
false,
map[string]interface{}{"x-delayed-type": "direct"})
if err != nil {
log.Fatal(err)
}
//初始化队列
InitDelayMQ(Queue1)
// 限制未ack的最多有10个,必须设置为手动ack才有效
ch.Qos(10, 0, false)
//启动消费者
Queue1Consume()
}
func InitDelayMQ(queue string) {
// 声明 queue
_, err = ch.QueueDeclare(queue,
//队列持久化
true,
false,
false,
false,
nil)
if err != nil {
panic(err)
}
// 将 queue 与 exchange绑定
err = ch.QueueBind(queue, queue, DelayExchange, false, nil)
if err != nil {
panic(err)
}
}
// SendDelayMessage 发送延时消息
// delay: 延迟时间 单位秒
func SendDelayMessage(queue string, message string, delay int) {
middleware.Info("发送时间: ", time.Now().Unix())
err := ch.Publish(DelayExchange, queue, true, false, amqp.Publishing{
Headers: map[string]interface{}{"x-delay": delay * 1000},
Body: []byte(fmt.Sprintf("%v", message)),
})
if err != nil {
panic(err)
}
}
// SendPersistentDelayMessage 发送持久化延时消息
// delay: 延迟时间 单位秒
func SendPersistentDelayMessage(queue string, message string, delay int) {
middleware.Info("发送时间: ", time.Now().Unix())
err := ch.Publish(DelayExchange, queue, true, false, amqp.Publishing{
Headers: map[string]interface{}{"x-delay": delay * 1000},
Body: []byte(fmt.Sprintf("%v", message)),
DeliveryMode: 2,
})
if err != nil {
panic(err)
}
}
func Subscribe(queue string, callback func(<-chan amqp.Delivery)) {
msgs, err := ch.Consume(queue, queue, false, false, false, false, nil)
if err != nil {
panic(err)
}
callback(msgs)
}
消费者
/rabbitmq/MqConsume.go
package rabbitmq
import (
"fmt"
"github.com/streadway/amqp"
"golang/middleware"
"time"
)
func Queue1Consume() {
go Subscribe(Queue1, func(msgs <-chan amqp.Delivery) {
for msg := range msgs {
go func(msg amqp.Delivery) {
middleware.Info("收到消息时间: ", time.Now().Unix())
fmt.Printf("%s 收到消息:%v\n", Queue1, string(msg.Body))
msg.Ack(false)
//重新入列
//msg.Reject(true)
}(msg)
}
})
}
func main() {
//初始化配置文件
InitConfig()
//初始化数据库
db := common.InitDB()
defer db.Close()
//初始化redis
common.InitRedis()
//初始化mq
rabbitmq.InitMq()
//路由配置
r := Router()
//端口配置
port := viper.GetString("server.port")
//项目启动
panic(r.Run(":" + port))
}
测试
老规矩 用 GetUser
func GetUser(c *gin.Context) {
rabbitmq.SendDelayMessage(rabbitmq.Queue1, "我是mq消息", 5)
//获取参数 a b
id, _ := strconv.Atoi(c.Query("id"))
var user model.User
service.GetUser(uint(id), &user)
response.Success(c, user)
}
调用接口后,延时5s控制台输出
queue1 收到消息:我是mq消息
持久化消息
MqConfig.go 中的 SendPersistentDelayMessage
发送持久化的消息,根据场景使用,mq挂了消息不会丢失,但性能会差一些
添加任务
/task/TaskConfig.go
package task
import (
"github.com/robfig/cron/v3"
"pro/middleware"
)
var Crons = cron.New(cron.WithSeconds())
func Start() {
//开启定时任务
Crons.Start()
//添加工作任务
TesJob()
}
// cron格式 秒 分 小时 日 月 星期
// @hourly 每小时 -- 0 0 * * * *
// @daily 每天 -- 0 0 0 * * *
// @every 10s 每十秒钟
func TesJob() {
Crons.AddFunc("*/10 * * * * *", func() {
//Crons.AddFunc("@every 10s", func() {
middleware.Info("定时任务输出")
})
}
启动定时任务
main方法肯定是又不够优雅了
func main() {
//初始化配置文件
InitConfig()
//初始化数据库
db := common.InitDB()
defer db.Close()
//启动各模块
Start()
//路由配置
r := Router()
//端口配置
port := viper.GetString("server.port")
//项目启动
panic(r.Run(":" + port))
}
func Start() {
//初始化redis
common.InitRedis()
//开启定时任务
task.Start()
//初始化mq
rabbitmq.InitMq()
}
/util/UUIDUtil.go
package util
import (
"crypto/rand"
uuid "github.com/satori/go.uuid"
"math/big"
"strings"
)
func Get32() string {
return strings.ReplaceAll(uuid.NewV4().String(), "-", "")
}
func GetRandStr(n int) string {
var letters = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789")
result := make([]byte, n)
for i := range result {
n, _ := rand.Int(rand.Reader, big.NewInt(int64(len(letters))))
result[i] = letters[n.Int64()]
}
return string(result)
}
func GetFileName(n int) string {
var letters = []byte("abcdefghijklmnopqrstuvwxyz123456789")
result := make([]byte, n)
for i := range result {
n, _ := rand.Int(rand.Reader, big.NewInt(int64(len(letters))))
result[i] = letters[n.Int64()]
}
return string(result)
}
/util/KeyLock.go
package util
import (
"context"
"crypto/rand"
"math/big"
"pro/common"
"time"
)
const (
waitTimeOut = 30 * 1000 //最长等待时间 单位毫秒 等待30s
lockExpireTime = 10 //锁过期时间 单位秒 10s过期
)
var ctx = context.Background()
// keyLock
func Lock(key, val string) bool {
println(val + "获取锁")
var wait = waitTimeOut
for wait > 0 {
//获取锁
success := common.SetNx(key, val, lockExpireTime)
if success {
println(val + "获取锁成功")
return true
}
println(val + "休眠等待锁")
//随机睡眠时间
random, _ := rand.Int(rand.Reader, big.NewInt(1000))
sleepTime := int(random.Int64())
//扣除等待时间
wait -= sleepTime
//线程睡眠
time.Sleep(time.Duration(sleepTime) * time.Millisecond)
}
return false
}
func Unlock(key, val string) {
/println(val + "释放锁")
exist := common.Get(key)
if exist == val {
println(val + "试释锁成功")
common.Del(key)
}
}
测试
get请求便于测试
/controller/UserController.go
func GG(c *gin.Context) {
val := util.GetRandStr(6)
get := util.Lock("a", val)
if get {
defer util.Unlock("a", val)
time.Sleep(2 * time.Second)
}
}
/router.go
func StartRouter(r *gin.Engine) *gin.Engine {
//用户
user := r.Group("/user")
{
user.POST("/save", controller.SaveUser)
user.POST("/del", controller.DelUser)
user.POST("/get", controller.GetUser)
user.GET("/gg", controller.GG)
}
return r
}
浏览器连续多次请求
http://localhost:1016/user/gg
控制台输出
hNWZ3v获取锁
hNWZ3v获取锁成功
nTJUUz获取锁
nTJUUz休眠等待锁
nTJUUz休眠等待锁
LagyID获取锁
LagyID休眠等待锁
nTJUUz休眠等待锁
LagyID休眠等待锁
nTJUUz休眠等待锁
hNWZ3v释放锁
hNWZ3v试释锁成功
LagyID获取锁成功
nTJUUz休眠等待锁
nTJUUz休眠等待锁
nTJUUz休眠等待锁
LagyID释放锁
LagyID试释锁成功
nTJUUz获取锁成功
nTJUUz释放锁
nTJUUz试释锁成功
/util/SnowUtil.go
package util
import (
"sync"
"time"
)
func GetId() int64 {
return worker.Get()
}
const (
workerBits uint8 = 10 // 每台机器(节点)的ID位数 10位最大可以有2^10=1024个节点
numberBits uint8 = 12 // 表示每个集群下的每个节点,1毫秒内可生成的id序号的二进制位数 即每毫秒可生成 2^12-1=4096个唯一ID
workerMax int64 = -1 ^ (-1 << workerBits) // 节点ID的最大值,用于防止溢出
numberMax int64 = -1 ^ (-1 << numberBits) // 同上,用来表示生成id序号的最大值
timeShift uint8 = workerBits + numberBits // 时间戳向左的偏移量
workerShift uint8 = numberBits // 节点ID向左的偏移量
// 41位字节作为时间戳数值的话 大约68年就会用完
// 假如你2010年1月1日开始开发系统 如果不减去2010年1月1日的时间戳 那么白白浪费40年的时间戳啊!
// 这个一旦定义且开始生成ID后千万不要改了 不然可能会生成相同的ID
epoch int64 = 1525705533000 // 这个是我在写epoch这个变量时的时间戳(毫秒)
)
type Worker struct {
mu sync.Mutex // 添加互斥锁 确保并发安全
timestamp int64 // 记录时间戳
workerId int64 // 该节点的ID
number int64 // 当前毫秒已经生成的id序列号(从0开始累加) 1毫秒内最多生成4096个ID
}
var worker = Worker{
timestamp: time.Now().UnixNano() / 1e6,
workerId: 1,
number: 0,
}
func (w *Worker) Get() int64 {
// 获取id最关键的一点 加锁 加锁 加锁
w.mu.Lock()
defer w.mu.Unlock() // 生成完成后记得 解锁 解锁 解锁
// 获取生成时的时间戳
now := time.Now().UnixNano() / 1e6 // 纳秒转毫秒
if w.timestamp == now {
w.number++
// 这里要判断,当前工作节点是否在1毫秒内已经生成numberMax个ID
if w.number > numberMax {
// 如果当前工作节点在1毫秒内生成的ID已经超过上限 需要等待1毫秒再继续生成
for now <= w.timestamp {
now = time.Now().UnixNano() / 1e6
}
}
} else {
// 如果当前时间与工作节点上一次生成ID的时间不一致 则需要重置工作节点生成ID的序号
w.number = 0
w.timestamp = now // 将机器上一次生成ID的时间更新为当前时间
}
// 第一段 now - epoch 为该算法目前已经奔跑了xxx毫秒
// 如果在程序跑了一段时间修改了epoch这个值 可能会导致生成相同的ID
ID := int64((now-epoch)<<timeShift | (w.workerId << workerShift) | (w.number))
return ID
}
使用
println(util.GetId())
/controller/FileController.go
package controller
import (
"github.com/gin-gonic/gin"
"path/filepath"
"pro/response"
"pro/util"
)
// Upload 上传文件
func Upload(ctx *gin.Context) {
file, err := ctx.FormFile("file")
if err != nil {
panic(err)
}
//文件名
fileName := util.GetFileName(16) + filepath.Ext(file.Filename)
//保存文件
ctx.SaveUploadedFile(file, "/"+fileName)
response.Success(ctx, fileName)
}
// BatchUpload 批量上传文件
func BatchUpload(ctx *gin.Context) {
form, _ := ctx.MultipartForm()
files := form.File["file[]"]
var list []string
for _, file := range files {
//文件名
fileName := util.GetFileName(16) + filepath.Ext(file.Filename)
//保存文件
ctx.SaveUploadedFile(file, "/"+fileName)
//文件名添加到切片
list = append(list, fileName)
}
response.Success(ctx, list)
}
限制文件大小
/main.go
func Router() *gin.Engine {
//禁用gin输出
gin.DefaultWriter = io.Discard
r := gin.Default()
//文件上传大小
r.MaxMultipartMemory = 8 << 20 // 8 MiB
r.Use(
//异常处理
middleware.RecoveryMiddleware(),
//跨域
middleware.CORSMiddleware(),
//请求参数处理
middleware.RequestParamsMiddleware(),
//日志
middleware.LogMiddle(),
)
return StartRouter(r)
}
/util/RSAUtil.go
package util
import (
"github.com/golang-module/dongle"
)
// 私钥加密
func EncryptByPriKey(data string) string {
return dongle.Encrypt.FromString(data).ByRsa(pri).ToBase64String()
}
// 公钥解密
func DecryptByPubKey(data string) string {
return dongle.Decrypt.FromBase64String(data).ByRsa(pub).ToString()
}
// 公钥加密
func EncryptByPubKey(data string) string {
return dongle.Encrypt.FromString(data).ByRsa(pub).ToBase64String()
}
// 私钥解密
func DecryptByPriKey(data string) string {
return dongle.Decrypt.FromBase64String(data).ByRsa(pri).ToString()
}
func catch() {
if err := recover(); err != nil {
println(err)
}
}
var pri = `
-----BEGIN PRIVATE KEY-----
MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAJlDYH0MoSaHRw04loTi2bl2Jaoz
O0I4ChaoWhuGiJhnx6pRmQW5ojT3pWaE+UA5VEW0tQEzuqawLr6zTtp36x9NeIolvQkZHLudjo8G
bkLQuA+HVUQ43GL6+eepSA/mfUIA6brq7rDWP7fDe8SWua9s4V6tR/AtGCs5TvEYCCX7AgMBAAEC
gYBVFZmYco14VTt1tIejaEjU9Ck+zshEH9ZB895qT4q/iUXIYRphmkfZve399y5koC8Pr52Y+D3T
0hVxWxwYnuBRFLlMnUlyveonGD3bncI0YFvC0eHzwnWagOGsvdDD+cCCT0a6/0+iieF5jrryPYsY
/mP/chMFSpckMeBxpRGBsQJBAPP2LUQf3ONeGAC+oL3ZibWnrEWV3SydZnzXvfwmqMqRFCDksPzD
OE3GfDEvZzcB08l69zt24JFnUYI/dwYp/ikCQQCg03XVs33SJbL0sgN+VQerJsTPWMnQFWGWD/9y
wt158AVDadv9iFROjRCXtKRokUKlRY9cWlCIzzwIUx6k5z+DAkEA3VvM3Nhwa5mv+9T8MucU7c/T
H1yIz/eNy89R4l4Nn6ed9O6srNxR1Tg47cQOSjoNOe6qL7mAsE5oBd+iFuS5aQJAaVn8X9AjxOzD
LP4Lwc8LpfdQh49fLHtFINs7+D5kfQNZP07yOEP9DjPkQayo4oL9iGxnvBTBms0+QynH8jg15wJB
ALOvJmUw5KEh7zrGE6Rp8/1tnv2ZHPeUHS07n4Tas68vBqA7i3MJ9oYDO6ol7yH09OxNxt72KuaP
mD/AcOw/sJM=
-----END PRIVATE KEY-----`
var pub = `
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCZQ2B9DKEmh0cNOJaE4tm5diWqMztCOAoWqFob
hoiYZ8eqUZkFuaI096VmhPlAOVRFtLUBM7qmsC6+s07ad+sfTXiKJb0JGRy7nY6PBm5C0LgPh1VE
ONxi+vnnqUgP5n1CAOm66u6w1j+3w3vElrmvbOFerUfwLRgrOU7xGAgl+wIDAQAB
-----END PUBLIC KEY-----`
测试
UserController.go
func GG(c *gin.Context) {
en := util.EncryptByPubKey("{\"aaa\": 124}")
println("en :" + en)
de := util.DecryptByPriKey(en)
println("de :" + de)
en1 := util.EncryptByPriKey("{\"aaa\": 124}")
println("en1 :" + en1)
de1 := util.DecryptByPubKey(en1)
println("de1 :" + de1)
//val := util.GetRandStr(6)
//get := util.Lock("a", val)
//if get {
// defer util.Unlock("a", val)
// time.Sleep(2 * time.Second)
//}
}
http://localhost:1016/user/gg
控制台输出
en:Ynu2L4SrZbwH1m7T2eRrYTxtL06R7ci4SErVMmY9yW0/ux1kFMfnhWVBabA/C8A9tRVYxxV2dFQoy8w42i0SdGRxOH/+xN3STHCMxGoHl98BPrS6u/8aPQ7kneZf9OvFspFVAgeonpwN/skHrQJWfR4PgGMHffrQkAFz/GE2l24=
de :{"aaa": 124}
en1 :Ly3dRV+UnJkCUnZr8sRe32Rkb2OX9ZU036fjkh45E6bte1Dqrehyhri8BUh3bbAx9AOK/z9ZPjLZ9ZLAbVc8RvdgFrKsXiR4WVeGiFMVd8z+Cij8ZLLTew9izlpLDYbhyF/0pIo2GH6FRVqVsZyE3BQjAijYT1xLU9Yn7LppzLA=
de1 :{"aaa": 124}
/util/DateUtil.go
package util
import "time"
// 当天开始时间
func GetStart(date time.Time) time.Time {
return time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, time.Local)
}
// 当天结束时间 精确到毫秒
func GetEnd(date time.Time) time.Time {
return time.Date(date.Year(), date.Month(), date.Day(), 23, 59, 59, 999000000, time.Local)
}
// 加减天数
func AddDays(date time.Time, num int) time.Time {
return date.AddDate(0, 0, num)
}
// 格式化日期 年月日
func FormatDay(date time.Time) string {
return date.Format("2006-01-02")
}
// 格式化日期 年月日 时分秒
func FormatSec(date time.Time) string {
return date.Format("2006-01-02 15:04:05")
}
// 格式化日期 年月日 时分秒 毫秒
func FormatMil(date time.Time) string {
return date.Format("2006-01-02 15:04:05.000")
}
// 字符串转时间
func DateToTime(date string) time.Time {
times, _ := time.Parse("2006-01-02 15:04:05", date)
return times
}
golang入门实战(二)