Gin服务搭建以及简单middleware使用

Go项目初始化

go mod init [project_name] 

初始化go项目,生成go.mod文件

项目目录结构

项目目录结构
internal:项目主体
- domain:领域对象,例如用户领域,配置领域对象的entity
- repostitory:数据相关内容
-- cache:缓存相关内容
-- dao:数据库相关内容
- service:业务逻辑相关内容
- web:web服务相关内容
-- middleware:中间件,介于发出请求和业务逻辑处理之间,用途示例:前端调用接口需要校验session,可以在middleware中忽略登录注册接口的session校验
pkg:项目导出的文件(待修改)
script:放置一些环境脚本,例如 mysql新建数据库的脚本
docker-compose.yaml:配置应用程序所需要的docker容器,使用docker compose up 或 docker-compose up 来创建和启动应用所需要的容器;后台运行:docker compose up &;停止和删除容器:docker compose down
go.mod:包管理工具,使用go mod tidy 命令来刷新项目中所引用的包依赖
main.go:程序入口,写初始化服务器相关代码

初始化

初始化Gin服务

main.go

// 初始化gin服务器 在main.go入口中
server := gin.Default()
server.GET("/ping", func(c *gin.Context) {
        c.JSON(200, gin.H{
            "message": "pong",
        })
    })
server.Run(":8080") 

gin.Default():创建gin服务(engine);
server.GET(path, handlerFunc):创造一个GET接口;path是接口的url; handleFunc的入参是gin的context,封装HTTP请求和响应的信息;
server.Run("ip:port"):来启动服务器监听服务端口;

path的几种路由方法
  • "/hello" 静态路由
  • "/user/:name" 参数路由 使用 context.Param("name")获取参数name
  • "/views/.html" 通配符路由 使用context.Param(".html")获取".html"
  • "/order" 查询参数,url:"/order?id=1" 使用context.Query("id")获取参数id
gin.Context结构体部分参数(仅包含Export的和部分内部参数)解释
type Context struct {
    writermem responseWriter // 响应处理
    Request   *http.Request  // 请求,包含方法、URL、Header、Body、Response、context.Context...
    Writer    ResponseWriter // 响应,包含响应Header、writer...
    Params    Params         // url参数 key-value结构体数组
    handlers  HandlersChain  // handleFunc数组
    index     int8           // handlerFunc数组的索引
    fullPath  string         // 请求地址
    engine    *Engine        // gin的engine
    params    *Params        // URL数组的切片
    Keys      map[string]any // 每个请求的context的key/value对
    Errors    errorMsgs      // 错误数组
    Accepted  []string       // 请求接收的内容类型格式 例如"application/json, text/plain, */*"
}
gin.Context常用方法
  • AbortWithStatus(code int) 修改请求的状态码,例如返回401表示用户验证失败
  • Bind(obj any) 将Request->Body中的数据绑定到创建的结构体上,根据context-type获取结构体类型,支持json类型等,一般绑定指针 例:var req UserReq -- context.Bind(&req),返回值error,输出反序列化失败的错误信息
  • String(code int, format string, values ...any) code-请求状态码, format-response body的信息,values-额外的信息;返回信息:format%!(EXTRA string=values)
  • Header(key, value string) 将key/value对放到Response Header里面,如果value为空字符串,删除header里的这个key
  • Set(key, value string) 存储key/value对数据到context中
  • Get(key string) 获取context中存储的value
    -JSON(code int, obj any) code-请求状态码,obj-response body 中的JSON
    -Param(key string) 获取URL中的参数
    -Query(key string) 获取请求payload中的参数

初始化数据库

使用gorm middleware来调用数据库;GORM 官方支持的数据库类型有:MySQL, PostgreSQL, SQLite, SQL Server 和 TiDB

gorm middleware地址:https://github.com/go-gorm/gorm
gorm文档:https://gorm.io/zh_CN/docs

main.go

func initDB() *gorm.DB {
    db, err := gorm.Open(mysql.Open("root:root@tcp(localhost:13316)/webook"))
    if err != nil {
        panic(err)
    }
    err = dao.InitTable(db)
    if err != nil {
        panic(err)
    }
    return db
}

gorm.Open(mysql.Open("用户名:密码@tcp(host:port)/数据库名称)) ,返回值为*gorm.DB;
这里的InitTable(db)方法是使用db.AutoMigrate(&User{})来建表,一般不这样操作,可以忽略;
结构体User位于数据层dao/user.go中,与数据库表结构对应,具体实现后续数据层;

初始化Redis

main.go

redisClient := redis.NewClient(&redis.Options{
    Addr: "localhost:6379",
})

Options中可配置地址、端口、用户名、密码等

接口实现分层逻辑

各层的作用

domain/User 领域,包含结构体User ,是一个实体类,独立于分层逻辑中

  1. web/UserHandler web层,封装接口,实现接口分组路由和handlerFunc
  2. service/UserService 业务层 实现业务逻辑
  3. repository/UserRepository 数据仓库层 封装数据的查询和存储逻辑
  4. repository/dao/UserDao 数据层,与数据库交互,获取数据
    注:UserHandler、UserService、UserRepository、UserDao为结构体名称而非文件名,文件名可都是用user.go

各层依次初始化

main.go

userHandler := initUser(db)
userHandler.RegisterRoutes(server)

func initUser(db *gorm.DB) *web.UserHandler {
    userDao := dao.NewUserDao(db)
    userRepository := repository.NewUserRepository(userDao)
    userService := service.NewUserService(userRepository)
    userHandler := web.NewUserHandler(userService)
    return userHandler
}

从userDao开始依次向上初始化;使用RegisterRoutes(server *gin.Engine)方法注册User相关路由;

User - domain领域 User结构体

domain/User.go

// User 领域对象 是 DDD 中的 entity
// BO - business object
type User struct {
    Id           int64
    Email        string
    Password     string
    Ctime        time.Time
    Name         string
    Birthday     int64
    Introduction string
}

UserHandler - web层接口封装

web/user.go
将用户相关的路由封装在web/UserHandler中,实现各接口的业务逻辑;
调用业务层UserService中的方法;

type UserHandler struct {
    svc              *service.UserService
}

通过NewUserHandler()方法来构建UserHandler

func NewUserHandler(userService *service.UserService) *UserHandler {
    return &UserHandler{
        svc: userService,
    }
}

路由注册:实现RegisterRoutes(server *gin.Engine)接口 将url路由注册到gin服务器server上;

func (u *UserHandler) RegisterRoutes(server *gin.Engine) {
    // 分组路由
    ug := server.Group("/users")
    ug.GET("/profile", u.ProfileJWT)
    //ug.GET("/profile", u.Profile)
    ug.POST("/signup", u.SignUp)
    //ug.POST("/login", u.Login)
    ug.POST("/login", u.LoginJWT)
    ug.POST("/edit", u.Edit)
}

ug := server.Group(path)实现分组路由;
ug.Method(path, handlerFunc) 绑定url和方法;


handlerFunc的具体实现 - SignUp

func (u *UserHandler) SignUp(context *gin.Context) {
    // 内部结构体
    type SignupReq struct {
        Email           string `json:"email"`
        ConfirmPassword string `json:"confirmPassword"`
        Password        string `json:"password"`
    }
    var req SignupReq
    // bind方法会根据 Content-Type 来解析数据到req里面
    // 解析错了,会直接写回一个 400 的错误
    if err := context.Bind(&req); err != nil {
        return
    }
    // 调用service的方法
    err = u.svc.SignUp(context, domain.User{
        Email:    req.Email,
        Password: req.Password,
    })
    if err != nil {
        context.String(http.StatusOK, "系统异常")
        return
    }
    context.String(http.StatusOK, "注册成功")
}

context *gin.Context为请求的上下文信息;
context.Bind(&req) 绑定数据到自定义结构体中;
u.svc.SignUp(context, domain.User) 调用service层方法;
context.String(http.StatusOK, "注册成功") 设置请求状态和返回消息;

UserService - 业务层逻辑实现

service/user.go

type UserService struct {
    repo *repository.UserRepository
}

func NewUserService(repo *repository.UserRepository) *UserService {
    return &UserService{
        repo: repo,
    }
}

func (svc *UserService) SignUp(ctx context.Context, u domain.User) error {
    return svc.repo.Create(ctx, u)
}

UserRepository - 数据查询和存储的封装

repository/user.go

type UserRepository struct {
    dao *dao.UserDAO
}

func NewUserRepository(dao *dao.UserDAO) *UserRepository {
    return &UserRepository{
        dao: dao,
    }
}

func (r *UserRepository) Create(ctx context.Context, u domain.User) error {
    return r.dao.Insert(ctx, dao.User{
        Email:    u.Email,
        Password: u.Password,
    })
}

UserDao - 数据层,与数据库交互

repository/dao/user.go

type UserDAO struct {
    db *gorm.DB
}

func NewUserDao(db *gorm.DB) *UserDAO {
    return &UserDAO{
        db: db,
    }
}

func (dao *UserDAO) Insert(ctx context.Context, u User) error {
    // 获取毫秒数
    now := time.Now().UnixMilli()
    u.Utime = now
    u.Ctime = now
    err := dao.db.WithContext(ctx).Create(&u).Error
    return err
}

type User struct {
    // 设置为唯一索引 主键,自增
    Id int64 `gorm:"primaryKey,autoIncrement"`
    // 全部用户唯一
    Email    string `gorm:"unique"`
    Password string
    Name         string
    Birthday     int64
    Introduction string
    // 创建时间 毫秒数
    Ctime int64
    // 更新时间 毫秒数
    Utime int64
}

使用毫秒数存储时间,可以规避不同时间导致的时间差异;
结构体User对应数据库表user;primaryKey,autoIncrement,unique对应字段属性;
上述代码中withContext设置单会话操作,可以省略;
设置多会话操作:

tx := db.WithContext(ctx)
tx.First(&user, 1)
tx.Model(&user).Update("role", "admin")

设置超时时间

ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
db.WithContext(ctx).Find(&users)

Gin middleware使用

server := gin.Default()
server.User()

使用server.User(middleware handleFunc) 来使用各种中间件

Cors 解决跨域问题

cors的地址:https://github.com/gin-contrib/cors

跨域:协议、域名、端口任意一个不同都是跨域请求,跨域请求不成功是因为浏览器不允许;在跨域时,浏览器回先发送204 prefight OPTIONS请求来获取服务器是否允许,允许再发送200正常的xhr请求;解决跨域问题的关键是在preflight请求里面告诉浏览器自己愿意接收请求;
可以使用gin middleware的cors解决跨域问题;
main.go

server.Use(cors.New(cors.Config{
    //AllowOrigins: []string{"https://localhost:3000"},
    //AllowMethods:     []string{"POST"},
    AllowHeaders: []string{"Content-Type", "Authorization"},
    ExposeHeaders: []string{"x-jwt-token"},
    AllowCredentials: true,
    AllowOriginFunc: func(origin string) bool {
    if strings.HasPrefix(origin, "http://localhost") {
        // 开发环境
        return true
    }
    return strings.Contains(origin, "company.com")
    MaxAge: 12 * time.Hour,
}))
  • AllowOrigins:允许接收请求的地址,可以使用*来接收任何请求,最好不要使用;
  • AllowMethods:允许的请求方法,不写默认允许所有方法;
  • AllowHeaders:request header里允许携带的内容;
  • ExposeHeaders:允许前端接收的response header中的内容,不写前端无法接收;
  • AllowCredentials: 是否允许请求携带cookie之类的东西;
  • AllowOriginFunc:允许请求的域名,可写方法
  • MaxAge:最长允许时间,指从返回204 prefight请求之后到发送200 xhr请求直接的这段时间,一般很快就会发送

Session 携带信息

使用Session

func main() {
    server := gin.Default()
    store := cookie.NewStore([]byte("secret"))
    server.Use(sessions.Sessions("ssid", store))
    server.POST("/setSession", setSession)
    server.POST("/getSession", getSession)
    server.Run(":8080")
}

func setSession(ctx *gin.Context) {
    // 存储session
    sess := sessions.Default(ctx)
    sess.Set("userId", "id12345678")
    sess.Save()
    ctx.String(http.StatusOK, "session存储成功")
}

func getSession(ctx *gin.Context) {
    // 获取session
    sess := sessions.Default(ctx)
    userId := sess.Get("userId")
    ctx.String(http.StatusOK, "获取到的UserId: %s", userId)
}

cookie.NewStore([]byte("secret"))方法初始化一个基于cookie的存储引擎,secret是密钥,用于加解密;
sessions.Session("ssid", store)设置session的名字和存储引擎,可以使用memstore、redis等其他存储引擎;
sess.Save() 修改完之后必须调用该方法,否则session不生效

Session和cookie的区别:

session是存储在服务器上的,cookie是存储在客户端的;但是session的id标识是存储在cookie中的。

JWT 校验

JWT : json web token,用于对用户进行身份验证和授权;JWT包含header头部、payload载荷、signature签名三部分,header存储元数据和加密算法,payload存储要传输的数据,signature使用头部指定的加密算法对头部和载荷进行签名,如果令牌被修改,校验签名无法通过;

func main() {
    server := gin.Default()
    server.POST("/setJWT", setJWT)
    server.GET("/getJWT", getJWT)
    server.Run(":8080")
}

type UserClaims struct {
    jwt.RegisteredClaims
    // 声明你自己的要放进去 token 的数据
    Uid int64
    // 浏览器信息
    UserAgent string
}

func setJWT(ctx *gin.Context) {
    agent := ctx.Request.UserAgent()
    uid, _ := strconv.ParseInt(ctx.Query("userId"), 10, 64)
    // 设置jwt默认配置和载荷数据
    userClaims := UserClaims{
        RegisteredClaims: jwt.RegisteredClaims{},
        Uid:              uid,
        UserAgent:        agent,
    }
    // 设置加密方法,返回token
    token := jwt.NewWithClaims(jwt.SigningMethodHS512, userClaims)
    // 设置签名,返回具体加密后的字符串tokenStr
    tokenStr, _ := token.SignedString([]byte("O*1Wlr$iIons2&@X6U$fBm66VjPyoNd4"))
    // 将token放到response header里
    ctx.Header("x-jwt-token", tokenStr)
    ctx.String(http.StatusOK, "token设置成功")
}

func getJWT(ctx *gin.Context) {
    // 从请求header里拿加密后的字符串
    tokenStr := ctx.GetHeader("x-jwt-token")
    claims := &UserClaims{}
    // 校验JWT 提取载荷中的值
    token, _ := jwt.ParseWithClaims(tokenStr, claims, func(token *jwt.Token) (interface{}, error) {
        return []byte("O*1Wlr$iIons2&@X6U$fBm66VjPyoNd4"), nil
    })
    // token校验不通过
    if !token.Valid {
        ctx.AbortWithStatus(http.StatusUnauthorized)
        return
    }
    if ctx.Request.UserAgent() != claims.UserAgent {
        // 浏览器信息不同
        ctx.AbortWithStatus(http.StatusUnauthorized)
        return
    }
    uid := claims.Uid
    ctx.String(http.StatusOK, "token校验成功, userId: %d", uid)
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,245评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,749评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,960评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,575评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,668评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,670评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,664评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,422评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,864评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,178评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,340评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,015评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,646评论 3 323
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,265评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,494评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,261评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,206评论 2 352

推荐阅读更多精彩内容