国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

Go學(xué)習(xí)圣經(jīng):Go語言實(shí)現(xiàn)高并發(fā)CRUD業(yè)務(wù)開發(fā)

這篇具有很好參考價(jià)值的文章主要介紹了Go學(xué)習(xí)圣經(jīng):Go語言實(shí)現(xiàn)高并發(fā)CRUD業(yè)務(wù)開發(fā)。希望對(duì)大家有所幫助。如果存在錯(cuò)誤或未考慮完全的地方,請(qǐng)大家不吝賜教,您也可以點(diǎn)擊"舉報(bào)違法"按鈕提交疑問。

說在前面:

現(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ù)。

Go學(xué)習(xí)圣經(jīng):Go語言實(shí)現(xiàn)高并發(fā)CRUD業(yè)務(wù)開發(fā)

雖然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。

Go學(xué)習(xí)圣經(jīng):Go語言實(shí)現(xiàn)高并發(fā)CRUD業(yè)務(wù)開發(fā)

當(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、switchselect 的代碼塊,

break 語句可以在語句后面添加標(biāo)簽,表示退出某個(gè)標(biāo)簽對(duì)應(yīng)的代碼塊,
 
break 后面的標(biāo)簽,要求必須定義在對(duì)應(yīng)的 for、switchselect 的代碼塊上。

最后說一下: 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, &param)
	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

Go學(xué)習(xí)圣經(jīng):Go語言實(shí)現(xiàn)高并發(fā)CRUD業(yè)務(wù)開發(fā)

在完成了上述的設(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

Go學(xué)習(xí)圣經(jīng):Go語言實(shí)現(xiàn)高并發(fā)CRUD業(yè)務(wù)開發(fā)

生產(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, &param)
	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(&param, &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, &param)

	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(&param)
	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, &param)
	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(&param)
	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, &param)
	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(&param)
	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í)行的效果如下

Go學(xué)習(xí)圣經(jīng):Go語言實(shí)現(xiàn)高并發(fā)CRUD業(yè)務(wù)開發(fā)

(2)驗(yàn)證獲取商品

$ curl -X GET http://192.168.56.1:9099/api/seckillskus/1?title=demo

執(zhí)行的效果如下

Go學(xué)習(xí)圣經(jīng):Go語言實(shí)現(xiàn)高并發(fā)CRUD業(yè)務(wù)開發(fā)

這里的獲取商品詳情的 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è)專題

尼恩 架構(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)!

本文來自互聯(lián)網(wǎng)用戶投稿,該文觀點(diǎn)僅代表作者本人,不代表本站立場(chǎng)。本站僅提供信息存儲(chǔ)空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如若轉(zhuǎn)載,請(qǐng)注明出處: 如若內(nèi)容造成侵權(quán)/違法違規(guī)/事實(shí)不符,請(qǐng)點(diǎn)擊違法舉報(bào)進(jìn)行投訴反饋,一經(jīng)查實(shí),立即刪除!

領(lǐng)支付寶紅包贊助服務(wù)器費(fèi)用

相關(guān)文章

  • Go語言程序設(shè)計(jì)-第9章--使用共享變量實(shí)現(xiàn)并發(fā)

    一個(gè)能在串行程序中正確工作的函數(shù)。如果這個(gè)函數(shù)在并發(fā)調(diào)用時(shí)仍然能正確工作,那么這個(gè)函數(shù)是并發(fā)安全的。在這里并發(fā)調(diào)用是指,在沒有額外同步機(jī)制的情況下,從兩個(gè)或者多個(gè) goroutine 同時(shí)調(diào)用這個(gè)函數(shù)。如果一個(gè)類型的所有可訪問方法和操作都是并發(fā)安全時(shí),則它

    2024年02月02日
    瀏覽(94)
  • 掌握Go并發(fā):Go語言并發(fā)編程深度解析

    掌握Go并發(fā):Go語言并發(fā)編程深度解析

    ??? 個(gè)人主頁 :鼠鼠我捏,要死了捏的主頁? ??? 系列專欄 :Golang全棧-專欄 ??? 個(gè)人學(xué)習(xí)筆記,若有缺誤,歡迎評(píng)論區(qū)指正 ? 前些天發(fā)現(xiàn)了一個(gè)巨牛的人工智能學(xué)習(xí)網(wǎng)站,通俗易懂,風(fēng)趣幽默,忍不住分享一下給大家。點(diǎn)擊跳轉(zhuǎn)到網(wǎng)站AI學(xué)習(xí)網(wǎng)站。 當(dāng)我們開發(fā)一個(gè)W

    2024年02月20日
    瀏覽(24)
  • 從0到1開發(fā)go-tcp框架【1-搭建server、封裝連接與業(yè)務(wù)綁定、實(shí)現(xiàn)基礎(chǔ)Router、抽取全局配置文件】

    從0到1開發(fā)go-tcp框架【1-搭建server、封裝連接與業(yè)務(wù)綁定、實(shí)現(xiàn)基礎(chǔ)Router、抽取全局配置文件】

    本期主要完成對(duì)Server的搭建、封裝連接與業(yè)務(wù)綁定、實(shí)現(xiàn)基礎(chǔ)Router(處理業(yè)務(wù)的部分)、抽取框架的全局配置文件 從配置文件中讀取數(shù)據(jù)(服務(wù)器監(jiān)聽端口、監(jiān)聽I(yíng)P等),通過自定義Router完成具體業(yè)務(wù)操作 第一版最終項(xiàng)目結(jié)構(gòu): 1.1 編寫server端 編寫iserver.go,用于定義server的

    2024年02月06日
    瀏覽(17)
  • 大數(shù)據(jù)HBase學(xué)習(xí)圣經(jīng):一本書實(shí)現(xiàn)HBase學(xué)習(xí)自由

    大數(shù)據(jù)HBase學(xué)習(xí)圣經(jīng):一本書實(shí)現(xiàn)HBase學(xué)習(xí)自由

    本文是《大數(shù)據(jù)HBase學(xué)習(xí)圣經(jīng)》 V1版本,是 《尼恩 大數(shù)據(jù) 面試寶典》姊妹篇。 這里特別說明一下:《尼恩 大數(shù)據(jù) 面試寶典》5個(gè)專題 PDF 自首次發(fā)布以來, 已經(jīng)匯集了 好幾百題,大量的大廠面試 干貨、正貨 。 《尼恩 大數(shù)據(jù) 面試寶典》面試題集合, 將變成大數(shù)據(jù)學(xué)習(xí)和面

    2024年02月10日
    瀏覽(48)
  • Go語言并發(fā)

    出色的并發(fā)性是Go語言的特色之一 ? 理解并發(fā)與并行 ? 理解進(jìn)程和線程 ? 掌握Go語言中的Goroutine和channel ? 掌握select分支語句 ? 掌握sync包的應(yīng)用 并發(fā)與并行的概念這里不再贅述, 可以看看之前java版寫的并發(fā)實(shí)踐; 程序、進(jìn)程與線程這里也不贅述 一個(gè)進(jìn)程可以包括多個(gè)線

    2024年02月06日
    瀏覽(20)
  • 【Go】Go語言并發(fā)編程:原理、實(shí)踐與優(yōu)化

    在當(dāng)今的計(jì)算機(jī)世界,多核處理器和并發(fā)編程已經(jīng)成為提高程序執(zhí)行效率的關(guān)鍵。Go語言作為一門極富創(chuàng)新性的編程語言,憑借其強(qiáng)大的并發(fā)能力,在這方面表現(xiàn)出色。本文將深入探討Go語言并發(fā)編程的原理,通過實(shí)際代碼示例展示其應(yīng)用,并討論可能的優(yōu)化策略。 在了解G

    2024年02月10日
    瀏覽(25)
  • GO語言網(wǎng)絡(luò)編程(并發(fā)編程)并發(fā)介紹,Goroutine

    GO語言網(wǎng)絡(luò)編程(并發(fā)編程)并發(fā)介紹,Goroutine

    進(jìn)程和線程 并發(fā)和并行 協(xié)程和線程 協(xié)程:獨(dú)立的??臻g,共享堆空間,調(diào)度由用戶自己控制,本質(zhì)上有點(diǎn)類似于用戶級(jí)線程,這些用戶級(jí)線程的調(diào)度也是自己實(shí)現(xiàn)的。 線程:一個(gè)線程上可以跑多個(gè)協(xié)程,協(xié)程是輕量級(jí)的線程。 goroutine 只是由官方實(shí)現(xiàn)的超級(jí)\\\"線程池\\\"。 每個(gè)

    2024年02月09日
    瀏覽(92)
  • 掌握Go語言:Go語言通道,并發(fā)編程的利器與應(yīng)用實(shí)例(20)

    通道(Channel)是用來在 Go 程序中傳遞數(shù)據(jù)的一種數(shù)據(jù)結(jié)構(gòu)。它是一種類型安全的、并發(fā)安全的、阻塞式的數(shù)據(jù)傳輸方式,用于在不同的 Go 協(xié)程之間傳遞消息。 基本概念 創(chuàng)建通道 :使用 make() 函數(shù)創(chuàng)建一個(gè)通道。 發(fā)送數(shù)據(jù) :使用 - 操作符向通道發(fā)送數(shù)據(jù)。 接收數(shù)據(jù) :使用

    2024年03月21日
    瀏覽(33)
  • Go語言并發(fā)模式視角思考

    Go語言并發(fā)模式視角思考

    猶記得2019年中旬進(jìn)行知識(shí)點(diǎn)的學(xué)習(xí)和demo的練習(xí),熟悉各種語法和并發(fā)調(diào)度的場(chǎng)景, 在2019年末開始參與項(xiàng)目實(shí)戰(zhàn)開發(fā)和邏輯梳理 Go語言的接觸也是更多探索和業(yè)務(wù)的拆件,做一些雛形工具,來慢慢的孵化業(yè)務(wù)生態(tài) 后來陸陸續(xù)續(xù),在主營(yíng)業(yè)務(wù)是PHP的情況下,盡量在業(yè)務(wù)腳本的

    2024年01月21日
    瀏覽(24)
  • Go語言并發(fā)之WaitGroup

    goroutine 和 chan,一個(gè)用于并發(fā),另一個(gè)用于通信。沒有緩沖的通道具有同步的功能,除此之外,sync 包也提 供了多個(gè) goroutine 同步的機(jī)制,主要是通過 WaitGroup 實(shí)現(xiàn)的。 WaitGroup 用來等待多個(gè) goroutine 完成,main goroutine 調(diào)用 Add 設(shè)置需要等待 goroutine 的數(shù)目,每一個(gè) goroutine 結(jié)束

    2024年02月08日
    瀏覽(13)

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請(qǐng)作者喝杯咖啡吧~博客贊助

支付寶掃一掃領(lǐng)取紅包,優(yōu)惠每天領(lǐng)

二維碼1

領(lǐng)取紅包

二維碼2

領(lǐng)紅包