Beego API Quickstart


Beego API 快速入门

本文将通过一个简单的案例帮助初学者打通 beego api 开发流程。

环境配置

  1. 安装 golang,配置GOPROXY, GOPATH, GOROOT,启用 go module。

  2. 在 shell 配置文件中加入export GOPATH="yourPath"。并且将 GOPATH 中的 bin 目录加入 PATH。

  3. 安装 bee。go install github.com/beego/bee/v2@latest

  4. 创建新项目。

bee api quickstart
go mod tidy
bee generate docs

项目结构与执行逻辑

beego 是一个典型的 MVC 架构。它的执行逻辑如下图所示。

当前创建的是 api 项目,没有前端部分。

相对应的项目目录如下所示。

运行项目

在项目目录下使用bee run -gendoc=true -downdoc=true运行项目。第一次运行时会自动下载调试工具swagger

访问http://127.0.0.1:8080/swagger/可以看到调试界面。

源码分析

Entry

首先来看入口文件main.go

package main

import (
	_ "quickstart/routers"

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

func main() {
	if beego.BConfig.RunMode == "dev" {
		beego.BConfig.WebConfig.DirectoryIndex = true
		beego.BConfig.WebConfig.StaticDir["/swagger"] = "swagger"
	}
	beego.Run()
}

main函数中第一条语句是设置在 dev 运行模式下启动 swagger 调试器,这与项目主干无关,暂时不管它。

第二条语句调用了一个Run方法,运行整个程序。那么到底是怎么运行的呢,看到一个关键的引入_ "quickstart/routers"。这个包只引入了其中的 init 函数,我们去到routers/router.go查看到底干了什么。

Routers

package routers

import (
	"quickstart/controllers"

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

func init() {
	ns := beego.NewNamespace("/v1",
		beego.NSNamespace("/object",
			beego.NSInclude(
				&controllers.ObjectController{},
			),
		),
		beego.NSNamespace("/user",
			beego.NSInclude(
				&controllers.UserController{},
			),
		),
	)
	beego.AddNamespace(ns)
}

以上init函数做的工作,简单地说,就是将不同的请求对应到不同的控制器。

本目录中还有另一个文件commentsRouter_controllers.go。以下列出部分内容。

package routers

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

func init() {

    beego.GlobalControllerRouter["quickstart/controllers:UserController"] = append(beego.GlobalControllerRouter["quickstart/controllers:UserController"],
        beego.ControllerComments{
            Method: "Get",
            Router: "/:uid",
            AllowHTTPMethods: []string{"get"},
            MethodParams: param.Make(),
            Filters: nil,
            Params: nil})

}

看到quickstart/controllers:UserController,这其实对应了 controllers 这一层的一个控制器。再看到Method: "Get",这是该控制器实现的一个方法。所以这个文件可以看作是注册控制器与函数。其余的配置暂时不管,重点关注Router: "/:uid"。这个配置的意思是当有 Get 请求http://127.0.0.1:8080/v1/user/xxx(可以直接用浏览器访问该地址)时,会执行控制器中的Get函数。

xxx 可以任意但是必须有,这里写的 :uid 是在实现控制器方法的时候读取输入用的,不是指 xxx 必须是 :uid。

那么,至此便知道如何用不同的请求执行不同的函数了。

然后再去到 controllers/user.go,查看这一层干了什么。

Controllers

package controllers

import (
	"quickstart/models"
	"encoding/json"

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

// Operations about Users
type UserController struct {
	beego.Controller
}

// @Title CreateUser
// @Description create users
// @Param	body		body 	models.User	true		"body for user content"
// @Success 200 {int} models.User.Id
// @Failure 403 body is empty
// @router / [post]
func (u *UserController) Post() {
	var user models.User
	json.Unmarshal(u.Ctx.Input.RequestBody, &user)
	uid := models.AddUser(user)
	u.Data["json"] = map[string]string{"uid": uid}
	u.ServeJSON()
}

// @Title GetAll
// @Description get all Users
// @Success 200 {object} models.User
// @router / [get]
func (u *UserController) GetAll() {
	users := models.GetAllUsers()
	u.Data["json"] = users
	u.ServeJSON()
}

// @Title Get
// @Description get user by uid
// @Param	uid		path 	string	true		"The key for staticblock"
// @Success 200 {object} models.User
// @Failure 403 :uid is empty
// @router /:uid [get]
func (u *UserController) Get() {
	uid := u.GetString(":uid")
	if uid != "" {
		user, err := models.GetUser(uid)
		if err != nil {
			u.Data["json"] = err.Error()
		} else {
			u.Data["json"] = user
		}
	}
	u.ServeJSON()
}

// @Title Update
// @Description update the user
// @Param	uid		path 	string	true		"The uid you want to update"
// @Param	body		body 	models.User	true		"body for user content"
// @Success 200 {object} models.User
// @Failure 403 :uid is not int
// @router /:uid [put]
func (u *UserController) Put() {
	uid := u.GetString(":uid")
	if uid != "" {
		var user models.User
		json.Unmarshal(u.Ctx.Input.RequestBody, &user)
		uu, err := models.UpdateUser(uid, &user)
		if err != nil {
			u.Data["json"] = err.Error()
		} else {
			u.Data["json"] = uu
		}
	}
	u.ServeJSON()
}

// @Title Delete
// @Description delete the user
// @Param	uid		path 	string	true		"The uid you want to delete"
// @Success 200 {string} delete success!
// @Failure 403 uid is empty
// @router /:uid [delete]
func (u *UserController) Delete() {
	uid := u.GetString(":uid")
	models.DeleteUser(uid)
	u.Data["json"] = "delete success!"
	u.ServeJSON()
}

// @Title Login
// @Description Logs user into the system
// @Param	username		query 	string	true		"The username for login"
// @Param	password		query 	string	true		"The password for login"
// @Success 200 {string} login success
// @Failure 403 user not exist
// @router /login [get]
func (u *UserController) Login() {
	username := u.GetString("username")
	password := u.GetString("password")
	if models.Login(username, password) {
		u.Data["json"] = "login success"
	} else {
		u.Data["json"] = "user not exist"
	}
	u.ServeJSON()
}

// @Title logout
// @Description Logs out current logged in user session
// @Success 200 {string} logout success
// @router /logout [get]
func (u *UserController) Logout() {
	u.Data["json"] = "logout success"
	u.ServeJSON()
}

首先定义了UserController,该结构体获取了beego.Controller的所有方法。后面的所有内容则都是在重写这些方法。观察方法的名称可以轻易地发现,这些方法对应不同类型的 http 请求。

方法内使用的结构体及其方法来自models/user.go。于是,再看models/user.go

Models

package models

import (
	"errors"
	"strconv"
	"time"
)

var (
	UserList map[string]*User
)

func init() {
	UserList = make(map[string]*User)
	u := User{"user_11111", "astaxie", "11111", Profile{"male", 20, "Singapore", "astaxie@gmail.com"}}
	UserList["user_11111"] = &u
}

type User struct {
	Id       string
	Username string
	Password string
	Profile  Profile
}

type Profile struct {
	Gender  string
	Age     int
	Address string
	Email   string
}

func AddUser(u User) string {
	u.Id = "user_" + strconv.FormatInt(time.Now().UnixNano(), 10)
	UserList[u.Id] = &u
	return u.Id
}

func GetUser(uid string) (u *User, err error) {
	if u, ok := UserList[uid]; ok {
		return u, nil
	}
	return nil, errors.New("User not exists")
}

func GetAllUsers() map[string]*User {
	return UserList
}

func UpdateUser(uid string, uu *User) (a *User, err error) {
	if u, ok := UserList[uid]; ok {
		if uu.Username != "" {
			u.Username = uu.Username
		}
		if uu.Password != "" {
			u.Password = uu.Password
		}
		if uu.Profile.Age != 0 {
			u.Profile.Age = uu.Profile.Age
		}
		if uu.Profile.Address != "" {
			u.Profile.Address = uu.Profile.Address
		}
		if uu.Profile.Gender != "" {
			u.Profile.Gender = uu.Profile.Gender
		}
		if uu.Profile.Email != "" {
			u.Profile.Email = uu.Profile.Email
		}
		return u, nil
	}
	return nil, errors.New("User Not Exist")
}

func Login(username, password string) bool {
	for _, u := range UserList {
		if u.Username == username && u.Password == password {
			return true
		}
	}
	return false
}

func DeleteUser(uid string) {
	delete(UserList, uid)
}

流程总结

至此,虽然还没有完整分析所有文件,但项目的主体逻辑已经明了。项目运行之后,若接收到 http 请求,则首先通过 router 到对应的 controller,在 controller 中完成所有操作。如果操作较为复杂,则可以编写 model,增加层数,提高复用性。

案例:用户注册与查询

预备技能

与数据库交互

本案例用到数据库,请自行安装并配置 mysql 数据库。案例使用"github.com/beego/beego/v2/client/orm""github.com/go-sql-driver/mysql"实现数据库交互。mysql driver 需要单独安装,go get github.com/go-sql-driver/mysql

先新建一个项目。

mkdir database
cd database
go mod init database
go get github.com/beego/beego/v2/client/orm
go get github.com/go-sql-driver/mysql

编写main.go

package main

import (
	"github.com/beego/beego/v2/client/orm"
	_ "github.com/go-sql-driver/mysql"
)

type User struct {
	ID       int    `orm:"column(id)"`
	Name     string `orm:"column(name)"`
	Password string `orm:"column(password)"`
}

func init() {
	// register model
	orm.RegisterModel(new(User))

	// register default database
	// modify username, password and database for yourself
	orm.RegisterDataBase("default", "mysql", "username:password@tcp(127.0.0.1:3306)/database?charset=utf8")
}

func main() {
	orm.Debug = true

	// automatically build table
	orm.RunSyncdb("default", false, true)

	// create orm object
	o := orm.NewOrm()

	// data
	user := new(User)
	user.Name = "mike"

	// insert data
	o.Insert(user)
}

运行go run main.go并查看 mysql 中的数据。发现新建了一个表并且填入了数据。

这是一个很简单的案例,其余操作可以参考相关文档

测试

纯粹的测试很简单,可以参考该系列文章

现在将上面的代码稍微修改一下,写入models/user.go

package models

import (
	"github.com/beego/beego/v2/client/orm"
	_ "github.com/go-sql-driver/mysql"
)

type User struct {
	ID       int    `orm:"column(id)"`
	Name     string `orm:"column(name)"`
	Password string `orm:"column(password)"`
}

func Execute() {
	// set to debug mode
	orm.Debug = true
	// register model and database
	orm.RegisterModel(new(User))
	orm.RegisterDataBase("default", "mysql", "username:password@tcp(127.0.0.1:3306)/database?charset=utf8")
	// automatically build table
	orm.RunSyncdb("default", false, true)
	// create orm object
	o := orm.NewOrm()
	// data
	user := new(User)
	user.Name = "mike"
	// insert data
	o.Insert(user)
}

然后编写tests/models_user_test.go文件。

package tests

import (
	"quickstart/models"
	"testing"
)

func TestExecute(t *testing.T) {
	models.Execute()
}

使用go test ./tests即可完成测试。当然这只是最简单的写法,要进行更精确友好的测试可以自行添加代码。

案例

为了方便起见,我们参考原有的程序,从下往上写。

Models

config.go是读取配置的文件,自行在项目根目录下建立一个 config.json 文件,并且把对应字段写好即可(sql_user等)。

package models

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"os"
)

type Config struct {
	SqlUser     string `json:"sql_user"`
	SqlPassword string `json:"sql_password"`
	SqlDatabase string `json:"sql_database"`
	SqlPort     int `json:"sql_port"`
}

func ReadConfig(configPath string) Config {
	configFile, err := os.Open(configPath)
	if err != nil {
		fmt.Println(err)
	}
	defer configFile.Close()

	byteValue, _ := ioutil.ReadAll(configFile)

	var config Config
	json.Unmarshal([]byte(byteValue), &config)

	return config
}

user.go定义了 user 这个表的模型。

package models

type User struct {
	ID       uint   `orm:"column(id);auto"`
	Name     string `orm:"column(name);unique;size(20)"`
	Password string `orm:"column(password);size(16)"`
}

operation.go定义了针对数据库的一系列操作。其中orm.Ormer已在 v2 版本中修改,文档建议设置成全局变量,这里在外面包了一层加强安全性。另外因为只是一个简单的案例,方法只是简单地写了几个。

package models

import (
	"fmt"
	"strconv"

	"github.com/beego/beego/v2/client/orm"
	_ "github.com/go-sql-driver/mysql"
)

type DBHandle struct {
	orm orm.Ormer
}

var DBH DBHandle

func init() {
	// read config
	config := ReadConfig("./config.json")

	// set to debug mode
	orm.Debug = true

	// register model and database
	orm.RegisterModel(new(User))
	databaseUrl := config.SqlUser + ":" + config.SqlPassword + "@tcp(127.0.0.1:" +
		strconv.Itoa(config.SqlPort) + ")/" + config.SqlDatabase + "?charset=utf8"
	orm.RegisterDataBase("default", "mysql", databaseUrl)

	// automatically build table
	orm.RunSyncdb("default", false, true)

	// create orm object with specified database as recommended in the document
	DBH.orm = orm.NewOrmUsingDB("default")
}

// @Title Insert
// @Description insert new data
// @Param mode: must be a pointer
func (this *DBHandle) Insert(mode interface{}) {
	_, err := this.orm.Insert(mode)
	if err != nil {
		fmt.Println(err)
	}
}

// @Title Delete
// @Description delete by primary key
// @Param mode: must be a pointer
func (this *DBHandle) Delete(mode interface{}) {
	_, err := this.orm.Delete(mode)
	if err != nil {
		fmt.Println(err)
	}
}

// @Title Update
// @Description update by primary key
// @Param mode: must be a pointer
func (this *DBHandle) Update(mode interface{}) {
	_, err := this.orm.Update(mode)
	if err != nil {
		fmt.Println(err)
	}
}

// @Title QueryByPK
// @Description query by primary key
//				remenber to specify the primary key for mode before pass it
// @Param mode: must be a pointer
func (this *DBHandle) QueryByPK(mode interface{}) {
	err := this.orm.Read(mode)
	if err != nil {
		fmt.Println(err)
	}
}

// @Title QueryByField
// @Description query by field
// @Param mode: must be a pointer
func (this *DBHandle) QueryByField(mode interface{}, table string, field string,
	value interface{}) {
	_, err := this.orm.QueryTable(table).Filter(field, value).All(mode)
	if err != nil {
		fmt.Println(err)
	}
}

Controllers

user.go定义了UserController这个控制器。

package controllers

import (
	"quickstart/models"
	"strconv"

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

type UserController struct {
	beego.Controller
}

func (u *UserController) Get() {
	uid := u.GetString(":uid")
	user := models.User{}
	if uid == "create" {
		user.Name = "Bob"
		user.Password = "asdj"
		models.DBH.Insert(&user)
		u.Data["json"] = "succeed to add a user"
	} else if uid != "" {
		id, _ := strconv.Atoi(uid)
		user.ID = uint(id)
		models.DBH.QueryByPK(&user)
		u.Data["json"] = user.Name
	}
	u.ServeJSON()
}

Routers

router.go只需要 user 的部分。

package routers

import (
	"quickstart/controllers"

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

func init() {
	ns := beego.NewNamespace("/v1",
		beego.NSNamespace("/object",
			beego.NSInclude(
				&controllers.ObjectController{},
			),
		),
		beego.NSNamespace("/user",
			beego.NSInclude(
				&controllers.UserController{},
			),
		),
	)
	beego.AddNamespace(ns)
}

commentsRouter_controllers.go只留下 user 的 get 函数。

package routers

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

func init() {

    beego.GlobalControllerRouter["quickstart/controllers:UserController"] = append(beego.GlobalControllerRouter["quickstart/controllers:UserController"],
        beego.ControllerComments{
            Method: "Get",
            Router: "/:uid",
            AllowHTTPMethods: []string{"get"},
            MethodParams: param.Make(),
            Filters: nil,
            Params: nil})

}

测试

在浏览器中访问127.0.0.1:8080/v1/user/create可以创建一个用户,然后看到创建成功的信息。

再访问127.0.0.1:8080/v1/user/1就可以查询到 id 为 1 的用户的名称。

启用 https 协议

为什么需要 https,一句话,只有 http 寸步难行。

这一部分的解决方案在网上随便搜一下就有,这里再写一下,省的继续查。

实现的方法很简单,首先需要一个 SSL 证书。自己做一个或者买一个都可以。不用问怎么选,买的显然更好,自己做的只能用于开发环境。

下面介绍一下怎么自制一个 SSL 证书。因为只是用在开发环境,所以搞得简单点就行了。

# 生成服务器私钥
openssl genrsa -out server.key 1024
# 根据私钥和输入的信息生成证书请求文件,相关信息知道的就填,不清楚的随便填
openssl req -new -key server.key -out server.csr
# 用第一步的私钥和第二步的请求文件生成证书
openssl x509 -req -in server.csr -out server.crt -signkey server.key -days 3650

然后把server.keyserver.crt放到 beego api 项目的 conf 目录下,并且修改app.conf如下。

EnableHTTPS = true
EnableHttpTLS = true
EnableHttp = false
HttpsPort = 8000
HTTPSCertFile = "conf/server.crt"
HTTPSKeyFile = "conf/server.key"

最好把 http 关了,防止可能出现的一系列问题。

现在就只能通过https://……进行请求了,注意由于使用自己制作的 SSL 证书,浏览器一定会报安全问题,不用管它就行。如果客户端不是浏览器,那必须在客户端信任自己生成的证书或者关闭证书校验,否则两边是连不上的。

另外,将项目打包之后可执行文件同一级目录下必须要有一个 conf 目录,且其中包含这两个证书文件。


文章作者: niuiic
版权声明: 本博客所有文章除特別声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 niuiic !
评论
  目录