Skip to content

Commit f11a937

Browse files
committed
[init] first commit
0 parents  commit f11a937

22 files changed

+1715
-0
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.idea

README.md

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# gin-restful-best-practice
2+
该项目以最简单易懂的框架向大家展示了我在开发gin web服务的过程中的一些最佳实践。
3+
4+
数据实体:用户(user)、深度学习模型(model)
5+
场景:用户可以上传深度学习模型、下载深度学习模型。
6+
7+
涉及到的技术有:
8+
- [x] GIN
9+
- [x] GORM & PostgreSQL
10+
- [x] JWT
11+
- [x] gRPC (与python机器学习服务通过rpc通信)
12+
- [ ] Travis-ci
13+
- [ ] and more…
14+
15+
## server.go
16+
## conf
17+
### config.go
18+
存放所有服务器配置,环境分为`dev, test, prod`,根据环境变量`ENV`的不同会调用不同的函数来初始化配置,部分配置也可以从环境变量中读取。
19+
20+
由loadConfig函数载入配置,配置由`conf.Conf()`读取,使用了`sync.Once`保证配置只被初始化一次。
21+
22+
## controllers
23+
存放各接口的逻辑以及单元测试(注意,该路径下的单元测试会绕过中间件,这其实是正确的,中间件的测试就应该放在所属文件夹下测试)。
24+
### common.go
25+
一些controller中通用的函数,例如对错误的返回`ErrorResponse`、方便单元测试发送JSON网络请求的`testRequest`(参数通过`gin.H`类型传入,该函数会自动把参数转换为Body或Query)。
26+
### models.go
27+
28+
## middleware
29+
## models
30+
## photos
31+
## routes
32+
## services
33+
## utils
34+
35+
## Build Setup
36+
37+
```shell script
38+
# clone the project
39+
git clone https://github.com/Bingmang/gin-restful-best-practice.git
40+
41+
# enter the project directory
42+
cd gin-restful-best-practice
43+
44+
# install dependency
45+
go run server.go
46+
```
47+
48+
This will automatically open http://localhost:8000
49+
50+
## Build
51+
52+
```shell script
53+
go build server.go
54+
```
55+
56+
## gRPC
57+
58+
如果修改了proto文件需要重新编译pb文件,输入以下命令(要先安装protoc):
59+
https://github.com/protocolbuffers/protobuf/releases/tag/v3.12.3
60+
61+
```shell script
62+
export PATH="$PATH:$(go env GOPATH)/bin"
63+
protoc --go_out=plugins=grpc:. -I./protos ./protos/ml_service.proto
64+
```
65+
66+
## PostgreSQL initialization
67+
68+
```shell script
69+
docker run -d --name isp_test -p 5432:5432 postgres
70+
docker exec -it isp_test /bin/bash
71+
su postgres
72+
create user dev_user with password 'dev_password';
73+
create database dev_database owner dev_user;
74+
grant all on database dev_database to dev_user;
75+
```
76+
77+
## Deploy
78+
79+
```shell script
80+
go build server.go
81+
ENV=dev ./server
82+
ENV=test ./server
83+
ENV=prod ./server
84+
...
85+
```

conf/config.go

+103
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package conf
2+
3+
import (
4+
"fmt"
5+
"github.com/gin-gonic/gin"
6+
"log"
7+
"os"
8+
"strconv"
9+
"sync"
10+
)
11+
12+
var ENV = os.Getenv("ENV")
13+
var loadConfigOnce sync.Once
14+
15+
type Config struct {
16+
ENV string
17+
18+
// server
19+
HOST string
20+
PORT int
21+
URL string
22+
23+
// jwt
24+
JWT_ISSUER string
25+
JWT_SECRET string
26+
27+
// db
28+
DB_HOST string
29+
DB_PORT int
30+
DB_USER string
31+
DB_PASSWORD string
32+
DB_DATABASE string
33+
DB_CONN_STR string
34+
35+
// service
36+
ISP_ML_SERVICE_HOST string
37+
ISP_ML_SERVICE_PORT int
38+
ISP_ML_SERVICE_URL string
39+
}
40+
41+
var config Config
42+
43+
func Conf() Config {
44+
loadConfigOnce.Do(loadConfig)
45+
return config
46+
}
47+
48+
func dev(config *Config) {
49+
log.Println("Using dev environment, loading config...")
50+
config.DB_HOST = "localhost"
51+
config.DB_PORT = 5432
52+
config.DB_USER = "dev_user"
53+
config.DB_PASSWORD = "dev_password"
54+
config.DB_DATABASE = "dev_database"
55+
56+
config.JWT_ISSUER = "dev_issuer"
57+
config.JWT_SECRET = "dev_secret"
58+
59+
config.ISP_ML_SERVICE_HOST = "localhost"
60+
config.ISP_ML_SERVICE_PORT = 50051
61+
}
62+
63+
func test(config *Config) {
64+
log.Println("Using test environment, loading config...")
65+
panic("config: not implemented")
66+
}
67+
68+
func prod(config *Config) {
69+
log.Println("Using prod environment, loading config...")
70+
gin.SetMode(gin.ReleaseMode)
71+
panic("config: not implemented")
72+
}
73+
74+
func loadConfig() {
75+
config = Config{
76+
ENV: ENV,
77+
HOST: "localhost",
78+
PORT: 8000,
79+
}
80+
81+
switch ENV {
82+
case "test":
83+
test(&config)
84+
case "prod":
85+
prod(&config)
86+
default:
87+
config.ENV = "dev"
88+
dev(&config)
89+
}
90+
91+
if os.Getenv("HOST") != "" {
92+
config.HOST = os.Getenv("HOST")
93+
}
94+
if os.Getenv("PORT") != "" {
95+
config.PORT, _ = strconv.Atoi(os.Getenv("PORT"))
96+
}
97+
98+
config.URL = fmt.Sprintf("%s:%d", config.HOST, config.PORT)
99+
config.DB_CONN_STR = fmt.Sprintf(
100+
"host=%s port=%d user=%s password=%s dbname=%s sslmode=disable TimeZone=Asia/Shanghai",
101+
config.DB_HOST, config.DB_PORT, config.DB_USER, config.DB_PASSWORD, config.DB_DATABASE)
102+
config.ISP_ML_SERVICE_URL = fmt.Sprintf("%s:%d", config.ISP_ML_SERVICE_HOST, config.ISP_ML_SERVICE_PORT)
103+
}

controllers/common.go

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package controllers
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"fmt"
7+
"github.com/gin-gonic/gin"
8+
"net/http"
9+
"net/http/httptest"
10+
)
11+
12+
type testResponse struct {
13+
*httptest.ResponseRecorder
14+
Data gin.H
15+
}
16+
17+
func testRequest(controller func(*gin.Context), method, url string, params gin.H) *testResponse {
18+
response := httptest.NewRecorder()
19+
c, _ := gin.CreateTestContext(response)
20+
if method == "GET" {
21+
query := "?"
22+
first := true
23+
for k, v := range params {
24+
if first {
25+
first = false
26+
} else {
27+
query += "&"
28+
}
29+
query += fmt.Sprintf("%s=%v", k, v)
30+
}
31+
c.Request, _ = http.NewRequest(method, url+query, nil)
32+
} else {
33+
jsonBytes, _ := json.Marshal(params)
34+
c.Request, _ = http.NewRequest(method, url, bytes.NewBuffer(jsonBytes))
35+
}
36+
c.Request.Header.Set("Content-Type", "application/json")
37+
controller(c)
38+
var data gin.H
39+
_ = json.Unmarshal(response.Body.Bytes(), &data)
40+
return &testResponse{
41+
ResponseRecorder: response,
42+
Data: data,
43+
}
44+
}
45+
46+
func ErrorResponse(ctx *gin.Context, statusCode int, message string) {
47+
ctx.JSON(statusCode, gin.H{
48+
"error": message,
49+
})
50+
}

controllers/login.go

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package controllers
2+
3+
import (
4+
"github.com/gin-gonic/gin"
5+
"github.com/jinzhu/gorm"
6+
"gin-restful-best-practice/middlewares"
7+
"gin-restful-best-practice/models"
8+
"net/http"
9+
)
10+
11+
type LoginForm struct {
12+
Username string `json:"username" binding:"required,gte=2,lte=20"`
13+
Password string `json:"password" binding:"required"`
14+
}
15+
16+
func Login(ctx *gin.Context) {
17+
var form LoginForm
18+
if err := ctx.ShouldBindJSON(&form); err != nil {
19+
ErrorResponse(ctx, http.StatusBadRequest, err.Error())
20+
return
21+
}
22+
user, err := models.GetUserByUsername(form.Username)
23+
// invalid username or password
24+
if err == gorm.ErrRecordNotFound || user.Password != form.Password {
25+
ErrorResponse(ctx, http.StatusForbidden, "invalid username or password")
26+
return
27+
}
28+
// sql error
29+
if err != nil {
30+
ErrorResponse(ctx, http.StatusInternalServerError, err.Error())
31+
return
32+
}
33+
token, err := middlewares.CreateJWT(user)
34+
if err != nil {
35+
ErrorResponse(ctx, http.StatusInternalServerError, err.Error())
36+
return
37+
}
38+
ctx.JSON(http.StatusOK, gin.H{
39+
"token": "Bearer " + token,
40+
})
41+
}

controllers/models.go

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package controllers
2+
3+
import (
4+
"github.com/gin-gonic/gin"
5+
"gin-restful-best-practice/models"
6+
"gin-restful-best-practice/services"
7+
"net/http"
8+
)
9+
10+
type FetchModelListForm struct {
11+
Offset int `form:"offset" binding:"number"`
12+
Limit int `form:"limit" binding:"required,number"`
13+
UserID int `form:"user_id" binding:"number"`
14+
}
15+
16+
func FetchModelList(ctx *gin.Context) {
17+
var form FetchModelListForm
18+
if err := ctx.ShouldBindQuery(&form); err != nil {
19+
ErrorResponse(ctx, http.StatusBadRequest, err.Error())
20+
return
21+
}
22+
modelList, err := models.GetModelList(map[string]interface{}{
23+
"yn": true,
24+
}, form.Offset, form.Limit)
25+
if err != nil {
26+
ErrorResponse(ctx, http.StatusInternalServerError, err.Error())
27+
return
28+
}
29+
ctx.JSON(http.StatusOK, gin.H{
30+
"data": modelList,
31+
"total": len(modelList),
32+
"offset": form.Offset,
33+
"limit": form.Limit,
34+
})
35+
return
36+
}
37+
38+
type CreateModelForm struct {
39+
Name string `json:"name" binding:"required,gte=2,lte=20"`
40+
Type uint `json:"type" binding:"required,number"`
41+
URL string `json:"url"`
42+
Desc string `json:"desc"`
43+
PriceMonthly uint `json:"price_monthly" binding:"number"`
44+
PriceYearly uint `json:"price_yearly" binding:"number"`
45+
PriceTotal uint `json:"price_total" binding:"number"`
46+
UserID uint `json:"user_id" binding:"required,number"`
47+
}
48+
49+
func CreateModel(ctx *gin.Context) {
50+
var form CreateModelForm
51+
if err := ctx.ShouldBindJSON(&form); err != nil {
52+
ErrorResponse(ctx, http.StatusBadRequest, err.Error())
53+
return
54+
}
55+
model := models.Model{
56+
Name: form.Name,
57+
Type: form.Type,
58+
URL: form.URL,
59+
Desc: form.Desc,
60+
PriceMonthly: form.PriceMonthly,
61+
PriceYearly: form.PriceYearly,
62+
PriceTotal: form.PriceTotal,
63+
UserID: form.UserID,
64+
}
65+
if err := model.Insert(); err != nil {
66+
ErrorResponse(ctx, http.StatusInternalServerError, err.Error())
67+
return
68+
}
69+
ctx.JSON(http.StatusOK, model)
70+
}
71+
72+
func GetModelFile(ctx *gin.Context) {
73+
model, err := services.DownloadModel("name")
74+
if err != nil {
75+
ctx.JSON(http.StatusInternalServerError, gin.H{
76+
"error": err,
77+
})
78+
}
79+
ctx.JSON(http.StatusOK, gin.H{
80+
"model": model,
81+
})
82+
}

0 commit comments

Comments
 (0)