
Go言語でGorilla Muxというルーティング用のライブラリを使って簡単なCRUDのREST APIを作る方法を紹介します。
はじめに
Go言語にはRubyのRailsやPythonのDjango、JavaScriptのExpressのようなAPIを作る上で絶対的なフレームワークはなく、複数のフレームワークが乱立しているような状態です。例えば、Revel、Gin、Martini、Web.go、Gojiなどがありますが、今回はGorilla web toolkitの中のGorilla Muxというルーティング用のライブラリを使ってAPIを実装することにします。
題材は何でも良いのですが、Twitterっぽいツイートを管理するAPIにしてみましょう。それでは、Go言語で簡単なREST APIを作っていきましょう!
Gorilla Muxとは?
Gorilla Muxは、Gorilla web toolkitというGo言語用のWebツールキット郡の一つで、主にルーティング用のライブラリです。
前提
以下の準備が完了している必要があります。
- Goがインストールされていること
- Dockerがインストールされていること
詳しいバージョンは「環境」を参照してください。
PostgreSQLの準備とテーブル作成
テーブル作成用のSQLファイルの準備
今回はデータベースとしてPostgreSQLを使い、Dockerで準備します。Dockerで起動時にテーブルを作成したいので、それ用のSQLファイルを準備します。
$ mkdir initdb
$ touch initdb/1_create_tables.sql
「1_create_tables.sql」は以下のようにします。
create table tweets
(
id serial primary key,
content text not null,
user_name text not null,
comment_num integer not null default 0,
star_num integer not null default 0,
re_tweet_num integer not null default 0
);
「tweets」テーブルを作成するSQLです。
データベースとテーブルの作成
DockerでPostgreSQLを準備し、起動時に先程作成したSQLを読み込ませます。
$ docker run --rm --name my_postgres -p 5432:5432 -e POSTGRES_USER=puser -e POSTGRES_PASSWORD=ppassword -e POSTGRES_DB=testdb -v $PWD/initdb:/docker-entrypoint-initdb.d -d postgres
$ psql -h localhost -p 5432 -U puser -d testdb
Password for user puser:
psql (11.1)
Type "help" for help.
testdb-# \dt
List of relations
Schema | Name | Type | Owner
--------+--------+-------+-------
public | tweets | table | puser
(1 row)
testdb-# \d tweets
Table "public.tweets"
Column | Type | Collation | Nullable | Default
--------------+---------+-----------+----------+------------------------------------
id | integer | | not null | nextval('tweets_id_seq'::regclass)
content | text | | not null |
user_name | text | | not null |
comment_num | integer | | not null | 0
star_num | integer | | not null | 0
re_tweet_num | integer | | not null | 0
Indexes:
"tweets_pkey" PRIMARY KEY, btree (id)
testdb=# \q
うまくテーブル作成まで完了しました。
今回は一時的なデータベースなので、「––rm」オプションを指定しており、停止するとコンテナが削除されます。(データベース及びデータは残りません)また、公開ポートはデフォルトの5432としているので、ローカルでPostgreSQLが起動している場合は、ポート番号を変更してください。
APIの作成
まずは、必要なパッケージやデータベースを準備しましょう。
ベースプロジェクトの作成
プロジェクト用のフォルダを作成し、パッケージをインストールします。それから、今回実装に必要なフォルダとファイルを作成します。
$ mkdir golang-rest-api
$ cd golang-rest-api/
$ go mod init golang-rest-api
$ go get -u github.com/gorilla/mux
$ go get -u github.com/lib/pq
$ go mod graph
golang-rest-api github.com/gorilla/mux@v1.6.2
golang-rest-api github.com/lib/pq@v1.0.0
$ mkdir models
$ mkdir controllers
$ mkdir utils
$ touch main.go
$ touch models/tweet.go
$ touch models/error.go
$ touch controllers/controller.go
$ touch controllers/tweet.go
$ touch utils/respond.go
$ tree
.
├── controllers
│ ├── controller.go
│ └── tweet.go
├── go.mod
├── go.sum
├── main.go
├── models
│ ├── error.go
│ └── tweet.go
└── utils
└── respond.go
全体像が見えるようになりました。
APIの実装
さくさく実装しましょう。
models/tweet.go
package models
type Tweet struct {
ID int `json:"id"`
Content string `json:"content"`
UserName string `json:"user_name"`
CommentNum int `json:"comment_num"`
StarNum int `json:"star_num"`
ReTweetNum int `json:"re_tweet_num"`
}
models/error.go
package models
type Error struct {
Message string `json:"message"`
}
utils/respond.go
package utils
import (
"encoding/json"
"net/http"
)
func Respond(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
controllers/controller.go
package controllers
type Controller struct{}
controllers/tweet.go
package controllers
import (
"database/sql"
"encoding/json"
"golang-rest-api/models"
"golang-rest-api/utils"
"log"
"net/http"
"strconv"
"github.com/gorilla/mux"
)
func (c Controller) GetTweets(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var tweet models.Tweet
tweets := make([]models.Tweet, 0)
var errorObj models.Error
rows, err := db.Query("select * from tweets")
if err != nil {
log.Println(err)
errorObj.Message = "Server error"
utils.Respond(w, http.StatusInternalServerError, errorObj)
return
}
defer rows.Close()
for rows.Next() {
err := rows.Scan(&tweet.ID, &tweet.Content, &tweet.UserName, &tweet.CommentNum, &tweet.StarNum,
&tweet.ReTweetNum)
if err != nil {
log.Println(err)
errorObj.Message = "Server error"
utils.Respond(w, http.StatusInternalServerError, errorObj)
return
}
tweets = append(tweets, tweet)
}
if err := rows.Err(); err != nil {
log.Println(err)
errorObj.Message = "Server error"
utils.Respond(w, http.StatusInternalServerError, errorObj)
return
}
utils.Respond(w, http.StatusOK, tweets)
}
}
func (c Controller) GetTweet(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var tweet models.Tweet
var errorObj models.Error
params := mux.Vars(r)
id, err := strconv.Atoi(params["id"])
if err != nil {
errorObj.Message = "\"id\" is wrong"
utils.Respond(w, http.StatusBadRequest, errorObj)
return
}
rows := db.QueryRow("select * from tweets where id=$1", id)
err = rows.Scan(&tweet.ID, &tweet.Content, &tweet.UserName, &tweet.CommentNum, &tweet.StarNum,
&tweet.ReTweetNum)
if err != nil {
if err == sql.ErrNoRows {
errorObj.Message = "The tweet does not exist"
utils.Respond(w, http.StatusBadRequest, errorObj)
return
}
log.Println(err)
errorObj.Message = "Server error"
utils.Respond(w, http.StatusInternalServerError, errorObj)
return
}
utils.Respond(w, http.StatusOK, tweet)
}
}
func (c Controller) AddTweet(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var tweet models.Tweet
var errorObj models.Error
json.NewDecoder(r.Body).Decode(&tweet)
if tweet.Content == "" {
errorObj.Message = "\"content\" is missing"
utils.Respond(w, http.StatusBadRequest, errorObj)
return
}
if tweet.UserName == "" {
errorObj.Message = "\"user_name\" is missing"
utils.Respond(w, http.StatusBadRequest, errorObj)
return
}
if tweet.CommentNum < 0 {
errorObj.Message = "\"comment_num\" must be greater than or equal to 0"
utils.Respond(w, http.StatusBadRequest, errorObj)
return
}
if tweet.StarNum < 0 {
errorObj.Message = "\"star_num\" must be greater than or equal to 0"
utils.Respond(w, http.StatusBadRequest, errorObj)
return
}
if tweet.ReTweetNum < 0 {
errorObj.Message = "\"re_tweet_num\" must be greater than or equal to 0"
utils.Respond(w, http.StatusBadRequest, errorObj)
return
}
err := db.QueryRow(
"insert into tweets (content, user_name, comment_num, star_num, re_tweet_num)"+
" values($1, $2, $3, $4, $5) RETURNING id;",
tweet.Content, tweet.UserName, tweet.CommentNum, tweet.StarNum, tweet.ReTweetNum).Scan(&tweet.ID)
if err != nil {
log.Println(err)
errorObj.Message = "Server error"
utils.Respond(w, http.StatusInternalServerError, errorObj)
return
}
utils.Respond(w, http.StatusCreated, tweet)
}
}
func (c Controller) PutTweet(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var tweet models.Tweet
var errorObj models.Error
params := mux.Vars(r)
id, err := strconv.Atoi(params["id"])
if err != nil {
errorObj.Message = "\"id\" is wrong"
utils.Respond(w, http.StatusBadRequest, errorObj)
return
}
json.NewDecoder(r.Body).Decode(&tweet)
if tweet.CommentNum < 0 {
errorObj.Message = "\"comment_num\" must be greater than or equal to 0"
utils.Respond(w, http.StatusBadRequest, errorObj)
return
}
if tweet.StarNum < 0 {
errorObj.Message = "\"star_num\" must be greater than or equal to 0"
utils.Respond(w, http.StatusBadRequest, errorObj)
return
}
if tweet.ReTweetNum < 0 {
errorObj.Message = "\"re_tweet_num\" must be greater than or equal to 0"
utils.Respond(w, http.StatusBadRequest, errorObj)
return
}
var update models.Tweet
rows := db.QueryRow("select * from tweets where id=$1", id)
err = rows.Scan(&update.ID, &update.Content, &update.UserName, &update.CommentNum,
&update.StarNum, &update.ReTweetNum)
if err != nil {
if err == sql.ErrNoRows {
errorObj.Message = "The tweet does not exist"
utils.Respond(w, http.StatusBadRequest, errorObj)
return
}
log.Println(err)
errorObj.Message = "Server error"
utils.Respond(w, http.StatusInternalServerError, errorObj)
return
}
if tweet.Content != "" {
update.Content = tweet.Content
}
if tweet.UserName != "" {
update.UserName = tweet.UserName
}
if tweet.CommentNum > 0 {
update.CommentNum = tweet.CommentNum
}
if tweet.StarNum > 0 {
update.StarNum = tweet.StarNum
}
if tweet.ReTweetNum > 0 {
update.ReTweetNum = tweet.ReTweetNum
}
result, err := db.Exec(
"update tweets set content=$1, user_name=$2, comment_num=$3, star_num=$4, re_tweet_num=$5"+
" where id=$6 RETURNING id",
&update.Content, &update.UserName, &update.CommentNum, &update.StarNum, &update.ReTweetNum, id)
if err != nil {
log.Println(err)
errorObj.Message = "Server error"
utils.Respond(w, http.StatusInternalServerError, errorObj)
return
}
_, err = result.RowsAffected()
if err != nil {
if err == sql.ErrNoRows {
errorObj.Message = "The tweet does not exist"
utils.Respond(w, http.StatusBadRequest, errorObj)
return
}
log.Println(err)
errorObj.Message = "Server error"
utils.Respond(w, http.StatusInternalServerError, errorObj)
return
}
utils.Respond(w, http.StatusOK, update)
}
}
func (c Controller) RemoveTweet(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var errorObj models.Error
params := mux.Vars(r)
id, err := strconv.Atoi(params["id"])
if err != nil {
errorObj.Message = "\"id\" is wrong"
utils.Respond(w, http.StatusBadRequest, errorObj)
return
}
result, err := db.Exec("delete from tweets where id = $1", id)
if err != nil {
log.Println(err)
errorObj.Message = "Server error"
utils.Respond(w, http.StatusInternalServerError, errorObj)
return
}
_, err = result.RowsAffected()
if err != nil {
if err == sql.ErrNoRows {
errorObj.Message = "The tweet does not exist"
utils.Respond(w, http.StatusBadRequest, errorObj)
return
}
log.Println(err)
errorObj.Message = "Server error"
utils.Respond(w, http.StatusInternalServerError, errorObj)
return
}
utils.Respond(w, http.StatusNoContent, "")
}
}
main.go
package main
import (
"database/sql"
"golang-rest-api/controllers"
"log"
"net/http"
"github.com/gorilla/mux"
"github.com/lib/pq"
)
func main() {
pgURL, err := pq.ParseURL("postgres://puser:ppassword@localhost:5432/testdb?sslmode=disable")
if err != nil {
log.Fatal(err)
}
db, err := sql.Open("postgres", pgURL)
if err != nil {
log.Fatal(err)
}
controller := controllers.Controller{}
router := mux.NewRouter()
router.HandleFunc("/api/tweets", controller.GetTweets(db)).Methods("GET")
router.HandleFunc("/api/tweets/{id}", controller.GetTweet(db)).Methods("GET")
router.HandleFunc("/api/tweets", controller.AddTweet(db)).Methods("POST")
router.HandleFunc("/api/tweets/{id}", controller.PutTweet(db)).Methods("PUT")
router.HandleFunc("/api/tweets/{id}", controller.RemoveTweet(db)).Methods("DELETE")
log.Println("Server up on port 3000...")
log.Fatal(http.ListenAndServe(":3000", router))
}
これで実装完了です。ブログ記事にする関係でモジュール化を控えめにしているので、少々長いコードになっていますがご了承ください(笑)
動作確認
最後に作ったAPIの動作確認をしてみましょう。
APIを起動します。
$ go run main.go
2019/01/20 21:47:03 Server up on port 3000...
curlでCRUDを確認します。
$ curl -v -H "Accept:application/json" -H "Content-Type:application/json" -X POST -d '{"content":"This is my first tweet.","user_name":"@keidrun","comment_num":5,"star_num":15,"re_tweet_num":25}' http://localhost:3000/api/tweets | jq
...
< HTTP/1.1 201 Created
...
{
"id": 1,
"content": "This is my first tweet.",
"user_name": "@keidrun",
"comment_num": 5,
"star_num": 15,
"re_tweet_num": 25
}
$ curl -v -H "Accept:application/json" -H "Content-Type:application/json" -X POST -d '{"content":"Golang is my favorite language!","user_name":"@superdeveloper","comment_num":22,"star_num":222,"re_tweet_num":2222}' http://localhost:3000/api/tweets | jq
...
< HTTP/1.1 201 Created
...
{
"id": 2,
"content": "Golang is my favorite language!",
"user_name": "@superdeveloper",
"comment_num": 22,
"star_num": 222,
"re_tweet_num": 2222
}
$ curl -v -H "Accept:application/json" -H "Content-Type:application/json" -X POST -d '{"content":"I am nothing. Just an ordinary guy.","user_name":"@person"}' http://localhost:3000/api/tweets | jq
...
< HTTP/1.1 201 Created
...
{
"id": 3,
"content": "I am nothing. Just an ordinary guy.",
"user_name": "@person",
"comment_num": 0,
"star_num": 0,
"re_tweet_num": 0
}
$ curl -v -H "Accept:application/json" http://localhost:3000/api/tweets | jq
...
< HTTP/1.1 200 OK
...
[
{
"id": 1,
"content": "This is my first tweet.",
"user_name": "@keidrun",
"comment_num": 5,
"star_num": 15,
"re_tweet_num": 25
},
{
"id": 2,
"content": "Golang is my favorite language!",
"user_name": "@superdeveloper",
"comment_num": 22,
"star_num": 222,
"re_tweet_num": 2222
},
{
"id": 3,
"content": "I am nothing. Just an ordinary guy.",
"user_name": "@person",
"comment_num": 0,
"star_num": 0,
"re_tweet_num": 0
}
]
$ curl -v -H "Accept:application/json" http://localhost:3000/api/tweets/1 | jq
...
< HTTP/1.1 200 OK
...
{
"id": 1,
"content": "This is my first tweet.",
"user_name": "@keidrun",
"comment_num": 5,
"star_num": 15,
"re_tweet_num": 25
}
$ curl -v -H "Accept:application/json" http://localhost:3000/api/tweets/2 | jq
...
< HTTP/1.1 200 OK
...
{
"id": 2,
"content": "Golang is my favorite language!",
"user_name": "@superdeveloper",
"comment_num": 22,
"star_num": 222,
"re_tweet_num": 2222
}
$ curl -v -H "Accept:application/json" http://localhost:3000/api/tweets/3 | jq
...
< HTTP/1.1 200 OK
...
{
"id": 3,
"content": "I am nothing. Just an ordinary guy.",
"user_name": "@person",
"comment_num": 0,
"star_num": 0,
"re_tweet_num": 0
}
$ curl -v -H "Accept:application/json" -H "Content-Type:application/json" -X PUT -d '{"content":"I am excellent guy!!","user_name":"@awesomeperson","comment_num":99,"star_num":999,"re_tweet_num":9999}' http://localhost:3000/api/tweets/3 | jq
...
< HTTP/1.1 200 OK
...
{
"id": 3,
"content": "I am excellent guy!!",
"user_name": "@awesomeperson",
"comment_num": 99,
"star_num": 999,
"re_tweet_num": 9999
}
$ curl -v -H "Accept:application/json" -X DELETE http://localhost:3000/api/tweets/2 | jq
...
< HTTP/1.1 204 No Content
...
$ curl -v -H "Accept:application/json" http://localhost:3000/api/tweets | jq
...
< HTTP/1.1 200 OK
...
[
{
"id": 1,
"content": "This is my first tweet.",
"user_name": "@keidrun",
"comment_num": 5,
"star_num": 15,
"re_tweet_num": 25
},
{
"id": 3,
"content": "I am excellent guy!!",
"user_name": "@awesomeperson",
"comment_num": 99,
"star_num": 999,
"re_tweet_num": 9999
}
]
大丈夫そうですね。これで動作確認は完了です。
最後に
いかがでしたか?これでGo言語でGorilla Muxを使ってAPIを作成できるようになったと思います。では。
環境
- Go: 1.11.4
- mux: 1.6.2
- pq: 1.6.2
- Docker: 18.09.1, build 4c52b90