# 基础使用
# 将 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"
- 使用g.Cfg,常用方法 (opens new window)
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代码文件。
- 自定义打包示例,方法介绍 (opens new window)
// 将项目根目录下的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:
开发流程 →