文章

Beego 最简实践

Beego 最简实践

Go 环境

从这里下载:https://go.dev/dl/

在 Debian 上:

1
2
3
4
5
wget https://go.dev/dl/go1.20.4.linux-amd64.tar.gz --inet4-only

tar -zxvf go1.20.4.linux-amd64.tar.gz

sudo mv go /usr/local/

建立GOPATH

1
mkdir ~/go

设置环境变量,我的 shell 是 zsh,

1
vi .zshrc

添加环境设置,增加如下内容:

1
export PATH=$PATH:/usr/local/go/bin:/home/digg/go/bin

Beego 脚手架工具

下载安装:

1
go install github.com/beego/bee/v2@latest

其中脚手架工具 bee 就在 /home/digg/go/bin 下面,前边的步骤已经将其加入环境变量,现在可以执行一下:

1
bee version

查看是否正常运行。

生成 API 类型项目框架

使用 bee 工具,在 GOPATH 目录的 src 目录下,生成名叫 notepad 的项目框架:

1
bee api notepad

然后进入 notepad 目录,执行:

1
go get notepad

以安装涉及到的模块。安装完毕后,可以在此目录下运行此命令:

1
bee run

如果运行无误,可以打开浏览器查看运行效果。

提示:也可以直接运行 main.go 文件, 例如: go run main.go,这种方式每次修改文件,需要手动编译。

最简实践

如果不是需要全功能 CRUD 代码,建议手工在对应目录建立 controller, routers, test 文件,这样可以保证代码的简洁。

新建控制器

手工在 controller 目录下,建立文件 hello.go (此文件也算是最简控制器模板,甚至没有数据库模块):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package controllers

import (
	beego "github.com/beego/beego/v2/server/web"
)

// Operations about Hello
type HelloController struct {
	beego.Controller
}

func (c *HelloController) URLMapping() {
	c.Mapping("Hello", c.Hello)
}

// @Title Hello
// @Description say hello
// @Success 200 {String} "hello"
// @router / [get]
func (c *HelloController) Hello() {
	c.Ctx.WriteString("hello")
}

注意这一行,

1
// @router / [get]

虽然是注解行,但是从 beego 1.3 版本开始支持了注解路由,用户无需在 router 中注册路由,只需要 Include 相应地 controller,然后在 controller 的 method 方法上面写上 router 注释(// @router)就可以了,而且在 Beego 2.0.4 版本,如果没有这行注释,即便在 routes.go 和 commentsRouter_controllers.go 还有 commentsRouter.go 中增加了对应的路由描述,也是 404 ,找不到对应路由,此事待了解。

增加路由

在项目目录执行:

1
bee generate routers

执行后,会将路由描述写入到 commentsRouter_controllers.go 还有 commentsRouter.go 文件中。

在 routers/router.go 中增加:

1
2
3
4
5
beego.NSNamespace("/hello",
	beego.NSInclude(
		&controllers.HelloController{},
	),
),

最简获参和输出 JSON 控制器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package controllers

import (
	beego "github.com/beego/beego/v2/server/web"
)

// Operations about Bye
type ByeController struct {
	beego.Controller
}

func (c *ByeController) URLMapping() {
	c.Mapping("Bye", c.Bye)
}

// @router / [get]
func (c *ByeController) Bye() {
	username := c.GetString("name")
	c.Data["json"] = map[string]string{"message": "Bye " + username}
	c.ServeJSON()
}

测试

手工在 test 目录下,建立文件 hello_test.go (此文件也算是最简测试模板),判断接口状态和返回值是否符合预期:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package test

import (
	"net/http"
	"net/http/httptest"
	_ "notepad/routers"
	"path/filepath"
	"runtime"
	"testing"

	"github.com/beego/beego/v2/core/logs"
	beego "github.com/beego/beego/v2/server/web"
	. "github.com/smartystreets/goconvey/convey"
)

func init() {
	_, file, _, _ := runtime.Caller(0)
	apppath, _ := filepath.Abs(filepath.Dir(filepath.Join(file, ".."+string(filepath.Separator))))
	beego.TestBeegoInit(apppath)
}

func TestHello(t *testing.T) {
	r, _ := http.NewRequest("GET", "/v1/hello", nil)
	w := httptest.NewRecorder()
	beego.BeeApp.Handlers.ServeHTTP(w, r)

	logs.Info("testing", "TestGet", "Code[%d]\n%s", w.Code, w.Body.String())

	Convey("Subject: Test Station Endpoint\n", t, func() {
		Convey("Status Code Should Be 200", func() {
			So(w.Code, ShouldEqual, 200)
		})
		Convey("The Result Should Not Be Empty", func() {
			So(w.Body.String(), ShouldEqual, "hello")
		})
	})
}

完成记事本功能开发

不使用 generate scaffold 实现一个记事本表的增删改查功能,对应表:note。

本节中所有动作对应的实例名都是:note,不使用复数形式。

生成数据库迁移文件

1
bee generate migration CreateTableNote

修改产生的文件

比如我的文件名:20230512_163151_CreateTableNote,修改 up 和 down 两个函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Run the migrations
func (m *CreateTableNote_20230512_163151) Up() {
	// use m.SQL("CREATE TABLE ...") to make schema update
	m.SQL(`
	CREATE TABLE note
	(
		ID int NOT NULL AUTO_INCREMENT,
		NOTETEXT varchar(255) NOT NULL,
		PRIMARY KEY (ID)
	);
	`)
}

// Reverse the migrations
func (m *CreateTableNote_20230512_163151) Down() {
	// use m.SQL("DROP TABLE ...") to reverse schema update
	m.SQL("DROP TABLE if exists note")
}

进行数据库迁移

回到项目目录,执行:

1
bee migrate --conn="notepad:notepad123@tcp(127.0.0.1:3306)/notepad?charset=utf8"

如果发现数据库设置有误,可以回退这次迁移:

1
bee migrate rollback --conn="notepad:notepad123@tcp(127.0.0.1:3306)/notepad?charset=utf8"

修改完成后,可以继续迁移。

如果迁移失败,有可能是数据库中表:migrations 中的迁移记录没有删除,只能手工删除后在继续迁移。

配置数据库链接

修改配置文件中的数据库链接信息,conf/app.conf:

1
sqlconn = "notepad:notepad123@tcp(127.0.0.1:3306)/notepad?charset=utf8"

引入包

在 main.go 中,引入如下两个包:

1
2
3
4
5
// 导入orm包
"github.com/beego/beego/v2/client/orm"

// 导入mysql驱动
_ "github.com/go-sql-driver/mysql"

执行 go mod 安装关联模块:

1
go mod tidy

此处有坑:main.go 增加 orm 的代码后,go 自动填充到源码中的包如果不是:github.com/beego/beego/v2/client/orm,要修改成它。如果没碰到,忽视此行。

生成 model

1
bee generate model note -fields="id:int64,notetext:string"

生成 controller

1
bee generate controller note

设置 routers

在 routers/router.go 文件中,追加:

1
2
3
4
5
6
7
8
9
ns := beego.NewNamespace("/v1",
...
	beego.NSNamespace("/note",
	  beego.NSInclude(
		  &controllers.NoteController{},
	  ),
  ),
...

查看结果

查看所有记事:

1
curl -s "http://127.0.0.1:8080/v1/note"

记事项分类管理功能

一步完成

使用 bee generate scaffold 生成框架,因为是 API 框架,不生成 view,不迁移数据库。

1
bee generate scaffold catelog -fields="id:int64,name:string"

再手工迁移数据库,如果有问题,此时还可以修改,比如自动生成的建表代码中,id 不是自增的,我们可以修改文件 database/migrations/20230512_181612_catelog.go 的 up 函数,从:

1
2
3
4
func (m *Catelog_20230512_181612) Up() {
	// use m.SQL("CREATE TABLE ...") to make schema update
	m.SQL("CREATE TABLE catelog(`id` int(11) DEFAULT NULL,`name` varchar(128) NOT NULL)")
}

1
2
3
4
5
// Run the migrations
func (m *Catelog_20230512_181612) Up() {
	// use m.SQL("CREATE TABLE ...") to make schema update
	m.SQL("CREATE TABLE catelog(`id` int(11) NOT NULL AUTO_INCREMENT,`name` varchar(128) NOT NULL, PRIMARY KEY (ID))")
}

迁移数据

1
bee migrate --conn="notepad:notepad123@tcp(127.0.0.1:3306)/notepad?charset=utf8"

设置 routers

在 routers/router.go 文件中,追加:

1
2
3
4
5
6
7
8
9
ns := beego.NewNamespace("/v1",
...
	beego.NSNamespace("/catelog",
	  beego.NSInclude(
		  &controllers.NoteController{},
	  ),
  ),
...

查看结果

查看所有分类:

1
curl -s "http://127.0.0.1:8080/v1/catelog"

数据库 SQL 查询最简实践

前文已经引入数据库相关的包,并链接数据成功,本节在其基础上,实现数据库的 SQL 语句查询。

返回单行数据到结构体

新建 model

在 models 目录下,新增 hello.go 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package models

import (
	"github.com/beego/beego/logs"
	"github.com/beego/beego/v2/client/orm"
)

// 定义 Hello 结构体,用以接受数据库查询结果,我们只查询返回字段是 one 的,结果是 1 的记录,所以结构体定义如下:
type Hello struct {
	One int
}

// init 中注册结构体
func init() {
	orm.RegisterModel(new(Hello))
}

// 获取一条字段是 one 的,结果是 1 的记录,并将其返回。
func GetHelloOne() (v *Hello, err error) {
	// 创建数据库链接实例
	o := orm.NewOrm()

	// 定义 hello 的变量 h
	var h Hello

	// 查询返回字段名为 one 的,结果是 1 的记录到变量 h
	// 此处有需要注意的,SQL 语句中的字段名要和对应结构体
	// 的命名一致,但都小写。
	error := o.Raw("select 1 as one").QueryRow(&h)

	// 打印查询结果
	logs.Info(h)

	// 如果查询有错,返回空结构体和错误信息,并打印到 logs
	if error != nil {
		logs.Error(error)
		return nil, error
	}

	// 返回查询结果
	return &h, nil
}

修改控制器

修改控制器中的 hello.go 文件,将 hello 函数修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// @router / [get]
func (c *HelloController) Hello() {
	// 注释或删除原内容
	// c.Ctx.WriteString("hello")

	// 调用数据模块的函数,获取数据库返回
	h, err := models.GetHelloOne()
	
	// 如果有错误则直接返回错误原因
	if err != nil {
		c.Data["json"] = err.Error()
	}
	
	// 如果查询没问题,将返回值输出成 json
	c.Data["json"] = h
	c.ServeJSON()
}

测试输出

1
curl -s "http://localhost:8080/v1/hello"

返回值:

1
2
3
{
  "One": 1
}

返回多行数据到结构体

修改 model

在 models 下的 hello.go 文件中增加结构体:

1
2
3
4
5
6
type VMInfo struct {
	Id         int64
	Name       string `orm:"size(128)"`
	Cluster    string `orm:"size(128)"`
	CreateTime *time.Time
}

init() 函数中,增加注册 model 的过程:

1
orm.RegisterModel(new(VMInfo))

增加数据获取方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 获取多条 vm 信息,并将其返回。
func GetVMInfos() (VMs *[]VMInfo, err error) {
	// 创建数据库链接实例
	o := orm.NewOrm()

	// 测试起见,仅取 10 条数据
	sql := `
		select 
			id, 
			JSON_UNQUOTE(insdetail->'$.metadata.name') as name, 
			cluster, 
			STR_TO_DATE(JSON_UNQUOTE(insdetail->'$.metadata.creationTimestamp'), '%Y-%m-%dT%H:%i:%s') as create_time
		from vminstance 
		where insdetail <> '' 
			and insdetail->'$.status.ready' = true
		order by insdetail->'$.metadata.creationTimestamp' desc
		limit 10;
	`
	var lVMs []VMInfo
	_, error := o.Raw(sql).QueryRows(&lVMs)

	// 打印查询结果
	logs.Info(lVMs)

	// 如果查询有错,返回空结构体和错误信息,并打印到 logs
	if error != nil {
		logs.Error(error)
		return nil, error
	}

	// 返回查询结果
	return &lVMs, nil
}

修改控制器

在 controllers 下的 hello.go 文件中,增加函数:

1
2
3
4
5
6
7
8
9
10
11
12
// @router /getvmsinfo [get]
func (c *HelloController) GetVMsInfo() {
	
	VMs, err := models.GetVMInfos()

	if err != nil {
		c.Data["json"] = err.Error()
	}

	c.Data["json"] = VMs
	c.ServeJSON()
}

并修改 URLMapping :

1
2
3
4
func (c *HelloController) URLMapping() {
	c.Mapping("Hello", c.Hello)
	c.Mapping("GetVMsInfo", c.GetVMsInfo)
}

测试输出

1
curl -s "http://localhost:8080/v1/hello/getvmsinfo"

返回值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[
  {
    "Id": 3626,
    "Name": "i-a5**********3-*****02",
    "Cluster": "*****02",
    "CreateTime": "2019-11-18T03:16:37+08:00"
  },
  ...
  {
    "Id": 3517,
    "Name": "i-3d**********1-c*****01",
    "Cluster": "******01",
    "CreateTime": "2020-09-13T06:07:06+08:00"
  }
]

一点缺陷

在上边的返回多行记录的例子中,控制器是这么调用函数的:

1
models.GetVMInfos()

package models 下(可能)有很多文件,每个文件又有很多函数,显然如果将一类函数关联到一个类型上,在被外部调用的时候会更明晰一些,比如这样的调用:

1
models.HelloModel.GetVMInfos()

在Go语言中,可以将一个方法关联到一个类型上,使得该方法只能通过该类型的实例来调用。这种关联关系就是方法和类型之间的绑定。在这段代码中,GetVMInfos() 方法被绑定到 helloModel 类型上,所以只能通过 HelloModel 变量来调用该方法。

如需实现这种效果,需要修改 models 下的 hello.go 文件。

在 import 下面,追加如下两行:

1
2
3
4
5
6
7
8
// `helloModel`是一个无字段的结构体类型,它的目的是作为接收者类型,
// 即方法所属的类型。
type helloModel struct{}

// `HelloModel`是一个全局变量,它是一个指向`helloModel`类型的指针。
// 由于Go语言中允许通过指针来调用方法,所以可以使用
// `models.HelloModel.GetVMInfos()`这样的方式来调用`GetVMInfos()`方法。
var HelloModel *helloModel

修改 GetVMInfos() 函数,从:

1
func GetVMInfos() (VMs *[]VMInfo, err error)

修改为:

1
func (*helloModel) GetVMInfos() (VMs *[]VMInfo, err error)

加了函数头:

1
(*helloModel)

表明 GetVMInfos() 是一个方法,它属于 helloModel 类型。

本文由作者按照 CC BY 4.0 进行授权