goframe2

gf发行版 在线文档 离线文档

代码生成

框架的目的是让开发者将精力聚焦于业务逻辑实现本身。controller,dao/entity/do,service 都可以通过开发工具生成 需要注意的是,manifest/config.yaml 是部署用的配置,而开发时使用的配置在 hack/config.yml

请确认,您已在一个 goframe2 的项目中,或者通过 gf init demoProj -u 生成一个项目。

生成dao

代码生成依赖于数据库表结构,所以我们先创建一个实际数据库和表结构

创建数据库及表结构

sqlite3 db.sqlite3

进入 sqlite命令行界面

CREATE TABLE IF NOT EXISTS "detect_item" (
  "id" INTEGER,
  "name" VARCHAR(55),
  "create_at" DATETIME,
  "updated_at" DATETIME,
  "deleted_at" DATETIME,
  PRIMARY KEY("id")
);
INSERT INTO detect_item VALUES(1,'C++源代码基础检测','2024-01-01 01:01:01',NULL,NULL);
INSERT INTO detect_item VALUES(2,'C++源代码规范检测','2024-01-01 01:01:01',NULL,NULL);
INSERT INTO detect_item VALUES(3,'浏览器JS源代码检测','2024-01-01 01:01:01',NULL,NULL);
INSERT INTO detect_item VALUES(4,'浏览器CSS源代码检测','2024-01-01 01:01:01',NULL,NULL);
INSERT INTO detect_item VALUES(5,'浏览器HTML源代码检测','2024-01-01 01:01:01',NULL,NULL);
INSERT INTO detect_item VALUES(6,'二进制文件检测','2024-01-01 01:01:01',NULL,NULL);
INSERT INTO detect_item VALUES(7,'二进制依赖检测','2024-01-01 01:01:01',NULL,NULL);
.tables -- 查看所有表
.schema user -- 查看user表结构
.exit -- 退出sqlite命令行界面

有关时间维护的文档见此处 sqlite 备份还原操作

sqlite3 db.sqlite3 ".output sqlite3.data.sql" ".dump"
sqlite3 db.sqlite3 ".read sqlite3.data.sql"

配置开发数据库

# hack/config.yaml
gfcli:
  gen:
    dao: # 注意层级关系
      #link: "mysql:root:root@tcp(127.0.0.1:3306)/goframe?loc=Local&parseTime=true"
      link: "sqlite::@file(./resource/db.sqlite3)"
      tables: "detect_item" # 对应要生成的表

更多配置选项请参考这里

确定配置数据库表都正确时,可以直接在项目根目录下执行 make dao

$ make dao
generated: internal/dao/detect_item.go
generated: internal/dao/internal/detect_item.go
generated: internal/model/do/detect_item.go
generated: internal/model/entity/detect_item.go
done!

编写好诸如 `mysql` 的字段注释,可以生成更详细的字段注释信息

生成ctrl

make ctrl命令会分析给定的api接口定义目录下的代码,自动生成对应的控制器/SDK Go代码文件

如何编写 api呢?可以先看看以下示例代码

// 默认值的struct tag名称为d(也可以使用default)
type GetListReq struct {
	g.Meta `path:"/" method:"get"`
	Type   string `v:"required#请选择内容模型" dc:"内容模型"`
	Page   int    `v:"min:0#分页号码错误"      dc:"分页号码" d:"1"`
	Size   int    `v:"max:50#分页数量最大50条" dc:"分页数量,最大50" d:"10"`
	Sort   int    `v:"in:0,1,2#排序类型不合法" dc:"排序类型(0:最新, 默认。1:活跃, 2:热度)"`
}
type GetListRes struct {
	Items []Item `dc:"内容列表"`
}
type Item struct {
	Id    int64  `dc:"内容ID"`
	Title string `dc:"内容标题"`
}
// RegisterReq 注册请求数据结构
type RegisterReq struct {
	Name  string `p:"username"  v:"required|length:4,30#请输入账号|账号长度为:{min}到:{max}位"`
	Pass  string `p:"password1" v:"required|length:6,30#请输入密码|密码长度不够"`
	Pass2 string `p:"password2" v:"required|length:6,30|same:password1#请确认密码|密码长度不够|两次密码不一致"`
}
// RegisterRes 注册返回数据结构
type RegisterRes struct {
	Code  int         `json:"code"`
	Error string      `json:"error"`
	Data  interface{} `json:"data"`
}

参考 以上代码示例 新建自己的 api 接口文件。还可以参考这里这里

GoFramestruct tag(标签) 有哪些?

参数请求、数据校验、OpenAPIv3、命令管理、数据库ORM。

Tag(简写) 全称 描述 相关文档
v valid 数据校验标签。 Struct校验-基本使用
p param 自定义请求参数匹配。 请求输入-对象处理
d default 请求参数默认值绑定。 请求输入-默认值绑定
orm orm ORM标签,用于指定表名、关联关系。 数据规范-gen dao 模型关联-静态关联-With特性
dc description 通用结构体属性描述,ORM和接口使用

我这里使用以下内容成功进行了control代码生成。

// internal/model/common.go // 综合考虑后,将以下内容存放在 model 下可以共同使用
package model

type CommonPaginationReq struct {
	Page int `json:"page" in:"query" d:"1"  v:"min:0#分页号码错误"     dc:"分页号码,默认1"`
	Size int `json:"size" in:"query" d:"10" v:"max:50#分页数量最大50条" dc:"分页数量,最大50"`
}

type CommonPaginationRes struct {
	Total int `dc:"总数"`
	Page  int `json:"page" in:"query" d:"1"  v:"min:0#分页号码错误"     dc:"分页号码,默认1"`
	Size  int `json:"size" in:"query" d:"10" v:"max:50#分页数量最大50条" dc:"分页数量,最大50"`
}
// /api/compat/v1/detect_item.go
package v1

import (
	"demo/internal/model"
	"demo/internal/model/entity"

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

type DetectItemGetAllReq struct {
	g.Meta `path:"/detect_item/all" method:"get" summary:"查询所有的检测项目" tags:"检测项目"`
}
type DetectItemGetAllRes struct {
	List []*entity.DetectItem `json:"list"            dc:"所有的检测项"` //
}
type DetectItemGetListReq struct {
	g.Meta `path:"/detect_item" method:"get" summary:"查询检测项目列表(分页)" tags:"检测项目"`
	model.CommonPaginationReq
}
type DetectItemGetListRes struct {
	List []*entity.DetectItem `json:"list"            dc:"检测项列表(分页)"` //
	model.CommonPaginationRes
}
type DetectItemSearchReq struct {
	g.Meta `path:"/detect_item/search" method:"get" summary:"查询检测项目列表(搜索分页)" tags:"检测项目"`
	Name   string `json:"name" v:"required|length:2,30#搜索项目名|项目名长度为:{min}到:{max}位" dc:"检测项编号"`
	model.CommonPaginationReq
}
type DetectItemSearchRes struct {
	List []*entity.DetectItem `json:"list"            dc:"检测项列表(搜索分页)"` //
	model.CommonPaginationRes
}
type DetectItemGetOneReq struct {
	g.Meta `path:"/detect_item/{id}" method:"get" tags:"检测项目" summary:"查询单个检测项目信息"`
	Id     uint `json:"id" in:"path"  v:"min:1#检测项的编号不正确" dc:"检测项编号"`
}
type DetectItemGetOneRes struct {
	entity.DetectItem
	// Id   uint   `json:"id"                dc:"检测项编号"` //
	// Name string `json:"name"              dc:"检测项名称"` //
}
type DetectItemCreateReq struct {
	g.Meta `path:"/detect_item/create" method:"get" tags:"检测项目" summary:"创建检测项目信息"`
	Name   string `json:"name" v:"required|length:2,50#项目名|项目名长度为:{min}到:{max}位"   dc:"检测项名称"` //
	// entity.DetectItem
}
type DetectItemCreateRes struct {
	NewID uint `json:"new_id"           dc:"ID"`
}
type DetectItemUpdateReq struct {
	g.Meta `path:"/detect_item/update/{id}" method:"post" tags:"检测项目" summary:"修改内容接口"`
	Id     uint   `json:"id" in:"path" v:"min:1#检测项的编号不正确" dc:"检测项编号"`
	Name   string `json:"name"         v:"required|length:2,50#项目名|项目名长度为:{min}到:{max}位"  dc:"检测项名称"` //
}
type DetectItemUpdateRes struct {
}
type DetectItemDeleteReq struct {
	g.Meta `path:"/detect_item/delete" method:"delete" tags:"检测项目" summary:"删除内容接口"`
	Id     uint `json:"id"             v:"min:1#检测项的编号不正确" dc:"检测项编号"`
}
type DetectItemDeleteRes struct {
}

编写好自己的 api 接口文件后,就可以通过 make ctrl 来生成控制器

$ make ctrl
generated: /home/lee/Projects/compat-detect-tools/demo/api/compat/compat.go
generated: internal/controller/compat/compat.go
generated: internal/controller/compat/compat_new.go
generated: internal/controller/compat/compat_v1_detect_item_get_list.go
generated: internal/controller/compat/compat_v1_detect_item_get_one.go
generated: internal/controller/compat/compat_v1_detect_item_create.go
generated: internal/controller/compat/compat_v1_detect_item_update.go
generated: internal/controller/compat/compat_v1_detect_item_delete.go
generated: /home/lee/Projects/compat-detect-tools/demo/api/hello/hello.go
done!

路由注册

// internal/cmd/cmd.go
var (
	Main = gcmd.Command{
		Name:  "main",
		Usage: "main",
		Brief: "start http server",
		Func: func(ctx context.Context, parser *gcmd.Parser) (err error) {
			s := g.Server()
			s.Group("/", func(group *ghttp.RouterGroup) {
				group.Middleware(ghttp.MiddlewareHandlerResponse)
				group.Bind(
					compat.NewV1(), // 添加这一行,注册路由,就可以开始访问接口了
					hello.NewV1(),
				)
			})
			s.Run()
			return nil
		},
	}
)

使用swagger接口文档

参考这篇接口文档-自定义ui 粘贴以下htmlresource/template/apidoc.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <meta name="description" content="SwaggerUI"/>
  <title>SwaggerUI</title>
  <link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@latest/swagger-ui.css" />
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://unpkg.com/swagger-ui-dist@latest/swagger-ui-bundle.js" crossorigin></script>
<script>
	window.onload = () => {
		window.ui = SwaggerUIBundle({
			url:    '/api.json',
			dom_id: '#swagger-ui',
		});
	};
</script>
</body>
</html>

internal/cmd/cmd.go 中添加路由绑定

var (
	Main = gcmd.Command{
		Name:  "main",
		Usage: "main",
		Brief: "start http server",
		Func: func(ctx context.Context, parser *gcmd.Parser) (err error) {
			s := g.Server()
      // --- begin ------------------------------
      // 添加下面三行
			s.BindHandler("/apidoc", func(r *ghttp.Request) {
				r.Response.WriteTpl("/apidoc.html")
			})
      // --- end --------------------------------
			s.Group("/", func(group *ghttp.RouterGroup) {
				group.Middleware(ghttp.MiddlewareHandlerResponse)
				group.Bind(
					compat.NewV1(),
					hello.NewV1(),
				)
			})
			s.Run()
			return nil
		},
	}
)

然后服务自动重启,就可以访问 http://localhost:8000/apidoc 尝试 try it out 了。

另外值得一提的是:如果从 swagger-ui 下载最新版本,解压其中的 distserve/resource/public/resource/ 目录下 ,比如 swagger-ui-5.11.2.dist 目录。 然后在 manifest/config/config.yaml 下配置 server:serverRoot 的值为 /resource/public/resource ,手动重启服务,就可以通过 http://localhost:8000/swagger-ui-5.11.2.dist/ 来访问了。此时打开的是别的接口地址,只需要替换 resource/public/resource/swagger-ui-5.11.2.dist/swagger-initializer.js 中的 url/api.json 即可。

虽然可以访问所有接口,但是其实,我们的接口都只是构造好了,还没有具体实现。 下面我们就开始实现具体的接口功能。

生成service

make service该命令通过分析给定的logic业务逻辑模块目录下的代码,自动生成service目录接口代码。

所以现在我们开始尝试编写 logic 业务逻辑代码。

// /internal/logic/detectitem/detectitem.go
package detectitem

import (
	"context"
	v1 "demo/api/compat/v1"
	"demo/internal/dao"
	"demo/internal/service"

	"github.com/gogf/gf/frame/g"
)

type (
	sDetectItem struct{}
)

func New() *sDetectItem {
	return &sDetectItem{}
}

func (s *sDetectItem) GetAll(ctx context.Context, in *v1.DetectItemGetAllReq) (out *v1.DetectItemGetAllRes, e error) {
	out = &v1.DetectItemGetAllRes{}
	e = dao.DetectItem.Ctx(ctx).Scan(&out.List, true)
	return
}
func (s *sDetectItem) GetList(ctx context.Context, in *v1.DetectItemGetListReq) (out *v1.DetectItemGetListRes, e error) {
	out = &v1.DetectItemGetListRes{}
	out.Page = in.Page
	out.Size = in.Size
	orm := dao.DetectItem.Ctx(ctx)
	e = orm.Page(in.Page, in.Size).ScanAndCount(&out.List, &out.Total, true)
	return
}
func (s *sDetectItem) Search(ctx context.Context, in *v1.DetectItemSearchReq) (out *v1.DetectItemSearchRes, e error) {
	out = &v1.DetectItemSearchRes{}
	out.Page = in.Page
	out.Size = in.Size
	orm := dao.DetectItem.Ctx(ctx)
	orm = orm.WhereLike(dao.DetectItem.Columns().Name, "%"+in.Name+"%")
	e = orm.Page(in.Page, in.Size).ScanAndCount(&out.List, &out.Total, true)
	return
}
func (s *sDetectItem) GetOne(ctx context.Context, in *v1.DetectItemGetOneReq) (out *v1.DetectItemGetOneRes, e error) {
	out = &v1.DetectItemGetOneRes{}
	if err := dao.DetectItem.Ctx(ctx).WherePri(in.Id).Scan(&out); err != nil {
		return nil, err
	}
	return
}
func (s *sDetectItem) Create(ctx context.Context, in *v1.DetectItemCreateReq) (out *v1.DetectItemCreateRes, e error) {
	lastInsertID, err := dao.DetectItem.Ctx(ctx).Data(in).InsertAndGetId()
	if err != nil {
		return out, err
	}
	out = &v1.DetectItemCreateRes{NewID: uint(lastInsertID)}
	return
}
func (s *sDetectItem) Update(ctx context.Context, in *v1.DetectItemUpdateReq) (out *v1.DetectItemUpdateRes, e error) {
	_, e = dao.DetectItem.Ctx(ctx).Data(in).
		FieldsEx(dao.DetectItem.Columns().Id).
		Where(dao.DetectItem.Columns().Id, in.Id).
		Update()
	return
}
func (s *sDetectItem) Delete(ctx context.Context, in *v1.DetectItemDeleteReq) (out *v1.DetectItemDeleteRes, e error) {
	_, e = dao.DetectItem.Ctx(ctx).Where(g.Map{
		dao.DetectItem.Columns().Id: in.Id,
	}).Delete()
	return
}

然后执行 make service 命令,即可生成 service 目录下的 detectitem.go 文件。

执行后,再修改一次 logic 文件,注册 service,即添加 func init()

func init() {
	service.RegisterDetectItem(New())
}

Visual Studio Code 可以安装插件 RunOnSave 随后配置插件,可以在修改 logic(比如新增函数) 时自动更新 service。:

		"emeraldwalk.runonsave": {
			"commands": [
				{
					"match": ".*logic.*go",
					"isAsync": true,
					"cmd": "gf gen service"
				}
			]
		}

注意配置数据库 manifest/config/config.yaml

# https://goframe.org/pages/viewpage.action?pageId=1114245
database:
  default:
    link: "sqlite::@file(./resource/db.sqlite3)"

注意导入驱动 /main.go

// https://github.com/gogf/gf/blob/master/contrib/drivers/README.MD
import _ "github.com/gogf/gf/contrib/drivers/mysql/v2" // MySQL/MariaDB/TiDB
import _ "github.com/gogf/gf/contrib/drivers/sqlite/v2" // SQLite
import _ "github.com/gogf/gf/contrib/drivers/pgsql/v2" // PostgreSQL
import _ "github.com/gogf/gf/contrib/drivers/mssql/v2" // SQL Server
import _ "github.com/gogf/gf/contrib/drivers/oracle/v2" // Oracle
import _ "github.com/gogf/gf/contrib/drivers/clickhouse/v2" // ClickHouse
import _ "github.com/gogf/gf/contrib/drivers/dm/v2" // DM (达梦)

最后访问接口,发现接口均提示"未实现"。下面我们将 controller 和 service 对接上。

// internal/controller/compat/compat_v1_detect_item_create.go
func (c *ControllerV1) DetectItemCreate(ctx context.Context, req *v1.DetectItemCreateReq) (res *v1.DetectItemCreateRes, err error) {
	res, err = service.DetectItem().Create(ctx, req)
	return
}
// internal/controller/compat/compat_v1_detect_item_delete.go
func (c *ControllerV1) DetectItemDelete(ctx context.Context, req *v1.DetectItemDeleteReq) (res *v1.DetectItemDeleteRes, err error) {
	res, err = service.DetectItem().Delete(ctx, req)
	return
}
// internal/controller/compat/compat_v1_detect_item_get_all.go
func (c *ControllerV1) DetectItemGetAll(ctx context.Context, req *v1.DetectItemGetAllReq) (res *v1.DetectItemGetAllRes, err error) {
	res, err = service.DetectItem().GetAll(ctx, req)
	return
}
// internal/controller/compat/compat_v1_detect_item_get_list.go
func (c *ControllerV1) DetectItemGetList(ctx context.Context, req *v1.DetectItemGetListReq) (res *v1.DetectItemGetListRes, err error) {
	res, err = service.DetectItem().GetList(ctx, req)
	return
}
// internal/controller/compat/compat_v1_detect_item_get_one.go
func (c *ControllerV1) DetectItemGetOne(ctx context.Context, req *v1.DetectItemGetOneReq) (res *v1.DetectItemGetOneRes, err error) {
	res, err = service.DetectItem().GetOne(ctx, req)
	return
}
// internal/controller/compat/compat_v1_detect_item_search.go
func (c *ControllerV1) DetectItemSearch(ctx context.Context, req *v1.DetectItemSearchReq) (res *v1.DetectItemSearchRes, err error) {
	res, err = service.DetectItem().Search(ctx, req)
	return
}
// internal/controller/compat/compat_v1_detect_item_update.go
func (c *ControllerV1) DetectItemUpdate(ctx context.Context, req *v1.DetectItemUpdateReq) (res *v1.DetectItemUpdateRes, err error) {
	res, err = service.DetectItem().Update(ctx, req)
	return
}

其实 controller 本身是介于API接口service服务 之间的存在,一个 controller 可能需要调用多个 service 才能完成接口功能。此处示例较为简单,所以不需要调用多个 service

上述代码,应该可以完成一个 基础的增删改查了。