■はじめに
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
package main
func main() {
}
■ Model を作成
Go 言語では、struct と呼ばれる構造体で、Model を作成する。
Model は、データの元になるものだ。
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 と関連づけている。
■ エンドポイントの作成
次は、エンドポイントを作成する。
エンドポイントとは、あるプログラムが外部に公開している機能の所在を示す識別名。
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:08 Get all articles
■ データの追加
あとで、ちゃんとしたデータベースを使用するが、とりあえず静的なデータを追加して、動作確認を行う。
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 ....
うまくいった! 次!!
■ 単一データ取得
次は、Article の単一データを取得する。
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)
}
}
}
動作確認を行う。
// サーバー再起動
go run main.go
http://localhost:8000/articles/1
を指定して Send ボタンを押下する。
先ほどとは違い、今度は、指定した ID のデータのみ取得できた。
■ INSERT 機能
RESTAPI を介して、データの登録機能を作ろう。
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
成功すると 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 ....
200 OK が出れば、OK。
■ DELETE 機能
続いて、データ削除機能を作ろう。
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
ひとまず、200 OK が出てるので、OKとする。
ちなみに、コンソールには、以下が出力された。
params: map[id:1]
id: 1
articles: []
■ elephantsql
データベースを導入しよう。
簡単に使える elephantsql を使用する。
* 無料枠を使う
Sign up をしたら DB を作成するページに移動する。
ひとまず、Article という DB を作成した。
Article をクリックすると下記の画面に飛ぶことができる。
この画面に出てくる 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;
ひとまず、データベースの導入は、以上だ。
■ DB 接続
先ほど登録した DB に接続する。
接続先の URL を .env ファイルに書き込む
$ touch .env
ELEPHANTSQL_URL= "Here ELEPHANTSQL_URL"
URL は、先ほど DB を作成した時に確認した URL を入力する。
main.go に DB
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 文を発行できるように変更する。
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
へリクエストを送る。
200 ok と データが返却されてきたら OK
一応、elephantsql からも確認する。
同一データが取得できたようだ。
■ Refactor - getArticle
getArticle 関数の単一データの取得を行う処理を DB 版に書き換える。
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
へリクエストを送る。
200 OK が帰ってきたので、OK!
■ Refactor - addArticle
addArticle 関数の INSERT 処理を DB 版に書き換える。
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"}
ちゃんと値が帰ってきている。
elephantsql からも確認しよう。
Insert したデータが格納されていることがわかる。
■ Refactor - updateArticle
updateArticle 関数の UPDATE 処理を DB 版に書き換える。
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"}
200 OK なので、OK!
elephantsql からも確認しよう。
Update Data が確認できた。
■ Refactor - removeArticle
removeArticle 関数の DELETE 処理を DB 版に書き換える。
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
へリクエストを送る。
200 OK なので、OK!
elephantsql からも確認しよう。
OK!
■ 誤り
postdate に値が入っていないことに気が付いた。
以下のデータで、INSERT / UPDATE 可能だ。
{"title":"Update Data","author":"Gophar","postdate":"2019"}
year -> postdate の誤りだ。
■ まとめ
簡単ではあったが、RESTAPI で CRUD を実装した。
Go 言語に慣れたらもっと複雑なことをしよう。