LoginSignup
0
1

More than 3 years have passed since last update.

golangでミニマムなREST APIを作る

Last updated at Posted at 2020-07-13

この記事は何?

注) golang初学者の私が初学者のために書いています。

https://tutorialedge.net/golang/creating-restful-api-with-golang/ のチュートリアル(英語だったが平易な文章で書かれてる。本当に)の内容をベースにして、challengeにあるupdate機能の追加の問題とMySQLを導入してみたという掛け算的内容です。
なので、上記チュートリアルが終了した段階からスタートするため悪しからず。

チュートリアル中のコードで参考になったこと。
https://qiita.com/ngplus6655/items/a38660313383d3ff2136

環境

ubuntu18.04LTS
go1.12.17 linux/amd64

github.com/gorilla/mux
github.com/jinzhu/gorm

updateArticleを追加

まずはチュートリアル中のchallengeの内容を説いてみた


func updateArticle(w http.ResponseWriter, r *http.Request){
    vars := mux.Vars(r)
    id := vars["id"]
    reqBody, _ := ioutil.ReadAll(r.Body)
    var updateArticle Article
    json.Unmarshal(reqBody, &updateArticle)
    for index, article := range Articles {
        if article.Id == id {
            updateArticle.Id = id
            Articles[index] = updateArticle
        }
    }
}

returnSingleArticleと同様urlのidパラメータから編集するarticleを特定し、直接Articlesスライスの要素に代入しています。
直前にupdateArticle.Id = idとしているのは、パラメータidと送られたjsonデータのidが不一致だとArticleに対してIDがユニークではなくなるため。

マルチプレクサにupdateArticleを登録

myRouter.HandleFunc("/article/{id}", updateArticle).Methods("PUT")

curlしてみる

curl -X PUT  -H "Content-Type: application/json" -d '{"id": "1", "Title": "Updated Post", "desc": "the description for my updated post", "content": "my articles content"}' http://localhost:10000/article/1

curl -X GET http://localhost:10000/all
[{"Id":"1","Title":"Updated Post","desc":"the description for my updated post","content":"my articles content"},
{"Id":"2","Title":"Hello 2","desc":"Article Description","content":"Article Content"},
{"Id":"3","Title":"Hello 3","desc":"Article Description","content":"Article Content"}]

意図したとおりにidが1の要素を更新できました。

mysqlの導入

完成形

main.go
package main

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

    "github.com/gorilla/mux"
    "github.com/jinzhu/gorm"
    _ "github.com/jinzhu/gorm/dialects/mysql"
)

type Article struct {
    gorm.Model
    Title string `json:"Title"`
    Desc string `json:"desc"`
    Content string `json:"content"`
}

type Articles []Article

func connectDB() (*gorm.DB, error) {
    db, err := gorm.Open("mysql", "ユーザ名:パスワード@/test?charset=utf8&parseTime=True&loc=Local")
    return db, err
}

func initDb() *gorm.DB {
    db, err := connectDB()
    if err != nil {
        log.Fatalln("データベースの接続に失敗しました。")
    }
    return db
}

func idParamToUint(r *http.Request) uint {
    vars := mux.Vars(r)
    id, _ := strconv.Atoi(vars["id"])
    var uid uint = uint(id)
    return uid
}

func ParseJsonArticle(w http.ResponseWriter ,r *http.Request) Article {
    reqBody, _ := ioutil.ReadAll(r.Body)
    var article Article
    json.Unmarshal(reqBody, &article)
    return article
}

func homePage(w http.ResponseWriter, r *http.Request){
    fmt.Fprintf(w, "Welcome to the HomePage!")
    fmt.Println("Endpoint Hit: homePage")
}

func returnAllArticles(w http.ResponseWriter, r *http.Request){
    fmt.Println("Endpoint Hit: returnAllArticles")
    db := initDb()
    var articles Articles
    db.Find(&articles)
    json.NewEncoder(w).Encode(articles)
}

func returnSingleArticle(w http.ResponseWriter, r *http.Request){
    fmt.Println("called returnSingleArticle")
    uid := idParamToUint(r)
    db := initDb()
    var article Article
    db.Where("id = ?", uid).First(&article)
    json.NewEncoder(w).Encode(article)
}

func createNewArticle(w http.ResponseWriter, r *http.Request) {
    fmt.Println("called createNewArticle")
    db := initDb()
    article := ParseJsonArticle(w, r)
    db.Create(&article)
    if db.NewRecord(article) {
        log.Println("新規articleの保存に失敗しました。")
    }
}

func updateArticle(w http.ResponseWriter, r *http.Request){
    fmt.Println("called updateAtricle")
    uid := idParamToUint(r)
    db := initDb()

    updatedArticle := ParseJsonArticle(w, r)

    var article Article
    db.Where("id = ?", uid).First(&article)

    article.Title = updatedArticle.Title
    article.Desc = updatedArticle.Desc
    article.Content = updatedArticle.Content
    db.Save(&article)
}

func deleteArticle(w http.ResponseWriter, r *http.Request) {
    fmt.Println("called deleteAtricle")
    uid := idParamToUint(r)
    db := initDb()
    db.Delete(Article{}, "id = ?", uid)
}

func handleRequests() {
    myRouter := mux.NewRouter().StrictSlash(true)
    myRouter.HandleFunc("/", homePage)
    myRouter.HandleFunc("/all", returnAllArticles)
    myRouter.HandleFunc("/article/{id}", returnSingleArticle).Methods("GET")
    myRouter.HandleFunc("/article", createNewArticle).Methods("POST")
    myRouter.HandleFunc("/article/{id}", updateArticle).Methods("PUT")
    myRouter.HandleFunc("/article/{id}", deleteArticle).Methods("DELETE")
    log.Fatal(http.ListenAndServe(":10000", myRouter))
}


func main() {
    fmt.Println("Rest API v2.0 - Mux Routers")
    db, err := connectDB()
    if err != nil {
        log.Fatalln("データベースの接続に失敗しました。")
    }
    defer db.Close()
    db.AutoMigrate(&Article{})

    handleRequests()
}

MySQLのインストール

$ sudo apt install mysql-server mysql-client
$ mysql --version
mysql  Ver 14.14 Distrib 5.7.30, for Linux (x86_64) using  EditLine wrapper

起動

$ sudo service mysql start

初期設定->データベース作成->ユーザ追加

参考 https://qiita.com/houtarou/items/a44ce783d09201fc28f5

$ sudo mysql_secure_installation
$ sudo mysql -u root -p

mysql> CREATE DATABASE test;
mysql> set global validate_password_policy=LOW;
mysql> CREATE USER '名前'@'localhost' IDENTIFIED BY 'パスワード';
mysql> grant all on test.* to ユーザ名@localhost;

Article構造体の再定義

type Article struct {
    gorm.Model
    Title string `json:"Title"`
    Desc string `json:"desc"`
    Content string `json:"content"`
}
type Articles []Article

IDやらタイムスタンプをGORMのモデル構造体に切り替えます。
スライスのArticlesはもう使いませんが一括で扱うところが出てくるので型として定義します。

データベース接続

func connectDB() (*gorm.DB, error) {
    db, err := gorm.Open("mysql", "ユーザ名:パスワード@/テーブル名?charset=utf8&parseTime=True&loc=Local")
    return db, err
}

func initDb() *gorm.DB {
    db, err := connectDB()
    if err != nil {
        log.Fatalln("データベースの接続に失敗しました。")
    }
    return db
}

/all

func returnAllArticles(w http.ResponseWriter, r *http.Request){
    db := initDb()
    var articles Articles
    db.Find(&articles)
    json.NewEncoder(w).Encode(articles)
}

先に定義した構造体をインスタンス化し、db.find関数ですべての行のデータを格納します。

/article/{id} GET

func idParamToUint(r *http.Request) uint {
    vars := mux.Vars(r)
    id, _ := strconv.Atoi(vars["id"])
    var uid uint = uint(id)
    return uid
}

func returnSingleArticle(w http.ResponseWriter, r *http.Request){
    uid := idParamToUint(r)
    db := initDb()
    var article Article
    db.Where("id = ?", uid).First(&article)
    json.NewEncoder(w).Encode(article)
}

urlパラメータのidはstring型ですが、MySQLに登録されているidはuint型です。そのためstring型をuint型に型変換する関数の定義をしています。
strconvパッケージのstringToUintは、uint64型がreturnされてしまうため使わず、いったんint型にしています。
それから、GORMのdb.Where関数つかってidが一致するarticleを一つだけ取り出します。

/article POST

func ParseJsonArticle(w http.ResponseWriter ,r *http.Request) Article {
    reqBody, _ := ioutil.ReadAll(r.Body)
    var article Article
    json.Unmarshal(reqBody, &article)
    json.NewEncoder(w).Encode(article)
    return article
}

func createNewArticle(w http.ResponseWriter, r *http.Request) {
    fmt.Println("called createNewArticle")
    db := initDb()
    article := ParseJsonArticle(w, r)
    db.Create(&article)
    if db.NewRecord(article) {
        log.Println("新規articleの保存に失敗しました。")
    }
}

まずは、関数として(updateでも再利用するため)リクエストボディで送られてくるJSONをパースし、構造体として扱えるようにします。
GORMのCreate関数でMySqlにあたらしい行を追加します。

※ 編集しました(2020/7/16)
log.Fatalln -> log.Printlnに変更しました。
データの保存に失敗したときにプログラムを終了させる必要はないと考えるからです。

/article/{id} PUT


func updateArticle(w http.ResponseWriter, r *http.Request){
    uid := idParamToUint(r)
    db := initDb()

    updatedArticle := ParseJsonArticle(w, r)

    var article Article
    db.Where("id = ?", uid).First(&article)

    article.Title = updatedArticle.Title
    article.Desc = updatedArticle.Desc
    article.Content = updatedArticle.Content
    db.Save(&article)
}

CRUDで最も複雑なupdateですが、今までの組み合わせで実現できました。ここでは、Article構造体を二つインスタンス化させています。
一つはMySQLから引っ張ってきたデータ用、もう一つはリクエストボディから更新後のデータとして入ってくるupdatedArticleで、一つ目のArticleに二つ目のupdatedArticleを代入後Save関数を呼び出しています。

/article/{id} DELETE

func deleteArticle(w http.ResponseWriter, r *http.Request) {
    uid := idParamToUint(r)
    db := initDb()
    db.Delete(Article{}, "id = ?", uid)
}

GORMのDelete関数によって簡単に実装できました:relaxed:

handleRequests関数


func handleRequests() {
    myRouter := mux.NewRouter().StrictSlash(true)
    myRouter.HandleFunc("/", homePage)
    myRouter.HandleFunc("/all", returnAllArticles)
    myRouter.HandleFunc("/article/{id}", returnSingleArticle).Methods("GET")
    myRouter.HandleFunc("/article", createNewArticle).Methods("POST")
    myRouter.HandleFunc("/article/{id}", updateArticle).Methods("PUT")
    myRouter.HandleFunc("/article/{id}", deleteArticle).Methods("DELETE")
    log.Fatal(http.ListenAndServe(":10000", myRouter))
}

ポイントは、"article/{id}" の.Methods("GET")省略していないところです。
仮に省略してしまうとPUTやDELETEをつけてcurlしても、すべてGETメソッドのreturnSingleArticle関数にルーティングされてしまいます。

まとめ

GORMとGorilla/muxパッケージでいい感じにRestAPIを作れた。
特にGORMに関しては、公式ページが日本語化されていて楽だった。
急いでクライアント側も作らなくては!

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1