go?doudou開(kāi)發(fā)單體RESTful服務(wù)快速上手教程
引言
筆者2015年開(kāi)始接觸go語(yǔ)言并采用go語(yǔ)言從事web項(xiàng)目開(kāi)發(fā)至今,先后用過(guò)beego、gin、grpc等框架。這些框架非常優(yōu)秀,通過(guò)學(xué)習(xí)它們的源碼,也學(xué)到了很多。筆者之前在公司一直是單打獨(dú)斗,一個(gè)人就把前后端的活包了,用現(xiàn)成的框架其實(shí)也蠻好。只是后來(lái)帶了團(tuán)隊(duì),接了不少項(xiàng)目,開(kāi)始接觸和學(xué)習(xí)敏捷開(kāi)發(fā)、項(xiàng)目管理等方面的理論和實(shí)踐,發(fā)現(xiàn)前后端不同成員之間溝通和聯(lián)調(diào)也是需要很多成本的,特別是如果前端同事完全不懂后端,后端同事完全不懂前端的情況下會(huì)遇到不少頭疼的事。于是萌生了用go語(yǔ)言開(kāi)發(fā)一套低代碼的、易于快速開(kāi)發(fā)的、同時(shí)方便前后端同事溝通和聯(lián)調(diào)的微服務(wù)框架,這就是go-doudou微服務(wù)框架。go-doudou框架主要基于gorilla的mux路由庫(kù)做RESTful接口的快速生成,基于hashicorp公司開(kāi)源的memberlist庫(kù)做服務(wù)注冊(cè)與發(fā)現(xiàn)和故障檢測(cè),同時(shí)支持開(kāi)發(fā)單體應(yīng)用和微服務(wù)應(yīng)用。本教程將通過(guò)一個(gè)用戶(hù)管理服務(wù)的案例來(lái)分幾篇文章介紹如何用go-doudou開(kāi)發(fā)單體RESTful接口。
需求清單
- 用戶(hù)注冊(cè)
- 用戶(hù)登錄
- 用戶(hù)詳情
- 用戶(hù)分頁(yè)
- 上傳頭像
- 下載頭像
學(xué)習(xí)目標(biāo)
- 用戶(hù)詳情、用戶(hù)分頁(yè)和上傳頭像需要采用jwt做權(quán)限校驗(yàn)
- 用戶(hù)注冊(cè)、用戶(hù)登錄和下載頭像接口可以公開(kāi)訪問(wèn),無(wú)須鑒權(quán)
- 提供在線接口文檔
- 提供go語(yǔ)言客戶(hù)端SDK
- 提供mock接口實(shí)現(xiàn)
- 實(shí)現(xiàn)真實(shí)業(yè)務(wù)邏輯
- go-doudou內(nèi)建的ddl表結(jié)構(gòu)同步工具
- go-doudou內(nèi)建的dao層代碼生成和使用
開(kāi)發(fā)環(huán)境準(zhǔn)備
- docker環(huán)境: 推薦下載安裝docker官方的desktop軟件,官方安裝文檔地址
- IDE:推薦goland,當(dāng)然vscode也可以
安裝go-doudou
- 配置goproxy.cn代理,加速依賴(lài)下載
export GOPROXY=https://goproxy.cn,direct
- 如果你用的go版本是1.16以下版本:
GO111MODULE=on go get -v github.com/unionj-cloud/go-doudou@v0.8.6
如果你用的go是1.16及以上版本:
go get -v github.com/unionj-cloud/go-doudou@v0.8.6
- goproxy.cn的同步會(huì)延遲一些,如果執(zhí)行以上命令失敗,可以關(guān)閉代理,科學(xué)上網(wǎng)
export GOPROXY=https://proxy.golang.org,direct
- 以上辦法都不行,可以直接克隆同步到gitee的源碼,本地安裝
git clone git@gitee.com:unionj-cloud/go-doudou.git
切到根路徑下,執(zhí)行命令:
go install
- 執(zhí)行命令
go-doudou -v,如果輸出如下內(nèi)容,表示安裝成功:
? ~ go-doudou -v go-doudou version v0.8.6
初始化工程
執(zhí)行命令:
go-doudou svc init usersvc
切到usersvc路徑下,可以看到生成了如下文件結(jié)構(gòu):
? tutorials ll total 0 drwxr-xr-x 9 wubin1989 staff 288B 10 24 20:05 usersvc ? tutorials cd usersvc ? usersvc git:(master) ? ll total 24 -rw-r--r-- 1 wubin1989 staff 707B 10 24 20:05 Dockerfile -rw-r--r-- 1 wubin1989 staff 439B 10 24 20:05 go.mod -rw-r--r-- 1 wubin1989 staff 247B 10 24 20:05 svc.go drwxr-xr-x 3 wubin1989 staff 96B 10 24 20:05 vo
- svc.go文件:做接口設(shè)計(jì)和定義
- vo文件夾:定義接口入?yún)⒑统鰠⒌慕Y(jié)構(gòu)體
- Dockerfile:用于docker鏡像打包
定義接口
我們打開(kāi)svc.go文件看一下:
package service
import (
"context"
v3 "github.com/unionj-cloud/go-doudou/openapi/v3"
"os"
"usersvc/vo"
)
// Usersvc 用戶(hù)管理服務(wù)
// 調(diào)用用戶(hù)詳情、用戶(hù)分頁(yè)和上傳頭像接口需要帶上Bearer Token請(qǐng)求頭
// 用戶(hù)注冊(cè)、用戶(hù)登錄和下載頭像接口可以公開(kāi)訪問(wèn),無(wú)須鑒權(quán)
type Usersvc interface {
// PageUsers 用戶(hù)分頁(yè)接口
// 展示如何定義POST請(qǐng)求且Content-Type為application/json的接口
PageUsers(ctx context.Context,
// 分頁(yè)請(qǐng)求參數(shù)
query vo.PageQuery) (
// 分頁(yè)結(jié)果
data vo.PageRet,
// 錯(cuò)誤信息
err error)
// GetUser 用戶(hù)詳情接口
// 展示如何定義帶查詢(xún)字符串參數(shù)的GET請(qǐng)求接口
GetUser(ctx context.Context,
// 用戶(hù)ID
userId int) (
// 用戶(hù)詳情
data vo.UserVo,
// 錯(cuò)誤信息
err error)
// PublicSignUp 用戶(hù)注冊(cè)接口
// 展示如何定義POST請(qǐng)求且Content-Type是application/x-www-form-urlencoded的接口
PublicSignUp(ctx context.Context,
// 用戶(hù)名
username string,
// 密碼
password string,
// 圖形驗(yàn)證碼
code string,
) (
// 成功返回OK
data string, err error)
// PublicLogIn 用戶(hù)登錄接口
// 展示如何鑒權(quán)并返回token
PublicLogIn(ctx context.Context,
// 用戶(hù)名
username string,
// 密碼
password string) (
// token
data string, err error)
// UploadAvatar 上傳頭像接口
// 展示如何定義文件上傳接口
// 函數(shù)簽名的入?yún)⒗锉仨氁兄辽僖粋€(gè)[]*v3.FileModel或者*v3.FileModel類(lèi)型的參數(shù)
UploadAvatar(ctx context.Context,
// 用戶(hù)頭像
avatar *v3.FileModel) (
// 成功返回OK
data string, err error)
// GetPublicDownloadAvatar 下載頭像接口
// 展示如何定義文件下載接口
// 函數(shù)簽名的出參里必須有且只有一個(gè)*os.File類(lèi)型的參數(shù)
GetPublicDownloadAvatar(ctx context.Context,
// 用戶(hù)ID
userId int) (
// 文件二進(jìn)制流
data *os.File, err error)
}
以上代碼里每個(gè)方法都有注釋。請(qǐng)仔細(xì)閱讀。接口定義支持文檔注釋?zhuān)恢С謌o語(yǔ)言常見(jiàn)的//注釋。這些注釋會(huì)作為OpenAPI3.0規(guī)范里的description參數(shù)值導(dǎo)出到生成的json文檔和go-doudou內(nèi)建的在線文檔里,下文會(huì)做演示。
生成代碼
執(zhí)行如下命令,即可生成啟動(dòng)一個(gè)服務(wù)所需的全部代碼
go-doudou svc http --handler -c go --doc
解釋一下命令中的flag參數(shù):
- --handler:表示需要生成http handler接口實(shí)現(xiàn),就是把解析http請(qǐng)求參數(shù)和編碼返回值的代碼都生成出來(lái)
- -c:表示生成服務(wù)接口的客戶(hù)端SDK,目前只支持
go。如果不需要生成客戶(hù)端SDK,可以不設(shè)置這個(gè)flag,因?yàn)橄鄬?duì)其他代碼來(lái)說(shuō),生成過(guò)程比較耗時(shí) - --doc:表示生成OpenAPI3.0規(guī)范的json文檔 這行命令是筆者常用的命令,推薦大家也這樣使用。并且這行命令可以在每次修改了svc.go文件里的接口定義以后執(zhí)行,可以增量的生成代碼。規(guī)則是:
- handler.go文件和OpenAPI3.0規(guī)范的json文檔總是會(huì)重新生成
- handlerimpl.go文件和svcimpl.go文件只會(huì)增量生成,不會(huì)修改現(xiàn)有代碼
- 其他文件都會(huì)先判斷同名文件是否存在,如果存在就跳過(guò)
為了確保依賴(lài)都已經(jīng)下載下來(lái),最好再執(zhí)行一下這個(gè)命令:
go mod tidy
我們來(lái)看一下此時(shí)的項(xiàng)目結(jié)構(gòu):
? usersvc git:(master) ? ll total 296 -rw-r--r-- 1 wubin1989 staff 707B 10 24 20:05 Dockerfile drwxr-xr-x 3 wubin1989 staff 96B 10 24 23:10 client drwxr-xr-x 3 wubin1989 staff 96B 10 24 23:10 cmd drwxr-xr-x 3 wubin1989 staff 96B 10 24 23:10 config drwxr-xr-x 3 wubin1989 staff 96B 10 24 23:10 db -rw-r--r-- 1 wubin1989 staff 514B 10 24 23:10 go.mod -rw-r--r-- 1 wubin1989 staff 115K 10 24 23:10 go.sum -rw-r--r-- 1 wubin1989 staff 1.7K 10 24 23:21 svc.go -rw-r--r-- 1 wubin1989 staff 1.6K 10 25 09:18 svcimpl.go drwxr-xr-x 3 wubin1989 staff 96B 10 24 23:10 transport -rwxr-xr-x 1 wubin1989 staff 5.9K 10 25 09:18 usersvc_openapi3.go -rwxr-xr-x 1 wubin1989 staff 5.7K 10 25 09:18 usersvc_openapi3.json drwxr-xr-x 3 wubin1989 staff 96B 10 24 23:07 vo
- Dockerfile文件:用于打包docker鏡像
- client包:生成的go客戶(hù)端代碼
- cmd包:里面有main.go文件,用于啟動(dòng)服務(wù)
- config包:用于加載配置
- db包:用于連接數(shù)據(jù)庫(kù)
- svc.go文件:設(shè)計(jì)接口
- svcimpl.go文件:里面有mock的接口實(shí)現(xiàn),后續(xù)在里面根據(jù)業(yè)務(wù)需求編寫(xiě)真實(shí)的業(yè)務(wù)邏輯
- transport包:里面是http handler接口和實(shí)現(xiàn),負(fù)責(zé)具體的接口入?yún)⒔馕龊统鰠⑿蛄谢?/li>
- usersvc_openapi3.go文件:用于在線接口文檔功能
- usersvc_openapi3.json文件:遵循OpenAPI 3.0規(guī)范的接口文檔
- vo包:里面是接口的入?yún)⒑统鰠⒔Y(jié)構(gòu)體類(lèi)型
啟動(dòng)服務(wù)
go-doudou svc run
我們可以看到如下輸出:
? usersvc git:(master) ? go-doudou svc run
INFO[2021-12-28 22:39:35] Initializing logging reporter
INFO[2021-12-28 22:39:35] ================ Registered Routes ================
INFO[2021-12-28 22:39:35] +----------------------+--------+-------------------------+
INFO[2021-12-28 22:39:35] | NAME | METHOD | PATTERN |
INFO[2021-12-28 22:39:35] +----------------------+--------+-------------------------+
INFO[2021-12-28 22:39:35] | PageUsers | POST | /page/users |
INFO[2021-12-28 22:39:35] | User | GET | /user |
INFO[2021-12-28 22:39:35] | PublicSignUp | POST | /public/sign/up |
INFO[2021-12-28 22:39:35] | PublicLogIn | POST | /public/log/in |
INFO[2021-12-28 22:39:35] | UploadAvatar | POST | /upload/avatar |
INFO[2021-12-28 22:39:35] | PublicDownloadAvatar | GET | /public/download/avatar |
INFO[2021-12-28 22:39:35] | GetDoc | GET | /go-doudou/doc |
INFO[2021-12-28 22:39:35] | GetOpenAPI | GET | /go-doudou/openapi.json |
INFO[2021-12-28 22:39:35] | Prometheus | GET | /go-doudou/prometheus |
INFO[2021-12-28 22:39:35] | GetRegistry | GET | /go-doudou/registry |
INFO[2021-12-28 22:39:35] +----------------------+--------+-------------------------+
INFO[2021-12-28 22:39:35] ===================================================
INFO[2021-12-28 22:39:35] Started in 233.424µs
INFO[2021-12-28 22:39:35] Http server is listening on :6060
當(dāng)出現(xiàn)"Http server is listening on :6060"時(shí),表示服務(wù)已經(jīng)啟動(dòng),并且我們已經(jīng)有了mock的服務(wù)接口實(shí)現(xiàn)。例如,我們可以執(zhí)行如下命令請(qǐng)求/user接口,看看返回什么數(shù)據(jù):
? usersvc git:(master) ? http http://localhost:6060/user
HTTP/1.1 200 OK
Content-Encoding: gzip
Content-Length: 109
Content-Type: application/json; charset=UTF-8
Date: Mon, 01 Nov 2021 15:21:10 GMT
Vary: Accept-Encoding
{
"data": {
"Dept": "ZkkCmcLU",
"Id": -1941954111002502016,
"Name": "aiMtQ",
"Phone": "XMAqXf"
}
}
此時(shí)你可能注意到返回的數(shù)據(jù)的字段名稱(chēng)是首字母大寫(xiě)的,這可能不是你想要的。在vo包下的vo.go文件里有一行go generate命令:
//go:generate go-doudou name --file $GOFILE
這行命令里用到了go-doudou框架內(nèi)置的一個(gè)工具name。它可以根據(jù)指定的命名規(guī)則生成結(jié)構(gòu)體字段后面的json標(biāo)簽。默認(rèn)生成策略是首字母小寫(xiě)的駝峰命名策略,同時(shí)支持蛇形命名。未導(dǎo)出的字段會(huì)跳過(guò),只修改導(dǎo)出字段的json標(biāo)簽。命令行執(zhí)行命令:
go generate ./...
然后重啟服務(wù),請(qǐng)求/user接口,可以看到字段名稱(chēng)已經(jīng)變成首字母小寫(xiě)的駝峰命名了。
? usersvc git:(master) ? http http://localhost:6060/user
HTTP/1.1 200 OK
Content-Encoding: gzip
Content-Length: 114
Content-Type: application/json; charset=UTF-8
Date: Tue, 02 Nov 2021 08:25:39 GMT
Vary: Accept-Encoding
{
"data": {
"dept": "wGAEEeveHp",
"id": -816946940349962228,
"name": "hquwOKl",
"phone": "AriWmKYB"
}
}
關(guān)于name工具的更多用法,請(qǐng)參考文檔。 此時(shí),因?yàn)関o包里的結(jié)構(gòu)體修改了json標(biāo)簽,所以O(shè)penAPI文檔需要重新生成,否則在線文檔里的字段名稱(chēng)還是修改前的。需要執(zhí)行如下命令:
go-doudou svc http --doc
然后我們重啟一下服務(wù),在地址欄輸入http://localhost:6060/go-doudou/doc, 再輸入http basic用戶(hù)名admin,密碼admin,看一下在線文檔是什么效果:


在線文檔里的接口說(shuō)明和參數(shù)說(shuō)明都取自svc.go的接口方法注釋和參數(shù)注釋。
數(shù)據(jù)庫(kù)和表結(jié)構(gòu)準(zhǔn)備
為了支持中文字符,需先在根目錄下創(chuàng)建mysql配置文件my/custom.cnf,貼進(jìn)去如下內(nèi)容:
[client] default-character-set=utf8mb4 [mysql] default-character-set=utf8mb4 [mysqld] character_set_server=utf8mb4 collation-server=utf8mb4_general_ci default-authentication-plugin=mysql_native_password init_connect='SET NAMES utf8mb4'
在根目錄下創(chuàng)建數(shù)據(jù)庫(kù)初始化腳本sqlscripts/init.sql,貼進(jìn)去如下內(nèi)容:
CREATE SCHEMA `tutorial` DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci;
CREATE TABLE `tutorial`.`t_user`
(
`id` INT NOT NULL AUTO_INCREMENT,
`username` VARCHAR(45) NOT NULL COMMENT '用戶(hù)名',
`password` VARCHAR(60) NOT NULL COMMENT '密碼',
`name` VARCHAR(45) NOT NULL COMMENT '真實(shí)姓名',
`phone` VARCHAR(45) NOT NULL COMMENT '手機(jī)號(hào)',
`dept` VARCHAR(45) NOT NULL COMMENT '所屬部門(mén)',
`create_at` DATETIME NULL DEFAULT current_timestamp,
`update_at` DATETIME NULL DEFAULT current_timestamp on update current_timestamp,
`delete_at` DATETIME NULL,
PRIMARY KEY (`id`)
);
INSERT INTO tutorial.t_user (id, username, password, name, phone, dept, create_at, update_at, delete_at, avatar) VALUES (2, 'peter', '$2a$14$VaQLa/GbLAhRZvvTlgE8OOQgsBY4RDAJC5jkz13kjP9RlntdKBZVW', '張三豐', '13552053960', '技術(shù)部', '2021-12-28 06:41:00', '2021-12-28 14:59:20', null, 'out/wolf-wolves-snow-wolf-landscape-985ca149f06cd03b9f0ed8dfe326afdb.jpg');
INSERT INTO tutorial.t_user (id, username, password, name, phone, dept, create_at, update_at, delete_at, avatar) VALUES (4, 'john', '$2a$14$AKCs.u9vFUOCe5VwcmdfwOAkeiDtQYEgIB/nSU8/eemYwd91.qU.i', '李世民', '13552053961', '行政部', '2021-12-28 12:12:32', '2021-12-28 14:59:20', null, '');
INSERT INTO tutorial.t_user (id, username, password, name, phone, dept, create_at, update_at, delete_at, avatar) VALUES (5, 'lucy', '$2a$14$n0.l54axUqnKGagylQLu7ee.yDrtLubxzM1qmOaHK9Ft2P09YtQUS', '朱元璋', '13552053962', '銷(xiāo)售部', '2021-12-28 12:13:17', '2021-12-28 14:59:20', null, '');
INSERT INTO tutorial.t_user (id, username, password, name, phone, dept, create_at, update_at, delete_at, avatar) VALUES (6, 'jack', '$2a$14$jFCwiZHcD7.DL/teao.Dl.HAFwk8wM2f1riH1fG2f52WYKqSiGZlC', '張無(wú)忌', '', '總裁辦', '2021-12-28 12:14:19', '2021-12-28 14:59:20', null, '');
在根目錄下創(chuàng)建docker-compose.yml文件,貼進(jìn)入如下內(nèi)容:
version: '3.9'
services:
db:
container_name: db
image: mysql:5.7
restart: always
environment:
MYSQL_ROOT_PASSWORD: 1234
ports:
- 3306:3306
volumes:
- $PWD/my:/etc/mysql/conf.d
- $PWD/sqlscripts:/docker-entrypoint-initdb.d
networks:
- tutorial
networks:
tutorial:
driver: bridge
在根目錄下執(zhí)行docker compose命令,即可啟動(dòng)mysql數(shù)據(jù)庫(kù)容器:
docker-compose -f docker-compose.yml up -d
可以通過(guò)docker ps命令查看正在運(yùn)行的容器
? usersvc git:(master) ? docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES df6af6362c41 mysql:5.7 "docker-entrypoint.s…" 13 minutes ago Up 13 minutes 0.0.0.0:3306->3306/tcp, :::3306->3306/tcp, 33060/tcp db
生成domain和dao層代碼
因?yàn)槲覀兂跏蓟膕chema名稱(chēng)是tutorial,所以我們先要把.env文件里的環(huán)境變量DB_SCHEMA的值改成tutorial
DB_SCHEMA=tutorial
執(zhí)行如下命令,生成domain和dao層代碼:
go-doudou ddl -r --dao --pre=t_
解釋一下:
- -r:表示從數(shù)據(jù)庫(kù)表結(jié)構(gòu)生成go結(jié)構(gòu)體
- --dao:表示生成dao層代碼
- --pre:表示表名稱(chēng)有前綴t_ 此時(shí),你可以看到項(xiàng)目里多了兩個(gè)目錄:

具體用法請(qǐng)參考ddl文檔 這里我們看一下dao/base.go文件里提供了哪些CRUD方法,后面實(shí)現(xiàn)具體業(yè)務(wù)邏輯的時(shí)候會(huì)用到:
package dao
import (
"context"
"github.com/unionj-cloud/go-doudou/ddl/query"
)
type Base interface {
Insert(ctx context.Context, data interface{}) (int64, error)
Upsert(ctx context.Context, data interface{}) (int64, error)
UpsertNoneZero(ctx context.Context, data interface{}) (int64, error)
DeleteMany(ctx context.Context, where query.Q) (int64, error)
Update(ctx context.Context, data interface{}) (int64, error)
UpdateNoneZero(ctx context.Context, data interface{}) (int64, error)
UpdateMany(ctx context.Context, data interface{}, where query.Q) (int64, error)
UpdateManyNoneZero(ctx context.Context, data interface{}, where query.Q) (int64, error)
Get(ctx context.Context, id interface{}) (interface{}, error)
SelectMany(ctx context.Context, where ...query.Q) (interface{}, error)
CountMany(ctx context.Context, where ...query.Q) (int, error)
PageMany(ctx context.Context, page query.Page, where ...query.Q) (query.PageRet, error)
}
再修改一下svcimpl.go文件的UsersvcImpl結(jié)構(gòu)體
type UsersvcImpl struct {
conf *config.Config
db *sqlx.DB
}
以及NewUsersvc方法
func NewUsersvc(conf *config.Config, db *sqlx.DB) Usersvc {
return &UsersvcImpl{
conf,
db,
}
}
生成的main方法里已經(jīng)為我們注入了mysql連接實(shí)例,所以不用改
svc := service.NewUsersvc(conf, conn)
后面我們直接在接口實(shí)現(xiàn)里面調(diào)用UsersvcImpl結(jié)構(gòu)體的db屬性即可
用戶(hù)注冊(cè)接口
修改domain
因?yàn)橥ǔ?lái)說(shuō)用戶(hù)名都必須是唯一的,所以我們需要改一下domain/user.go文件:
Username string `dd:"type:varchar(45);extra:comment '用戶(hù)名';unique"`
再執(zhí)行ddl命令
go-doudou ddl --pre=t_
這行命令沒(méi)有-r參數(shù)了,表示從go結(jié)構(gòu)體更新到表結(jié)構(gòu)。

PublicSignUp方法實(shí)現(xiàn)
要實(shí)現(xiàn)注冊(cè)邏輯,我們需要先給dao層代碼加一個(gè)方法CheckUsernameExists,判斷一下傳進(jìn)來(lái)的用戶(hù)名是否已經(jīng)被注冊(cè)。先改一下dao/userdao.go文件
package dao
import "context"
type UserDao interface {
Base
CheckUsernameExists(ctx context.Context, username string) (bool, error)
}
再新建一個(gè)文件dao/userdaoimplext.go文件,加入如下代碼
package dao
import (
"context"
"github.com/unionj-cloud/go-doudou/ddl/query"
"usersvc/domain"
)
func (receiver UserDaoImpl) CheckUsernameExists(ctx context.Context, username string) (bool, error) {
many, err := receiver.SelectMany(ctx, query.C().Col("username").Eq(username))
if err != nil {
return false, err
}
users := many.([]domain.User)
if len(users) > 0 {
return true, nil
}
return false, nil
}
這樣就實(shí)現(xiàn)了對(duì)生成的dao層代碼的自定義擴(kuò)展。以后如果user實(shí)體的字段新增或者減少,只需要?jiǎng)h除userdaosql.go文件,再次執(zhí)行go-doudou ddl --dao --pre=t_命令,重新生成userdaosql.go文件即可,已存在的dao層文件不會(huì)被修改。 然后就是SignUp方法的具體實(shí)現(xiàn)了
func (receiver *UsersvcImpl) PublicSignUp(ctx context.Context, username string, password string, code string) (data string, err error) {
hashPassword, _ := lib.HashPassword(password)
userDao := dao.NewUserDao(receiver.db)
var exists bool
exists, err = userDao.CheckUsernameExists(ctx, username)
if err != nil {
panic(err)
}
if exists {
panic(lib.ErrUsernameExists)
}
_, err = userDao.Insert(ctx, domain.User{
Username: username,
Password: hashPassword,
})
if err != nil {
panic(err)
}
return "OK", nil
}
遇到報(bào)錯(cuò),可以直接panic,也可以return "", lib.ErrUsernameExists。因?yàn)橐呀?jīng)加了ddhttp.Recover中間件,可以自動(dòng)從panic里恢復(fù),并返回錯(cuò)誤信息給前端。需要注意的是,http狀態(tài)碼為500,不是200。只要從接口方法里返回了error類(lèi)型的參數(shù),生成的http handler代碼里默認(rèn)設(shè)置的http狀態(tài)碼就是500。如果想自定義修改默認(rèn)生成的http handler里的代碼,是完全可以的。當(dāng)有接口定義新增或者修改的時(shí)候,再次執(zhí)行命令go-doudou svc http --handler -c go --doc不會(huì)覆蓋已存在的代碼,只會(huì)增量生成代碼。
Postman測(cè)試
測(cè)試一下接口,這是第一次請(qǐng)求

這是第二次請(qǐng)求

用戶(hù)登錄接口
PublicLogIn方法實(shí)現(xiàn)
func (receiver *UsersvcImpl) PublicLogIn(ctx context.Context, username string, password string) (data string, err error) {
userDao := dao.NewUserDao(receiver.db)
many, err := userDao.SelectMany(ctx, query.C().Col("username").Eq(username).And(query.C().Col("delete_at").IsNull()))
if err != nil {
return "", err
}
users := many.([]domain.User)
if len(users) == 0 || !lib.CheckPasswordHash(password, users[0].Password) {
panic(lib.ErrUsernameOrPasswordIncorrect)
}
now := time.Now()
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"userId": users[0].Id,
"exp": now.Add(10 * time.Minute).Unix(),
//"iat": now.Unix(),
//"nbf": now.Unix(),
})
return token.SignedString(receiver.conf.JWTConf.Secret)
}
這段代碼的邏輯是先根據(jù)入?yún)sername查出來(lái)數(shù)據(jù)庫(kù)中的用戶(hù),如果沒(méi)查到或者密碼不對(duì),返回“用戶(hù)名或密碼錯(cuò)誤”的報(bào)錯(cuò),如果密碼對(duì)了,則簽發(fā)token返回。用的jwt庫(kù)是golang-jwt/jwt。
Postman測(cè)試

上傳頭像接口
修改domain
表里面少了一個(gè)avatar字段,現(xiàn)在我們加上:
Avatar string `dd:"type:varchar(255);extra:comment '用戶(hù)頭像'"`
因?yàn)槭切略隽俗侄危砸葎h除dao/userdaosql.go文件,再執(zhí)行ddl命令
go-doudou ddl --dao --pre=t_
如果增刪的字段比較多,涉及多個(gè)實(shí)體,可以通過(guò)如下命令一次刪掉所有*sql.go文件,再重新生成
rm -rf dao/*sql.go
修改.env配置
加入三行配置。JWT_為前綴的是JWT token校驗(yàn)相關(guān)的配置。Biz_為前綴的是實(shí)際業(yè)務(wù)相關(guān)的配置。
JWT_SECRET=secret JWT_IGNORE_URL=/public/sign/up,/public/log/in,/public/get/download/avatar,/public/** BIZ_OUTPUT=out
JWT_IGNORE_URL的值設(shè)置成/public/**就可以了,我都列上,是想說(shuō)明這里同時(shí)支持通配符匹配和完整匹配。 同時(shí),config/config.go文件也需要相應(yīng)的改動(dòng)。當(dāng)然也可以直接調(diào)用os.Getenv方法。
package config
import (
"github.com/kelseyhightower/envconfig"
"github.com/sirupsen/logrus"
)
type Config struct {
DbConf DbConfig
JWTConf JWTConf
BizConf BizConf
}
type BizConf struct {
Output string
}
type JWTConf struct {
Secret []byte
IgnoreUrl []string `split_words:"true"`
}
type DbConfig struct {
Driver string `default:"mysql"`
Host string `default:"localhost"`
Port string `default:"3306"`
User string
Passwd string
Schema string
Charset string `default:"utf8mb4"`
}
func LoadFromEnv() *Config {
var dbconf DbConfig
err := envconfig.Process("db", &dbconf)
if err != nil {
logrus.Panicln("Error processing env", err)
}
var jwtConf JWTConf
err = envconfig.Process("jwt", &jwtConf)
if err != nil {
logrus.Panicln("Error processing env", err)
}
var bizConf BizConf
err = envconfig.Process("biz", &bizConf)
if err != nil {
logrus.Panicln("Error processing env", err)
}
return &Config{
dbconf,
jwtConf,
bizConf,
}
}
JWT校驗(yàn)中間件
因?yàn)間o-doudou的http router采用的是gorilla/mux,所以與gorilla/mux的middleware是完全兼容的,自定義中間件的寫(xiě)法也是完全一樣的。
package middleware
import (
"context"
"fmt"
"github.com/dgrijalva/jwt-go"
"github.com/gobwas/glob"
"net/http"
"os"
"strings"
)
type ctxKey int
const userIdKey ctxKey = ctxKey(0)
func NewContext(ctx context.Context, id int) context.Context {
return context.WithValue(ctx, userIdKey, id)
}
func FromContext(ctx context.Context) (int, bool) {
userId, ok := ctx.Value(userIdKey).(int)
return userId, ok
}
func Jwt(inner http.Handler) http.Handler {
g := glob.MustCompile(fmt.Sprintf("{%s}", os.Getenv("JWT_IGNORE_URL")))
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if g.Match(r.RequestURI) {
inner.ServeHTTP(w, r)
return
}
authHeader := r.Header.Get("Authorization")
tokenString := strings.TrimSpace(strings.TrimPrefix(authHeader, "Bearer "))
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
}
return []byte(os.Getenv("JWT_SECRET")), nil
})
if err != nil || !token.Valid {
w.WriteHeader(401)
w.Write([]byte("Unauthorised.\n"))
return
}
claims := token.Claims.(jwt.MapClaims)
if userId, exists := claims["userId"]; !exists {
w.WriteHeader(401)
w.Write([]byte("Unauthorised.\n"))
return
} else {
inner.ServeHTTP(w, r.WithContext(NewContext(r.Context(), int(userId.(float64)))))
}
})
}
UploadAvatar方法實(shí)現(xiàn)
func (receiver *UsersvcImpl) UploadAvatar(ctx context.Context, avatar *v3.FileModel) (data string, err error) {
defer avatar.Close()
_ = os.MkdirAll(receiver.conf.BizConf.Output, os.ModePerm)
out := filepath.Join(receiver.conf.BizConf.Output, avatar.Filename)
var f *os.File
f, err = os.OpenFile(out, os.O_WRONLY|os.O_CREATE, os.ModePerm)
if err != nil {
panic(err)
}
defer f.Close()
_, err = io.Copy(f, avatar.Reader)
if err != nil {
panic(err)
}
userId, _ := middleware.FromContext(ctx)
userDao := dao.NewUserDao(receiver.db)
_, err = userDao.UpdateNoneZero(ctx, domain.User{
Id: userId,
Avatar: out,
})
if err != nil {
panic(err)
}
return "OK", nil
}
這里需要注意的是,defer avatar.Close()這行代碼一定要盡早寫(xiě)上,這是釋放文件描述符資源的代碼。
下載頭像接口
GetPublicDownloadAvatar方法實(shí)現(xiàn)
func (receiver *UsersvcImpl) GetPublicDownloadAvatar(ctx context.Context, userId int) (data *os.File, err error) {
userDao := dao.NewUserDao(receiver.db)
var get interface{}
get, err = userDao.Get(ctx, userId)
if err != nil {
panic(err)
}
return os.Open(get.(domain.User).Avatar)
}
用戶(hù)詳情接口
GetUser方法實(shí)現(xiàn)
func (receiver *UsersvcImpl) GetUser(ctx context.Context, userId int) (data vo.UserVo, err error) {
userDao := dao.NewUserDao(receiver.db)
var get interface{}
get, err = userDao.Get(ctx, userId)
if err != nil {
panic(err)
}
user := get.(domain.User)
return vo.UserVo{
Id: user.Id,
Username: user.Username,
Name: user.Name,
Phone: user.Phone,
Dept: user.Dept,
}, nil
}
Postman測(cè)試

用戶(hù)分頁(yè)接口
導(dǎo)入測(cè)試數(shù)據(jù)
INSERT INTO tutorial.t_user (id, username, password, name, phone, dept, create_at, update_at, delete_at, avatar) VALUES (2, 'peter', '$2a$14$VaQLa/GbLAhRZvvTlgE8OOQgsBY4RDAJC5jkz13kjP9RlntdKBZVW', '張三豐', '13552053960', '技術(shù)部', '2021-12-28 06:41:00', '2021-12-28 14:59:20', null, 'out/wolf-wolves-snow-wolf-landscape-985ca149f06cd03b9f0ed8dfe326afdb.jpg'); INSERT INTO tutorial.t_user (id, username, password, name, phone, dept, create_at, update_at, delete_at, avatar) VALUES (4, 'john', '$2a$14$AKCs.u9vFUOCe5VwcmdfwOAkeiDtQYEgIB/nSU8/eemYwd91.qU.i', '李世民', '13552053961', '行政部', '2021-12-28 12:12:32', '2021-12-28 14:59:20', null, ''); INSERT INTO tutorial.t_user (id, username, password, name, phone, dept, create_at, update_at, delete_at, avatar) VALUES (5, 'lucy', '$2a$14$n0.l54axUqnKGagylQLu7ee.yDrtLubxzM1qmOaHK9Ft2P09YtQUS', '朱元璋', '13552053962', '銷(xiāo)售部', '2021-12-28 12:13:17', '2021-12-28 14:59:20', null, ''); INSERT INTO tutorial.t_user (id, username, password, name, phone, dept, create_at, update_at, delete_at, avatar) VALUES (6, 'jack', '$2a$14$jFCwiZHcD7.DL/teao.Dl.HAFwk8wM2f1riH1fG2f52WYKqSiGZlC', '張無(wú)忌', '', '總裁辦', '2021-12-28 12:14:19', '2021-12-28 14:59:20', null, '');
PageUsers方法實(shí)現(xiàn)
func (receiver *UsersvcImpl) PageUsers(ctx context.Context, pageQuery vo.PageQuery) (data vo.PageRet, err error) {
userDao := dao.NewUserDao(receiver.db)
var q query.Q
q = query.C().Col("delete_at").IsNull()
if stringutils.IsNotEmpty(pageQuery.Filter.Name) {
q = q.And(query.C().Col("name").Like(fmt.Sprintf(`%s%%`, pageQuery.Filter.Name)))
}
if stringutils.IsNotEmpty(pageQuery.Filter.Dept) {
q = q.And(query.C().Col("dept").Eq(pageQuery.Filter.Dept))
}
var page query.Page
if len(pageQuery.Page.Orders) > 0 {
for _, item := range pageQuery.Page.Orders {
page = page.Order(query.Order{
Col: item.Col,
Sort: sortenum.Sort(item.Sort),
})
}
}
if pageQuery.Page.PageNo == 0 {
pageQuery.Page.PageNo = 1
}
page = page.Limit((pageQuery.Page.PageNo-1)*pageQuery.Page.Size, pageQuery.Page.Size)
var ret query.PageRet
ret, err = userDao.PageMany(ctx, page, q)
if err != nil {
panic(err)
}
var items []vo.UserVo
for _, item := range ret.Items.([]domain.User) {
var userVo vo.UserVo
_ = copier.DeepCopy(item, &userVo)
items = append(items, userVo)
}
data = vo.PageRet{
Items: items,
PageNo: ret.PageNo,
PageSize: ret.PageSize,
Total: ret.Total,
HasNext: ret.HasNext,
}
return data, nil
}
Postman測(cè)試

服務(wù)部署
最后介紹一下docker-compose部署服務(wù) 首先修改Dockerfile
FROM golang:1.16.6-alpine AS builder
ENV GO111MODULE=on
ARG user
ENV HOST_USER=$user
ENV GOPROXY=https://goproxy.cn,direct
WORKDIR /repo
ADD go.mod .
ADD go.sum .
ADD . ./
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
RUN apk add --no-cache bash tzdata
ENV TZ="Asia/Shanghai"
RUN go mod vendor
RUN export GDD_VER=$(go list -mod=vendor -m -f '{{ .Version }}' github.com/unionj-cloud/go-doudou) && \
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -v -ldflags="-X 'github.com/unionj-cloud/go-doudou/svc/config.BuildUser=$HOST_USER' -X 'github.com/unionj-cloud/go-doudou/svc/config.BuildTime=$(date)' -X 'github.com/unionj-cloud/go-doudou/svc/config.GddVer=$GDD_VER'" -mod vendor -o api cmd/main.go
ENTRYPOINT ["/repo/api"]
然后修改docker-compose.yml
version: '3.9'
services:
db:
container_name: db
image: mysql:5.7
restart: always
environment:
MYSQL_ROOT_PASSWORD: 1234
ports:
- 3306:3306
volumes:
- $PWD/my:/etc/mysql/conf.d
- $PWD/sqlscripts:/docker-entrypoint-initdb.d
networks:
- tutorial
usersvc:
container_name: usersvc
build:
context: .
environment:
- GDD_BANNER=off
- GDD_PORT=6060
- DB_HOST=db
expose:
- "6060"
ports:
- "6060:6060"
networks:
- tutorial
depends_on:
- db
networks:
tutorial:
driver: bridge
最后執(zhí)行命令
docker-compose -f docker-compose.yml up -d
如果usersvc容器沒(méi)有啟動(dòng)成功,可能是因?yàn)閐b容器還沒(méi)有完全啟動(dòng),可以多執(zhí)行幾遍上面的命令。
總結(jié)
到這里,我們達(dá)到了全部的學(xué)習(xí)目標(biāo),也實(shí)現(xiàn)了需求清單中的全部接口。教程的全部源碼都在這里。
以上就是go doudou開(kāi)發(fā)單體RESTful服務(wù)快速上手教程的詳細(xì)內(nèi)容,更多關(guān)于go doudou單體RESTful服務(wù)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- go?doudou應(yīng)用中使用枚舉類(lèi)型教程示例
- go?doudou應(yīng)用中使用注解示例詳解
- go?doudou開(kāi)發(fā)gRPC服務(wù)快速上手實(shí)現(xiàn)詳解
- Golang?gRPC?HTTP協(xié)議轉(zhuǎn)換示例
- Go Grpc Gateway兼容HTTP協(xié)議文檔自動(dòng)生成網(wǎng)關(guān)
- Go?gRPC進(jìn)階教程gRPC轉(zhuǎn)換HTTP
- Go?gRPC服務(wù)proto數(shù)據(jù)驗(yàn)證進(jìn)階教程
- Go?gRPC服務(wù)進(jìn)階middleware使用教程
相關(guān)文章
Go語(yǔ)言中關(guān)于set的實(shí)現(xiàn)思考分析
Go?開(kāi)發(fā)過(guò)程中有時(shí)我們需要集合(set)這種容器,但?Go?本身未內(nèi)置這種數(shù)據(jù)容器,故常常我們需要自己實(shí)現(xiàn),下面我們就來(lái)看看具體有哪些實(shí)現(xiàn)方法吧2024-01-01
gorm update傳入struct對(duì)象,零值字段不更新的解決方案
這篇文章主要介紹了gorm update傳入struct對(duì)象,零值字段不更新的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-04-04
初探Golang數(shù)據(jù)結(jié)構(gòu)之Slice的使用
在學(xué)習(xí)Go語(yǔ)言時(shí),一直對(duì)數(shù)組和切片的使用場(chǎng)景好奇,不明白為什么推薦使用切片來(lái)代替數(shù)組,所以本文就來(lái)和大家梳理一下Slice切片的相關(guān)知識(shí)吧2023-09-09
Go語(yǔ)言斷言和類(lèi)型查詢(xún)的實(shí)現(xiàn)
Go語(yǔ)言變量類(lèi)型包含基礎(chǔ)類(lèi)型和復(fù)合類(lèi)型,類(lèi)型斷言一般是對(duì)基礎(chǔ)類(lèi)型的處理,本文主要介紹了Go語(yǔ)言斷言和類(lèi)型查詢(xún)的實(shí)現(xiàn),感興趣的可以了解一下2024-01-01
Go語(yǔ)言中比較兩個(gè)map[string]interface{}是否相等
本文主要介紹了Go語(yǔ)言中比較兩個(gè)map[string]interface{}是否相等,我們可以將其轉(zhuǎn)化成順序一樣的 slice ,然后再轉(zhuǎn)化未json,具有一定的參考價(jià)值,感興趣的可以了解一下2023-08-08

