0utputab1e

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を立ててみたが、比較的簡単に実装できてお手軽感があっていいな、と思った。

動きも早いのでこれからもっと触っていくための走り書きをここにして終わりにしたいと思う。

おしまい。

参考: go-chi公式 JSONの扱い方

 

あわせて読みたい記事

>> Homeに戻る