Help us understand the problem. What is going on with this article?

RESTAPI WITH Go言語!

More than 1 year has passed since last update.

学習履歴

■はじめに

Go 言語で、RESTAPI を勉強している。

以下に備忘を残す。

■ Install Package

今回使うパッケージは、以下になる。

go get -u github.com/gorilla/mux
go get github.com/subosito/gotenv
go get github.com/lib/pq

■ ディレクトリ構成

シンプルに main.go のみ作成しておく。

$ tree
.
└── main.go
main.go
package main

func main() {
}

■ Model を作成

Go 言語では、struct と呼ばれる構造体で、Model を作成する。

Model は、データの元になるものだ。

main.go
package main

type Article struct {
    ID       int    `json:id`
    Title    string `json:title`
    Author   string `json:author`
    PostDate string `json:year`
}

func main() {
}

struct で、 Article モデルを作成した。

先に言っておくと、API サーバーを Go 言語で作るつもりなので、Google Chrome の Restlet Client というツールをインストールしておこう。

このツールは、クライアント(ここでは、筆者の PC)から Golang で作った API サーバーへリクエストを送り、データの受け渡しを行う。

データの受け渡しには、json を使用するので、json:~ で、json と関連づけている。

■ エンドポイントの作成

次は、エンドポイントを作成する。

エンドポイントとは、あるプログラムが外部に公開している機能の所在を示す識別名。

main.go
package main

import (
    "log"
    "net/http"

    "github.com/gorilla/mux"
)

//
type Article struct {
    ID       int    `json:id`
    Title    string `json:title`
    Author   string `json:author`
    PostDate string `json:year`
}

func getArticles(w http.ResponseWriter, r *http.Request) {
    log.Printf("Get all articles")
}

func getArticle(w http.ResponseWriter, r *http.Request) {
    log.Println("Get article is called")
}

func addArticle(w http.ResponseWriter, r *http.Request) {
    log.Println("Add article is called")
}

func updateArticle(w http.ResponseWriter, r *http.Request) {
    log.Println("Update article is called")
}

func removeArticle(w http.ResponseWriter, r *http.Request) {
    log.Println("Remove article is called")
}

func main() {
    // リクエストを裁くルーターを作成
    router := mux.NewRouter()

    // エンドポイント
    router.HandleFunc("/articles", getArticles).Methods("GET")
    router.HandleFunc("/articles/{id}", getArticle).Methods("GET")
    router.HandleFunc("/articles", addArticle).Methods("POST")
    router.HandleFunc("/articles", updateArticle).Methods("PUT")
    router.HandleFunc("/articles/{id}", removeArticle).Methods("DELETE")

    // Start Server
    log.Println("Listen Server ....")
    // 異常があった場合、処理を停止したいため、log.Fatal で囲む
    log.Fatal(http.ListenAndServe(":8000", router))
}

全部で 5 つのエンドポイントを設置した。

エンドポイントには、gorilla/mux の Router 機能を使用している。

この Router にて、クライアントのリクエストに該当する関数の呼び出しを行う。

getArticles だけ動作確認を行う。

# server 起動
go run main.go

2019/05/25 21:55:57 Listen Server ....

サーバーが起動した場合、:8000 番 port で処理を受け付けるようになっている。

Restlet Client を開き、METHOD を GET にし、SCHME に http://localhost:8000/articles を入力して、Send を押下する。

200 OK が出れば、正常に起動している。

スクリーンショット 2019-05-25 21.57.29.png

コンソール上でも以下のように出力される。

2019/05/25 21:57:08 Get all articles

■ データの追加

あとで、ちゃんとしたデータベースを使用するが、とりあえず静的なデータを追加して、動作確認を行う。

main.go
package main

import (
    "encoding/json"
    "log"
    "net/http"

    "github.com/gorilla/mux"
)

type Article struct {
    ID       int    `json:id`
    Title    string `json:title`
    Author   string `json:author`
    PostDate string `json:year`
}

// スライスを用意
var articles []Article

func getArticles(w http.ResponseWriter, r *http.Request) {
      // strct を json に変換
    json.NewEncoder(w).Encode(articles)
}

func main() {
// リクエストを裁くルーターを作成
    router := mux.NewRouter()

    articles = append(articles,
        Article{ID: 1, Title: "Article1", Author: "Gopher", PostDate: "2019/1/1"},
        Article{ID: 2, Title: "Article2", Author: "Gopher", PostDate: "2019/2/2"},
        Article{ID: 3, Title: "Article3", Author: "Gopher", PostDate: "2019/3/3"},
        Article{ID: 4, Title: "Article4", Author: "Gopher", PostDate: "2019/4/4"},
        Article{ID: 5, Title: "Article5", Author: "Gopher", PostDate: "2019/5/5"},
    )

...
}

Article モデルにデータを追加した後、サーバーを再起動して動作確認を行う。

go run main.go
2019/05/25 22:11:16 Listen Server ....

スクリーンショット 2019-05-25 22.12.07.png

うまくいった! 次!!

■ 単一データ取得

次は、Article の単一データを取得する。

main.go
package main

import (
    "encoding/json"
    "log"
    "net/http"
    "reflect"
    "strconv"

    "github.com/gorilla/mux"
)

func getArticle(w http.ResponseWriter, r *http.Request) {
    // get http://localhost:8000/books/hoge -> hoge を取得
    params := mux.Vars(r)
    log.Println(params) // map[id:1]

    // /What's params type?
    log.Println(reflect.TypeOf(params["id"])) // -> Get String

    // Convert Type from String -> Int
    // Not handling err -> _
    i, _ := strconv.Atoi(params["id"])

    // URL に指定した ID の情報を取得
    for _, article := range articles {
        if article.ID == i {
            json.NewEncoder(w).Encode(&article)
        }
    }
}

動作確認を行う。

main.go
// サーバー再起動
go run main.go

http://localhost:8000/articles/1 を指定して Send ボタンを押下する。

スクリーンショット 2019-05-25 22.25.32.png

先ほどとは違い、今度は、指定した ID のデータのみ取得できた。

■ INSERT 機能

RESTAPI を介して、データの登録機能を作ろう。

main.go
func addArticle(w http.ResponseWriter, r *http.Request) {
    var article Article
    // json -> struct
    json.NewDecoder(r.Body).Decode(&article)
    fmt.Println("article: ", article)

    articles = append(articles, article)

    // struct -> json
    json.NewEncoder(w).Encode(articles)
}

func main() {
    // リクエストを裁くルーターを作成
    router := mux.NewRouter()

    // エンドポイント
    router.HandleFunc("/articles", getArticles).Methods("GET")
    router.HandleFunc("/articles/{id}", getArticle).Methods("GET")
    router.HandleFunc("/articles", addArticle).Methods("POST")
    router.HandleFunc("/articles", updateArticle).Methods("PUT")
    router.HandleFunc("/articles/{id}", removeArticle).Methods("DELETE")

    // Start Server
    log.Println("Listen Server ....")
    // 異常があった場合、処理を停止したいため、log.Fatal で囲む
    log.Fatal(http.ListenAndServe(":8000", router))
}

もともと、静的データとして main 関数に記述していた articles を削除し、代わりに addArticle にて、データを挿入できるようにした。

Restlet Client からデータを挿入してみよう。

# サーバー再起動
go run main.go

スクリーンショット 2019-05-26 17.59.55.png

成功すると 200 OK が表示される。

■ UPDATE 機能

続いて、データ更新機能を作ろう。

func updateArticle(w http.ResponseWriter, r *http.Request) {
    var article Article
    json.NewDecoder(r.Body).Decode(&article)

    for i, item := range articles {
        if item.ID == article.ID {
            articles[i] = article
        }
    }

    json.NewEncoder(w).Encode(article)
}

動作確認を行う。

$ go run main.go 
2019/05/26 18:07:18 Listen Server ....

スクリーンショット 2019-05-26 18.08.16.png

200 OK が出れば、OK。

■ DELETE 機能

続いて、データ削除機能を作ろう。

main.go
    params := mux.Vars(r)
    fmt.Println("params: ", params)

    id, _ := strconv.Atoi(params["id"])
    fmt.Println("id: ", id)

    fmt.Println("articles: ", articles)

    for i, item := range articles {
        if item.ID == id {
            articles = append(articles[:i], articles[i+1:]...)
        }
    }
    json.NewEncoder(w).Encode(articles)
}

動作確認を行う。

go run main.go

スクリーンショット 2019-05-26 18.13.58.png

ひとまず、200 OK が出てるので、OKとする。

ちなみに、コンソールには、以下が出力された。

params:  map[id:1]
id:  1
articles:  []

■ elephantsql

データベースを導入しよう。

簡単に使える elephantsql を使用する。
* 無料枠を使う

Sign up をしたら DB を作成するページに移動する。

スクリーンショット 2019-05-26 17.15.45.png

スクリーンショット 2019-05-26 17.15.59.png

スクリーンショット 2019-05-26 17.16.11.png

スクリーンショット 2019-05-26 17.16.19.png

ひとまず、Article という DB を作成した。

Article をクリックすると下記の画面に飛ぶことができる。

スクリーンショット 2019-05-26 17.18.53.png

この画面に出てくる URL は、あとで使用するので、メモしておくこと。

また、BROWSER メニューから SQL を発行できる画面に飛ぶことができるので、そこでテーブルを作成する。

# SQL 文
create table articles(id serial, title varchar, author varchar, postdate varchar);

insert into articles (title, author, postdate) values('Golang is great', 'Gophar', '2019');

select * from articles;

スクリーンショット 2019-05-26 17.24.24.png

ひとまず、データベースの導入は、以上だ。

■ DB 接続

先ほど登録した DB に接続する。

接続例

接続先の URL を .env ファイルに書き込む

$ touch .env

ELEPHANTSQL_URL= "Here ELEPHANTSQL_URL"

URL は、先ほど DB を作成した時に確認した URL を入力する。

main.go に DB

main.go
mport (
    "database/sql"
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "os"
    "reflect"
    "strconv"

    "github.com/gorilla/mux"
    "github.com/lib/pq"
    "github.com/subosito/gotenv"
)

var db *sql.DB

func init() {
    // .env 読み込み
    gotenv.Load()
}

func logFatal(err error) {
    if err != nil {
        log.Fatal(err)
    }
}

func main() {
    pgURL, err := pq.ParseURL(os.Getenv("ELEPHANTSQL_URL"))
    logFatal(err)
    log.Println("pgUrl: ", pgURL)

    // Connect to postgres
    db, err = sql.Open("postgres", pgURL)
    logFatal(err)

    err = db.Ping()
    logFatal(err)

    // リクエストを裁くルーターを作成
    router := mux.NewRouter()

    ...
}

サーバーを起動して、DB に接続できるか確認する。

$ go run main.go
2019/05/26 18:36:39 pgUrl:  xxxxxxx
2019/05/26 18:36:39 Listen Server ....

エラーが帰ってこなければ、OK。

■ Refactor - getArticles

getArticles 関数から SQL 文を発行できるように変更する。

main.go
func getArticles(w http.ResponseWriter, r *http.Request) {
    var article Article
    articles = []Article{}

    rows, err := db.Query("SELECT * FROM Articles;")
    logFatal(err)

    defer rows.Close()

    for rows.Next() {
        err := rows.Scan(&article.ID, &article.Title, &article.Author, &article.PostDate)
        logFatal(err)

        articles = append(articles, article)
    }
    json.NewEncoder(w).Encode(articles)
}

動作確認のため、サーバーを再起動する。

go run main.go

Restlet Client から http://localhost:8000/articles へリクエストを送る。

スクリーンショット 2019-05-27 6.07.32.png

200 ok と データが返却されてきたら OK

一応、elephantsql からも確認する。

スクリーンショット 2019-05-27 6.09.25.png

同一データが取得できたようだ。

■ Refactor - getArticle

getArticle 関数の単一データの取得を行う処理を DB 版に書き換える。

main.go
func getArticle(w http.ResponseWriter, r *http.Request) {
    var article Article
    params := mux.Vars(r)

    rows := db.QueryRow("SELECT * FROM ARTICLES WHERE id=$1", params["id"])

    err := rows.Scan(&article.ID, &article.Title, &article.Author, &article.PostDate)
    logFatal(err)

    json.NewEncoder(w).Encode(article)
}

サーバーを再起動する。

go run main.go

Restlet Client から http://localhost:8000/articles/1 へリクエストを送る。

スクリーンショット 2019-05-27 20.47.45.png

200 OK が帰ってきたので、OK!

■ Refactor - addArticle

addArticle 関数の INSERT 処理を DB 版に書き換える。

main.go
func addArticle(w http.ResponseWriter, r *http.Request) {
    var article Article
    var articleID int

    // json -> struct
    json.NewDecoder(r.Body).Decode(&article)

    err := db.QueryRow("INSERT INTO ARTICLES (title, author, postdate) values($1, $2, $3) RETURNING id;",
        article.Title, article.Author, article.PostDate).Scan(&articleID)

    logFatal(err)

    json.NewEncoder(w).Encode(articleID)
}

サーバーを再起動する。

go run main.go

Restlet Client から http://localhost:8000/articles/ へリクエストを送る。

# 送信データ
{"title":"Insert Data","author":"Gophar","year":"2019"}

スクリーンショット 2019-05-27 20.56.35.png

ちゃんと値が帰ってきている。

elephantsql からも確認しよう。

スクリーンショット 2019-05-27 20.57.42.png

Insert したデータが格納されていることがわかる。

■ Refactor - updateArticle

updateArticle 関数の UPDATE 処理を DB 版に書き換える。

main.go
func updateArticle(w http.ResponseWriter, r *http.Request) {
    var article Article
    json.NewDecoder(r.Body).Decode(&article)

    result, err := db.Exec("UPDATE ARTICLES SET title=$1, author=$2, postdate=$3 WHERE id=$4 RETURNING id",
        &article.Title, &article.Author, &article.PostDate, &article.ID)
    logFatal(err)

    rowsUpdated, err := result.RowsAffected()
    logFatal(err)

    json.NewEncoder(w).Encode(rowsUpdated)
}

サーバーを再起動する。

go run main.go

Restlet Client から http://localhost:8000/articles/ へリクエストを送る。

{"id": 1,"title":"Update Data","author":"Gophar","year":"2019"}

スクリーンショット 2019-05-27 21.04.41.png

200 OK なので、OK!

elephantsql からも確認しよう。

スクリーンショット 2019-05-27 21.06.48.png

Update Data が確認できた。

■ Refactor - removeArticle

removeArticle 関数の DELETE 処理を DB 版に書き換える。

main.go
func removeArticle(w http.ResponseWriter, r *http.Request) {
    params := mux.Vars(r)

    result, err := db.Exec("DELETE FROM ARTICLES WHERE id=$1", params["id"])
    logFatal(err)

    rowsDeleted, err := result.RowsAffected()
    logFatal(err)

    fmt.Println("rowsDeleted", rowsDeleted)
    json.NewEncoder(w).Encode(rowsDeleted)
}

サーバーを再起動する。

go run main.go

Restlet Client から http://localhost:8000/articles/2 へリクエストを送る。

スクリーンショット 2019-05-27 21.22.12.png

200 OK なので、OK!

elephantsql からも確認しよう。

スクリーンショット 2019-05-27 21.22.42.png

OK!

■ 誤り

postdate に値が入っていないことに気が付いた。

以下のデータで、INSERT / UPDATE 可能だ。

{"title":"Update Data","author":"Gophar","postdate":"2019"}

year -> postdate の誤りだ。

■ まとめ

簡単ではあったが、RESTAPI で CRUD を実装した。

Go 言語に慣れたらもっと複雑なことをしよう。

__init__
PythonとGo言語が一番好きです。どちらも仕事で使っています!
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away