Featured image of post GoでサイコロAPIを作る

GoでサイコロAPIを作る

Web APIつくってみる

Web APIを自分で最初から作ったことがなかったので, Goの練習も兼ねてやってみた.


やったことのまとめ

以下のようにランダムなサイコロの出目(number)とサイコロの面の数(faces)をJSONで返すだけのWeb APIをGoで作った.

# 普通に叩くと6面サイコロを振る
$ curl -X GET "localhost:8080"
{"number":1,"faces":6}
$ curl -X GET "localhost:8080"
{"number":5,"faces":6}

# クエリパラメータで面の数を指定できる
$ curl -X GET "localhost:8080?faces=100"
{"number":71,"faces":100}

# ズルをするためのエンドポイントも用意
$ curl -X GET "localhost:8080/cheat"
{"number":6,"faces":6}
$ curl -X GET "localhost:8080/cheat?faces=100&number=99"
{"number":99,"faces":100}

つかうもの

  • macOS Mojave 10.14
  • anyenv 1.1.1
    • インストール済み
  • goenv 2.0.0beta11
    • インストール済み
  • go version go1.14.6 darwin/amd64
    • 今回入れる

やったこと

準備

まずは開発環境の準備.

公式によると現在(2020/07/16)Goの最新版が 1.14.6 なので, これを使うようにする.

# goenvの更新
$ anyenv install goenv
anyenv: /Users/uzimihsr/.anyenv/envs/goenv already exists
Reinstallation keeps versions directories
continue with installation? (y/N) y
...

Install goenv succeeded!
Please reload your profile (exec $SHELL -l) or open a new session.
$ exec $SHELL -l

# Go 1.14.6のインストール
$ cd
$ goenv install 1.14.6
$ goenv global 1.14.6
$ goenv rehash
$ exec $SHELL -l

# 確認
$ goenv version
1.14.6 (set by /Users/uzimihsr/.anyenv/envs/goenv/version)
$ go version
go version go1.14.6 darwin/amd64
$ echo $GOPATH
/Users/uzimihsr/go/1.14.6

次にGitHubリポジトリを新規作成する.
GitHub

作ったリポジトリ : https://github.com/uzimihsr/dice-api

作成したリポジトリをローカルにcloneして, Go Modulesとか.gitignoreの準備をする.
Go Modulesの使い方は公式を参考にする.
.gitignoregitignore.ioを使って作るのが楽.

# 適当なディレクトリで作業
$ cd workspace
$ git clone https://github.com/uzimihsr/dice-api.git
Cloning into 'dice-api'...
warning: You appear to have cloned an empty repository.
$ cd dice-api

# Go Moduleの初期化
$ go mod init github.com/uzimihsr/dice-api
go: creating new go.mod: module github.com/uzimihsr/dice-api
$ ls
go.mod

# gitignore.ioのAPIを利用して.gitignoreを作成する
$ curl -o ./.gitignore https://www.toptal.com/developers/gitignore/api/go,macos,linux

# いったんpushしておく
$ echo "# dice-api" > README.md
$ git add .
$ git commit -m "first commit"
$ git push origin master

このときのリポジトリはこんなかんじ.

サイコロの作成

まずはサイコロの動作を作ってみる.
以前作ったものを流用する.

$ mkdir dice && cd dice
$ vim dice.go

今回は普通にサイコロの出目を返すメソッドRoll()の他に指定した出目を返すCheat()を作ってみた.

ためしに動かしてみる.

$ cd ..
$ vim main.go
$ go run main.go
6
5
$ go run main.go
1
5

いい感じ.

HTTPハンドラの作成

次にこれをWeb APIとして動かすためのハンドラ関数を作る.

$ mkdir handler && cd handler
$ vim handler.go

ポイントはハンドラ関数DiceHandler(), CheatDiceHandler()の引数でinterfaceを受けるようにしているところ.
こうしておくと後でテストを書くときにmockを使いやすくなる.

これらのハンドラ関数を扱ってHTTPサーバーを動かすため, main.goを修正する.

$ cd ..
$ vim main.go
$ go run main.go
# 動作を確認したら Ctrl+C で終了

こちらはhttp.NewServeMux()を使ってリクエストパスごとに異なるハンドラを呼び出すようにしているのがポイント.

main.goを実行するとMacの警告が出るので, 許可を選択するとアプリが動く.
許可する

試しに別のターミナルからAPIを叩いてみる.

# 正常系
$ curl "localhost:8080"
{"number":1,"faces":6}
$ curl "localhost:8080?faces=12"
{"number":10,"faces":12}
$ curl "localhost:8080/cheat"
{"number":6,"faces":6}
$ curl "localhost:8080/cheat?number=1"
{"number":1,"faces":6}
$ curl "localhost:8080/cheat?number=1&faces=18"
{"number":1,"faces":18}

# 異常系
$ curl -i -X POST "localhost:8080"
HTTP/1.1 404 Not Found
Content-Type: application/json
Date: Thu, 23 Jul 2020 08:12:39 GMT
Content-Length: 24

{"message": "not found"}
$ curl -i -X GET "localhost:8080?faces=hoge"
HTTP/1.1 500 Internal Server Error
Content-Type: text/plain; charset=utf-8
X-Content-Type-Options: nosniff
Date: Thu, 23 Jul 2020 08:13:04 GMT
Content-Length: 45

strconv.Atoi: parsing "hoge": invalid syntax
$ curl -i -X POST "localhost:8080/cheat"
HTTP/1.1 404 Not Found
Content-Type: application/json
Date: Thu, 23 Jul 2020 08:13:58 GMT
Content-Length: 24

{"message": "not found"}
$ curl -i -X GET "localhost:8080/cheat?number=hoge"
HTTP/1.1 500 Internal Server Error
Content-Type: text/plain; charset=utf-8
X-Content-Type-Options: nosniff
Date: Thu, 23 Jul 2020 08:14:12 GMT
Content-Length: 45

strconv.Atoi: parsing "hoge": invalid syntax

サイコロの出目(number)と面の数(faces)がJSONで返ってきて,
異常なクエリパラメータやHTTPメソッドでリクエストした場合には指定したステータスコード(404, 500)が返ってくることが確認できた.
やったぜ.

このときのリポジトリはこんなかんじ.

テストの作成

作りたいものは作れたのでここで終わってもいいけど, せっかくなのでテストコードを書いてみる.

まずはdiceパッケージから.

$ cd ./dice
$ vim dice_test.go

カバレッジ100%になるように書いたけど, 無駄なテストも含まれている.

次にhandlerパッケージのテストを作る.
handlerパッケージは自作のdiceパッケージに依存しているので, diceパッケージのmockを用意したい.
(今回はDBとかに接続してないのでdiceを直接呼び出してもいいんだけど, サイコロの出目が確率で変わっちゃうのでテストがしづらい)

こんなときにはgomockを使う.
mockしたいinterfaceのファイルを指定するだけでmock用のコードを作成してくれるのでめちゃ便利.

# mockパッケージのインストール
$ cd ..
$ go get github.com/golang/mock/mockgen

# diceのmockを作成
$ mockgen -source ./dice/dice.go -destination ./mock_dice/mock_dice.go

このmockを使ったテストを書いてみる.

$ cd handler
$ vim handler_test.go

準備したmockを各ハンドラ関数の引数に渡してやることで, 実際のdiceを呼び出すことなくハンドラ関数のテストができるようになる.
ハンドラ関数を作るときにdiceinterfaceを引数で受けるようにしたのはこのため.

テストコードが作れたので, 実際にテストを実行する.

$ cd ..
# diceパッケージのテスト
$ go test -cover ./dice
ok  	github.com/uzimihsr/dice-api/dice	0.006s	coverage: 100.0% of statements
# handlerパッケージのテスト
$ go test -cover ./handler
ok  	github.com/uzimihsr/dice-api/handler	0.016s	coverage: 100.0% of statements
# 全部まとめてテスト
$ go test -cover ./...
?   	github.com/uzimihsr/dice-api	[no test files]
ok  	github.com/uzimihsr/dice-api/dice	(cached)	coverage: 100.0% of statements
ok  	github.com/uzimihsr/dice-api/handler	(cached)	coverage: 100.0% of statements
?   	github.com/uzimihsr/dice-api/mock_dice	[no test files]

これでテストも(一応)できた.

最終的なディレクトリの構成はこんなかんじ.
テストを実行したのでパッケージの依存関係を管理するためのファイルgo.modgo.sumが修正/追加されている.
こいつらも一緒にGitで管理しておくと, 別の環境でこれをビルドするときに便利だったりする. らしい.

$ tree .
.
├── README.md
├── dice
│   ├── dice.go
│   └── dice_test.go
├── go.mod
├── go.sum
├── handler
│   ├── handler.go
│   └── handler_test.go
├── main.go
└── mock_dice
    └── mock_dice.go

3 directories, 9 files

最終的なリポジトリはこんなかんじ.

おわり

Web APIっぽいものを0から作ってみた. テストまでやったのでけっこうしんどかった.
あとはビルドすれば普通に動くはずなので,
暇があったらKubernetesで動かすところまでやってみたい.

おまけ

おすわりするねこ(ごはんがほしい)

参考にしたもの