Go言語で手早くJSON APIを構築してみた
2020-12-14
Go言語で手早くJSON APIを構築してみたのでメモっておく。
環境構築
手短にいきたいのでDockerで。
軽量なalpineを選択。
コンテナ側の使用PORTも忘れず開放しておく。
ビルダーイメージ側のプラットフォームも実行側イメージに合わせておかないとGoサーバが起動しないので注意。
- Dockerfile
FROM golang:1.15.3-alpine3.12 as builder
ENV GO111MODULE=on
ENV GOPATH=
WORKDIR /usr/src/app
COPY . ./
RUN go mod init gochi1 && go get && go build .
FROM alpine:3.12 as gochi1
COPY --from=builder /usr/src/app .
EXPOSE 8080
CMD ["./gochi1"]
- .dockerignore
イメージ化の際避けたいファイルなどを指定しておく。
.gitignore
go.mod
go.sum
ここからgo moduleに準じたプロジェクトを作る。
$ mkdir gochi1
$ cd gochi1
$ touch main.go
プログラムの構築
サーバーを作る。
ライブラリで「chi」というのがあったので今回はそれを利用してみる。
- main.go
package main
import (
"gochi1/resources"
"net/http"
"github.com/go-chi/chi"
"github.com/go-chi/chi/middleware"
"os"
"fmt"
)
func main() {
r := chi.NewRouter()
r.Use(middleware.RequestID)
r.Use(middleware.RealIP)
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Use(middleware.BasicAuth("secret-room", map[string]string{"user1": "value1"}))
r.Mount("/users", resources.UsersResource{}.Routes())
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
http.ListenAndServe(fmt.Sprintf(":%s", port), r)
}
- resources/todos.go
ロジックの分離。
DBの代わりにグローバル変数で状態変化を再現している。
package resources
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"github.com/go-chi/chi"
)
// Response レスポンス(もしくはCreateで作成するデータフォーム)
type Response struct {
Result bool `json:"result"`
Reason string `json:"reason"`
ID string `json:"id"`
Name string `json:"name"`
Todos []string `json:"todos"`
}
// State 現在のTodoオブジェクトの状態を返すための入れ物
type State struct {
Name string `json:"name"`
Todos []string `json:"todos"`
}
var state map[string]State = map[string]State{}
type UsersResource struct{}
func (ur UsersResource) Routes() chi.Router {
r := chi.NewRouter()
r.Get("/", ur.List)
r.Post("/", ur.Create)
r.Get("/{id}", ur.Get)
r.Put("/", ur.Update)
r.Delete("/", ur.Delete)
return r
}
/*
List
現在登録しているタスク一覧を取得
*/
func (ur UsersResource) List(w http.ResponseWriter, r *http.Request) {
resList := map[string]Response{}
resList["failed"] = Response{Result: false, Reason: "Object does not exist"}
fmt.Println(state)
if len(state) > 0 {
fmt.Println("not nil")
delete(resList, "failed")
for i, v := range state {
resList[i] = Response{Result: true, Name: v.Name, Todos: v.Todos}
}
}
w.Header().Set("Content-Type", "application/json; charset=UTF-8") // <- Added
w.WriteHeader(http.StatusOK)
if err := json.NewEncoder(w).Encode(resList); err != nil {
panic(err)
}
return
}
/*
Create
1タスクを一意のIDで登録する(IDが既存の場合、失敗)
*/
func (ur UsersResource) Create(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
body, err := ioutil.ReadAll(r.Body)
if err != nil {
panic(err)
}
var response Response
error := json.Unmarshal(body, &response)
if error != nil {
panic(err)
}
res := &Response{
Result: false,
Reason: "already appended"}
// 登録しようとするIDが既存であるか確認
_, ok := state[response.ID]
// 未登録であれば登録できる
if !ok {
state[response.ID] = State{
Name: response.Name,
Todos: response.Todos}
res = &Response{
Result: true,
ID: response.ID,
Name: response.Name,
Todos: response.Todos}
}
w.Header().Set("Content-Type", "application/json; charset=UTF-8") // <- Added
w.WriteHeader(http.StatusOK)
if err := json.NewEncoder(w).Encode(*res); err != nil {
panic(err)
}
return
}
/*
Get
クエリパラメータのタスクIDをもとに該当の状態を持ってくる
key: ID string
*/
func (ur UsersResource) Get(w http.ResponseWriter, r *http.Request) {
ID := chi.URLParam(r, "id")
fmt.Println(ID)
res := &Response{Result: false, Reason: "NO such ID exist"}
s, ok := state[ID]
if ok {
res = &Response{Result: true, ID: ID, Name: s.Name, Todos: s.Todos}
}
w.Header().Set("Content-Type", "application/json; charset=UTF-8") // <- Added
w.WriteHeader(http.StatusOK)
if err := json.NewEncoder(w).Encode(*res); err != nil {
panic(err)
}
return
}
func (ur UsersResource) Update(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
body, err := ioutil.ReadAll(r.Body)
if err != nil {
panic(err)
}
var response Response
error := json.Unmarshal(body, &response)
if error != nil {
panic(err)
}
res := &Response{
Result: false,
Reason: "No such ID exists"}
// 登録しようとするIDが既存であるか確認
_, ok := state[response.ID]
if ok {
state[response.ID] = State{
Name: response.Name,
Todos: response.Todos}
res = &Response{
Result: true,
ID: response.ID,
Name: response.Name,
Todos: response.Todos}
}
w.Header().Set("Content-Type", "application/json; charset=UTF-8") // <- Added
w.WriteHeader(http.StatusOK)
if err := json.NewEncoder(w).Encode(*res); err != nil {
panic(err)
}
return
}
func (ur UsersResource) Delete(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
body, err := ioutil.ReadAll(r.Body)
if err != nil {
panic(err)
}
var response Response
error := json.Unmarshal(body, &response)
if error != nil {
panic(err)
}
res := &Response{
Result: false,
Reason: "No such ID exists"}
_, ok := state[response.ID]
if ok {
delete(state, response.ID)
res = &Response{
Result: true}
}
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
w.WriteHeader(http.StatusOK)
if err := json.NewEncoder(w).Encode(*res); err != nil {
panic(err)
}
return
}
サーバ起動、アクセスしてみる
サーバ起動
$ go run main.go
以下のコマンド or ブラウザアクセスでレスポンスをみてみる。
- 一件取得
$ curl -X GET localhost:8080/users/first_item -u user1:value1
- 一覧取得
$ curl -X GET localhost:8080/users -u user1:value1
BASIC認証がかけられており、ユーザー名、パスワードが求められる。
認証が失敗すれば、レスポンスヘッダ
WWW-Authenticate: `Basic realm=secret-room`
のように返ってくる。
また、以下のようにPOSTでデータを送ってみると、送るたびにレスポンスが変化しているのを確認した。
- タスク新規登録
$ curl -X POST localhost:8080/users -d '{"id": "first_item", "name": "todo1", "todos": ["run", "sleep", "eat"]}' -u user1:value1
- タスク更新
$ curl -X PUT localhost:8080/users -d '{"id": "first_item", "name": "todo2", "todos": ["run", "sleep", "eat"]}' -u user1:value1
- タスク一件削除
$ curl -X DELETE localhost:8080/users -d '{"id": "first_item"}' -u user1:value1
dockerでやってみる
カレントディレクトリの構成は、前述のファイル群を含んだ以下の構成で行う。
$ tree -a
.
├── .dockerignore
├── go.mod
├── Dockerfile
├── go.sum
├── main.go
└── resources
└── users.go
$ docker build -t tester1/gochi1:1.0 .
$ docker run -it --rm -d -p 8080:8080 --name tester1-gochi1 tester1/gochi1:1.0
前項で起動していたGoサーバを一旦停止し、dockerコンテナ作成・起動後、前述のURLにアクセスすると非コンテナ時と同様に動作した。
後片付けは
$ docker ps -a //で該当のコンテナIDを探して
$ docker stop [コンテナID]
$ docker images //で該当イメージIDを探して
$ docker rmi [イメージID]
で完了した。
最後に
とりあえずGoのライブラリを使って即席でJSON APIを立ててみたが、比較的簡単に実装できてお手軽感があっていいな、と思った。
動きも早いのでこれからもっと触っていくための走り書きをここにして終わりにしたいと思う。
おしまい。