說在前面:
現(xiàn)在拿到offer超級(jí)難,甚至連面試電話,一個(gè)都搞不到。
尼恩的技術(shù)社群中(50+),很多小伙伴憑借 “左手云原生+右手大數(shù)據(jù)”的絕活,拿到了offer,并且是非常優(yōu)質(zhì)的offer,據(jù)說年終獎(jiǎng)都足足18個(gè)月。
第二個(gè)案例就是:前段時(shí)間,一個(gè)2年小伙伴希望漲薪到18K, 尼恩把GO 語言的項(xiàng)目架構(gòu),給他寫入了簡(jiǎn)歷,導(dǎo)致他的簡(jiǎn)歷金光閃閃,脫胎換骨,完全可以去拿頭條、騰訊等30K的offer, 年薪可以直接多 20W。
第二個(gè)案例就是:一個(gè)6年小伙伴憑借Java+go雙語言云原生架構(gòu),年薪60W。
從Java高薪崗位和就業(yè)崗位來看,云原生、K8S、GO 現(xiàn)在對(duì)于 高級(jí)工程師/架構(gòu)師 來說,越來越重要。
所以,尼恩從架構(gòu)師視角出發(fā),基于尼恩 3高架構(gòu)知識(shí)宇宙,寫一本《GO學(xué)習(xí)圣經(jīng)》,請(qǐng)到文末【技術(shù)自由圈】取。
《GO學(xué)習(xí)圣經(jīng)》已經(jīng)完成的內(nèi)容有:
Go學(xué)習(xí)圣經(jīng):0基礎(chǔ)精通GO開發(fā)與高并發(fā)架構(gòu)
Go學(xué)習(xí)圣經(jīng):隊(duì)列削峰+批量寫入 超高并發(fā)原理和實(shí)操
Go學(xué)習(xí)圣經(jīng):從0開始,精通Go語言Rest微服務(wù)架構(gòu)和開發(fā)
《GO學(xué)習(xí)圣經(jīng)》PDF的最終目標(biāo)
咱們的目標(biāo),不僅僅在于 GO 應(yīng)用編程自由,更在于 GO 架構(gòu)自由。
另外,前面尼恩的云原生是沒有涉及GO的,但是,沒有GO的云原生是不完整的。
所以, GO語言、GO架構(gòu)學(xué)習(xí)完了之后,咱們?cè)偃ゴ騻€(gè)回馬槍,完成云原生的第二部分: 《Istio + K8S CRD的架構(gòu)與開發(fā)實(shí)操》 , 幫助大家徹底穿透云原生。
業(yè)務(wù)CRUD的Restful API接口層的設(shè)計(jì)與開發(fā)
本文,圍繞一個(gè)簡(jiǎn)單的商品微服務(wù)CRUD案例進(jìn)行介紹。
業(yè)務(wù)CRUD的Restful API接口的設(shè)計(jì)
參照 CRUD的Restful API接口的設(shè)計(jì)規(guī)范,完成類似下面的CRUD接口設(shè)計(jì)。
功能 | HTTP 方法 | 路徑 |
---|---|---|
新增文章 | POST | /articles |
刪除指定文章 | DELETE | /articles/:id |
更新指定文章 | PUT | /articles/:id |
獲取指定文章 | GET | /articles/:id |
獲取文章列表 | GET | /articles |
增刪改查的 RESTful API 設(shè)計(jì)和編寫,在 RESTful API 中 HTTP 方法對(duì)應(yīng)的行為動(dòng)作分別如下:
- GET:讀取/檢索動(dòng)作。
- POST:新增/新建動(dòng)作。
- PUT:更新動(dòng)作,用于更新一個(gè)完整的資源,要求為冪等。
- PATCH:更新動(dòng)作,用于更新某一個(gè)資源的一個(gè)組成部分,也就是只需要更新該資源的某一項(xiàng),就應(yīng)該使用 PATCH 而不是 PUT,可以不冪等。
- DELETE:刪除動(dòng)作。
Restful API路由管理,類似于SpringMVC 控制器
Restful API路由管理,類似于SpringMVC 控制器。
Restful API路由管理,放在 internal/routers
目錄下,并新建 router.go 文件,代碼參考:
func NewRouter() *gin.Engine {
r := gin.New()
r.Use(gin.Logger())
r.Use(gin.Recovery())
article := v1.NewArticle()
apiv1 := r.Group("/api/v1")
{
apiv1.POST("/articles", article.Create)
apiv1.DELETE("/articles/:id", article.Delete)
apiv1.PUT("/articles/:id", article.Update)
apiv1.PATCH("/articles/:id/state", article.Update)
apiv1.GET("/articles/:id", article.Get)
apiv1.GET("/articles", article.List)
}
return r
}
Handler 處理器的設(shè)計(jì)和實(shí)現(xiàn)
Handler 處理器 ,對(duì)應(yīng)的就是 SpringMVC的 Controller。
Handler 處理器 的位置,這里是放在 internal/routers/api/v1
文件夾。
這里的Handler處理器 ,文件名稱叫做 article.go。
參考的代碼如下:
type Article struct{}
func NewArticle() Article {
return Article{}
}
func (a Article) Get(c *gin.Context) {}
func (a Article) List(c *gin.Context) {}
func (a Article) Create(c *gin.Context) {}
func (a Article) Update(c *gin.Context) {}
func (a Article) Delete(c *gin.Context) {}
啟動(dòng)Gin WEB服務(wù)器
在完成了模型、路由的代碼編寫后,修改 main.go 文件,把它改造為這個(gè)項(xiàng)目的啟動(dòng)文件,
修改代碼如下:
func main() {
router := routers.NewRouter()
s := &http.Server{
Addr: ":8080",
Handler: router,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20,
}
s.ListenAndServe()
}
我們通過自定義 http.Server
,設(shè)置了監(jiān)聽的 TCP Endpoint、處理的程序、允許讀取/寫入的最大時(shí)間、請(qǐng)求頭的最大字節(jié)數(shù)等基礎(chǔ)參數(shù),最后調(diào)用 ListenAndServe
方法開始監(jiān)聽。
業(yè)務(wù)CRUD的Restful API服務(wù)層的設(shè)計(jì)與開發(fā)
服務(wù)層的職責(zé)和功能,類似于SpringMVC 服務(wù)層。
服務(wù)層的基礎(chǔ)類
package service
import (
"context"
"crazymakercircle.com/gin-rest/common/global"
"crazymakercircle.com/gin-rest/internal/dao"
otgorm "github.com/eddycjy/opentracing-gorm"
)
type Service struct {
ctx context.Context
dao *dao.Dao
}
func New(ctx context.Context) Service {
svc := Service{ctx: ctx}
svc.dao = dao.New(global.DBEngine)
svc.dao = dao.New(otgorm.WithContext(svc.ctx, global.DBEngine))
return svc
}
基礎(chǔ)類封層了兩個(gè)對(duì)象:
- context.Context 類型的上下文對(duì)象
- dao.Dao 類型的 dao層 基礎(chǔ)對(duì)象
context.Context 類型的上下文對(duì)象
在 Go http包的Server中,每一個(gè)請(qǐng)求在都有一個(gè)對(duì)應(yīng)的 goroutine 去處理。
每一個(gè)請(qǐng)求handler處理函數(shù),很多的時(shí)候,也是需要并發(fā)處理的,往往會(huì)啟動(dòng)額外的 goroutine 用來訪問后端服務(wù),比如訪問數(shù)據(jù)庫、調(diào)用RPC服務(wù)。
雖然db協(xié)程、rpc協(xié)程和req協(xié)程都是并發(fā)執(zhí)行的。
但是,當(dāng)req協(xié)程請(qǐng)求被取消時(shí),所有用來處理該請(qǐng)求的 goroutine 都應(yīng)該迅速退出,然后系統(tǒng)迅速釋放這些 goroutine 占用的資源。
Context的原理
Go1.7加入了一個(gè)新的標(biāo)準(zhǔn)庫context,它定義了Context類型。
Context上下文 專門用來簡(jiǎn)化單個(gè)WEB請(qǐng)求的多個(gè) goroutine 之間與請(qǐng)求有關(guān)的數(shù)據(jù)、取消信號(hào)、截止時(shí)間等相關(guān)操作,這些操作可能涉及多個(gè) API 調(diào)用。
對(duì)服務(wù)器傳入的請(qǐng)求應(yīng)該創(chuàng)建上下文,而對(duì)服務(wù)器的傳出調(diào)用應(yīng)該接受上下文。
它們之間的函數(shù)調(diào)用鏈必須傳遞上下文,或者可以使用WithCancel、WithDeadline、WithTimeout或WithValue創(chuàng)建的派生上下文。
注意: 當(dāng)一個(gè)上下文被取消時(shí),它派生的所有上下文也被取消。
Context接口
context.Context是一個(gè)接口,該接口定義了四個(gè)需要實(shí)現(xiàn)的方法。具體簽名如下:
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
其中:
- Deadline方法需要返回當(dāng)前Context被取消的時(shí)間,也就是完成工作的截止時(shí)間(deadline);
- Done方法需要返回一個(gè)Channel,這個(gè)Channel會(huì)在當(dāng)前工作完成或者上下文被取消之后關(guān)閉,多次調(diào)用Done方法會(huì)返回同一個(gè)Channel;
- Err方法會(huì)返回當(dāng)前Context結(jié)束的原因,它只會(huì)在Done返回的Channel被關(guān)閉時(shí)才會(huì)返回非空的值;
- 如果當(dāng)前Context被取消就會(huì)返回Canceled錯(cuò)誤;
- 如果當(dāng)前Context超時(shí)就會(huì)返回DeadlineExceeded錯(cuò)誤;
- Value方法會(huì)從Context中返回鍵對(duì)應(yīng)的值,對(duì)于同一個(gè)上下文來說,多次調(diào)用Value 并傳入相同的Key會(huì)返回相同的結(jié)果,該方法僅用于傳遞跨API和進(jìn)程間跟請(qǐng)求域的數(shù)據(jù);
內(nèi)置函數(shù)Background()和TODO()
Go內(nèi)置兩個(gè)函數(shù):Background()和TODO(),這兩個(gè)函數(shù)分別返回一個(gè)實(shí)現(xiàn)了Context接口的background和todo。
我們代碼中最開始都是以這兩個(gè)內(nèi)置的上下文對(duì)象作為最頂層的partent context,衍生出更多的子上下文對(duì)象。
Background()主要用于main函數(shù)、初始化以及測(cè)試代碼中,作為Context這個(gè)樹結(jié)構(gòu)的最頂層的Context,也就是根Context。
TODO(),它目前還不知道具體的使用場(chǎng)景,如果我們不知道該使用什么Context的時(shí)候,可以使用這個(gè)。
background和todo本質(zhì)上都是emptyCtx結(jié)構(gòu)體類型,是一個(gè)不可取消,沒有設(shè)置截止時(shí)間,沒有攜帶任何值的Context。
Context的With系列函數(shù)
context包中定義了四個(gè)With系列函數(shù)。
對(duì)服務(wù)器的傳入請(qǐng)求應(yīng)該創(chuàng)建一個(gè)Context上下文,對(duì)服務(wù)器的傳出調(diào)用應(yīng)該接受一個(gè)Context上下文。它們之間的函數(shù)調(diào)用鏈必須傳播 Context,可選擇將其替換為使用 WithCancel、WithDeadline、WithTimeout 或 WithValue 創(chuàng)建的派生 Context。
當(dāng)一個(gè)上下文 Context 被取消時(shí),所有從它派生的上下文也被取消。
WithCancel
WithCancel的函數(shù)簽名如下:
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
WithCancel返回帶有新Done通道的父節(jié)點(diǎn)的副本。當(dāng)調(diào)用返回的cancel函數(shù)或當(dāng)關(guān)閉父上下文的Done通道時(shí),將關(guān)閉返回上下文的Done通道,無論先發(fā)生什么情況。
取消此上下文將釋放與其關(guān)聯(lián)的資源,因此代碼應(yīng)該在此上下文中運(yùn)行的操作完成后立即調(diào)用cancel。
func generate(ctx context.Context) <-chan int {
dst := make(chan int)
n := 1
go func() {
for {
select {
case <-ctx.Done():
return // return結(jié)束該goroutine,防止泄露
case dst <- n:
n++
}
}
}()
return dst
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // 當(dāng)我們?nèi)⊥晷枰恼麛?shù)后調(diào)用cancel
for n := range generate(ctx) {
fmt.Println(n)
if n == 5 {
break
}
}
}
上面的示例代碼中,generate函數(shù)在單獨(dú)的goroutine中生成整數(shù),并將它們發(fā)送到返回的通道。
generate的調(diào)用者在使用生成的整數(shù)之后需要取消上下文,以免generate啟動(dòng)的內(nèi)部goroutine發(fā)生泄漏。
WithDeadline
WithDeadline的函數(shù)簽名如下:
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
返回父上下文的副本,并將deadline調(diào)整為不遲于d。
如果父上下文的deadline已經(jīng)早于d,則WithDeadline(parent, d)
在語義上等同于父上下文。
當(dāng)截止日過期時(shí),當(dāng)調(diào)用返回的cancel函數(shù)時(shí),或者當(dāng)父上下文的Done通道關(guān)閉時(shí),返回上下文的Done通道將被關(guān)閉,以最先發(fā)生的情況為準(zhǔn)。
取消此上下文將釋放與其關(guān)聯(lián)的資源,因此代碼應(yīng)該在此上下文中運(yùn)行的操作完成后立即調(diào)用cancel。
func main() {
d := time.Now().Add(50 * time.Millisecond)
ctx, cancel := context.WithDeadline(context.Background(), d)
// 盡管ctx會(huì)過期,但在任何情況下調(diào)用它的cancel函數(shù)都是很好的實(shí)踐。
// 如果不這樣做,可能會(huì)使上下文及其父類存活的時(shí)間超過必要的時(shí)間。
defer cancel()
select {
case <-time.After(1 * time.Second):
fmt.Println("overslept")
case <-ctx.Done():
fmt.Println(ctx.Err())
}
}
上面的代碼中,定義了一個(gè)50毫秒之后過期的deadline,
然后我們調(diào)用context.WithDeadline(context.Background(), d)
得到一個(gè)上下文(ctx)和一個(gè)取消函數(shù)cancel,然后使用一個(gè)select讓主程序陷入等待:等待1秒后打印overslept退出,或者等待ctx過期后退出。
因?yàn)閏tx50秒后就過期,所以ctx.Done()會(huì)先接收到值,上面的代碼會(huì)打印ctx.Err()取消原因。
WithTimeout
WithTimeout的函數(shù)簽名如下:
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
WithTimeout返回WithDeadline(parent, time.Now().Add(timeout))
。
取消此上下文將釋放與其相關(guān)的資源,因此代碼應(yīng)該在此上下文中運(yùn)行的操作完成后立即調(diào)用cancel,通常用于數(shù)據(jù)庫或者網(wǎng)絡(luò)連接的超時(shí)控制。
具體示例如下:
package main
import (
"context"
"fmt"
"sync"
"time"
)
// context.WithTimeout
var wg sync.WaitGroup
func worker(ctx context.Context) {
LOOP:
for {
fmt.Println("db connecting ...")
time.Sleep(time.Millisecond * 10) // 假設(shè)正常連接數(shù)據(jù)庫耗時(shí)10毫秒
select {
case <-ctx.Done(): // 50毫秒后自動(dòng)調(diào)用
break LOOP
default:
}
}
fmt.Println("worker done!")
wg.Done()
}
func main() {
// 設(shè)置一個(gè)50毫秒的超時(shí)
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50)
wg.Add(1)
go worker(ctx)
time.Sleep(time.Second * 5)
cancel() // 通知子goroutine結(jié)束
wg.Wait()
fmt.Println("over")
}
WithValue
WithValue函數(shù)能夠?qū)⒄?qǐng)求作用域的數(shù)據(jù)與 Context 對(duì)象建立關(guān)系。
WithValue聲明如下:
func WithValue(parent Context, key, val interface{}) Context
WithValue返回父節(jié)點(diǎn)的副本,其中與key關(guān)聯(lián)的值為val。
下面的一個(gè)例子, 在所有的 協(xié)程之中,使用 context進(jìn)行 日志編碼 的傳播。
package main
import (
"context"
"fmt"
"sync"
"time"
)
// context.WithValue
type TraceCode string
var wg sync.WaitGroup
func worker(ctx context.Context) {
key := TraceCode("TRACE_CODE")
traceCode, ok := ctx.Value(key).(string) // 在子goroutine中獲取trace code
if !ok {
fmt.Println("invalid trace code")
}
LOOP:
for {
fmt.Printf("worker, trace code:%s\n", traceCode)
time.Sleep(time.Millisecond * 10) // 假設(shè)正常連接數(shù)據(jù)庫耗時(shí)10毫秒
select {
case <-ctx.Done(): // 50毫秒后自動(dòng)調(diào)用
break LOOP
default:
}
}
fmt.Println("worker done!")
wg.Done()
}
func main() {
// 設(shè)置一個(gè)50毫秒的超時(shí)
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50)
// 在系統(tǒng)的入口中設(shè)置trace code傳遞給后續(xù)啟動(dòng)的goroutine實(shí)現(xiàn)日志數(shù)據(jù)聚合
ctx = context.WithValue(ctx, TraceCode("TRACE_CODE"), "12512312234")
wg.Add(1)
go worker(ctx)
time.Sleep(time.Second * 5)
cancel() // 通知子goroutine結(jié)束
wg.Wait()
fmt.Println("over")
}
上面的 break LOOP,表示跳出循環(huán)
Go語言中 break 語句可以結(jié)束 for、switch 和 select 的代碼塊,
break 語句可以在語句后面添加標(biāo)簽,表示退出某個(gè)標(biāo)簽對(duì)應(yīng)的代碼塊,
break 后面的標(biāo)簽,要求必須定義在對(duì)應(yīng)的 for、switch 和 select 的代碼塊上。
最后說一下: context.WithValue上下文所提供的鍵必須是可比較的,并且不應(yīng)該是string類型或任何其他內(nèi)置類型,以避免使用上下文在包之間發(fā)生沖突。context.WithValue上下文的鍵,應(yīng)該是用戶定義自己的類型。
使用Context的注意事項(xiàng)
- 規(guī)則1:推薦以參數(shù)的方式顯示傳遞Context
- 規(guī)則2:以Context作為參數(shù)的函數(shù)方法,應(yīng)該把Context作為第一個(gè)參數(shù)。
- 規(guī)則3:給一個(gè)函數(shù)方法傳遞Context的時(shí)候,不要傳遞nil,如果不知道傳遞什么,就使用context.TODO()
- 規(guī)則4:Context的Value相關(guān)方法應(yīng)該傳遞請(qǐng)求域的必要數(shù)據(jù),不應(yīng)該用于傳遞可選參數(shù)
- 規(guī)則5:Context是線程安全的,可以放心的在多個(gè)goroutine中傳遞
尼恩提示: 在咱們的案例中,為了編碼的簡(jiǎn)單,不是以參數(shù)的方式顯示傳遞Context,而是用封裝的方式放在了service基類里邊,這點(diǎn),違背了 規(guī)則1.
總之,咱們的案例,違背了 規(guī)則1.
業(yè)務(wù)CRUD的Dao層設(shè)計(jì)與開發(fā)
Dao層的基礎(chǔ)類
package dao
import "github.com/jinzhu/gorm"
type Dao struct {
engine *gorm.DB
}
func New(engine *gorm.DB) *Dao {
return &Dao{engine: engine}
}
注意這個(gè) engine 是外部注入的,主要在外部進(jìn)行 構(gòu)建。
什么時(shí)候創(chuàng)建的 gorm.DB ORM engine 對(duì)象呢? 是在構(gòu)建 service的時(shí)候:
type Service struct {
ctx context.Context
dao *dao.Dao
}
func New(ctx context.Context) Service {
svc := Service{ctx: ctx}
//svc.dao = dao.New(global.DBEngine)
svc.dao = dao.New(otgorm.WithContext(svc.ctx, global.DBEngine))
return svc
}
在構(gòu)建 service的時(shí)候,構(gòu)建了 一個(gè)dao對(duì)象,并且把 一個(gè)全局的 gorm 數(shù)據(jù)持久化組件對(duì)象,作為參數(shù)進(jìn)行的注入。
再看這個(gè)全局的 global.DBEngine 對(duì)象的定義:
package global
import (
"github.com/elastic/go-elasticsearch/v8"
rdb "github.com/go-redis/redis/v8"
"github.com/gomodule/redigo/redis"
"github.com/jinzhu/gorm"
"github.com/opentracing/opentracing-go"
"github.com/sirupsen/logrus"
"go.mongodb.org/mongo-driver/mongo"
)
var (
DBEngine *gorm.DB
RedisPool *redis.Pool
Redis *rdb.Client
Logger *logrus.Logger
Tracer opentracing.Tracer
Es *elasticsearch.Client
Mongo *mongo.Client
ModelPath string
ModelReplace string
)
global.DBEngine 對(duì)象的初始化
這個(gè)全局的 global.DBEngine 對(duì)象的初始化,在model 模塊的model 基礎(chǔ)類中。
package model
import (
"fmt"
"crazymakercircle.com/gin-rest/common/global"
"crazymakercircle.com/gin-rest/pkg/toolkit/cast"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql"
log "github.com/sirupsen/logrus"
"time"
cfg "crazymakercircle.com/gin-rest/internal/config"
)
func Init() {
var err error
//關(guān)于讀寫分離我們可以定義兩個(gè)變量,一個(gè)讀和一個(gè)寫的,
//然后分別初始化,
//然后在查詢和寫入操作的時(shí)候使用不同的連接,或者使用一個(gè)map保存讀和寫的連接
global.DBEngine, err = NewDBEngine()
if err != nil {
panic(err)
}
}
func NewDBEngine() (*gorm.DB, error) {
dsn := cfg.AppDbDsn.Load()
driver := cfg.AppDbDriver.Load()
switch driver {
case "mysql":
log.Printf("%s:%s",
driver,
dsn)
default:
log.Fatalf("invalid db driver %v\n", cfg.AppDbDriver.Load())
}
db, err := gorm.Open("mysql", dsn)
if err != nil {
log.Fatalf("Open "+cfg.AppDbDriver.Load()+" failed. %v\n", err)
return nil, err
}
db.LogMode(true)
db.DB().SetConnMaxLifetime(cast.ToDuration(cfg.AppDbMaxLifetime.Load())) //最大連接周期,超過時(shí)間的連接就close
db.DB().SetMaxOpenConns(cast.ToInt(cfg.AppDbMaxOpens.Load())) //設(shè)置最大連接數(shù)
db.DB().SetMaxIdleConns(cast.ToInt(cfg.AppDbMaxIdles.Load())) //設(shè)置閑置連接數(shù)
//設(shè)置全局表名禁用復(fù)數(shù)
db.SingularTable(true)
return db, nil
}
.....
model領(lǐng)域?qū)ο髮?/h4>
model層,類似SpringMVC 中的PO層。
下面是model層的一個(gè)類:
package model
import (
"crazymakercircle.com/gin-rest/pkg/app"
"github.com/jinzhu/gorm"
)
type ArticleSwagger struct {
List []*Article
Pager *app.Pager
}
type Article struct {
Id uint64 `json:"id"`
Title string `json:"title"`
Content string `json:"content"`
Introduction string `json:"introduction"`
Views int `json:"views"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"-"`
}
func (a Article) TableName() string {
return "article"
}
func (a Article) Count(db *gorm.DB) (int, error) {
var count int
if a.Title != "" {
db.Where("title like ?", "%"+a.Title+"%")
}
if err := db.Model(&a).Count(&count).Error; err != nil {
return 0, err
}
return count, nil
}
func (a Article) List(db *gorm.DB, pageOffset, pageSize int) ([]*Article, error) {
var list []*Article
if a.Title != "" {
db.Where("title like ?", "%"+a.Title+"%")
}
err := db.Limit(pageSize).Offset(pageOffset).Find(&list).Error
return list, err
}
注意,這個(gè)和springmvc不同:
- springmvc 的業(yè)務(wù)模型(領(lǐng)域?qū)ο螅┦秦氀P停?沒有對(duì)數(shù)據(jù)庫的任何操作的
- 這里的model領(lǐng)域?qū)ο?,是充血模?/li>
貧血模型是指model對(duì)象只用于在各層之間傳輸數(shù)據(jù)使用,只有數(shù)據(jù)字段和Get/Set方法,沒有邏輯在對(duì)象中。
充血模型是面向?qū)ο笤O(shè)計(jì)的本質(zhì),一個(gè)對(duì)象是擁有狀態(tài)和行為的。將大多數(shù)業(yè)務(wù)邏輯和持久化放在領(lǐng)域?qū)ο笾?,業(yè)務(wù)邏輯只是完成對(duì)業(yè)務(wù)邏輯的封裝、事務(wù)、權(quán)限、校驗(yàn)等的處理。
當(dāng)然, 這里僅僅是套用一下DDD里邊的概念。 和DDD的模式,并不是完全一致。
這里的Dao層對(duì)數(shù)據(jù)的訪問進(jìn)行了弱化, 很多的orm 數(shù)據(jù)訪問邏輯, 委托到了 model 領(lǐng)域?qū)ο髮印?/p>
使用GORM 鏈?zhǔn)讲僮?,完成orm持久層的訪問
上面的代碼在訪問數(shù)據(jù)庫的時(shí)候,使用了 GORM 鏈?zhǔn)讲僮鳌?/p>
比如,在Count方法中獲取Article 數(shù)據(jù)的時(shí)候,就是使用GORM 鏈?zhǔn)讲僮鳎唧w如下:
func (a Article) Count(db *gorm.DB) (int, error) {
var count int
if a.Title != "" {
db.Where("title like ?", "%"+a.Title+"%")
}
if err := db.Model(&a).Count(&count).Error; err != nil {
return 0, err
}
return count, nil
}
GORM 允許進(jìn)行鏈?zhǔn)讲僮?,所以大家可以像這樣寫代碼:
db.Where("name = ?", "jinzhu").Where("age = ?", 18).First(&user)
GORM 中有三種類型的方法: 鏈?zhǔn)椒椒?、終結(jié)方法、新建會(huì)話方法。
鏈?zhǔn)椒椒?/h5>
鏈?zhǔn)椒椒ㄊ菍?Clauses(sql字句)
修改或添加到當(dāng)前Statement 的方法,例如:
Where
, Select
, Omit
, Joins
, Scopes
, Preload
, 等
db.Table("users").Select("name, email").Where("age > ?", 18).Find(&users)
上面的 Select、Where 都是鏈?zhǔn)椒椒?/p>
終結(jié)方法(Finisher )
Finishers 是會(huì)立即執(zhí)行注冊(cè)回調(diào)的方法,然后生成并執(zhí)行 SQL,比如這些方法:
Create
, First
, Find
, Take
, Save
, Update
, Delete
, Scan
, Row
, Rows
… 等
db.Table("users").Select("name, email").Where("age > ?", 18).Find(&users)
上面的Find 都是終結(jié)方法, 會(huì)生成并執(zhí)行 SQL。
下面是幾個(gè) 終結(jié)方法(Finisher )的示例:
// 獲取第一條記錄,按主鍵排序
db.First(&user)
//生成的SQL: SELECT * FROM users ORDER BY id LIMIT 1;
// 獲取一條記錄,不指定排序
db.Take(&user)
//生成的SQL: SELECT * FROM users LIMIT 1;
// 獲取最后一條記錄,按主鍵排序
db.Last(&user)
//生成的SQL: SELECT * FROM users ORDER BY id DESC LIMIT 1;
// 獲取所有的記錄
db.Find(&users)
//生成的SQL: SELECT * FROM users;
// 通過主鍵進(jìn)行查詢 (僅適用于主鍵是數(shù)字類型)
db.First(&user, 10)
//生成的SQL: SELECT * FROM users WHERE id = 10;
新建會(huì)話方法
在 鏈?zhǔn)椒椒? 終結(jié)方法之后, GORM 返回一個(gè)初始化的*gorm.DB
實(shí)例。需要注意的是, *gorm.DB
不能安全地重復(fù)使用,并且新生成的 SQL 可能會(huì)被先前的條件污染,例如:
queryDB := DB.Where("name = ?", "jinzhu")
queryDB.Where("age > ?", 10).First(&user)
//生成的SQL: SELECT * FROM users WHERE name = "jinzhu" AND age > 10
queryDB.Where("age > ?", 20).First(&user2)
//生成的SQL: SELECT * FROM users WHERE name = "jinzhu" AND age > 10 AND age > 20
上面的代碼中第二個(gè)where生成的sql,明顯不需要前面where的條件,所以,后面的新生成的 SQL 被先前的條件污染。
為了重新使用初始化的 *gorm.DB
實(shí)例, 可以使用 新建會(huì)話方法, 創(chuàng)建一個(gè)可共享的 *gorm.DB
, 例如:
queryDB := DB.Where("name = ?", "jinzhu").Session(&gorm.Session{})
queryDB.Where("age > ?", 10).First(&user)
// SELECT * FROM users WHERE name = "jinzhu" AND age > 10
queryDB.Where("age > ?", 20).First(&user2)
// SELECT * FROM users WHERE name = "jinzhu" AND age > 20
關(guān)于gorm 的 session介紹,請(qǐng)參見下面的鏈接:
https://gorm.cn/zh_CN/docs/session.html
編寫公共組件
每個(gè)項(xiàng)目中,都會(huì)有一類組件,我們常稱其為基礎(chǔ)組件,又或是公共組件,它們是不帶強(qiáng)業(yè)務(wù)屬性的,串聯(lián)著整個(gè)應(yīng)用程序.
公共組件由架構(gòu)師、高級(jí)開發(fā)或技術(shù)中臺(tái)團(tuán)隊(duì)的同事進(jìn)行梳理和編寫,并且負(fù)責(zé)統(tǒng)一維護(hù),如果集中統(tǒng)一的管理,每個(gè)業(yè)務(wù)團(tuán)隊(duì)都寫一套,是非常糟糕的,既帶來重復(fù)建設(shè)重復(fù)開發(fā),也會(huì)帶來不同業(yè)務(wù)團(tuán)隊(duì)代碼復(fù)用和移植的難度。
公共組件其實(shí)非常多,這里簡(jiǎn)單的介紹幾個(gè)。
錯(cuò)誤處理組件
在應(yīng)用程序的運(yùn)行中,我們常常需要與客戶端進(jìn)行交互,而交互分別是兩點(diǎn),一個(gè)是正確響應(yīng)下的結(jié)果集返回,另外一個(gè)是錯(cuò)誤響應(yīng)的錯(cuò)誤碼和消息體返回,用于告訴客戶端,這一次請(qǐng)求發(fā)生了什么事,因?yàn)槭裁丛蚴×恕?/p>
因此在一個(gè)新項(xiàng)目搭建之初,其中重要的一項(xiàng)預(yù)備工作,那就是標(biāo)準(zhǔn)化我們的錯(cuò)誤碼格式,保證客戶端是“理解”我們的錯(cuò)誤碼規(guī)則,不需要每次都寫一套新的。
定義公共錯(cuò)誤碼
在項(xiàng)目目錄下的 pkg/errcode
目錄新建 common_code.go 文件,用于預(yù)定義項(xiàng)目中的一些公共錯(cuò)誤碼,便于引導(dǎo)和規(guī)范大家的使用,如下:
var (
Success = NewError(0, "成功")
ServerError = NewError(10000000, "服務(wù)內(nèi)部錯(cuò)誤")
InvalidParams = NewError(10000001, "入?yún)㈠e(cuò)誤")
NotFound = NewError(10000002, "找不到")
UnauthorizedAuthNotExist = NewError(10000003, "鑒權(quán)失敗,找不到對(duì)應(yīng)的 AppKey 和 AppSecret")
UnauthorizedTokenError = NewError(10000004, "鑒權(quán)失敗,Token 錯(cuò)誤")
UnauthorizedTokenTimeout = NewError(10000005, "鑒權(quán)失敗,Token 超時(shí)")
UnauthorizedTokenGenerate = NewError(10000006, "鑒權(quán)失敗,Token 生成失敗")
TooManyRequests = NewError(10000007, "請(qǐng)求過多")
)
定義處理公共方法
在項(xiàng)目目錄下的 pkg/errcode
目錄新建 errcode.go 文件,編寫常用的一些錯(cuò)誤處理公共方法,標(biāo)準(zhǔn)化我們的錯(cuò)誤輸出,如下:
type Error struct {
code int `json:"code"`
msg string `json:"msg"`
details []string `json:"details"`
}
var codes = map[int]string{}
func NewError(code int, msg string) *Error {
if _, ok := codes[code]; ok {
panic(fmt.Sprintf("錯(cuò)誤碼 %d 已經(jīng)存在,請(qǐng)更換一個(gè)", code))
}
codes[code] = msg
return &Error{code: code, msg: msg}
}
func (e *Error) Error() string {
return fmt.Sprintf("錯(cuò)誤碼:%d, 錯(cuò)誤信息::%s", e.Code(), e.Msg())
}
func (e *Error) Code() int {
return e.code
}
func (e *Error) Msg() string {
return e.msg
}
func (e *Error) Msgf(args []interface{}) string {
return fmt.Sprintf(e.msg, args...)
}
func (e *Error) Details() []string {
return e.details
}
func (e *Error) WithDetails(details ...string) *Error {
newError := *e
newError.details = []string{}
for _, d := range details {
newError.details = append(newError.details, d)
}
return &newError
}
func (e *Error) StatusCode() int {
switch e.Code() {
case Success.Code():
return http.StatusOK
case ServerError.Code():
return http.StatusInternalServerError
case InvalidParams.Code():
return http.StatusBadRequest
case UnauthorizedAuthNotExist.Code():
fallthrough
case UnauthorizedTokenError.Code():
fallthrough
case UnauthorizedTokenGenerate.Code():
fallthrough
case UnauthorizedTokenTimeout.Code():
return http.StatusUnauthorized
case TooManyRequests.Code():
return http.StatusTooManyRequests
}
return http.StatusInternalServerError
}
在錯(cuò)誤碼方法的編寫中,我們聲明了 Error
結(jié)構(gòu)體用于表示錯(cuò)誤的響應(yīng)結(jié)果,并利用 codes
作為全局錯(cuò)誤碼的存儲(chǔ)載體,便于查看當(dāng)前注冊(cè)情況,并在調(diào)用 NewError
創(chuàng)建新的 Error
實(shí)例的同時(shí)進(jìn)行排重的校驗(yàn)。
另外相對(duì)特殊的是 StatusCode
方法,它主要用于針對(duì)一些特定錯(cuò)誤碼進(jìn)行狀態(tài)碼的轉(zhuǎn)換,因?yàn)椴煌膬?nèi)部錯(cuò)誤碼在 HTTP 狀態(tài)碼中都代表著不同的意義,我們需要將其區(qū)分開來,便于客戶端以及監(jiān)控/報(bào)警等系統(tǒng)的識(shí)別和監(jiān)聽。
公共日志組件
一般來說,應(yīng)用代碼中,都是直接使用 Go 標(biāo)準(zhǔn)庫 log 來進(jìn)行的日志輸出。
使用 標(biāo)準(zhǔn)庫 log 來進(jìn)行的日志輸出有啥問題呢?
在一個(gè)項(xiàng)目中,我們的日志需要標(biāo)準(zhǔn)化的記錄一些的公共信息,例如:代碼調(diào)用堆棧、請(qǐng)求鏈路 ID、公共的業(yè)務(wù)屬性字段等等,而直接輸出標(biāo)準(zhǔn)庫的日志的話,并不具備這些數(shù)據(jù),也不夠靈活。
日志的信息的齊全與否在排查和調(diào)試問題中是非常重要的一環(huán),因此在應(yīng)用程序中我們也會(huì)有一個(gè)標(biāo)準(zhǔn)的日志組件會(huì)進(jìn)行統(tǒng)一處理和輸出。
logrus 日志組件的使用
logrus日志庫是一個(gè)結(jié)構(gòu)化、插件化的日志記錄庫。
完全兼容 golang 標(biāo)準(zhǔn)庫中的日志模塊。
它還內(nèi)置了 2 種日志輸出格式 JSONFormatter 和 TextFormatter,來定義輸出的日志格式。
github地址:https://github.com/sirupsen/logrus
這里使用 logrus 日志組件 作為基礎(chǔ)組件。
日志組件初始化
在項(xiàng)目目錄下的 pkg/
目錄新建 logger
目錄,并創(chuàng)建 logrus.go 文件,寫入日志組件初始化的代碼:
package logger
import (
"crazymakercircle.com/gin-rest/common/global"
"crazymakercircle.com/gin-rest/internal/config"
"crazymakercircle.com/gin-rest/pkg/helper/files"
"github.com/evalphobia/logrus_sentry"
"github.com/sirupsen/logrus"
)
func Init() {
global.Logger = logrus.New()
if config.AppSentryDsn.Load() != "" {
hook, err := logrus_sentry.NewSentryHook(config.AppSentryDsn.Load(), []logrus.Level{
logrus.PanicLevel,
logrus.FatalLevel,
logrus.ErrorLevel,
})
if err == nil {
global.Logger.Hooks.Add(hook)
hook.Timeout = 0
hook.StacktraceConfiguration.Enable = true
}
}
// 設(shè)置日志格式為json格式
global.Logger.SetFormatter(&logrus.JSONFormatter{})
//設(shè)置文件輸出
f, logFilePath := files.LogFile()
// 日志消息輸出可以是任意的io.writer類型,這里我們獲取文件句柄,將日志輸出到文件
global.Logger.SetOutput(f)
// 設(shè)置日志級(jí)別為debug以上
global.Logger.SetLevel(logrus.DebugLevel)
// 設(shè)置顯示文件名和行號(hào)
global.Logger.SetReportCaller(true)
// 設(shè)置rotatelogs日志分割Hook
global.Logger.AddHook(NewLfsHook(logFilePath))
}
包全局變量
在完成日志庫的編寫后,我們需要定義一個(gè) Logger 對(duì)象便于我們的應(yīng)用程序使用。
因此我們打開項(xiàng)目目錄下的 common/global/global.go 文件,新增如下內(nèi)容:
var (
...
Logger *logrus.Logger
)
我們?cè)诎肿兞恐行略隽?Logger 對(duì)象,用于日志組件的初始化。
分頁響應(yīng)處理
在項(xiàng)目目錄下的 pkg/app
目錄下新建 pagination.go 文件,如下:
package app
import (
"crazymakercircle.com/gin-rest/internal/config"
"crazymakercircle.com/gin-rest/pkg/toolkit/cast"
"crazymakercircle.com/gin-rest/pkg/toolkit/convert"
"github.com/gin-gonic/gin"
)
func GetPage(c *gin.Context) int {
page := convert.Str(c.Query("page")).ToInt()
if page <= 0 {
return 1
}
return page
}
func GetPageSize(c *gin.Context) int {
pageSize := convert.Str(c.Query("page_size")).ToInt()
if pageSize <= 0 {
return cast.ToInt(config.AppDefaultPageSize.Load())
}
return pageSize
}
func GetPageOffset(page, pageSize int) int {
result := 0
if page > 0 {
result = (page - 1) * pageSize
}
return result
}
響應(yīng)處理
在項(xiàng)目目錄下的 pkg/app
目錄下新建 app.go 文件,如下:
package app
import (
"bytes"
"crazymakercircle.com/gin-rest/common/dict"
"crazymakercircle.com/gin-rest/pkg/helper/gjson"
"github.com/gin-gonic/gin"
"io/ioutil"
"net/http"
"time"
)
type Response struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data interface{} `json:"data"`
Elapsed float64 `json:"elapsed"`
}
type Pager struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
TotalRows int `json:"total_rows"`
}
//Success 正常返回
func Success(c *gin.Context, data interface{}) {
if data == nil {
data = make([]string, 0)
}
response := Response{Code: 0, Msg: "success", Data: data, Elapsed: GetElapsed(c)}
c.Set("responseData", response)
c.JSON(http.StatusOK, response)
}
//SuccessList 分頁返回
func SuccessList(c *gin.Context, list interface{}, totalRows int) {
data := gin.H{
"list": list,
"pager": Pager{
Page: GetPage(c),
PageSize: GetPageSize(c),
TotalRows: totalRows,
},
}
e := dict.Success
response := Response{Code: e.Code(), Msg: e.Msg(), Data: data, Elapsed: GetElapsed(c)}
c.Set("responseData", response)
c.JSON(http.StatusOK, response)
}
//Error 使用公共配置的消息返回
func Error(c *gin.Context, err *dict.Error) {
response := Response{Code: err.Code(), Msg: err.Msg(), Elapsed: GetElapsed(c)}
details := err.Details()
if err.Level() == "" { //默認(rèn)錯(cuò)誤返回為warn,不記錄日志到sentry
err = err.WithLevel("warn")
}
SetLevel(c, err.Level())
if len(details) > 0 {
SetDetail(c, err.Details())
if err.Level() != dict.LevelError {
response.Data = details
}
}
c.Set("responseData", response)
c.JSON(err.StatusCode(), response)
}
func SetLevel(c *gin.Context, level interface{}) {
c.Set("level", level)
}
func SetDetail(c *gin.Context, detail interface{}) {
c.Set("detail", detail)
}
func GetLevel(c *gin.Context) interface{} {
return Get(c, "level")
}
func GetDetail(c *gin.Context) interface{} {
return Get(c, "detail")
}
func Get(c *gin.Context, key string) interface{} {
val, _ := c.Get(key)
return val
}
func GetElapsed(c *gin.Context) float64 {
elapsed := 0.00
if requestTime := Get(c, "beginTime"); requestTime != nil {
elapsed = float64(time.Since(requestTime.(time.Time))) / 1e9
}
return elapsed
}
func JsonParams(c *gin.Context) map[string]interface{} {
b, err := ioutil.ReadAll(c.Request.Body)
if err != nil {
panic(err)
}
// 將取出來的body內(nèi)容重新插入body,否則ShouldBindJSON無法綁定參數(shù)
c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(b))
return gjson.JsonDecode(string(b))
}
func Params(c *gin.Context) string {
b, err := ioutil.ReadAll(c.Request.Body)
if err != nil {
panic(err)
}
// 將取出來的body內(nèi)容重新插入body,否則ShouldBindJSON無法綁定參數(shù)
c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(b))
return string(b)
}
分頁返回的使用
我們可以找到其中一個(gè)接口方法,調(diào)用對(duì)應(yīng)的方法,如下:
func (a *Article) ArticleList(c *gin.Context) {
param := struct {
Title string `form:"title" binding:"max=100"`
}{}
valid, errs := app.BindAndCheck(c, ¶m)
if !valid {
app.Error(c, dict.InvalidParams.WithDetails(errs.Errors()...))
return
}
pager := app.Pager{Page: app.GetPage(c), PageSize: app.GetPageSize(c)}
svc := service.New(c.Request.Context())
totalRows, err := svc.CountArticle(param.Title)
if err != nil {
app.Error(c, dict.ErrGetArtCountFail)
return
}
articles, err := svc.GetArticleList(param.Title, &pager)
if err != nil {
app.Error(c, dict.ErrGetArtListFail)
return
}
for _, article := range articles {
num, err := redigo.GetNum("article" + strconv.Itoa(int(article.Id)))
if err != nil {
num = 1
}
article.Views += num
}
//分頁返回的使用
app.SuccessList(c, articles, totalRows)
return
}
Swagger接口文檔
如何維護(hù)rest api 接口文檔,是絕大部分開發(fā)人員都經(jīng)歷過的問題,因?yàn)榍岸?、后端、測(cè)試開發(fā)等等人員都要看,每個(gè)人都給一份的話,怎么維護(hù),這將是一個(gè)非常頭大的問題。
針對(duì)這類問題,市面上出現(xiàn)了大量的解決方案,Swagger 正是其中的佼佼者,它更加的全面和完善,具有相關(guān)聯(lián)的生態(tài)圈。
它是基于標(biāo)準(zhǔn)的 OpenAPI 規(guī)范進(jìn)行設(shè)計(jì)的,只要照著這套規(guī)范去編寫你的注解或通過掃描代碼去生成注解,就能生成統(tǒng)一標(biāo)準(zhǔn)的接口文檔和一系列 Swagger 工具。
OpenAPI & Swagger
在上文我們有提到 OpenAPI,你可能會(huì)對(duì)此產(chǎn)生疑惑,OpenAPI 和 Swagger 又是什么關(guān)系?
其實(shí) OpenAPI 規(guī)范是在 2015 年由 OpenAPI Initiative 捐贈(zèng)給 Linux 基金會(huì)的,并且 Swagger 對(duì)此更進(jìn)一步的針對(duì) OpenAPI 規(guī)范提供了大量與之相匹配的工具集,能夠充分利用 OpenAPI 規(guī)范去映射生成所有與之關(guān)聯(lián)的資源和操作去查看和調(diào)用 RESTful 接口,因此我們也常說 Swagger 不僅是一個(gè)“規(guī)范”,更是一個(gè)框架。
從功能使用上來講,OpenAPI 規(guī)范能夠幫助我們描述一個(gè) API 的基本信息,比如:
- 有關(guān)該 API 的描述。
- 可用路徑(/資源)。
- 在每個(gè)路徑上的可用操作(獲取/提交…)。
- 每個(gè)操作的輸入/輸出格式。
安裝 Swagger
Swagger 相關(guān)的工具集會(huì)根據(jù) OpenAPI 規(guī)范去生成各式各類的與接口相關(guān)聯(lián)的內(nèi)容,常見的流程是編寫注解 =》調(diào)用生成庫-》生成標(biāo)準(zhǔn)描述文件 =》生成/導(dǎo)入到對(duì)應(yīng)的 Swagger 工具。
因此接下來第一步,我們要先安裝 Go 對(duì)應(yīng)的開源 Swagger 相關(guān)聯(lián)的庫,在項(xiàng)目 blog-service 根目錄下執(zhí)行安裝命令,如下:
$ go get -u github.com/swaggo/swag/cmd/swag@v1.6.5
$ go get -u github.com/swaggo/gin-swagger@v1.2.0
$ go get -u github.com/swaggo/files
$ go get -u github.com/alecthomas/template
驗(yàn)證是否安裝成功,如下:
$ swag -vswag version v1.6.5
如果命令行提示尋找不到 swag 文件,可以檢查一下對(duì)應(yīng)的 bin 目錄是否已經(jīng)加入到環(huán)境變量 PATH 中。
寫入注解
在完成了 Swagger 關(guān)聯(lián)庫的安裝后,我們需要針對(duì)項(xiàng)目里的 API 接口進(jìn)行注解的編寫,以便于后續(xù)在進(jìn)行生成時(shí)能夠正確的運(yùn)行,接下來我們將使用到如下注解:
注解 | 描述 |
---|---|
@Summary | 摘要 |
@Produce | API 可以產(chǎn)生的 MIME 類型的列表,MIME 類型你可以簡(jiǎn)單的理解為響應(yīng)類型,例如:json、xml、html 等等 |
@Param | 參數(shù)格式,從左到右分別為:參數(shù)名、入?yún)㈩愋?、?shù)據(jù)類型、是否必填、注釋 |
@Success | 響應(yīng)成功,從左到右分別為:狀態(tài)碼、參數(shù)類型、數(shù)據(jù)類型、注釋 |
@Failure | 響應(yīng)失敗,從左到右分別為:狀態(tài)碼、參數(shù)類型、數(shù)據(jù)類型、注釋 |
@Router | 路由,從左到右分別為:路由地址,HTTP 方法 |
swagger注解的編寫
我們切換到項(xiàng)目目錄下的 internal/routers/api/
目錄,打開 article.go 文件,在 ArticleList 方法前面,加上swagger注解:
// ArticleList
// @Summary 獲取列表
// @Produce json
// @Param name query string false "名稱" maxlength(100)
// @Param page query int false "頁碼"
// @Param page_size query int false "每頁數(shù)量"
// @Success 200 {object} model.Article"成功"
// @Failure 400 {object} dict.Error "請(qǐng)求錯(cuò)誤"
// @Failure 500 {object} dict.Error "內(nèi)部錯(cuò)誤"
// @Router /api/articles [get]
func (a *Article) ArticleList(c *gin.Context) {
在這里我們只展示了一個(gè)接口注解編寫,接下來你應(yīng)當(dāng)按照注解的含義和參考上述接口注解,完成其他接口注解的編寫。
swagger 配置文件的生成
在完成了所有的注解編寫后,我們回到項(xiàng)目根目錄下,執(zhí)行如下命令:
$ swag init
在執(zhí)行命令完畢后,會(huì)發(fā)現(xiàn)在 docs 文件夾生成 docs.go、swagger.json、swagger.yaml 三個(gè)文件。
swagger中間件的注冊(cè)
那注解編寫完,也通過 swag init 把 Swagger API 所需要的文件都生成了,那接下來我們?cè)趺丛L問接口文檔呢?
其實(shí)很簡(jiǎn)單,我們只需要在 routers 中進(jìn)行默認(rèn)初始化和注冊(cè)對(duì)應(yīng)的路由就可以了,打開項(xiàng)目目錄下的 internal/routers
目錄中的 router.go 文件,新增代碼如下:
import (
...
_ "github.com/go-programming-tour-book/blog-service/docs"
ginSwagger "github.com/swaggo/gin-swagger"
"github.com/swaggo/gin-swagger/swaggerFiles"
)
func NewRouter() *gin.Engine {
r := gin.New()
r.Use(gin.Logger())
r.Use(gin.Recovery())
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
...
return r
}
從表面上來看,主要做了兩件事,分別是初始化 docs 包和注冊(cè)一個(gè)針對(duì) swagger 的路由,而在初始化 docs 包后,其 swagger.json 將會(huì)默認(rèn)指向當(dāng)前應(yīng)用所啟動(dòng)的域名下的 swagger/doc.json 路徑,如果有額外需求,可進(jìn)行手動(dòng)指定,如下:
url := ginSwagger.URL("http://127.0.0.1:8000/swagger/doc.json")
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler, url))
通過swagger查看接口文檔
http://localhost:9099/swagger/index.html
在完成了上述的設(shè)置后,我們重新啟動(dòng)服務(wù)端,在瀏覽器中訪問 Swagger 的地址 ,就可以看到上述圖片中的 Swagger 文檔展示,
其主要分為三個(gè)部分,分別是項(xiàng)目主體信息、接口路由信息、模型信息,這三部分共同組成了我們主體內(nèi)容。
Swagger 背后發(fā)生了什么
可能會(huì)疑惑,我明明只是初始化了個(gè) docs 包并注冊(cè)了一個(gè) Swagger 相關(guān)路由,Swagger 的文檔是怎么關(guān)聯(lián)上的呢,我在接口上寫的注解又到哪里去了?
其實(shí)主體是與swagger init 生成的文件有關(guān)的,分別是:
docs
├── docs.go
├── swagger.json
└── swagger.yaml
初始化 docs
在第一步中,我們初始化了 docs 包,對(duì)應(yīng)的其實(shí)就是 docs.go 文件,因?yàn)槟夸浵聝H有一個(gè) go 源文件,其源碼如下:
package docs
import (
"bytes"
"encoding/json"
"strings"
"github.com/alecthomas/template"
"github.com/swaggo/swag"
)
var doc = `{
"schemes": {{ marshal .Schemes }},
"swagger": "2.0",
"info": {
"description": "{{.Description}}",
"title": "{{.Title}}",
"contact": {},
"license": {},
"version": "{{.Version}}"
},
"host": "{{.Host}}",
"basePath": "{{.BasePath}}",
"paths": {
"/api/articles": {
"get": {
"produces": [
"application/json"
],
"summary": "獲取列表",
"parameters": [
{
"maxLength": 100,
"type": "string",
"description": "名稱",
"name": "name",
"in": "query"
},
{
"enum": [
0,
1
],
"type": "integer",
"default": 1,
"description": "狀態(tài)",
"name": "state",
"in": "query"
},
{
"type": "integer",
"description": "頁碼",
"name": "page",
"in": "query"
},
{
"type": "integer",
"description": "每頁數(shù)量",
"name": "page_size",
"in": "query"
}
],
"responses": {
"200": {
"description": "成功",
"schema": {
"$ref": "#/definitions/model.Article"
}
},
"400": {
"description": "請(qǐng)求錯(cuò)誤",
"schema": {
"$ref": "#/definitions/dict.Error"
}
},
"500": {
"description": "內(nèi)部錯(cuò)誤",
"schema": {
"$ref": "#/definitions/dict.Error"
}
}
}
}
}
},
"definitions": {
"dict.Error": {
"type": "object",
"properties": {
"code": {
"type": "integer"
},
"details": {
"type": "array",
"items": {
"type": "string"
}
},
"level": {
"type": "string"
},
"msg": {
"type": "string"
}
}
},
"model.Article": {
"type": "object",
"properties": {
"content": {
"type": "string"
},
"id": {
"type": "integer"
},
"introduction": {
"type": "string"
},
"title": {
"type": "string"
}
}
}
}
}`
type swaggerInfo struct {
Version string
Host string
BasePath string
Schemes []string
Title string
Description string
}
// SwaggerInfo holds exported Swagger Info so clients can modify it
var SwaggerInfo = swaggerInfo{
Version: "1.0",
Host: "",
BasePath: "",
Schemes: []string{},
Title: "gin系統(tǒng)",
Description: "gin開發(fā)的系統(tǒng)",
}
type s struct{}
func (s *s) ReadDoc() string {
sInfo := SwaggerInfo
sInfo.Description = strings.Replace(sInfo.Description, "\n", "\\n", -1)
t, err := template.New("swagger_info").Funcs(template.FuncMap{
"marshal": func(v interface{}) string {
a, _ := json.Marshal(v)
return string(a)
},
}).Parse(doc)
if err != nil {
return doc
}
var tpl bytes.Buffer
if err := t.Execute(&tpl, sInfo); err != nil {
return doc
}
return tpl.String()
}
func init() {
swag.Register(swag.Name, &s{})
}
通過對(duì)源碼的分析,我們可以得知實(shí)質(zhì)上在初始化 docs 包時(shí),會(huì)默認(rèn)執(zhí)行 init 方法,而在 init 方法中,會(huì)注冊(cè)相關(guān)方法,主體邏輯是 swag 會(huì)在生成時(shí)去檢索項(xiàng)目下的注解信息,然后將項(xiàng)目信息和接口路由信息按規(guī)范生成到包全局變量 doc 中去。
緊接著會(huì)在 ReadDoc 方法中做一些 template 的模板映射等工作,完善 doc 的輸出。
swagger注冊(cè)路由
在上一步中,我們知道了生成的注解數(shù)據(jù)源在哪,但是它們兩者又是怎么關(guān)聯(lián)起來的呢,實(shí)際上與我們調(diào)用的 ginSwagger.WrapHandler(swaggerFiles.Handler)
有關(guān),如下:
func WrapHandler(h *webdav.Handler, confs ...func(c *Config)) gin.HandlerFunc {
defaultConfig := &Config{URL: "doc.json"}
...
return CustomWrapHandler(defaultConfig, h)
}
實(shí)際上在調(diào)用 WrapHandler 后,swag 內(nèi)部會(huì)將其默認(rèn)調(diào)用的 URL 設(shè)置為 doc.json,但你可能會(huì)糾結(jié),明明我們生成的文件里沒有 doc.json,這又是從哪里來的,我們接著往下看,如下:
func CustomWrapHandler(config *Config, h *webdav.Handler) gin.HandlerFunc {
...
switch path {
case "index.html":
index.Execute(c.Writer, &swaggerUIBundle{
URL: config.URL,
})
case "doc.json":
doc, err := swag.ReadDoc()
if err != nil {
panic(err)
}
c.Writer.Write([]byte(doc))
return
default:
h.ServeHTTP(c.Writer, c.Request)
}
}
}
在 CustomWrapHandler 方法中,我們可以發(fā)現(xiàn)一處比較經(jīng)典 switch case 的邏輯。
在第一個(gè) case 中,處理是的 index.html,這又是什么呢,其實(shí)你可以回顧一下,我們?cè)谙惹笆峭ㄟ^
http://localhost:9099/swagger/index.html
訪問到 Swagger 文檔的,對(duì)應(yīng)的便是這里的邏輯。
為接口做參數(shù)校驗(yàn)
接下來我們將正式進(jìn)行編碼,在進(jìn)行對(duì)應(yīng)的業(yè)務(wù)模塊開發(fā)時(shí),第一步要考慮到的問題的就是如何進(jìn)行入?yún)⑿r?yàn),我們需要將整個(gè)項(xiàng)目,甚至整個(gè)團(tuán)隊(duì)的組件給定下來,形成一個(gè)通用規(guī)范,并完成標(biāo)簽?zāi)K的接口的入?yún)⑿r?yàn)。
validator 介紹
在本項(xiàng)目中我們將使用開源項(xiàng)目 go-playground/validator 作為我們的本項(xiàng)目的基礎(chǔ)庫,它是一個(gè)基于標(biāo)簽來對(duì)結(jié)構(gòu)體和字段進(jìn)行值驗(yàn)證的一個(gè)驗(yàn)證器。
那么,我們要單獨(dú)引入這個(gè)庫嗎,其實(shí)不然,因?yàn)槲覀兪褂玫?gin 框架,其內(nèi)部的模型綁定和驗(yàn)證默認(rèn)使用的是 go-playground/validator 來進(jìn)行參數(shù)綁定和校驗(yàn),使用起來非常方便。
在項(xiàng)目根目錄下執(zhí)行命令,進(jìn)行安裝:
$ go get -u github.com/go-playground/validator/v10
業(yè)務(wù)接口校驗(yàn)
接下來我們將正式開始對(duì)接口的入?yún)⑦M(jìn)行校驗(yàn)規(guī)則的編寫,也就是將校驗(yàn)規(guī)則寫在對(duì)應(yīng)的結(jié)構(gòu)體的字段標(biāo)簽上,常見的標(biāo)簽含義如下:
標(biāo)簽 | 含義 |
---|---|
required | 必填 |
gt | 大于 |
gte | 大于等于 |
lt | 小于 |
lte | 小于等于 |
min | 最小值 |
max | 最大值 |
oneof | 參數(shù)集內(nèi)的其中之一 |
len | 長(zhǎng)度要求與 len 給定的一致 |
文章接口校驗(yàn)
我們回到項(xiàng)目的 internal/service
目錄下的 article.go 文件,針對(duì)入?yún)⑿r?yàn)增加綁定/驗(yàn)證結(jié)構(gòu)體。
這塊與登錄模塊的驗(yàn)證規(guī)則差不多,主要是必填,長(zhǎng)度最小、最大的限制,以及要求參數(shù)值必須在某個(gè)集合內(nèi)的其中之一.
登錄模塊的驗(yàn)證規(guī)則, 回顧如下:
type LoginForm struct {
Username string `json:"username" binding:"required" validate:"required"`
Password string `json:"password" binding:"required" validate:"required"`
}
func Login(c *gin.Context) {
var form LoginForm
if err := c.ShouldBindWith(&form, binding.JSON); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
validate := validator.New()
if err := validate.Struct(form); err != nil {
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"msg": "login",
})
// TODO: 處理登錄邏輯
}
模塊開發(fā):商品管理
作為一個(gè)參考,接下來我們正式的進(jìn)入一個(gè)業(yè)務(wù)模塊的業(yè)務(wù)邏輯開發(fā),商品管理 的開發(fā),
首先進(jìn)行 rest api 的設(shè)計(jì), 涉及的api 接口如下:
功能 | HTTP 方法 | 路徑 |
---|---|---|
新增商品 | POST | /seckillskus |
刪除指定商品 | DELETE | /seckillskus/:id |
更新指定商品 | PUT | /seckillskus/:id |
獲取商品列表 | GET | /seckillskus |
商品model 領(lǐng)域?qū)娱_發(fā)
首先,根據(jù)數(shù)據(jù)庫里邊的表 seckill_sku,生產(chǎn)一個(gè)簡(jiǎn)單的 model
生產(chǎn)之后,放在了項(xiàng)目的 internal/model
目錄下。
這個(gè)對(duì)應(yīng)的model文件 seckillsku.go 文件。
接下來, 在 seckillsku.go 文件中添加 orm操作的方法,并且只添加與orm 實(shí)體相關(guān)的方法,代碼如下:
package model
import (
"github.com/jinzhu/gorm"
"time"
)
type SeckillSku struct {
SkuId int64 `json:"sku_id"` // 商品id
CostPrice float32 `json:"cost_price"` // 秒殺價(jià)格
CreateTime time.Time `json:"create_time"`
EndTime time.Time `json:"end_time"`
SkuImage string `json:"sku_image"`
SkuPrice float32 `json:"sku_price"` // 價(jià)格
StartTime time.Time `json:"start_time"`
StockCount int `json:"stock_count"` // 剩余庫存
SkuTitle string `json:"sku_title"`
RawStock int `json:"raw_stock"` // 原始庫存
ExposedKey string `json:"exposed_key"` // 秒殺md5
}
func (s SeckillSku) TableName() string {
return "seckill_sku"
}
func (s SeckillSku) Count(db *gorm.DB) (int, error) {
var count int
if s.SkuTitle != "" {
db = db.Where("sku_title = ?", s.SkuTitle)
}
if err := db.Model(&s).Count(&count).Error; err != nil {
return 0, err
}
return count, nil
}
func (s SeckillSku) List(db *gorm.DB, pageOffset, pageSize int) ([]*SeckillSku, error) {
var SeckillSkus []*SeckillSku
var err error
if pageOffset >= 0 && pageSize > 0 {
db = db.Offset(pageOffset).Limit(pageSize)
}
if s.SkuTitle != "" {
db = db.Where("sku_title = ?", s.SkuTitle)
}
if err = db.Find(&SeckillSkus).Error; err != nil {
return nil, err
}
return SeckillSkus, nil
}
func (s SeckillSku) Create(db *gorm.DB) error {
return db.Create(&s).Error
}
func (s SeckillSku) Update(db *gorm.DB) error {
return db.Model(&s).Where("sku_id = ? ", s.SkuId).Limit(1).Update(s).Error
}
func (s SeckillSku) Delete(db *gorm.DB) error {
return db.Where("sku_id = ?", s.SkuId).Delete(&s).Error
}
- Model:指定運(yùn)行 DB 操作的模型實(shí)例,默認(rèn)解析該結(jié)構(gòu)體的名字為表名,格式為大寫駝峰轉(zhuǎn)小寫下劃線駝峰。若情況特殊,也可以編寫該結(jié)構(gòu)體的 TableName 方法用于指定其對(duì)應(yīng)返回的表名。
- Where:設(shè)置篩選條件,接受 map,struct 或 string 作為條件。
- Offset:偏移量,用于指定開始返回記錄之前要跳過的記錄數(shù)。
- Limit:限制檢索的記錄數(shù)。
- Find:查找符合篩選條件的記錄。
- Updates:更新所選字段。
- Delete:刪除數(shù)據(jù)。
- Count:統(tǒng)計(jì)行為,用于統(tǒng)計(jì)模型的記錄數(shù)。
需要注意的是,在上述代碼中,我們采取的是將 db *gorm.DB
作為函數(shù)首參數(shù)傳入的方式,而在業(yè)界中也有另外一種方式,是基于結(jié)構(gòu)體傳入的,兩者本質(zhì)上都可以實(shí)現(xiàn)目的,讀者根據(jù)實(shí)際情況(使用習(xí)慣、項(xiàng)目規(guī)范等)進(jìn)行選用即可,其各有利弊。
商品 dao 層開發(fā)
我們?cè)陧?xiàng)目的 internal/dao
目錄下新建 seckillSku.go 文件,寫入如下代碼:
package dao
import (
"crazymakercircle.com/gin-rest/internal/model"
"crazymakercircle.com/gin-rest/pkg/app"
)
func (d *Dao) CountSeckillSku(title string) (int, error) {
tag := model.SeckillSku{SkuTitle: title}
return tag.Count(d.engine)
}
func (d *Dao) GetSeckillSkuList(title string, page, pageSize int) ([]*model.SeckillSku, error) {
seckillSku := model.SeckillSku{SkuTitle: title}
pageOffset := app.GetPageOffset(page, pageSize)
return seckillSku.List(d.engine, pageOffset, pageSize)
}
func (d *Dao) CreateSeckillSku(title string, rawStock int, createdBy string) error {
tag := model.SeckillSku{
SkuTitle: title,
RawStock: rawStock,
}
return tag.Create(d.engine)
}
func (d *Dao) UpdateSeckillSku(id int64, title string) error {
seckillSku := model.SeckillSku{
SkuTitle: title,
SkuId: id,
}
return seckillSku.Update(d.engine)
}
func (d *Dao) DeleteSeckillSku(id int64) error {
seckillSku := model.SeckillSku{SkuId: id}
return seckillSku.Delete(d.engine)
}
在上述代碼中,我們主要是在 dao 層進(jìn)行了數(shù)據(jù)訪問對(duì)象的封裝,并針對(duì)業(yè)務(wù)所需的字段進(jìn)行了處理。
商品 service 層開發(fā)
我們?cè)陧?xiàng)目的 internal/service
目錄下新建 service.go 文件,寫入如下代碼:
type Service struct {
ctx context.Context
dao *dao.Dao
}
func New(ctx context.Context) Service {
svc := Service{ctx: ctx}
svc.dao = dao.New(global.DBEngine)
return svc
}
接下來在同層級(jí)下新建 seckillSku.go 文件,用于處理商品模塊的業(yè)務(wù)邏輯,寫入如下代碼:
package service
import (
"crazymakercircle.com/gin-rest/internal/model"
"crazymakercircle.com/gin-rest/pkg/app"
)
type SeckillSkuListRequest struct {
title string `form:"title" binding:"max=100"`
}
type CreateSeckillSkuRequest struct {
title string `form:"title" binding:"required,max=100"`
rawStock int `form:"rawStock,default=1" binding:"required"`
}
type UpdateSeckillSkuRequest struct {
ID int64 `form:"id" binding:"required,gte=0"`
title string `form:"title" binding:"required,max=100"`
}
type DeleteSeckillSkuRequest struct {
ID int64 `form:"id" binding:"required,gte=0"`
}
func (svc *Service) CountSeckillSku(param *SeckillSkuListRequest) (int, error) {
return svc.dao.CountSeckillSku(param.title)
}
func (svc *Service) GetSeckillSkuList(param *SeckillSkuListRequest, pager *app.Pager) ([]*model.SeckillSku, error) {
return svc.dao.GetSeckillSkuList(param.title, pager.Page, pager.PageSize)
}
func (svc *Service) CreateSeckillSku(param *CreateSeckillSkuRequest) error {
return svc.dao.CreateSeckillSku(param.title, param.rawStock)
}
func (svc *Service) UpdateSeckillSku(param *UpdateSeckillSkuRequest) error {
return svc.dao.UpdateSeckillSku(param.ID, param.title)
}
func (svc *Service) DeleteSeckillSku(param *DeleteSeckillSkuRequest) error {
return svc.dao.DeleteSeckillSku(param.ID)
}
service 主要是 調(diào)用了dao中的方法,當(dāng)然,還可以在service 進(jìn)行了一些簡(jiǎn)單的邏輯編寫。
這里主要是為了演示,沒有添加任何的業(yè)務(wù)邏輯。
新增業(yè)務(wù)錯(cuò)誤碼
我們?cè)陧?xiàng)目的 common/dict/errcode.go 文件,針對(duì)商品模塊,寫入如下錯(cuò)誤代碼:
var (
ErrorGetSeckillSkuListFail = NewError(20010001, "獲取商品列表失敗")
ErrorCreateSeckillSkuFail = NewError(20010002, "創(chuàng)建商品失敗")
ErrorUpdateSeckillSkuFail = NewError(20010003, "更新商品失敗")
ErrorDeleteSeckillSkuFail = NewError(20010004, "刪除商品失敗")
ErrorCountSeckillSkuFail = NewError(20010005, "統(tǒng)計(jì)商品失敗")
)
新增商品的handler方法
我們打開 internal/api/
項(xiàng)目目錄下的 seckillsku.go 文件,寫入如下代碼:
func (t SeckillSku) List(c *gin.Context) {
param := service.SeckillSkuListRequest{}
response := app.NewCtxResponse(c)
valid, errs := app.BindAndValid(c, ¶m)
if !valid {
global.Logger.Errorf("app.BindAndValid errs: %v", errs)
response.ToErrorResponse(dict.InvalidParams.WithDetails(errs.Errors()...))
return
}
svc := service.New(c.Request.Context())
pager := app.Pager{Page: app.GetPage(c), PageSize: app.GetPageSize(c)}
totalRows, err := svc.CountSeckillSku(&service.SeckillSkuListRequest{Title: param.Title})
if err != nil {
global.Logger.Errorf("svc.CountSeckillSku err: %v", err)
response.ToErrorResponse(dict.ErrorCountSeckillSkuFail)
return
}
tags, err := svc.GetSeckillSkuList(¶m, &pager)
if err != nil {
global.Logger.Errorf("svc.GetSeckillSkuList err: %v", err)
response.ToErrorResponse(dict.ErrorGetSeckillSkuListFail)
return
}
response.ToCtxResponseList(tags, totalRows)
return
}
在上述代碼中,我們完成了獲取商品列表接口的處理方法,
我們?cè)诜椒ㄖ型瓿闪巳雲(yún)⑿r?yàn)和綁定、獲取商品總數(shù)、獲取商品列表、 序列化結(jié)果集等四大功能板塊的邏輯串聯(lián)和日志、錯(cuò)誤處理。
我們繼續(xù)寫入創(chuàng)建商品、更新商品、刪除商品的接口處理方法,如下:
func (t SeckillSku) Create(c *gin.Context) {
param := service.CreateSeckillSkuRequest{}
response := app.NewCtxResponse(c)
valid, errs := app.BindAndValid(c, ¶m)
if !valid {
global.Logger.Errorf("app.BindAndValid errs: %v", errs)
response.ToErrorResponse(dict.InvalidParams.WithDetails(errs.Errors()...))
return
}
svc := service.New(c.Request.Context())
err := svc.CreateSeckillSku(¶m)
if err != nil {
global.Logger.Errorf("svc.CreateSeckillSku err: %v", err)
response.ToErrorResponse(dict.ErrorCreateSeckillSkuFail)
return
}
app.Success(c, map[string]string{"title": c.Param("title")})
return
}
func (t SeckillSku) Update(c *gin.Context) {
response := app.NewCtxResponse(c)
_, err := cast.ToInt64E(c.Param("id"))
if err != nil {
global.Logger.Errorf("svc.UpdateSeckillSku err: %v", err)
response.ToErrorResponse(dict.ErrorUpdateSeckillSkuFail)
return
}
param := service.UpdateSeckillSkuRequest{}
valid, errs := app.BindAndValid(c, ¶m)
if !valid {
global.Logger.Errorf("app.BindAndValid errs: %v", errs)
response.ToErrorResponse(dict.InvalidParams.WithDetails(errs.Errors()...))
return
}
svc := service.New(c.Request.Context())
err1 := svc.UpdateSeckillSku(¶m)
if err1 != nil {
global.Logger.Errorf("svc.UpdateSeckillSku err: %v", err)
response.ToErrorResponse(dict.ErrorUpdateSeckillSkuFail)
return
}
app.Success(c, map[string]string{"id": c.Param("id")})
return
}
func (t SeckillSku) Delete(c *gin.Context) {
response := app.NewCtxResponse(c)
_, err := cast.ToInt64E(c.Param("id"))
if err != nil {
global.Logger.Errorf("svc.Delete err: %v", err)
response.ToErrorResponse(dict.ErrorUpdateSeckillSkuFail)
return
}
param := service.DeleteSeckillSkuRequest{}
valid, errs := app.BindAndValid(c, ¶m)
if !valid {
global.Logger.Errorf("app.BindAndValid errs: %v", errs)
response.ToErrorResponse(dict.InvalidParams.WithDetails(errs.Errors()...))
return
}
svc := service.New(c.Request.Context())
err1 := svc.DeleteSeckillSku(¶m)
if err1 != nil {
global.Logger.Errorf("svc.DeleteSeckillSku err: %v", err)
response.ToErrorResponse(dict.ErrorDeleteSeckillSkuFail)
return
}
app.Success(c, map[string]string{"id": c.Param("id")})
return
}
新增商品的api路由
seckillSku := api.NewSeckillsku()
apiRouterGroup.GET("/seckillskus", seckillSku.List)
apiRouterGroup.GET("/seckillskus/:id", seckillSku.Detail)
驗(yàn)證商品接口
我們重新啟動(dòng)服務(wù),也就是再執(zhí)行 go run main.go
,查看啟動(dòng)信息正常后,對(duì)商品模塊的接口進(jìn)行驗(yàn)證
(1)商品列表的驗(yàn)證
$ curl -X GET http://192.168.56.1:9099/api/seckillskus?page_size=2
執(zhí)行的效果如下
(2)驗(yàn)證獲取商品
$ curl -X GET http://192.168.56.1:9099/api/seckillskus/1?title=demo
執(zhí)行的效果如下
這里的獲取商品詳情的 DAO層、Service 層代碼,都沒有實(shí)現(xiàn),留個(gè)大家自己去實(shí)現(xiàn)。
刪除商品、刪除商品的驗(yàn)證,也留給大家自己去實(shí)現(xiàn)。
另外注意:在更新的時(shí)候,當(dāng)使用 GORM 中使用 struct 類型傳入進(jìn)行更新時(shí),GORM 是不會(huì)對(duì)值為零值的字段進(jìn)行變更。這又是為什么呢,根本的原因是因?yàn)樵谧R(shí)別這個(gè)結(jié)構(gòu)體中的這個(gè)字段值時(shí),很難判定是真的是零值,還是外部傳入恰好是該類型的零值,GORM 在這塊并沒有過多的去做特殊識(shí)別。
《Golang 圣經(jīng)》還有 5W字待發(fā)布
本文,僅僅是《Golang 圣經(jīng)》 的第4部分?!禛olang 圣經(jīng)》后面的內(nèi)容 更加精彩,涉及到高并發(fā)、分布式微服務(wù)架構(gòu)、 WEB開發(fā)架構(gòu)。
《Golang 圣經(jīng)》PDF, 請(qǐng)到文末【技術(shù)自由圈】取 。
最后,如果學(xué)習(xí)過程中遇到問題,可以來尼恩的 萬人高并發(fā)社區(qū) 中溝通。
參考資料
- Tracing Garbage Collection - wikipedia
- On-the-fly Garbage Collection: an exercise in cooperation.
- Garbage Collection
- Tracing Garbage Collection
- Copying Garbage Collection
- Generational Garbage Collection
- Golang Gc Talk
- Eliminate Rescan
技術(shù)自由的實(shí)現(xiàn)路徑 PDF:
實(shí)現(xiàn)你的 架構(gòu)自由:
《吃透8圖1模板,人人可以做架構(gòu)》
《10Wqps評(píng)論中臺(tái),如何架構(gòu)?B站是這么做的!??!》
《阿里二面:千萬級(jí)、億級(jí)數(shù)據(jù),如何性能優(yōu)化? 教科書級(jí) 答案來了》
《峰值21WQps、億級(jí)DAU,小游戲《羊了個(gè)羊》是怎么架構(gòu)的?》
《100億級(jí)訂單怎么調(diào)度,來一個(gè)大廠的極品方案》
《2個(gè)大廠 100億級(jí) 超大流量 紅包 架構(gòu)方案》
… 更多架構(gòu)文章,正在添加中
實(shí)現(xiàn)你的 響應(yīng)式 自由:
《響應(yīng)式圣經(jīng):10W字,實(shí)現(xiàn)Spring響應(yīng)式編程自由》
這是老版本 《Flux、Mono、Reactor 實(shí)戰(zhàn)(史上最全)》
實(shí)現(xiàn)你的 spring cloud 自由:
《Spring cloud Alibaba 學(xué)習(xí)圣經(jīng)》
《分庫分表 Sharding-JDBC 底層原理、核心實(shí)戰(zhàn)(史上最全)》
《一文搞定:SpringBoot、SLF4j、Log4j、Logback、Netty之間混亂關(guān)系(史上最全)》
實(shí)現(xiàn)你的 linux 自由:
《Linux命令大全:2W多字,一次實(shí)現(xiàn)Linux自由》
實(shí)現(xiàn)你的 網(wǎng)絡(luò) 自由:
《TCP協(xié)議詳解 (史上最全)》
《網(wǎng)絡(luò)三張表:ARP表, MAC表, 路由表,實(shí)現(xiàn)你的網(wǎng)絡(luò)自由??!》
實(shí)現(xiàn)你的 分布式鎖 自由:
《Redis分布式鎖(圖解 - 秒懂 - 史上最全)》
《Zookeeper 分布式鎖 - 圖解 - 秒懂》
實(shí)現(xiàn)你的 王者組件 自由:
《隊(duì)列之王: Disruptor 原理、架構(gòu)、源碼 一文穿透》
《緩存之王:Caffeine 源碼、架構(gòu)、原理(史上最全,10W字 超級(jí)長(zhǎng)文)》
《緩存之王:Caffeine 的使用(史上最全)》
《Java Agent 探針、字節(jié)碼增強(qiáng) ByteBuddy(史上最全)》
實(shí)現(xiàn)你的 面試題 自由:
4000頁《尼恩Java面試寶典 》 40個(gè)專題文章來源:http://www.zghlxwxcb.cn/news/detail-500102.html
尼恩 架構(gòu)筆記、面試題 的PDF文件更新,請(qǐng)到下面《技術(shù)自由圈》公號(hào)取↓↓↓文章來源地址http://www.zghlxwxcb.cn/news/detail-500102.html
到了這里,關(guān)于Go學(xué)習(xí)圣經(jīng):Go語言實(shí)現(xiàn)高并發(fā)CRUD業(yè)務(wù)開發(fā)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!