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 ,是一个实体类,独立于分层逻辑中
- web/UserHandler web层,封装接口,实现接口分组路由和handlerFunc
- service/UserService 业务层 实现业务逻辑
- repository/UserRepository 数据仓库层 封装数据的查询和存储逻辑
- 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 解决跨域问题
跨域:协议、域名、端口任意一个不同都是跨域请求,跨域请求不成功是因为浏览器不允许;在跨域时,浏览器回先发送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)
}