はじめに
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
関係の話をしていないので、そのあたりにもチャレンジできて行けたらいいなと思ってます。