golang入门实战(一)

golang入门实战

  • 项目介绍
  • github
  • redeme
  • 环境搭建
  • 项目搭建
  • 路由配置-gin框架
  • 配置文件 && 端口号
  • 整合mysql ---gorm框架
  • 事务
  • 响应
  • 异常处理
  • 请求参数处理
  • 日志处理
  • 跨域处理
  • 整合redis
  • rabbimq
  • 定时任务
  • 随机字符串 & UUID
  • keylock 全局锁
  • 雪花算法
  • 上传文件
  • RSA非对称加密
  • date工具类
  • 未完待续

项目介绍

数据库。。。。。。mysql
缓存。。。。。。。redis
MQ。。。。。。。 rabbitmq
路由框架。。。。。gin
ORM框架。。 。 。gorm
日志框架。。。。。logrus

github

项目完整代码–github

redeme

由于本篇文章篇幅偏多,代码会直接贴上来,细节地方不会特别标出,
建议按照博客内容进行实操(不建议直接复制),便会发现实际开发中会遇到的一些细节问题是如何处理的

环境搭建

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语句
完成后打开项目  目录结构如下

golang入门实战(一)_第1张图片

路由配置-gin框架

  • 准备接口

      /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导包等待下载即可
    

golang入门实战(一)_第2张图片

  • 路由配置

     编辑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
    

整合mysql —gorm框架

  • 建表
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
}

  • main文件初始化数据库连接
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入参,便以此为例
  • 请求参数处理中间件
  • /middleware/RequestParamsMiddleware.go
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(&params)
			//遍历入参 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)
}
  • 路由修改 get改为post
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)
}

整合redis

  • 配置

      /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:::

rabbimq

这里直接用的延时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)
		}
	})
}
  • 初始化mq
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()
}

随机字符串 & UUID

/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)
}

keylock 全局锁

/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)
}

RSA非对称加密

/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}

date工具类

/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入门实战(二)

你可能感兴趣的:(golang,gin,golang)