はじめに
Goを勉強し始めたので、APIサーバーも体験しておこうということで、作ってみました。
Dockerで環境構築して動かして、InsomniaとかからAPIの結果を見れるようにしてみます。
※途中からTodoデータをCRUDする感じのAPIを作り始めますが、DBの話はしないので、ご了承ください。
早速作ってみる
/api/sampleにアクセスすると、{ "message": "Hello World!!" }が帰ってくるサンプルを作ってみました。
Dockerでのサーバー起動コマンドはgo run cmd/main.goで動かしています。
とりあえず標準ライブラリのnet/httpを使ってます。
package main
import (
"encoding/json"
"net/http"
"os"
)
func main() {
// 環境変数のPORTを取得しています。(PORT=8000)
port := os.Getenv("PORT")
// `/api/sample`にアクセスがあるとgetHelloWorldメソッドが呼び出されます。
http.HandleFunc("/api/sample", getHelloWorld)
// 8000番ポートでWebサーバが起動し、リクエスト受付状態となります。
http.ListenAndServe(":" + port, nil)
}
func getHelloWorld(w http.ResponseWriter, _r *http.Request) {
// レスポンスで返す値を作ります。
ping := map[string]string{"message": "Hello World!!"}
// レスポンス値をjsonに変換します。
res, _ := json.Marshal(ping)
w.Write(res)
}
/api/sampleにアクセスすると、レスポンスが帰ってきました。
レスポンス値を変えてみる
-ping := map[string]string{"message": "Hello World!!"}
+ping := map[string]string{"message": "Hello World!"}
レスポンス値が変わりませんね。。。
Dockerで起動しているコンテナを再起動して再チャレンジ
値が変わってくれました。
ソースコードを変更するたびにDockerコンテナを再起動するのはめんどくさいですね。。。
Air を使ってホットリロードさせる
コンテナの再起動がめんどくさいのでソースコードを変更したらホットリロードするようにしてみます。
Airをインストール
RUN apk update \
&& go get -u github.com/cosmtrek/air \
&& chmod +x ${GOPATH}/bin/air
airコマンドで起動させる
-command: go run cmd/main.go
+command: air
Airの設定ファイルを追加します。
サンプルをコピペしてプロジェクトディレクトリ直下に.air.tomlファイルを作成します。
この状態で起動するとgo mod initしなさいと怒られました。
(プロジェクトを作った最初にやっとけって話ですよね。。。)
go: cannot find main module, but found .git/config in /myapp
to create a module there, run:
go mod init
failed to build, error: exit status 1
go mod initを実行します。
$ docker compose run app go mod init go-sample-api-server
go.modが生成されました。
module go-sample-api-server
go 1.17
もう一度起動すると別のエラーが出ました。
watching .
watching cmd
!exclude tmp
building...
no Go files in /myapp
failed to build, error: exit status 1
ルートディレクトリ見たけど、goのファイルが無かったよってことですかね。
goのファイルが置いてあるところを見るようにします。
-cmd = "go build -o ./tmp/main ."
+cmd = "go build -o ./tmp/main ./cmd"
もう一度起動すると動きました。
watching .
watching cmd
!exclude tmp
building...
running...
ホットリロードされてるか確かめます。
-ping := map[string]string{"message": "Hello World!"}
+ping := map[string]string{"message": "Hello World!!"}
ファイルを保存した瞬間にビルドし直してくれました。
running...
cmd/main.go has changed
building...
running...
レスポンス値も変わってくれましたね。
ginを使ってみる
net/httpでサンプルのAPIを作ったところではありますが、せっかくなら人気のginを使ってみましょう。
ginを取得する
$ docker compose run app go get -u github.com/gin-gonic/gin
ginの書き方に修正
import (
- "encoding/json"
"net/http"
"os"
+
+ "github.com/gin-gonic/gin"
)
func main() {
port := os.Getenv("PORT")
+ r := gin.Default()
+ r.GET("/api/sample", getGinSample)
- http.HandleFunc("/api/sample", getHelloWorld)
+ r.Run(":" + port)
- http.ListenAndServe(":" + port, nil)
}
+func getGinSample(c *gin.Context) {
+ c.JSON(http.StatusOK, gin.H { "message": "Hello Gin World!" })
+}
反映されました。
レスポンスの書き方が楽な気がしてます。
リスト取得APIを作ってみる
ありがちなAPIとして、Todoのリストを取得するAPIを作ってみます。
/api/todosにアクセスすると、Todoのリストが返されるようにしてみます。
レスポンスで返すTodoはtodosメソッドから取得できるようにしておきます。
type Todo struct {
Id int
Title string
}
func todos() [3]Todo {
return [3] Todo {
{ Id: 1, Title: "TODO 1" },
{ Id: 2, Title: "TODO 2" },
{ Id: 3, Title: "TODO 3" },
}
}
`/api/todos`のソースコード
+r.GET("/api/todos", getTodos)
func getTodos(c *gin.Context) {
c.JSON(http.StatusOK, gin.H { "todos": todos() })
}
Todoのリストが返ってきました。
特定のTodoを取得するAPIを作ってみる
ありがちなAPIとして、特定のTodoを取得するAPIを作ってみます。
/api/todos/:Id にアクセスすると、特定のTodoを返すようにします。
レスポンスで返すTodoはtodosのうちパラメタのIdに一致するものを返すようにします。
func findTodoById(id int) Todo {
for _, todo := range todos() {
if todo.Id == id {
return todo
}
}
// 一致するものがなければ、Idが-1のTodoを返しておく
return Todo{ Id: -1 }
}
パラメータのIdはId := c.Param("Id")で受け取れますが文字列として受け取ってしまいます。
id, _ := strconv.Atoi(Id)で数値に変換しましょう。
また、strconvを使うので、importもしておきます。
`/api/todos/:Id`のソースコード
import (
"net/http"
"os"
+ "strconv"
"github.com/gin-gonic/gin"
)
+r.GET("/api/todos/:Id", getTodo)
func getTodo(c *gin.Context) {
Id := c.Param("Id")
id, _ := strconv.Atoi(Id)
todo := findTodoById(id)
c.JSON(http.StatusOK, gin.H { "todo": todo })
}
指定したIdのTodoが返ってきました。
Todoを追加するAPIを作ってみる
ありがちなAPIとして、Todoを追加するAPIを作ってみます。
/api/todosにPOSTで投げると、Todoを追加できるようにします。
以下のデータを一緒に投げます。
{ "Title": "aaa" }
受け取るデータの型を宣言します。
type InputTodo struct {
Title string
}
データの受け取りは、こんな感じで受け取ります。
var inputTodo InputTodo
c.BindJSON(&inputTodo)
あとは、todosメソッドから既存データっぽく取ってきて連番のIdを用意して、渡されたTitleを使って新しいTodoを作り、それをレスポンスとしておきます。
POST `/api/todos`のソースコード
+r.POST("/api/todos", addTodo)
func addTodo(c *gin.Context) {
var inputTodo InputTodo
c.BindJSON(&inputTodo)
data := todos()
count := len(data)
newTodo := Todo { Id: count + 1, Title: inputTodo.Title }
c.JSON(http.StatusOK, gin.H { "todo": newTodo })
}
追加したTodoが返ってきました。
Todoを更新するAPIを作ってみる
ありがちなAPIとして、Todoを更新するAPIを作ってみます。
/api/todos/:IdにPATCHで投げると、Todoを更新できるようにします。
追加APIと同様で、以下のデータを一緒に投げます。
{ "Title": "aaa" }
受け取るデータの型は追加時と同じで良さそうです。
特定のTodoを取得するAPIを作ってみると同様にTodoを特定し、Todoを追加するAPIを作ってみると同様にデータを受け取り、変更を加えたTodoを返してみます。
但し、パラメタのIdで指定したTodoが見つからなかった場合は、投げたデータで変更されないような制御でも入れておきます。
PATCH `/api/todos/:Id`のソースコード
+r.PATCH("/api/todos/:Id", updateTodo)
func updateTodo(c *gin.Context) {
Id := c.Param("Id")
id, _ := strconv.Atoi(Id)
todo := findTodoById(id)
var inputTodo InputTodo
c.BindJSON(&inputTodo)
if todo.Id != -1 {
todo.Title = inputTodo.Title
}
c.JSON(http.StatusOK, gin.H { "todo": todo })
}
指定したTodoが変更された状態で返ってきました。
パラメタのIdで指定したTodoが見つからなかった場合は、Titleが入ってないので想定通り動いていますね。
Todoを削除するAPIを作ってみる
ありがちなAPIとして、Todoを削除するAPIを作ってみます。
/api/todos/:IdにDELETEで投げると、Todoを削除できるようにします。
パラメタのIdを受け取るだけなので、特定のTodoを取得するAPIを作ってみると同様にTodoを特定します。
削除されたかどうかを判断するために、削除しようとしているTodo以外のTodoをリストで返す感じにしてみます。
DELETE `/api/todos/:Id`のソースコード
+r.DELETE("/api/todos/:Id", deleteTodo)
func deleteTodo(c *gin.Context) {
Id := c.Param("Id")
id, _ := strconv.Atoi(Id)
var activeTodo [] Todo
for _, todo := range todos() {
if todo.Id != id {
activeTodo = append(activeTodo, todo)
}
}
c.JSON(http.StatusOK, gin.H { "todos": activeTodo })
}
削除しようとしているTodo以外のTodoがリストで返ってきました。
おわりに
慣れてない言語で書くのは少し大変ですが、Rubyを知っているので「あれ」をやりたいと思えば、調べるとなんとかなるもんですね。
今回はDB関係の話をしていないので、そのあたりにもチャレンジできて行けたらいいなと思ってます。













