# 基础使用

官网 (opens new window)

# 将 gf 框架作为依赖库添加到当前项目
go get -u -v github.com/gogf/gf/v2

# 全局安装 gf 命令行工具
go install github.com/gogf/gf/cmd/gf/v2@latest

# 创建项目模板,-u 是否更新项目中使用的 goframe 框架为最新版本
gf init demo -u

# 升级框架版本,在项目根目录下执行
gf up -a

# 执行代码生成
gf gen dao

# 生成控制器代码
gf gen ctrl

# 生成控制器代码,使用单源码文件来管理所有接口
gf gen ctrl -m

# 根据 logic 业务逻辑模块目录下的代码,自动生成 service 目录接口代码
gf gen service

# 交叉编译,支持从命令行参数和配置文件两种方式指定编译选项
gf build

# 简单示例

package main

import (
	"github.com/gogf/gf/frame/g"
	"github.com/gogf/gf/net/ghttp"
)

// 单例模式
func main() {
	// 获得一个默认的 Server 单例模式对象,多次调用该方法,返回的是同一个 Server 对象
	// g 组件是框架提供的一个耦合组件,封装和初始化一些常用的组件对象
	s := g.Server()
	// 请求对象r *ghttp.Request,该对象包含当前请求的上下文信息
	s.BindHandler("/", func(r *ghttp.Request) {
		r.Response.Writeln(r.Header.Values("User-Agent"))
	})
	// 没有设置端口的情况下,它默认会监听一个随机的端口
	s.SetPort(8199)
	// 通过 Run() 方法阻塞执行 Server 的监听运行,确保进程不会终止,服务一直可用
	s.Run()
}

// 多例模式
func main() {
	// API 服务器 - 监听 8000 端口
	apiServer := g.Server("api")
	apiServer.SetPort(8000)
	apiServer.BindHandler("/api/user", func(r *ghttp.Request) {
		r.Response.Write("API User")
	})

	// 管理后台 - 监听 9000 端口
	adminServer := g.Server("admin")
	adminServer.SetPort(9000)
	adminServer.BindHandler("/admin/dashboard", func(r *ghttp.Request) {
		r.Response.Write("Admin Dashboard")
	})

	// 同时启动多个服务器(需要 goroutine)
	go apiServer.Run()
	adminServer.Run() // 最后一个阻塞
}
  • 获取请求参数
s.BindHandler("/", func(r *ghttp.Request) {
	r.Response.Writef("name: %s, phone: %d",
		// Get方法能够获取所有HTTP Method提交的参数, 比如Query String/Form/Body等
		r.Get("name").String(),
		r.Get("phone", 13123456789).Int(),
	)
})
  • 将请求参数映射到请求对象上
type HelloReq struct {
    Name string
    Age  int
}

func main() {
    s.BindHandler("/", func(r *ghttp.Request) {
        var req HelloReq
		// r.Parse方法支持自动解析客户端提交参数,并赋值到指定对象上
        if err := r.Parse(&req); err != nil {
            r.Response.Write(err.Error())
            return
        }
        r.Response.Writef(
            "Hello %s! Your Age is %d",
            req.Name,
            req.Age,
        )
    })
}
  • 规范路由的使用
package main

import (
	"context"

	"github.com/gogf/gf/v2/frame/g"
	"github.com/gogf/gf/v2/net/ghttp"
)

type HelloReq struct {
	// 元数据对象,用于给结构体嵌入 一些定义的标签信息:path、method
	g.Meta `path:"/" method:"get"`
	// 属性定义,v:表示校验规则,为valid的缩写,dc:表示参数描述信息,description的缩写
	Name string `v:"required" dc:"姓名"`
	Age  int    `v:"required" dc:"年龄"`
}

// 需要定义一个返回对象。 目的是为了未来返回参数扩展的需要,以及未来标准化接口文档生成的需要
type HelloRes struct{}

// 该对象用于封装路由函数,其所有定义的公开方法都将被作为路由函数进行注册
type Hello struct{}

func (Hello) Say(ctx context.Context, req *HelloReq) (res *HelloRes, err error) {
	// 通过g.RequestFromCtx方法从ctx获取原始的*ghttp.Request请求对象
	r := g.RequestFromCtx(ctx)
	r.Response.Writef(
		"Hello %s! Your Age is %d",
		req.Name,
		req.Age,
	)
	return
}

func main() {
	s := g.Server()
	s.Group("/", func(group *ghttp.RouterGroup) {
		// 该方法会遍历路由对象的所有公开方法,读取方法的输入输出结构体定义,并对其执行路由注册
		group.Bind(
			new(Hello),
		)
	})
	s.SetPort(8000)
	s.Run()
}
  • 中间件的使用
func ErrorHandler(r *ghttp.Request) {
    // 执行路由函数
    r.Middleware.Next()
    // 判断是否产生错误
    if err := r.GetError(); err != nil {
        r.Response.Write("error occurs: ", err.Error())
        return
    }
}

func main() {
    s := g.Server()
    s.Group("/", func(group *ghttp.RouterGroup) {
		// 给该分组路由下的所有注册的路由,都绑定了错误处理的中间件
        group.Middleware(ErrorHandler)
        group.Bind(
            new(Hello),
        )
    })
    s.SetPort(8000)
    s.Run()
}
  • 统一返回结构
package main

import (
    "context"
    "fmt"

    "github.com/gogf/gf/v2/frame/g"
    "github.com/gogf/gf/v2/net/ghttp"
)

// 1、定义一个统一的数据结构
type Response struct {
    Message string      `json:"message" dc:"消息提示"`
    Data    interface{} `json:"data"    dc:"执行结果"`
}

type HelloReq struct {
    g.Meta `path:"/" method:"get"`
    Name   string `v:"required" json:"name" dc:"姓名"`
    Age    int    `v:"required" json:"age"  dc:"年龄"`
}
// 2、给HelloRes增加了一个Content属性,用于路由函数返回具体的数据
type HelloRes struct {
    Content string `json:"content" dc:"返回结果"`
}

type Hello struct{}

func (Hello) Say(ctx context.Context, req *HelloReq) (res *HelloRes, err error) {
	// 3、通过HelloRes返回数据结构
    res = &HelloRes{
        Content: fmt.Sprintf(
            "Hello %s! Your Age is %d", req.Name, req.Age,
        ),
    }
    return
}

// 4、中间件定义
func ResponseMiddleware(r *ghttp.Request) {
    r.Middleware.Next()

    var (
        msg string
        res = r.GetHandlerResponse()
        err = r.GetError()
    )
    if err != nil {
        msg = err.Error()
    } else {
        msg = "OK"
    }
    r.Response.WriteJson(Response{
        Message: msg,
        Data:    res,
    })
}

func main() {
    s := g.Server()
    s.Group("/", func(group *ghttp.RouterGroup) {
		// 5、使用中间件
        group.Middleware(ResponseMiddleware)
        group.Bind(
            new(Hello),
        )
    })
    s.SetPort(8000)
    s.Run()
}
  • 启用接口文档
// 接口定义完善
type HelloReq struct {
	// tags: 该接口属于哪个分类,或者接口模块,summary: 接口描述
    g.Meta `path:"/" method:"get" tags:"Test" summary:"Hello world test case"`
    Name   string `v:"required" json:"name" dc:"姓名"`
    Age    int    `v:"required" json:"age"  dc:"年龄"`
}
type HelloRes struct {
    Content string `json:"content" dc:"返回结果"`
} 

func main() {
    s := g.Server()
    s.Group("/", func(group *ghttp.RouterGroup) {
        group.Middleware(ResponseMiddleware)
        group.Bind(
            new(Hello),
        )
    })
	// 启用OpenAPIv3的接口文档生成,并指定生成的文件路径/api.json
    s.SetOpenApiPath("/api.json")
	// 启用内置的Swagger接口文档UI,并指定客访问的UI地址为/swagger
    s.SetSwaggerPath("/swagger")
    s.SetPort(8000)
    s.Run()
}

# 配置管理

推荐使用单例模式获取配置管理对象。可以方便地通过g.Cfg()获取默认的全局配置管理对象。也可以通过gcfg.Instance包方法获取配置管理对象单例。

  • 示例配置:
viewpath: "/home/www/templates/"
database:
  default:
  - link: "mysql:root:12345678@tcp(127.0.0.1:3306)/test"
    role: "master"
  - link: "mysql:root:12345678@tcp(127.0.0.1:3306)/test"
    role: "slave"
import (
    "fmt"

    "github.com/gogf/gf/v2/frame/g"
    "github.com/gogf/gf/v2/os/gctx"
)

func main() {
    var ctx = gctx.New()
    fmt.Println(g.Cfg().Get(ctx, "viewpath"))  // /home/www/templates/
    fmt.Println(g.Cfg().Get(ctx, "database.default.0.role")) // master
}
  • 使用 gcfg.Instance
import (
    "fmt"

    "github.com/gogf/gf/v2/os/gcfg"
    "github.com/gogf/gf/v2/os/gctx"
)

func main() {
    var ctx = gctx.New()
    fmt.Println(gcfg.Instance().Get(ctx, "viewpath"))
    fmt.Println(gcfg.Instance().Get(ctx, "database.default.0.role"))
}

注意

  • 单例对象在创建时会按照文件后缀 toml/yaml/yml/json/ini/xml/properties 自动检索配置文件
  • 默认情况下会自动检索配置文件 config.toml/yaml/yml/json/ini/xml/properties 并缓存
  • 配置文件在外部被修改时将会自动刷新缓存
  • 修改默认配置文件及目录
// 设置默认配置文件,默认读取的配置文件设置为 default.yaml
g.Cfg().GetAdapter().(*gcfg.AdapterFile).SetFileName("default.yaml")

// 后续读取时将会读取到 default.yaml 配置文件内容
g.Cfg().Get(ctx, "database")

// 修改默认目录
g.Cfg().GetAdapter().(*gcfg.AdapterFile).SetPath("/opt/config")
  • 修改配置内容
func (c *AdapterFile) SetContent(content string, file ...string)
func (c *AdapterFile) GetContent(file ...string) string
func (c *AdapterFile) RemoveContent(file ...string)
func (c *AdapterFile) ClearContent()

注意

  • 需要注意的是该配置是全局生效的,并且优先级会高于读取配置文件
  • 如果通过 SetContent("v = 1", "config.toml") 配置了 config.toml 的配置内容
  • 并且也同时存在 config.toml 配置文件,那么只会使用到 SetContent 的配置内容,而配置文件内容将会被忽略

# 日志组件

# 使用 g.Log(单例名称) 获取 Logger 单例对象时,会自动通过默认的配置管理对象获取对应的 Logger 配置
# 默认情况下会读取 logger.单例名称 配置项,当该配置项不存在时,将会读取默认的 logger 配置项
  • 完整配置文件配置项及说明如下,其中配置项名称不区分大小写:
logger:
  path:                  "/var/log/"           # 日志文件路径。默认为空,表示关闭,仅输出到终端
  file:                  "{Y-m-d}.log"         # 日志文件格式。默认为"{Y-m-d}.log"
  prefix:                ""                    # 日志内容输出前缀。默认为空
  level:                 "all"                 # 日志输出级别
  timeFormat:            "2006-01-02T15:04:05" # 自定义日志输出的时间格式,使用Golang标准的时间格式配置
  ctxKeys:               []                    # 自定义Context上下文变量名称,自动打印Context的变量到日志中。默认为空
  header:                true                  # 是否打印日志的头信息。默认true
  stdout:                true                  # 日志是否同时输出到终端。默认true
  rotateSize:            0                     # 按照日志文件大小对文件进行滚动切分。默认为0,表示关闭滚动切分特性
  rotateExpire:          0                     # 按照日志文件时间间隔对文件滚动切分。默认为0,表示关闭滚动切分特性
  rotateBackupLimit:     0                     # 按照切分的文件数量清理切分文件,当滚动切分特性开启时有效。默认为0,表示不备份,切分则删除
  rotateBackupExpire:    0                     # 按照切分的文件有效期清理切分文件,当滚动切分特性开启时有效。默认为0,表示不备份,切分则删除
  rotateBackupCompress:  0                     # 滚动切分文件的压缩比(0-9)。默认为0,表示不压缩
  rotateCheckInterval:   "1h"                  # 滚动切分的时间检测间隔,一般不需要设置。默认为1小时
  stdoutColorDisabled:   false                 # 关闭终端的颜色打印。默认开启
  writerColorEnable:     false                 # 日志文件是否带上颜色。默认false,表示不带颜色

注意

level配置项使用字符串配置,按照日志级别支持以下配置:DEBU < INFO < NOTI < WARN < ERRO < CRIT,也支持ALL、DEV、PROD常见部署模式配置名称

  • 多个配置项
logger:
  path:    "/var/log"
  level:   "all"
  stdout:  false
  logger1:
    path:    "/var/log/logger1"
    level:   "dev"
    stdout:  false
  logger2:
    path:    "/var/log/logger2"
    level:   "prod"
    stdout:  true
  • 以通过单例对象名称获取对应配置的Logger单例对象
// 对应 logger.logger1 配置项
l1 := g.Log("logger1")
// 对应 logger.logger2 配置项
l2 := g.Log("logger2")
// 对应默认配置项 logger
l3 := g.Log("none")
// 对应默认配置项 logger
l4 := g.Log()

# 错误处理

gerror 是 GoFrame 框架中用于增强标准错误(error)的组件,它在标准错误的基础上增加了堆栈跟踪、错误码、错误包装等高级功能,便于调试和错误链追踪。

# 常用方法

  • New/Newf:用于创建一个自定义错误信息的error对象,并包含堆栈信息
// 创建一个包含堆栈信息的新错误
New(text string) error

// 格式化创建错误,支持 printf 风格
Newf(format string, args ...interface{}) error
  • NewSkip/NewSkipf
// 跳过调用栈的指定层数后创建错误
func NewSkip(skip int, text string) error

// 同上,支持格式化
func NewSkipf(skip int, format string, args ...interface{}) error
  • Wrap/Wrapf
// 将已有错误 err 包装并附加新消息,保留原错误的堆栈
func Wrap(err error, text string) error

// 同上,支持格式化
func Wrapf(err error, format string, args ...interface{}) error
  • WrapSkip/WrapSkipf
// 包装时跳过指定层数的调用栈
func WrapSkip(skip int, err error, text string) error 

// 同上,支持格式化
func WrapSkipf(skip int, err error, format string, args ...interface{}) error

# 代码示例

  • 创建基础错误(带堆栈)
package main

import (
    "fmt"
    "github.com/gogf/gf/v2/errors/gerror"
)

func main() {
    err := gerror.New("数据库连接失败")
    err := gerror.Newf("用户 %s 不存在,ID: %d", "张三", 1001)
}
  • 包装错误(保留原错误链)
unc queryUser() error {
    return gerror.New("sql: no rows in result set")
}

func getUser() error {
    err := queryUser()
    if err != nil {
        // 包装原错误,添加上下文信息
        return gerror.Wrap(err, "获取用户信息失败")
    }
    return nil
}
  • 获取堆栈信息
err := gerror.New("panic")
stack := gerror.Stack(err)
fmt.Println(stack) // 打印堆栈文本

# 类型转换

# Map转换

gconv.Map 支持将任意的 map 或 struct/ *struct 类型转换为常用的 map[string]interface{} 类型。

  • 基本示例
import (
    "github.com/gogf/gf/v2/frame/g"
    "github.com/gogf/gf/v2/util/gconv"
)

func main() {
    type User struct {
        Uid  int    `c:"uid"`
        Name string `c:"name"`
    }
    // 对象
    g.Dump(gconv.Map(User{
        Uid:  1,
        Name: "john",
    }))
    // 对象指针
    g.Dump(gconv.Map(&User{
        Uid:  1,
        Name: "john",
    }))

    // 任意map类型
    g.Dump(gconv.Map(map[int]int{
        100: 10000,
    }))
}

// 输出
{
    "name": "john",
    "uid": 1,
}

{
    "name": "john",
    "uid": 1,
}

{
    "100": 10000,
}
  • 可以通过 c/gconv/json 标签来自定义转换后的 map 键名,当多个标签存在时,按照 gconv/c/json 的标签顺序进行优先级识别
import (
    "github.com/gogf/gf/v2/frame/g"
    "github.com/gogf/gf/v2/util/gconv"
)

func main() {
    type User struct {
        Uid      int
        Name     string `c:"-"`
        NickName string `c:"nickname, omitempty"`
        Pass1    string `c:"password1"`
        Pass2    string `c:"password2"`
    }
    user := User{
        Uid:   100,
        Name:  "john",
        Pass1: "123",
        Pass2: "456",
    }
    g.Dump(gconv.Map(user))
}

// 输出
{
    "Uid": 100,
    "password1": "123",
    "password2": "456",
    "nickname": "",
}
  • 也可以给 struct 的属性自定义自己的标签名称,并在 map 转换时通过第二个参数指定标签优先级
import (
    "github.com/gogf/gf/v2/frame/g"
    "github.com/gogf/gf/v2/util/gconv"
)

func main() {
    type User struct {
        Id   int    `c:"uid"`
        Name string `my-tag:"nick-name" c:"name"`
    }
    user := &User{
        Id:   1,
        Name: "john",
    }
    g.Dump(gconv.Map(user, "my-tag"))
}

// 输出
{
    "nick-name": "john",
    "uid": 1,
}
  • 递归转换
import (
    "fmt"
    "github.com/gogf/gf/v2/frame/g"
    "github.com/gogf/gf/v2/util/gconv"
    "reflect"
)

func main() {
    type Base struct {
        Id   int    `c:"id"`
        Date string `c:"date"`
    }
    type User struct {
        UserBase Base   `c:"base"`
        Passport string `c:"passport"`
        Password string `c:"password"`
        Nickname string `c:"nickname"`
    }
    user := &User{
        UserBase: Base{
            Id:   1,
            Date: "2019-10-01",
        },
        Passport: "john",
        Password: "123456",
        Nickname: "JohnGuo",
    }
    m1 := gconv.Map(user)
    m2 := gconv.MapDeep(user)
    g.Dump(m1, m2)
    fmt.Println(reflect.TypeOf(m1["base"]))
    fmt.Println(reflect.TypeOf(m2["base"]))
}

// 输出
{
    "base":     {
        Id:   1,
        Date: "2019-10-01",
    },
    "passport": "john",
    "password": "123456",
    "nickname": "JohnGuo",
}
{
    "base":     {
        "id":   1,
        "date": "2019-10-01",
    },
    "passport": "john",
    "password": "123456",
    "nickname": "JohnGuo",
}
main.Base
map[string]interface {}

# Struct转换

各种数据类型到 struct 的转换/赋值(特别是 json/ xml/各种协议编码转换)

// params 为需要转换到 struct 的变量参数,可以为任意数据类型,常见的数据类型为 map
// pointer 为需要执行转的目标 struct 对象,此参数必须为指针,转换成功后该对象的属性将会更新
// mapping 为自定义的 map键名 到 strcut属性 之间的映射关系,此时 params 参数必须为 map 类型
func Struct(params interface{}, pointer interface{}, mapping ...map[string]string) (err error)
  • 基本示例
import (
    "github.com/gogf/gf/v2/frame/g"
    "github.com/gogf/gf/v2/util/gconv"
)

func main() {
    type User struct {
        Uid  int
        Name string
    }
    params := g.Map{
        "uid":  1,
        "name": "john",
    }
    var user *User
    if err := gconv.Struct(params, &user); err != nil {
        panic(err)
    }
    g.Dump(user)
}

// 输出
{
    Uid:  1,
    Name: "john",
}
  • 递归转换
package main

import (
    "github.com/gogf/gf/v2/frame/g"
    "github.com/gogf/gf/v2/util/gconv"
)

func main() {
    type Ids struct {
        Id         int    `json:"id"`
        Uid        int    `json:"uid"`
    }
    type Base struct {
        Ids
        CreateTime string `json:"create_time"`
    }
    type User struct {
        Base
        Passport   string `json:"passport"`
        Password   string `json:"password"`
        Nickname   string `json:"nickname"`
    }
    data := g.Map{
        "id"          : 1,
        "uid"         : 100,
        "passport"    : "john",
        "password"    : "123456",
        "nickname"    : "John",
        "create_time" : "2019",
    }
    user := new(User)
    gconv.Struct(data, user)
    g.Dump(user)
}

// 输出
{
    Id:         1,
    Uid:        100,
    CreateTime: "2019",
    Passport:   "john",
    Password:   "123456",
    Nickname:   "John",
}

# Structs转换

建立在 Struct 方法的基础之上,所有的转换规则与 Struct 相同,只是增加了对 struct 数组类型的支持

// pointer 目标转换参数类型需要为 *[]struct / *[]*struct
func Structs(params interface{}, pointer interface{}, mapping ...map[string]string) (err error)
  • 基本示例
import (
    "github.com/gogf/gf/v2/frame/g"
    "github.com/gogf/gf/v2/util/gconv"
)

func main() {
    type User struct {
        Uid  int
        Name string
    }
    params := g.Slice{
        g.Map{
            "uid":  1,
            "name": "john",
        },
        g.Map{
            "uid":  2,
            "name": "smith",
        },
    }
    var users []*User
    if err := gconv.Structs(params, &users); err != nil {
        panic(err)
    }
    g.Dump(users)
}

// 输出
[
    {
        Uid:  1,
        Name: "john",
    },
    {
        Uid:  2,
        Name: "smith",
    },
]

# Scan转换

该方法可以实现对任意参数到 基础数据类型/struct/struct数组/map/map数组 的转换。

func Scan(srcValue any, dstPointer any, paramKeyToAttrMap ...map[string]string) (err error)
  • 基础数据类型转换
import (
    "github.com/gogf/gf/v2/frame/g"
    "github.com/gogf/gf/v2/util/gconv"
)

func main() {
    // 字符串转整数
    var num int
    if err := gconv.Scan("123", &num); err != nil {
        panic(err)
    }
    g.Dump(num) // 输出: 123
    
    // 浮点数转整数
    var num2 int
    if err := gconv.Scan(123.45, &num2); err != nil {
        panic(err)
    }
    g.Dump(num2) // 输出: 123
    
    // 布尔值转整数
    var num3 int
    if err := gconv.Scan(true, &num3); err != nil {
        panic(err)
    }
    g.Dump(num3) // 输出: 1
	
	
    // 整数转字符串
    var str string
    if err := gconv.Scan(123, &str); err != nil {
        panic(err)
    }
    g.Dump(str) // 输出: "123"
    
    // 浮点数转字符串
    var str2 string
    if err := gconv.Scan(123.45, &str2); err != nil {
        panic(err)
    }
    g.Dump(str2) // 输出: "123.45"
    
    // 布尔值转字符串
    var str3 string
    if err := gconv.Scan(true, &str3); err != nil {
        panic(err)
    }
    g.Dump(str3) // 输出: "true"
	
	
	// 值转指针
	var numPtr *int
	if err := gconv.Scan(123, &numPtr); err != nil {
		panic(err)
	}
	g.Dump(numPtr)  // 输出: 123 (指针指向的值)
	
	// 指针转指针
	var num = 456
	var numPtr2 *int
	if err := gconv.Scan(&num, &numPtr2); err != nil {
		panic(err)
	}
	g.Dump(numPtr2) // 输出: 456 (指针指向的值)
}
  • 自动识别转换
import (
    "github.com/gogf/gf/v2/frame/g"
    "github.com/gogf/gf/v2/util/gconv"
)

func main() {
    type User struct {
        Uid  int
        Name string
    }
    params1 := g.Map{
        "uid":  1,
        "name": "john",
    }
    var user *User
    if err := gconv.Scan(params1, &user); err != nil {
        panic(err)
    }
    g.Dump(user)
	// 输出
	// {
	//     Uid:  1,
	//     Name: "john",
	// }
	
	params2 := g.Slice{
		g.Map{
			"uid":  1,
			"name": "john",
		},
		g.Map{
			"uid":  2,
			"name": "smith",
		},
	}
	var users []*User
	if err := gconv.Scan(params2, &users); err != nil {
		panic(err)
	}
	g.Dump(users)
	// 输出
	// [
	//     {
	//         Uid:  1,
	//         Name: "john",
	//     },
	//     {
	//         Uid:  2,
	//         Name: "smith",
	//     },
	// ]
	
	var (
		user   map[string]string
		params = g.Map{
			"uid":  1,
			"name": "john",
		}
	)
	if err := gconv.Scan(params, &user); err != nil {
		panic(err)
	}
	g.Dump(user)
	// 输出
	// {
	//     "uid":  "1",
	//     "name": "john",
	// }
	
	var (
		users  []map[string]string
		params = g.Slice{
			g.Map{
				"uid":  1,
				"name": "john",
			},
			g.Map{
				"uid":  2,
				"name": "smith",
			},
		}
	)
	if err := gconv.Scan(params, &users); err != nil {
		panic(err)
	}
	g.Dump(users)
	// 输出
	// [
	//     {
	//         "uid":  "1",
	//         "name": "john",
	//     },
	//     {
	//         "uid":  "2",
	//         "name": "smith",
	//     },
	// ]
}
  • Scan 高级选项:ScanWithOptions 函数,支持更灵活的转换控制,特别是 OmitEmpty 和 OmitNil 选项
# OmitEmpty 选项用于在转换时跳过空值字段的赋值,保留目标结构体中已有的非空值
# OmitNil 选项用于在转换时跳过 nil 值字段的赋值,保留目标结构体中已有的值
import (
    "github.com/gogf/gf/v2/frame/g"
    "github.com/gogf/gf/v2/util/gconv"
)

func main() {
    type User struct {
        Name  string
        Age   int
        Email string
    }

    // 目标结构体包含初始值
    person := User{
        Name:  "张三",
        Age:   30,
        Email: "zhangsan@example.com",
    }

    // 源数据包含一些空值
    sourceData := g.Map{
        "Name":  "",     // 空字符串
        "Age":   25,     // 非空值
        "Email": "",     // 空字符串
    }

    // 使用 OmitEmpty 选项进行转换
    err := gconv.ScanWithOptions(sourceData, &person, gconv.ScanOption{
        OmitEmpty: true,
    })
    if err != nil {
        panic(err)
    }

    g.Dump(person)
	// 输出
	// {
	//     Name:  "张三",      // 保持原值,因为源值为空字符串
	//     Age:   25,          // 更新为源值
	//     Email: "zhangsan@example.com", // 保持原值,因为源值为空字符串
	// }
	
	type Person struct {
		Name  string
		Age   int
		Email string
	}

	// 目标结构体包含初始值
	person := Person{
		Name:  "李四",
		Age:   0,
		Email: "lisi@example.com",
	}

	// 源数据包含 nil 值
	sourceData := map[string]any{
		"Name":  nil,  // nil 值
		"Age":   30,   // 非 nil 值
		"Email": nil,  // nil 值
	}

	// 使用 OmitNil 选项进行转换
	err := gconv.ScanWithOptions(sourceData, &person, gconv.ScanOption{
		OmitNil: true,
	})
	if err != nil {
		panic(err)
	}

	g.Dump(person)
	// 输出
	// {
	//     Name:  "李四",      // 保持原值,因为源值为 nil
	//     Age:   30,          // 更新为源值
	//     Email: "lisi@example.com", // 保持原值,因为源值为 nil
	// }
}

# Converter 转换

# 是GoFrame v2.9版本新增的类型转换接口,提供了一种更严谨、更灵活的类型转换机制
# 与传统的gconv包方法不同,Converter接口在转换失败时会返回错误,而不是默认返回零值或空值
  • 创建Converter对象
converter := gconv.NewConverter()
  • 基本类型转换
package main

import (
	"fmt"

	"github.com/gogf/gf/v2/util/gconv"
)

func main() {
	// 创建转换对象
	converter := gconv.NewConverter()

	// 整数转换
	intValue, err := converter.Int("123")
	if err != nil {
		fmt.Println("转换失败:", err)
	} else {
		fmt.Println("Int转换结果:", intValue) // 输出: 123
	}

	// 浮点数转换
	floatValue, err := converter.Float64("123.456")
	if err != nil {
		fmt.Println("转换失败:", err)
	} else {
		fmt.Println("Float64转换结果:", floatValue) // 输出: 123.456
	}

	// 布尔值转换
	boolValue, err := converter.Bool("true")
	if err != nil {
		fmt.Println("转换失败:", err)
	} else {
		fmt.Println("Bool转换结果:", boolValue) // 输出: true
	}

	// 字符串转换
	strValue, err := converter.String(123)
	if err != nil {
		fmt.Println("转换失败:", err)
	} else {
		fmt.Println("String转换结果:", strValue) // 输出: 123
	}
}
  • 结构体转换
package main

import (
	"fmt"
    
	"github.com/gogf/gf/v2/util/gconv"
)

type User struct {
	Id   int
	Name string
	Age  int
}

func main() {
	converter := gconv.NewConverter()

	// Map转结构体
	data := map[string]interface{}{
		"id":   1,
		"name": "John",
		"age":  30,
	}

	var user User
	err := converter.Struct(data, &user, gconv.StructOption{})
	if err != nil {
		fmt.Println("转换失败:", err)
	} else {
		fmt.Printf("结构体转换结果: %+v\n", user) // 输出: {Id:1 Name:John Age:30}
	}
}
  • 自定义类型转换
package main

import (
	"fmt"
	"time"

	"github.com/gogf/gf/v2/util/gconv"
)

// UserInfo 自定义类型
type UserInfo struct {
	ID       int
	Name     string
	Birthday time.Time
}

type UserDTO struct {
	UserID   string
	UserName string
	Age      int
}

func userInfoT2UserDTO(in UserInfo) (*UserDTO, error) {
	if in.ID <= 0 {
		return nil, fmt.Errorf("invalid user ID: %d", in.ID)
	}
	// 计算年龄
	age := time.Now().Year() - in.Birthday.Year()
	return &UserDTO{
		UserID:   fmt.Sprintf("U%d", in.ID),
		UserName: in.Name,
		Age:      age,
	}, nil
}

func main() {
	converter := gconv.NewConverter()
	// 注册自定义类型转换函数 - 从UserInfo到UserDTO
	err := converter.RegisterTypeConverterFunc(userInfoT2UserDTO)
	if err != nil {
		fmt.Println("注册转换函数失败:", err)
		return
	}

	// 使用自定义类型转换
	userInfo := UserInfo{
		ID:       101,
		Name:     "张三",
		Birthday: time.Date(1990, 5, 15, 0, 0, 0, 0, time.Local),
	}

	var userDTO UserDTO
	err = converter.Scan(userInfo, &userDTO)
	if err != nil {
		fmt.Println("转换失败:", err)
	} else {
		fmt.Printf("自定义类型转换结果: %#v\n", userDTO)
		// 输出类似: 自定义类型转换结果: main.UserDTO{UserID:"U101", UserName:"张三", Age:35}
	}

	// 测试错误处理
	invalidUser := UserInfo{ID: -1, Name: "无效用户"}
	err = converter.Scan(invalidUser, &userDTO)
	if err != nil {
		fmt.Println("预期的错误:", err) // 输出错误信息: invalid user ID: -1
	}
}

# 缓存管理

gcache 统一的缓存管理模块,开发者可自定义接入缓存适配接口,默认提供了高速内存缓存适配实现。

import "github.com/gogf/gf/v2/os/gcache"

# 支持全局默认缓存(如 gcache.Set)和独立实例(gcache.New())两种使用方式
# 它的键和值均为 interface{} 类型,键可以是任意类型,值可以存储任意数据
# 获取值时可通过 Get* 系列方法便捷转换类型,或直接使用类型断言
# 通过 duration 参数控制过期行为:0 表示永不过期,负数表示立即过期,正数表示指定时长后过期
  • 关于键名数据类型
import (
    "fmt"
    "github.com/gogf/gf/v2/os/gcache"
    "github.com/gogf/gf/v2/os/gctx"
)

func main() {
    var (
        ctx           = gctx.New()
        key1  int32   = 1
        key2  float64 = 1
        value         = `value`
    )
    _ = gcache.Set(ctx, key1, value, 0)
    fmt.Println(gcache.MustGet(ctx, key1).Val())
    fmt.Println(gcache.MustGet(ctx, key2).Val())
}
// 输出
// value
// <nil>

// 虽然两个变量的数值都是 1,但它们的类型不同
// (int32)1 和 (float64)1 被视为两个完全不同的键,因此无法命中缓存,MustGet 返回 nil
  • 关于获取对象键值
import (
    "fmt"
    "github.com/gogf/gf/v2/os/gcache"
    "github.com/gogf/gf/v2/os/gctx"
)

func main() {
    type User struct {
        Id   int
        Name string
        Site string
    }
    var (
        ctx   = gctx.New()
        user  *User
        key   = `UserKey`
        value = &User{
            Id:   1,
            Name: "GoFrame",
            Site: "https://goframe.org",
        }
    )
    err := gcache.Set(ctx, key, value, 0)
    if err != nil {
        panic(err)
    }
	// 缓存组件在获取键值上做了改进,并不是直接返回 interface{} 的键值类型
	// 而是以框架泛型 *gvar.Var 对象返回
    v, err := gcache.Get(ctx, key)
    if err != nil {
        panic(err)
    }
	// 开发者根据业务场景转换为所需的数据类型
    if err = v.Scan(&user); err != nil {
        panic(err)
    }
    fmt.Printf(`%#v`, user)
}
// 输出:&main.User{Id:1, Name:"GoFrame", Site:"https://goframe.org"}

# 内存缓存

缓存组件默认提供了一个高速的内存缓存,操作效率非常高效,CPU性能损耗在ns纳秒级别。

import (
    "fmt"
    "github.com/gogf/gf/v2/os/gcache"
    "github.com/gogf/gf/v2/os/gctx"
)

func main() {
    // 创建一个缓存对象,
    // 当然也可以便捷地直接使用gcache包方法
    var (
        ctx   = gctx.New()
        cache = gcache.New()
    )

    // 设置缓存,不过期
    err := cache.Set(ctx, "k1", "v1", 0)
    if err != nil {
        panic(err)
    }

    // 获取缓存值
    value, err := cache.Get(ctx, "k1")
    if err != nil {
        panic(err)
    }
    fmt.Println(value)

    // 获取缓存大小
    size, err := cache.Size(ctx)
    if err != nil {
        panic(err)
    }
    fmt.Println(size)

    // 缓存中是否存在指定键名
    b, err := cache.Contains(ctx, "k1")
    if err != nil {
        panic(err)
    }
    fmt.Println(b)

    // 删除并返回被删除的键值
    removedValue, err := cache.Remove(ctx, "k1")
    if err != nil {
        panic(err)
    }
    fmt.Println(removedValue)

    // 关闭缓存对象,让GC回收资源
    if err = cache.Close(ctx); err != nil {
        panic(err)
    }
}
  • 过期控制
import (
    "fmt"
    "github.com/gogf/gf/v2/os/gcache"
    "github.com/gogf/gf/v2/os/gctx"
    "time"
)

func main() {
    var (
        ctx = gctx.New()
    )
    // 当键名不存在时写入,设置过期时间1000毫秒
    _, err := gcache.SetIfNotExist(ctx, "k1", "v1", time.Second)
    if err != nil {
        panic(err)
    }

    // 打印当前的键名列表
    keys, err := gcache.Keys(ctx)
    if err != nil {
        panic(err)
    }
    fmt.Println(keys)

    // 打印当前的键值列表
    values, err := gcache.Values(ctx)
    if err != nil {
        panic(err)
    }
    fmt.Println(values)

    // 获取指定键值,如果不存在时写入,并返回键值
    value, err := gcache.GetOrSet(ctx, "k2", "v2", 0)
    if err != nil {
        panic(err)
    }
    fmt.Println(value)

    // 打印当前的键值对
    data1, err := gcache.Data(ctx)
    if err != nil {
        panic(err)
    }
    fmt.Println(data1)

    // 等待1秒,以便k1:v1自动过期
    time.Sleep(time.Second)

    // 再次打印当前的键值对,发现k1:v1已经过期,只剩下k2:v2
    data2, err := gcache.Data(ctx)
    if err != nil {
        panic(err)
    }
    fmt.Println(data2)
}

# Redis 缓存

缓存组件同时提供了 gcache 的 Redis 缓存适配实现。 Redis 缓存在多节点保证缓存的数据一致性时非常有用,特别是 Session 共享、数据库查询缓存等场景中。

  • 使用示例
func ExampleCache_SetAdapter() {
    var (
        err         error
        ctx         = gctx.New()
        cache       = gcache.New()
        redisConfig = &gredis.Config{
            Address: "127.0.0.1:6379",
            Db:      9,
        }
        cacheKey   = `key`
        cacheValue = `value`
    )
	// 据配置(如服务器地址、端口、密码等)创建一个 Redis 客户端对象
    redis, err := gredis.New(redisConfig)
    if err != nil {
        panic(err)
    }
	// 将 GoFrame 框架中一个通用的缓存对象(gcache.Cache)
	// 从默认的本地内存存储,切换到基于 Redis 的分布式存储
    cache.SetAdapter(gcache.NewAdapterRedis(redis))

    err = cache.Set(ctx, cacheKey, cacheValue, time.Second)
    if err != nil {
        panic(err)
    }
    fmt.Println(cache.MustGet(ctx, cacheKey).String())

    fmt.Println(redis.MustDo(ctx, "GET", cacheKey).String())
}

# Redis的使用

  • 安装
# 通用Redis驱动 
go get github.com/redis/go-redis/v9

# 安装驱动,为GoFrame框架特别定制的
go get -u github.com/gogf/gf/contrib/nosql/redis/v2
  • 配置说明
redis:
  # 默认配置组,可以通过 g.Redis() 直接获取
  default:
    address: "127.0.0.1:6379"
    db: "2"  # 数据库索引
    pass: ""
    idleTimeout: "20" # 连接最大空闲时间 (单位秒, 不允许设置为0)
	maxIdle: 10	      # 允许闲置的连接数 (0表示不限制,默认10)
	maxActive: 100    # 最大连接数量限制 (0表示不限制,默认100)
	
  # 另一组名为 'cache' 的配置,可以通过 g.Redis("cache") 获取
  cache:   "127.0.0.1:6379,1,123456?idleTimeout=600"
  • 基本字符串操作 (SET/GET)
package main

import (
    "fmt"
    _ "github.com/gogf/gf/contrib/nosql/redis/v2"
    "github.com/gogf/gf/v2/frame/g"
    "github.com/gogf/gf/v2/os/gctx"
)

func main() {
    var (
        ctx = gctx.New()
    )
    // 设置键值,永不超时
    _, err := g.Redis().Do(ctx, "SET", "name", "GoFrame")
    if err != nil {
        panic(err)
    }

    // 获取值
    result, err := g.Redis().DoVar(ctx, "GET", "name")
    if err != nil {
        panic(err)
    }
    fmt.Println(result.String()) // 输出: GoFrame
}
  • 哈希表操作 (HSET/HGETALL)
// 使用 HSET 存储多个字段
_, err := g.Redis().Do(ctx, "HSET", "user:100", "name", "gf", "score", 100)
if err != nil {
    panic(err)
}
// 使用 HGETALL 获取所有字段
result, err := g.Redis().DoVar(ctx, "HGETALL", "user:100")
if err != nil {
    panic(err)
}
fmt.Println(result.Map()) // 输出: map[name:gf score:100]
  • 设置带过期时间的键 (SETEX)
// 设置一个 10 秒后过期的键值对
_, err := g.Redis().Do(ctx, "SETEX", "temp_key", 10, "temporary_value")
if err != nil {
    panic(err)
}
  • 使用 Do 方法执行任意命令
// 自动序列化 map 并存储
data := g.Map{
    "title":  "GoFrame",
    "author": "John",
}
// 自动将 data 序列化为 JSON 字符串并存入 Redis
_, err := g.Redis().Do(ctx, "SET", "book:1", data)
// ... 错误处理

// 获取并反序列化
result, err := g.Redis().DoVar(ctx, "GET", "book:1")
var book map[string]interface{}
// 将 JSON 字符串反序列化到 map 中
err = result.Scan(&book)
// ... 错误处理和打印
  • 获取原始 go-redis 客户端
// GoFrame 的 gredis 是基于 go-redis 库的适配
// 如果需要进行管道(Pipeline)、事务(Transaction)或 Lua 脚本等更高级的操作
// 可以通过 Client() 方法获取原始的 go-redis 客户端实例

import goredis "github.com/redis/go-redis/v9"

// ... 省略 ctx 变量定义,假设已有 redis := g.Redis()

// 获取原始客户端
client := redis.Client()
// 进行类型断言,以获得 UniversalClient
rawClient, ok := client.(goredis.UniversalClient)
if !ok {
    panic("failed to assert to UniversalClient")
}

// 使用管道示例
pipe := rawClient.Pipeline()
pipe.Set(ctx, "key1", "value1", 0)
pipe.Set(ctx, "key2", "value2", 0)
pipe.Get(ctx, "key1")
// 执行管道
_, err := pipe.Exec(ctx)
// ... 错误处理和结果打印
  • 管理长连接 (Conn)
// 对于需要维持长连接的操作,如订阅/发布(Pub/Sub)
// 应使用 Conn 方法从连接池获取一个专属连接对象,并在操作完成后手动 Close 归还

import "github.com/gogf/gf/v2/os/gctx"
// ... 省略 ctx 变量定义 ...

// 获取连接对象
conn := g.Redis().Conn(ctx)
defer conn.Close(ctx) // 确保函数结束时关闭连接

// 示例:订阅一个频道
_, err := conn.Subscribe(ctx, "my-channel")
if err != nil {
    panic(err)
}
// 接收消息
for {
    msg, err := conn.ReceiveMessage(ctx)
    if err != nil {
        panic(err)
    }
    fmt.Println("Received:", msg.Payload)
}

# 打包方法

# 目录打包

  • 可以通过gf命令行工具的pack命令实现对任意文件/目录的打包
# 将项目的config、public、template三个目录的文件打包到Go文件
gf pack config,public,template packed/data.go -n packed
  • 在boot包中优先引入packed资源包
import (
    // 作为第一个引入的包,以便于其他引入的包在初始化时能使用到资源内容
    _ "my-app/packed"

    // 其他包
)
  • 在main包中优先引入boot包
import (
    _ "my-app/boot"

    // 其他包
)

# 方法打包

通过 Pack*/ Unpack* 方法可以实现对任意文件的打包/解包功能,可以打包到二进制文件或者Go代码文件。

// 将项目根目录下的public和config目录打包为data.bin二进制文件
// 并通过gaes加密算法对生成的二进制内容进行加密
package main

import (
    "github.com/gogf/gf/v2/crypto/gaes"
    "github.com/gogf/gf/v2/os/gfile"
    "github.com/gogf/gf/v2/os/gres"
)

var (
    CryptoKey = []byte("x76cgqt36i9c863bzmotuf8626dxiwu0")
)

func main() {
    binContent, err := gres.Pack("public,config")
    if err != nil {
        panic(err)
    }
    binContent, err = gaes.Encrypt(binContent, CryptoKey)
    if err != nil {
        panic(err)
    }
    if err := gfile.PutBytes("data.bin", binContent); err != nil {
        panic(err)
    }
}
  • 自定义解包示例
package main

import (
    "github.com/gogf/gf/v2/crypto/gaes"
    "github.com/gogf/gf/v2/os/gfile"
    "github.com/gogf/gf/v2/os/gres"
)

var (
    CryptoKey = []byte("x76cgqt36i9c863bzmotuf8626dxiwu0")
)

func main() {
    binContent := gfile.GetBytes("data.bin")
    binContent, err := gaes.Decrypt(binContent, CryptoKey)
    if err != nil {
        panic(err)
    }
    if err := gres.Add(binContent); err != nil {
        panic(err)
    }
	// 打印出添加成功的文件列表查看资源文件是否添加成功
    gres.Dump()
}

# 项目部署

# 独立部署

  • nohup
# nohup
nohup ./gf-app &
  • systemctl
# systemctl
[Unit]
# 单元描述
Description=GF APP
# 在什么服务启动之后再执行本程序
After=mysql.service

[Service]
Type=simple
# 程序执行的目录
WorkingDirectory=/data/server/gfapp/
# 启动的脚本命令
ExecStart=/data/server/gfapp/gfapp
# 重启条件
Restart=always
# 几秒后重启
RestartSec=5

[Install]
WantedBy=multi-user.target
# 创建应用配置文件 /etc/systemd/system/gfapp.service, 内容如上;
# 使用 systemctl daemon-reload 重新加载服务;
# 执行 systemctl start gfapp 来启动服务;
# 最后执行 systemctl status gfapp 来查看服务运行的状态信息;
# 执行 systemctl enable gfapp 将服务添加到开机启动项;
# 注意:执行的 gfapp 是使用文件名作为服务名;

# Docker 部署

  • hack/config.yaml 中配置docker
gfcli:
  docker:
    build: "-a amd64 -s linux -p temp -ew"
    tagPrefixes:
      - my.image.pub/my-app
  • 命令会使用默认配置(例如,-a amd64 -s linux)构建镜像
# 系统已安装 Docker
# 项目根目录存在 Dockerfile 文件,存放在manifest/docker/Dockerfile路径下

# -tn 指定生成的镜像名称和标签,这个参数是必需的
# -f  默认 "manifest/docker/Dockerfile"
# -p  构建完成后,自动将镜像推送到指定的镜像仓库
# -b  自定义二进制文件的构建选项,覆盖默认值  -b "-a arm64 -s linux"
gf docker main.go -tn loads/gf-demos:test

# 生成的镜像
my.image.pub/my-app/loads/gf-demos
  • 一个基础的 docker-compose.yml 配置示例如下:
version: '3.8'

services:
  # 你的 GoFrame 应用
  goframe-alatest:
    build: .  # 指定Dockerfile所在路径
    ports:
      - "8000:8000" # 将宿主机的8000端口映射到容器的8000端口
    depends_on:
      - mysql
      - redis

  # MySQL 服务
  mysql:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: yourpassword
      MYSQL_DATABASE: yourdatabase
    ports:
      - "3306:3306"
    volumes:
      - mysql-data:/var/lib/mysql

  # Redis 服务
  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis-data:/data

volumes:
  mysql-data:
  redis-data: