Gin 开发实战
大约 6 分钟
使用go+gin自建一套脚手架。
目录结构
Gin
https://learnku.com/docs/gin-gonic/1.7
go get -u github.com/gin-gonic/gin
目录结构设计
路由初始化
/bootstrap/route.go
package bootstrap
import (
"gin-web/app/middleware"
"gin-web/routes"
"github.com/gin-gonic/gin"
"net/http"
"strings"
)
func SetupRouter(router *gin.Engine) {
// 注册中间件
registerGlobalMiddlewares(router)
// 注册理由
routes.RegisterApiRoutes(router)
routes.RegisterWebRoutes(router)
// 设置接口错误返回
setup404Handle(router)
}
func registerGlobalMiddlewares(router *gin.Engine) {
router.Use(
middleware.Cors(),
)
}
func setup404Handle(router *gin.Engine) {
router.NoRoute(func(c *gin.Context) {
acceptString := c.Request.Header.Get("Accept")
if strings.Contains(acceptString, "text/html") {
c.String(http.StatusNotFound, "页面不存在")
} else {
c.JSON(http.StatusOK, gin.H{
"code": http.StatusNotFound,
"msg": "接口不存在",
})
}
})
}
路由注册
/routes/web.go
package routes
import "github.com/gin-gonic/gin"
func RegisterWebRoutes(router *gin.Engine) {
router.Static("/assets", "web/dist/assets")
router.Static("/images", "web/dist/images")
router.LoadHTMLGlob("web/dist/index.html")
router.GET("/", func(c *gin.Context) {
c.HTML(200, "index.html", gin.H{
"title": "mysql慢日志解析",
})
})
}
main.go
package main
import (
"github.com/gin-gonic/gin"
"webscoket/bootstrap"
)
func main() {
gin.SetMode(gin.DebugMode)
router := gin.Default()
// 设置信任的代理
router.TrustedProxies = []string{"192.168.1.1"} // 替换为你信任的代理地址
bootstrap.SetupRouter(router)
err := router.Run(":3000")
if err != nil {
panic(err)
}
}
请求数据
import axios from "axios";
import router from "../router/index.js";
import LocalStorage from "../utils/LocalStorage.js";
import EnumData from "../utils/EnumData.js";
axios.defaults.headers.common["X-Requested-With"] = "XMLHttpRequest";
axios.defaults.withCredentials = true;
axios.defaults.timeout = 30000;
axios.interceptors.request.use((config)=>{
config.headers['Authorization'] = 'Bearer ' + LocalStorage.get(EnumData.tokenLabel);
config.headers['Content-Type'] = "multipart/form-data"; // 需要加上这个
return config;
},(error)=>{
return Promise.reject(error);
})
axios.interceptors.response.use((response)=>{
const status = response.data.code;
switch (status){
case 200:
case 202:
return response.data;
break;
case 419:
return router.push({path:"/login"})
break;
default:
// snackbar.error("请求错误,刷新重试");
break;
}
return response;
},(error)=>{
return Promise.reject(error);
})
export default axios;
有时候 c.postform
接收不到参数,可以这样设置
import Qs from 'qs'; // axios 带的
const service = axios.create({
baseURL: Setting.apiBaseURL,
timeout: 30000, // 请求超时时间 30s
// 重点是下载的内容
transformRequest: [
function (data) {
return Qs.stringify(data)
}
]
});
通用数据返回
app/response/resp/resp.go
package resp
import "github.com/gin-gonic/gin"
// 错误返回
func Success(c *gin.Context, msg ...string) {
message := "success"
if len(msg) > 0 {
message = msg[0]
}
c.JSON(200, gin.H{
"code": 202,
"msg": message,
})
}
// 错误返回
func Error(c *gin.Context, msg ...string) {
message := "success"
if len(msg) > 0 {
message = msg[0]
}
c.JSON(200, gin.H{
"code": 202,
"msg": message,
})
}
// 数据信息返回
func Data(c *gin.Context, data interface{}, msg ...string) {
message := "success"
if len(msg) > 0 {
message = msg[0]
}
c.JSON(200, gin.H{
"code": 200,
"data": data,
"msg": message,
})
}
// 保存成功
func SaveSuccess(c *gin.Context, msg ...string) {
message := "保存成功"
if len(msg) > 0 {
message = msg[0]
}
c.JSON(200, gin.H{
"code": 200,
"msg": message,
})
}
func UpdateSuccess(c *gin.Context, msg ...string) {
message := "更新成功"
if len(msg) > 0 {
message = msg[0]
}
c.JSON(200, gin.H{
"code": 200,
"msg": message,
})
}
// 删除成功
func DeleteSuccess(c *gin.Context, msg ...string) {
message := "删除成功"
if len(msg) > 0 {
message = msg[0]
}
c.JSON(200, gin.H{
"code": 202,
"msg": message,
})
}
func DeleteError(c *gin.Context, msg ...string) {
message := "删除失败"
if len(msg) > 0 {
message = msg[0]
}
c.JSON(200, gin.H{
"code": 202,
"msg": message,
})
}
// 列表数据返回
func List(c *gin.Context, data interface{}, total int64, msg ...string) {
message := "success"
if len(msg) > 0 {
message = msg[0]
}
c.JSON(200, gin.H{
"code": 200,
"data": data,
"total": total,
"msg": message,
})
}
渲染vue前端
@view-vue
package routes
import "github.com/gin-gonic/gin"
func RegisterWebRoutes(router *gin.Engine) {
router.Static("/assets", "web/dist/assets")
router.Static("/images", "web/dist/images")
router.LoadHTMLGlob("web/dist/index.html")
router.GET("/", func(c *gin.Context) {
c.HTML(200, "index.html", gin.H{
"title": "在线追番下载器",
})
})
}
jwt
package jwtToken
import (
"errors"
"github.com/dgrijalva/jwt-go"
"movie-cloud/pkg/config"
"time"
)
type jwtClaims struct {
ID uint `json:"id"`
jwt.StandardClaims
}
func GenerateToken(id int64, scope string) (string, error) {
claims := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"id": id,
"exp": time.Now().Add(time.Hour * 24 * 30).Unix(),
"iat": time.Now().Unix(), // 发布时间
"iss": "orangbus.cn",
})
token, err := claims.SignedString([]byte(scope))
if err != nil {
return "", err
}
return token, nil
}
func Parse(token string, scope string) (int64, error) {
var id uint
parse, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
return []byte(scope), nil
})
if err != nil {
return int64(id), err
}
if claims, ok := parse.Claims.(jwt.MapClaims); ok && parse.Valid {
id = uint(int64(claims["id"].(float64)))
return int64(id), nil
}
return 0, errors.New("invalid token")
}
func ValidateToken(token string) (bool, error) {
parse, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
return []byte(config.GetJwtAdminKey()), nil
})
if err != nil {
return false, err
}
if !parse.Valid {
return false, errors.New("invalid token")
}
return true, nil
}
加载配置文件
package config
import (
"fmt"
"github.com/spf13/viper"
)
const (
CustomerNum = 5 // 消费的协程数量
)
/*
*
加载配置文件,命令行 > 环境变量 > 默认值
1、先加载配置文件,没有的话设置默认值
*/
func LoadConfig() {
viper.SetConfigType("env")
viper.AddConfigPath(".")
viper.SetConfigFile(".env")
viper.AutomaticEnv() // 从环境变量读取配置
if err := viper.ReadInConfig(); err != nil {
panic(err)
}
viper.WatchConfig()
// 设置默认值,优先级:外部变量>环境变量>配置文件
}
func GetDbConnection() string {
return viper.GetString("DB_CONNECTION")
}
func GetMysqlUrl() string {
host := getEnvString("DB_HOST", "localhost")
port := getEnvInt("DB_PORT", 3306)
database := getEnvString("DB_DATABASE", "cloud_movie")
user_name := getEnvString("DB_USERNAME", "root")
password := getEnvString("DB_PASSWORD", "")
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local", user_name, password, host, port, database)
return dsn
}
func GetRedisHost() string {
host := getEnvString("REDIS_HOST", "localhost")
port := getEnvInt("REDIS_PORT", 6379)
dsn := fmt.Sprintf("%s:%d", host, port)
return dsn
}
func SetViperValue(name, value, def string) {
if value != "" {
viper.Set(name, value)
return
}
if def != "" {
viper.Set(name, def)
return
}
}
func getEnvString(key string, defVal ...string) string {
v := viper.GetString(key)
if v == "" && defVal != nil {
v = defVal[0]
}
return v
}
func getEnvInt(key string, defVal ...int) int {
v := viper.GetInt(key)
if v == 0 && defVal != nil {
v = defVal[0]
}
return v
}
func GetMqUrl() string {
host := viper.GetString("MQ_HOST")
port := viper.GetInt("MQ_PORT")
name := viper.GetString("MQ_USERNAME")
password := viper.GetString("MQ_PASSWORD")
return fmt.Sprintf("amqp://%s:%s@%s:%d", name, password, host, port)
}
gin模板
路由
@router
$routeName$ := new($cointroller$)
{
router.GET("list", $routeName$.List)
router.GET("store", $routeName$.Store)
router.POST("delete", $routeName$.Delete)
}
curd
@list
func (a $AdminLogController$) List(c *gin.Context) {
var list []models.$modelName$
var total int64
if err := database.DB.Scopes(models.Paginate(c)).Find(&list).Error; err != nil {
resp.Error(c, err.Error())
return
}
database.DB.Model(models.QueueError{}).Count(&total)
resp.List(c, list, total)
}
enum("User","Admin","Article")
@store
func (a $AdminPlanController$) Store(c *gin.Context) {
var $param$ $paramBody$
var $mnodelAlias$ models.$modelName$
if err := c.ShouldBind(¶mBody$); err != nil {
resp.Error(c, err.Error())
return
}
id := cast.ToInt(c.PostForm("id"))
if id > 0 {
if err := database.DB.Where("id = ?", id).First(&$mnodelAlias$).Error; err != nil {
resp.Error(c, err.Error())
return
}
err := mapstructure.Decode($param$, &$mnodelAlias$)
if err != nil {
return
}
if err = database.DB.Save(&$mnodelAlias$).Error; err != nil {
resp.Error(c, err.Error())
return
}
resp.Success(c, msg.UpdateSuccess)
}
if err := mapstructure.Decode($param$, &$mnodelAlias$); err != nil {
resp.Error(c, err.Error())
return
}
if err := database.DB.Create(&$mnodelAlias$).Error; err != nil {
resp.Error(c, err.Error())
return
}
resp.Success(c, msg.CreateSuccess)
}
@del
func (a $AdminLogController$) Delete(c *gin.Context) {
id := c.PostForm("id")
if assert.IsTrue(c, id == "", "请选择删除的数据") {
return
}
ids := []int{}
if strings.Contains(id, ",") {
list := strings.Split(id, ",")
for _, v := range list {
ids = append(ids, cast.ToInt(v))
}
}
if err := database.DB.Where("id in (?)", ids).Delete(models.QueueError{}).Error; err != nil {
resp.Error(c, err.Error())
return
}
resp.Success(c, msg.DeleteSuccess)
}
跨域
@cors
func Cors() gin.HandlerFunc {
return func(c *gin.Context) {
c.Header("Access-Control-Allow-Origin", "*")
c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
c.Header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization, AccessToken")
c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Cache-Control, Content-Language, Content-Type")
c.Header("Access-Control-Allow-Credentials", "true")
c.Next()
}
}
初始化gin
@gin-bootstrap
package bootstrap
import (
"github.com/gin-gonic/gin"
"movie-cloud/app/middleware"
"movie-cloud/pkg/config"
"movie-cloud/routes"
"net/http"
"strings"
)
func SetupRouter(router *gin.Engine) {
// 注册中间件
registerGlobalMiddlewares(router)
// 注册理由
routes.RegisterWebRoutes(router)
routes.RegisterApiRoutes(router)
routes.RegisterAdminRoutes(router)
routes.RegisterDemoRoutes(router)
// 设置接口错误返回
setup404Handle(router)
}
func registerGlobalMiddlewares(router *gin.Engine) {
router.Use(
middleware.Cors(),
gin.Recovery(),
)
if config.GetAppDebug() {
router.Use(gin.Logger())
}
}
func setup404Handle(router *gin.Engine) {
router.NoRoute(func(c *gin.Context) {
acceptString := c.Request.Header.Get("Accept")
if strings.Contains(acceptString, "text/html") {
c.String(http.StatusNotFound, "页面不存在")
} else {
c.JSON(http.StatusOK, gin.H{
"code": http.StatusNotFound,
"msg": "接口不存在",
"url": c.Request.RequestURI,
})
}
})
}
初始化接口
@router-api
package routes
import (
"movie-cloud/app/middleware"
)
func RegisterApiRoutes(r *gin.Engine) {
router := r.Group("/api/", middleware.ApiAuth())
{
apiUser := new(api_user.ApiUserController)
router.GET("user", apiUser.UserInfo) // 用户信息
router.POST("user/edit", apiUser.UserEdit) // 编辑
router.GET("user/token/change", apiUser.UserTokenChange) // 切换token
}
}
快捷返回
@res-data
resp.Data(c, $data$)
enum("list","data")
@success
resp.Success(c,$msg$)
enum("添加成功","保存成功","删除成功","更新成功","success",)
@error
resp.Error(c,$msg$)
enum("添加失败","保存失败","删除失败","更新失败","error")
@has_error
if assert.HasError(c,$msg$,err) {
return
}
enum("参数错误","手机号不能为空","密码不能为空","必填","error")
@res-list
resp.Data(c, $data$,total)
gorm 模板
父级模板
@base-model
package models
import (
"database/sql/driver"
"fmt"
"github.com/gin-gonic/gin"
"github.com/spf13/cast"
"gorm.io/gorm"
"time"
)
type TableId struct {
ID int64 `gorm:"primaryKey;autoIncrement:true" json:"id"`
}
type TableTime struct {
CreatedAt Time `gorm:"column:created_at;index;type:datetime" json:"created_at,omitempty"`
UpdatedAt Time `gorm:"column:updated_at;index;type:datetime" json:"updated_at,omitempty"` // https://github.com/go-gorm/datatypes
}
const timeFormat = "2006-01-02 15:04:05"
const timezone = "Asia/Shanghai"
type Time time.Time
func (t Time) MarshalJSON() ([]byte, error) {
b := make([]byte, 0, len(timeFormat)+2)
b = append(b, '"')
b = time.Time(t).AppendFormat(b, timeFormat)
b = append(b, '"')
return b, nil
}
func (t *Time) UnmarshalJSON(data []byte) (err error) {
now, err := time.ParseInLocation(`"`+timeFormat+`"`, string(data), time.Local)
*t = Time(now)
return
}
func (t Time) String() string {
return time.Time(t).Format(timeFormat)
}
func (t Time) local() time.Time {
loc, _ := time.LoadLocation(timezone)
return time.Time(t).In(loc)
}
func (t Time) Value() (driver.Value, error) {
var zeroTime time.Time
var ti = time.Time(t)
if ti.UnixNano() == zeroTime.UnixNano() {
return nil, nil
}
return ti, nil
}
func (t *Time) Scan(v interface{}) error {
value, ok := v.(time.Time)
if ok {
*t = Time(value)
return nil
}
return fmt.Errorf("can not convert %v to timestamp", v)
}
// 分页查询: db.Scopes(models.Paginate(c))
func Paginate(r *gin.Context) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
page := cast.ToInt(r.Query("page"))
limit := cast.ToInt(r.Query("limit"))
if page <= 0 {
page = 1
}
if limit <= 0 || limit > 100 {
limit = 10
}
return db.Offset((page - 1) * limit).Limit(limit).Order("id desc")
}
}
普通模板
@model
package models
import (
"github.com/google/uuid"
"gorm.io/gorm"
)
const TableName$Admin$ = "admins"
type $modelName$ struct {
TableId
TableTime
}
// 设置表名
func (*$modelName$) TableName() string {
return TableName$Admin$
}