Golang學習系列第五天: Golang和PostgreSQL開發 RESTful API

dongguangming發表於2020-07-21

我們經常會用csdn、部落格園、掘金、抖音、infoq、medium等寫文稿分享,我就以此寫個這樣的restful api介面,我寫的是前後端分離式的介面(至於他們的是不是前後端一起或分離我就不知道了,也不想考究)。

一篇文章會有標題、子標題、摘要、背景圖片、具體內容、文章分類、文章標籤、文章作者(可能是聯名寫的,就像寫書時多名作者)、釋出時間、更新時間、文章型別(自己寫的、轉載的、或翻譯老外的(比如掘金上就有不少))、是否置頂、是否被推薦、預計讀的時間、瀏覽量、文章狀態(擬稿、已釋出、已邏輯刪除)。

注:我只寫基本的文章api介面

2.1 建庫建表

進入到資料庫環境中,建立資料庫csdn,然後建立文章表結構,我沒有使用那麼多欄位列。

[root@master ~]# psql -h 192.168.8.200 -p 5432 -U postgres
psql (12.3)
Type "help" for help.

postgres=# CREATE DATABASE csdn;
CREATE DATABASE
postgres=# \c csdn
You are now connected to database "csdn" as user "postgres".
csdn=# CREATE TABLE article (
csdn(#     id SERIAL PRIMARY KEY,
csdn(#     title   char(100),
csdn(#     content text,
csdn(#     category  text ARRAY[4],
csdn(#     tag  text ARRAY[4],
csdn(#     type  char(10),
csdn(#     author  text ARRAY[10]
csdn(# );
CREATE TABLE
csdn=# INSERT INTO article VALUES (1,
csdn(# 'Golang學習系列第五天: Golang和PostgreSQL開發 RESTful API',
csdn(# '用go和postgresql開發restful api介面的過程',
csdn(# ARRAY['golang','postgresql'],
csdn(# ARRAY['golang', 'go','postgresql'],
csdn(# '原創',
csdn(# ARRAY['dongguangming', 'dgm']
csdn(# );
INSERT 0 1
csdn=# select * from article;
 id |                                                      title                                                      |                  conte
nt                  |      category       |          tag           |     type     |       author        
----+-----------------------------------------------------------------------------------------------------------------+-----------------------
--------------------+---------------------+------------------------+--------------+---------------------
  1 | Golang學習系列第五天: Golang和PostgreSQL開發 RESTful API                                                       | 用go和postgresql開發re
stful api介面的過程 | {golang,postgresql} | {golang,go,postgresql} | 原創         | {dongguangming,dgm}
(1 row)

csdn=# 

2. 2 開始golang開發介面

2.2.1 新建工程csdn

建工程目錄csdn,並建立相應的子資料夾(類似於mvc那套結構)

[root@master ~]# cd /dongguangming/goworkspace/
[root@master goworkspace]# pwd
/dongguangming/goworkspace
[root@master goworkspace]# mkdir csdn csdn/{models,middlewares,responses,api}
[root@master goworkspace]# cd csdn/

然後初始化自定義模組

go mod init csdn

檢視對應的目錄結構

接著建立具體的實物model,http響應資料格式,資料庫連線

2.2.2 新建文章實體

先建article.go檔案

[root@master csdn]# touch models/article.go

,鍵入以下程式碼

package models

import (
    "errors"
    "github.com/jinzhu/gorm"
    "strings"
)

type Article struct {
    gorm.Model
    Title        string `gorm:"size:100;not null;unique" json:"title"`
    Content      string `gorm:"not null"                 json:"content"`
    Category     string `gorm:"size:50;not null"         json:"category"`
    Tag          string `gorm:"size:50;not null"         json:"tag"`
  Author       string `gorm:"size:50;not null"         json:"author"`
}

func (article *Article) Prepare() {
    article.Title = strings.TrimSpace(article.Title)
    article.Content = strings.TrimSpace(article.Content)
    article.Category = strings.TrimSpace(article.Category)
    article.Tag = strings.TrimSpace(article.Tag)
    article.Author = strings.TrimSpace(article.Author)
}

func (article *Article) Validate() error {
    if article.Title == "" {
        return errors.New("Name is required")
    }
    if article.Content == "" {
        return errors.New("Description about venue is required")
    }

    if article.Category == "" {
        return errors.New("Category of venue is required")
    }
    if article.Tag == "" {
        return errors.New("Category of venue is required")
    }
    if article.Author == "" {
        return errors.New("Category of venue is required")
    }
    return nil
}

func (article *Article) SaveArticle(db *gorm.DB) (*Article, error) {
    var err error

    // Debug a single operation, show detailed log for this operation
    err = db.Debug().Create(&article).Error
    if err != nil {
        return &Article{}, err
    }
    return article, nil
}

func (article *Article) GetArticle(db *gorm.DB) (*Article, error) {
    newArticle := &Article{}
    if err := db.Debug().Table("article").Where("title = ?", article.Title).First(newArticle).Error; err != nil {
        return nil, err
    }
    return newArticle, nil
}

func GetArticles(db *gorm.DB) (*[]Article, error) {
    articles := []Article{}
    if err := db.Debug().Table("article").Find(&articles).Error; err != nil {
        return &[]Article{}, err
    }
    return &articles, nil
}

func GetArticleById(id int, db *gorm.DB) (*Article, error) {
    article := &Article{}
    if err := db.Debug().Table("article").Where("id = ?", id).First(article).Error; err != nil {
        return nil, err
    }
    return article, nil
}

func (article *Article) UpdateArticle(id int, db *gorm.DB) (*Article, error) {
    if err := db.Debug().Table("article").Where("id = ?", id).Updates(Article{
        Title : article.Title,
      Content : article.Content,
      Category : article.Category,
      Tag : article.Tag,
      Author : article.Author}).Error; err != nil {
        return &Article{}, err
    }
    return article, nil
}

func DeleteArticle(id int, db *gorm.DB) error {
    if err := db.Debug().Table("article").Where("id = ?", id).Delete(&Article{}).Error; err != nil {
        return err
    }
    return nil
}

2.2.3 建立json響應資料格式

繼續在csdn目錄下,建立響應格式檔案json.go,json很流行了,以前的xml資料交換格式被幹掉了

[root@master csdn]# touch responses/json.go

然後鍵入以下程式碼

package responses

import (
    "encoding/json"
    "fmt"
    "net/http"
)

// JSON returns a well formated response with a status code
func JSON(w http.ResponseWriter, statusCode int, data interface{}) {
    w.WriteHeader(statusCode)
    err := json.NewEncoder(w).Encode(data)
    if err != nil {
        fmt.Fprintf(w, "%s", err.Error())
    }
}

// ERROR returns a jsonified error response along with a status code.
func ERROR(w http.ResponseWriter, statusCode int, err error) {
    if err != nil {
        JSON(w, statusCode, struct {
            Error string `json:"error"`
        }{
            Error: err.Error(),
        })
        return
    }
    JSON(w, http.StatusBadRequest, nil)
}

2.2.4 建立公共中介軟體

在命令列輸入命令建立公共檔案

[root@master csdn]# touch middlewares/middlewares.go

鍵入以下程式碼

package middlewares

import (
    "context"
    "net/http"
    "os"
    "strings"

    jwt "github.com/dgrijalva/jwt-go"

    "csdn/responses"
)

// SetContentTypeMiddleware sets content-type to json
func SetContentTypeMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        next.ServeHTTP(w, r)
    })
}

2.2.5 建立具體的api介面

先建立公共基礎性程式碼檔案,比如資料庫連線、路由表和將來的token驗證等,在此僅資料庫連線為例

建立base.go檔案和具體的文章介面檔案article.go

[root@master csdn]# touch api/base.go  api/article.go 

然後分別編輯兩檔案,base.go檔案鍵入以下程式碼

package api

import (
    "fmt"
    "log"
    "net/http"

    "github.com/gorilla/mux"
    "github.com/jinzhu/gorm"
    _ "github.com/jinzhu/gorm/dialects/postgres" //postgres

    "csdn/middlewares"
    "csdn/models"
    "csdn/responses"
)

type App struct {
    Router *mux.Router
    DB     *gorm.DB
}

// 初始化資料庫連線,新增路由表
func (a *App) Initialize(DbHost, DbPort, DbUser, DbName, DbPassword string) {
    var err error
    DBURI := fmt.Sprintf("host=%s port=%s user=%s dbname=%s sslmode=disable password=%s", DbHost, DbPort, DbUser, DbName, DbPassword)

    a.DB, err = gorm.Open("postgres", DBURI)
    if err != nil {
        fmt.Printf("\n 不能連線到資料庫: %s", DbName)
        log.Fatal("發生了連線資料庫錯誤:", err)
    } else {
        fmt.Printf("恭喜連線到資料庫: %s", DbName)
    }

    a.DB.Debug().AutoMigrate(&models.Article{}) //database migration

    a.Router = mux.NewRouter().StrictSlash(true)
    a.initializeRoutes()
}

func (a *App) initializeRoutes() {
    a.Router.Use(middlewares.SetContentTypeMiddleware) // setting content-type to json

    a.Router.HandleFunc("/", home).Methods("GET")

    s := a.Router.PathPrefix("/api").Subrouter() // subrouter to add auth middleware

    s.HandleFunc("/article", a.CreateArticle).Methods("POST")
    s.HandleFunc("/article", a.GetArticles).Methods("GET")
    s.HandleFunc("/article/{id:[0-9]+}", a.GetArticle).Methods("GET")
    s.HandleFunc("/article/{id:[0-9]+}", a.UpdateArticle).Methods("PUT")
    s.HandleFunc("/article/{id:[0-9]+}", a.DeleteArticle).Methods("DELETE")
}

func (a *App) RunServer() {
    log.Printf("\nServer starting on port 9999")
    log.Fatal(http.ListenAndServe("192.168.8.200:9999", a.Router))
}

func home(w http.ResponseWriter, r *http.Request) {
    responses.JSON(w, http.StatusOK, "Golang學習系列第五天: Golang和PostgreSQL開發 RESTful API")
}

然後article.go檔案鍵入以下程式碼

package api

import (
    "encoding/json"
    "io/ioutil"
    "net/http"
    "strconv"

    "github.com/gorilla/mux"

    "csdn/models"
    "csdn/responses"
)

// 建立文章
func (a *App) CreateArticle(w http.ResponseWriter, r *http.Request) {
    var resp = map[string]interface{}{"status": "success", "message": "文章建立成功"}

    article := &models.Article{}
    body, err := ioutil.ReadAll(r.Body)
    if err != nil {
        responses.ERROR(w, http.StatusBadRequest, err)
        return
    }

    err = json.Unmarshal(body, &article)
    if err != nil {
        responses.ERROR(w, http.StatusBadRequest, err)
        return
    }

    article.Prepare()

    if err = article.Validate(); err != nil {
        responses.ERROR(w, http.StatusBadRequest, err)
        return
    }

    if existed, _ := article.GetArticle(a.DB); existed != nil {
        resp["status"] = "failed"
        resp["message"] = "文章已存在"
        responses.JSON(w, http.StatusBadRequest, resp)
        return
    }

    articleCreated, err := article.SaveArticle(a.DB)
    if err != nil {
        responses.ERROR(w, http.StatusBadRequest, err)
        return
    }

    resp["article"] = articleCreated
    responses.JSON(w, http.StatusCreated, resp)
    return
}

//獲取所有文章
func (a *App) GetArticles(w http.ResponseWriter, r *http.Request) {
    articles, err := models.GetArticles(a.DB)
    if err != nil {
        responses.ERROR(w, http.StatusInternalServerError, err)
        return
    }
    responses.JSON(w, http.StatusOK, articles)
    return
}

//獲取單篇文章
func (a *App) GetArticle(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)
    id, _ := strconv.Atoi(vars["id"])

    article, err := models.GetArticleById(id, a.DB)
    if err != nil {
        responses.ERROR(w, http.StatusInternalServerError, err)
        return
    }
    responses.JSON(w, http.StatusOK, article)
    return
}

//更改文章
func (a *App) UpdateArticle(w http.ResponseWriter, r *http.Request) {
    var resp = map[string]interface{}{"status": "success", "message": "修改文章成功!!!"}

    vars := mux.Vars(r)

    id, _ := strconv.Atoi(vars["id"])

    //article, err := models.GetArticleById(id, a.DB)

    body, err := ioutil.ReadAll(r.Body)
    if err != nil {
        responses.ERROR(w, http.StatusBadRequest, err)
        return
    }

    articleUpdate := models.Article{}
    if err = json.Unmarshal(body, &articleUpdate); err != nil {
        responses.ERROR(w, http.StatusBadRequest, err)
        return
    }

    articleUpdate.Prepare()

    _, err = articleUpdate.UpdateArticle(id, a.DB)
    if err != nil {
        responses.ERROR(w, http.StatusInternalServerError, err)
        return
    }

    responses.JSON(w, http.StatusOK, resp)
    return
}

//刪除文章
func (a *App) DeleteArticle(w http.ResponseWriter, r *http.Request) {
    var resp = map[string]interface{}{"status": "success", "message": "文章刪除成功!!!"}

    vars := mux.Vars(r)

    id, _ := strconv.Atoi(vars["id"])

    //article, err := models.GetArticleById(id, a.DB)

    err := models.DeleteArticle(id, a.DB)
    if err != nil {
        responses.ERROR(w, http.StatusInternalServerError, err)
        return
    }
    responses.JSON(w, http.StatusOK, resp)
    return
}

2.2.6 建立主檔案main.go

最後建立主檔案,類似於java裡啟動檔案

[root@master csdn]# touch main.go

鍵入以下程式碼

package main

import (
    "log"
    "os"

    "github.com/joho/godotenv"

    "csdn/api"
)

func main() {
    if err := godotenv.Load(); err != nil {
        log.Fatal("不能載入屬性.env檔案")
    }

    app := api.App{}
    app.Initialize(
        os.Getenv("DB_HOST"),
        os.Getenv("DB_PORT"),
        os.Getenv("DB_USER"),
        os.Getenv("DB_NAME"),
        os.Getenv("DB_PASSWORD"))

    app.RunServer()
}

建立資料庫配置檔案.env,

[root@master csdn]# touch  .env 

鍵入資料庫具體連線資訊

DB_HOST=192.168.8.200
DB_USER=postgres
DB_PASSWORD=12345678
DB_NAME=csdn
DB_PORT=5432

終於敲完了程式碼,執行主程式

[root@master csdn]# go  run main.go 

當出現以下介面時,表示成功!!!

go啟動

此時也可以通過瀏覽器或postman進行測試

2.3 api介面測試

2.3.1 建立一篇文章

用postman測試api介面,注意資料格式是json

api建立文章介面

然後檢視後臺日誌

文章建立日誌

最後通過資料庫檢視錶裡是否已有資料

可以多新增幾條記錄方便下面測試

2.3.2 查詢一篇文章

繼續用postman測試api介面,注意資料格式是json,注意是根據id查詢一篇文章

2.3.3 查詢所有文章

繼續用postman測試api介面,注意資料格式是json

​" class="reference-link">

2.3.4 修改一篇文章

繼續用postman測試api介面,注意資料格式是json

文章id為1的原資料

修改id為1的資料,這裡修改內容和作者舉例

修改文章

再次檢視id為1的文章

2.3.5 刪除一篇文章

繼續用postman測試api介面,注意資料格式是json,要刪除的文章id為6

預先新增一條要刪除的資料記錄

然後執行刪除操作,刪除前先查一次id為6的文章,看是否有記錄

最後執行刪除

再次查詢文章id為6的記錄就沒有了

至此,一個基於Golang和PostgreSQL開發的restful api介面就結束了!!!介面,一切皆資源api介面。

後續可以繼續完善(比如token、快取、mq、搜尋等),和ava版本可以pk

再次推薦下介面測試工具Postman,槓槓的。

參考:

  1. PostgreSQL JSON Types www.postgresql.org/docs/9.4/dataty...

  2. JSON Functions and Operators in PostgreSQL www.postgresql.org/docs/9.4/functi...

  3. Using PostgreSQL JSONB with Go www.alexedwards.net/blog/using-pos...

  4. Unmarshaling postgresql jsonb query response string into golang nested struct stackoverflow.com/questions/565588...

  5. Golang Guide: A List of Top Golang Frameworks, IDEs & Tools medium.com/@quintinglvr/golang-gui...

  6. Choosing a language for Freshdesk Microservices www.freshworks.com/company/java-vs...

  7. 7 Frameworks To Build A REST API In Go nordicapis.com/7-frameworks-to-bui...

  8. Best practice for Database Open and Close connection forum.golangbridge.org/t/best-prac...

  9. 輕量級 Web 框架 Gin 結構分析 blog.itpub.net/31561269/viewspace-2...

  10. How to build a REST API with Golang using Gin and Gorm

    blog.logrocket.com/how-to-build-a-...

  11. Building and Testing a REST API in Go with Gorilla Mux and PostgreSQL semaphoreci.com/community/tutorial...

  12. Build a RESTful API with GO and PostgreSQL. scotch.io/@walugembe-peter/build-a...

  13. Beautify your Golang project medium.com/m/global-identity?redir...

  14. ​" class="reference-link">Restful Web API (一切皆資源,設計介面必備書)restful api

本作品採用《CC 協議》,轉載必須註明作者和本文連結

人生,不應設限

相關文章