GVM 脚手架探险记02:那些让我拍大腿的Go技巧彩蛋
正确的快速的初始化环境
打开 GVM 项目的 main.go 文件,首先就是可以看到如下代码注释
//go:generate go env -w GO111MODULE=on
//go:generate go env -w GOPROXY=https://goproxy.cn,direct
//go:generate go mod tidy
//go:generate go mod download这些是 Go 语言的 //go:generate 指令,它们用于在构建前自动执行一些准备工作。
//go:generate go env -w GO111MODULE=on
作用 :设置 Go 模块模式为开启状态
详细解释 :GO111MODULE 是 Go 1.11 引入的环境变量,控制模块支持。设置为 on 表示强制启用 Go 模块模式。-w 表示将设置写入环境变量(持久化)。
//go:generate go env -w GOPROXY=https://goproxy.cn,direct
作用 :设置 Go 模块代理
详细解释 :GOPROXY 指定从哪里下载 Go 模块。https://goproxy.cn 是中国的一个 Go 模块镜像,可以加速国内下载。direct 表示如果代理找不到,直接从版本控制系统(如 GitHub)下载。多个地址用逗号分隔,会按顺序尝试。
//go:generate go mod tidy
作用 :整理和同步模块依赖
详细解释 :添加项目需要的但缺失的依赖。移除项目不再使用的依赖。更新 go.mod 和 go.sum 文件。
//go:generate go mod download
作用 :下载模块依赖到本地缓存
详细解释 :下载 go.mod 文件中指定的所有依赖。下载的模块会存储在 $GOPATH/pkg/mod 目录下。
指令执行方式: go generate
扩展的时间解析方法
GVM 脚手架中使用了自定义的时间解析方法,支持解析包含天的时长字符串,
如 “3d”、“2d6h30m”。
// utils/human_duration.go
package utils
import (
"strconv"
"strings"
"time"
)
// ParseDuration 自定义的解析时间的方法
// 支持解析包含天的时长字符串,如 "3d"、"2d6h30m"
func ParseDuration(d string) (time.Duration, error) {
// 去除字符串首尾空格
d = strings.TrimSpace(d)
// 尝试用标准库解析
dr, err := time.ParseDuration(d)
if err == nil {
return dr, nil
}
// 当字符串包含 "d" 时,将其视为天单位
if strings.Contains(d, "d") {
index := strings.Index(d, "d")
hour, _ := strconv.Atoi(d[:index])
dr = time.Hour * 24 * time.Duration(hour)
ndr, err := time.ParseDuration(d[index+1:])
if err != nil {
return dr, nil
}
return dr + ndr, nil
}
// 尝试将输入解析为整数
dv, err := strconv.ParseInt(d, 10, 64)
// 直接转换为 time.Duration 类型(单位为纳秒)
return time.Duration(dv), err
}time.Duration → 用于表示 时间间隔 适合计算时间差、延时等。(如 5s、2h)
time.Time → 用于表示 具体时间点 适合存储和操作日期时间。(如 2023-10-01 12:00:00)
利用配置文件配置模块名
可以参考读取配置文件的思想来做一些配置默认值。
// initialize/other.go
// 尝试打开当前目录下的 go.mod 文件
file, err := os.Open("go.mod")
if err == nil && global.GVA_CONFIG.AutoCode.Module == "" {
defer file.Close() // 确保文件被关闭
// bufio.NewScanner(file) 创建一个 Scanner 文件扫描器,用于逐行读取文件内容
scanner := bufio.NewScanner(file)
// scanner.Scan() 读取文件的下一行(这里只读取第一行)。
scanner.Scan()
// scanner.Text() 返回当前行的内容
global.GVA_CONFIG.AutoCode.Module = strings.TrimPrefix(scanner.Text(), "module ")
}优雅关机
// core/server_run.go
package core
import (
"context"
"fmt"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
// initServer 启动服务并实现优雅关闭
func initServer(address string, router *gin.Engine, readTimeout, writeTimeout time.Duration) {
// 创建服务
srv := &http.Server{
Addr: address,
Handler: router,
ReadTimeout: readTimeout,
WriteTimeout: writeTimeout,
MaxHeaderBytes: 1 << 20, // 限制了单个 HTTP 请求头部分的最大字节数 1MB (1048576 字节)
}
/*
防止恶意客户端发送超大请求头进行攻击
限制内存消耗,避免单个请求占用过多资源
是防范 HTTP 头攻击(如 Slowloris 攻击)的措施之一
*/
// 在goroutine中启动服务
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
fmt.Printf("listen: %s\n", err)
zap.L().Error("server启动失败", zap.Error(err))
os.Exit(1)
}
}()
// 等待中断信号以优雅地关闭服务器
quit := make(chan os.Signal, 1)
// kill (无参数) 默认发送 syscall.SIGTERM
// kill -2 发送 syscall.SIGINT
// kill -9 发送 syscall.SIGKILL,但是无法被捕获,所以不需要添加
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
zap.L().Info("关闭WEB服务...")
// 设置5秒的超时时间
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
zap.L().Fatal("WEB服务关闭异常", zap.Error(err))
}
zap.L().Info("WEB服务已关闭")
}目录结构划分
GVM 采用 路由 -> 控制器 -> 服务层 -> 模型层 的组织风格来处理 HTTP 请求。
在每层的包下面都存在 enter.go 文件,用做当前包的入口方法,统一对外暴漏方法和统一定义外面包的方法供内部使用。在一开始理解比较晕,但是后面使用起来发现比较方便,结构很清晰也极大的减少了包循环依赖的问题。
关于常见的只需要加载启动的方法,放到包 initialize 下。
关于自己定制的一些方法,放到包 core 下。
需要注意的是,存在特殊的包 internal ,这个包在 go 中只允许它的直接父包才可以导入使用,其他包是不允许导入的。在这个包里面做一些具体实现逻辑。
分页请求
封装了一个分页请求结构体模型 PageInfo ,用于接收分页请求的参数。
// model/common/request/common.go
type PageInfo struct {
Page int `json:"page" form:"page"` // 页码
PageSize int `json:"pageSize" form:"pageSize"` // 每页大小
Keyword string `json:"keyword" form:"keyword"` // 关键字
}在遇到需要分页的列表查询时,直接匿名嵌入即可。
例如:
type MyDemoSearch struct {
Name *string `json:"name" form:"name"`
Age *int `json:"age" form:"age"`
Desc *string `json:"desc" form:"desc"`
request.PageInfo
}此外,GVM 还为分页查询结构体提供了 GORM 分页查询的方法 Paginate ,可以直接在查询中使用。
// 定义分页查询方法
func (r *PageInfo) Paginate() func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
if r.Page <= 0 {
r.Page = 1
}
switch {
case r.PageSize > 100:
r.PageSize = 100
case r.PageSize <= 0:
r.PageSize = 10
}
offset := (r.Page - 1) * r.PageSize
return db.Offset(offset).Limit(r.PageSize)
}
}
// 使用分页查询方法
// 例如:
// err = db.Scopes(info.Paginate()).Order("updated_at desc").Find(&entities).Error十分巧妙的利用了 GORM scope 功能,将分页查询的逻辑封装到了 Paginate 方法中,使代码更加清晰和可维护。
配置结构体模型方法
之前接触 vipper 都是读取配置文件到 go 结构体中去就结束了。
在 GVM 中为了更方便得到一些需要依赖于配置的数据,在配置结构体上添加了便捷方法。
例如:
package config
import (
"path/filepath"
"strings"
)
type Autocode struct {
Web string `mapstructure:"web" json:"web" yaml:"web"`
Root string `mapstructure:"root" json:"root" yaml:"root"`
Server string `mapstructure:"server" json:"server" yaml:"server"`
Module string `mapstructure:"module" json:"module" yaml:"module"`
AiPath string `mapstructure:"ai-path" json:"ai-path" yaml:"ai-path"`
}
func (a *Autocode) WebRoot() string {
webs := strings.Split(a.Web, "/")
if len(webs) == 0 {
webs = strings.Split(a.Web, "\\")
}
return filepath.Join(webs...)
}减少了形参的传递,数据来源也很清晰。这种结构体挂载方法的思想很值得学习。