【Go】APIサーバの構築 ~データの読み取り(Read)編~

#はじめに
Go学習の一環として、簡単なAPIサーバを作成してみました。

その他のGoの記事については以下をご覧ください。

https://qiita.com/suzuki0430/items/6674ffa239dc539e0fd7#%E3%83%9D%E3%82%A4%E3%83%B3%E3%82%BF

https://qiita.com/suzuki0430/items/dc82131e5e26e2cf3ae0

#実装
DBにmovies, genres, movies_genresテーブルを作成し、指定したIDのmovieを取得するようなAPIをつくります。

movies
![スクリーンショット 2022-01-07 21.41.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/92ef94ef-8163-7b74-ad69-0cf08ff42ed8.png)
genres
![スクリーンショット 2022-01-07 21.41.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/80833812-100d-7fd0-a4b5-f8d6812f5f6e.png)
movie_genres
![スクリーンショット 2022-01-07 21.41.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/f512dfc5-b2ab-dffd-e976-d1b665174e37.png)

##導入
まずは以下のコマンドで外部からインポートするモジュールのパスを書いておくファイルを作成します。

```
go mod init backend
```
次に今回使う外部パッケージ(httprouterとpq)をインストールします。

```
go get -u github.com/julienschmidt/httprouter
go get -u github.com/lib/pq@v1.10.0
```

https://github.com/julienschmidt/httprouter

パッケージは`$GOPATH/pkg/mod`以下(古い環境の場合は`$GOPATH/src`以下)に配置されます。

##main.goの作成
`main.go`にDBとサーバを接続してlistenする部分を記述します(詳細はインラインコメント)。

```go:main.go
package main

import (
	"backend/models"
	"context"
	"database/sql"
	"flag"
	"fmt"
	"log"
	"net/http"
	"os"
	"time"

	_ "github.com/lib/pq"
)

const version = "1.0.0"

type config struct {
	port int
	env string
	db struct {
		dsn string
	}
}

type AppStatus struct {
	Status string `json:"status"`
	Environment string `json:"environment"`
	Version string `json:"version"`
}

type application struct {
	config config
	logger *log.Logger
	models models.Models
}

func main() {
	var cfg config

	// flagでconfigのプロパティを初期化する
	// 引数は変数のポインタ(メモリのアドレス値)、フラグの名前、デフォルト値、使い方の説明
	flag.IntVar(&cfg.port, "port", 4000, "Server port to listen on")
	flag.StringVar(&cfg.env, "env", "development", "Application environment (development|production")
	flag.StringVar(&cfg.db.dsn, "dsn", "postgres://localhost/go_movies?sslmode=disable", "Postgres connection string")
	flag.Parse()

	// Loggerオブジェクトを生成して出力フォーマットを設定する
	logger := log.New(os.Stdout, "", log.Ldate|log.Ltime)

	// DBと接続する
	db, err := openDB(cfg)
	if err != nil {
		logger.Fatal(err)
	}
	// main()がreturnするときにDBとの接続を閉じる
	defer db.Close()

	// アプリケーションの設定をする(参照渡しをおこなうためにポインタを使用)
	app := &application{
		config: cfg,
		logger: logger,
		models: models.NewModels(db),
	}

	// サーバー設定をカスタマイズする
	srv := &http.Server{
		Addr: fmt.Sprintf(":%d", cfg.port),
		Handler: app.routes(),
		IdleTimeout: 10 * time.Minute,
		WriteTimeout: 30 * time.Second,
	}

	logger.Println("Starting server on port", cfg.port)

	// サーバをlistenする
	err = srv.ListenAndServe()
	if err != nil {
		log.Println(err)
	}
}

func openDB(cfg config) (*sql.DB, error) {
	// DBへアクセスする(接続はまだ確立されない)
	db, err := sql.Open("postgres", cfg.db.dsn)
	// エラー処理
	if err != nil {
		return nil, err
	}

	// 5sでタイムアウトする
	ctx, cancel := context.WithTimeout(context.Background(), 5 * time.Second)
	// openDB()がreturnするまで実行されない
	defer cancel()

	// DBとの接続を検証する
	err = db.PingContext(ctx)
	//エラー処理
	if err != nil {
		return nil, err
	}

	return db, nil
}
```

##models.goの作成
`models.go`を作成し、`main.go`で定義した`application`のプロパティである`models`の構造体`Models`、`db`を元に`Models`を返す`NewModels()`、各テーブルの構造体`Movie` `Genre` `MovieGenre`を記述します。

```go:models.go
package models

import (
	"database/sql"
	"time"
)

// Models is the wrapper for database
type Models struct {
	DB DBModel
}

// NewModels returns models with db pool
func NewModels(db *sql.DB) Models {
	return Models{
		DB: DBModel{DB: db},
		// DBModel{db}
	}
}

type Movie struct {
	ID int `json:"id"`
	Title string `json:"title"`
	Description string `json:"description"`
	Year int `json:"year"`
	ReleaseDate time.Time `json:"release_date"`
	Runtime int `json:"runtime"`
	Rating int `json:"rating"`
	MPAARating string `json:"mpaa_rating"`
	CreatedAt time.Time `json:"-"`
	UpdatedAt time.Time `json:"-"`
	MovieGenre map[int]string `json:"genres"`
}

type Genre struct {
	ID int `json:"-"`
	GenreName string `json:"genre_name"`
	CreatedAt time.Time `json:"-"`
	UpdatedAt time.Time `json:"-"`
}

type MovieGenre struct {
	ID int `json:"-"`
	MovieID int `json:"-"`
	GenreID int `json:"-"`
	Genre Genre `json:"genre"`
	CreatedAt time.Time `json:"-"`
	UpdatedAt time.Time `json:"-"`
}
```

##routes.goの作成
`main.go`のサーバ設定のカスタマイズで`Handler: app.routes()`と記述しましたが、この部分でルートハンドラーを設定しています。
導入部分でインストールした`httprouter`モジュールはここで使われます。

```go:routes.go
package main

import (
	"net/http"

	"github.com/julienschmidt/httprouter"
)

// ルートハンドラーのレシーバ
func (app *application) routes() *httprouter.Router {
	router := httprouter.New()
	router.HandlerFunc(http.MethodGet, "/v1/movie/:id", app.getOneMovie)
	return router
}
```

`router.HandlerFunc(http.MethodGet, "/v1/movie/:id", app.getOneMovie)`でメソッド、パス、処理を指定しています。
処理の部分についてはまた別のファイル`movie-handlers.go`に記述します。

##movie-handlers.goの作成
ルートハンドラーの処理`getOneMovie`を記述します。
`ParamsFromContext`でクエリパラメータを取得するために、ここでも`httprouter`を使用します。

```go:movie-handlers.go 
package main

import (
	"errors"
	"net/http"
	"strconv"

	"github.com/julienschmidt/httprouter"
)

func (app *application) getOneMovie(w http.ResponseWriter, r *http.Request) {
	// クエリパラメータを取得する
	params := httprouter.ParamsFromContext(r.Context())

	// paramsを文字列から整数に変換する
	id, err := strconv.Atoi(params.ByName("id"))
	//エラー処理
	if err != nil {
		app.logger.Print(errors.New("invalid id parameter"))
		app.errorJSON(w, err)
		return 
	}

	// 指定したidのデータを取得する
	movie, err := app.models.DB.Get(id)

	// レスポンスをJSONで返す
	err = app.writeJSON(w, http.StatusOK, movie, "movie")
	if err != nil {
		app.errorJSON(w, err)
		return 
	}
}
```

##movies-db.goの作成
`movie-handlers.go`の`movie, err := app.models.DB.Get(id)`の部分で、指定したidのデータをDBから取得しています。
この`Get()`メソッドを`movies-db.go`に記述します。

```go:movies-db.go
package models

import (
	"context"
	"database/sql"
	"time"
)

type DBModel struct {
	DB *sql.DB
}

// 指定idのmovieかerrorを返すメソッド(DBModelのポインタレシーバ)
func (m *DBModel) Get(id int) (*Movie, error) {
	// 3sでタイムアウトする
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()

	// 指定したIDのmoviesを取得するクエリ
	query := `select id, title, description, year, release_date, runtime, rating, mpaa_rating,
							created_at, updated_at from movies where id = $1
	`

	// 指定したidのmoviesを取得する(1行)
	row := m.DB.QueryRowContext(ctx, query, id)

	var movie Movie

	// クエリの結果をmovieに割り当てる
	err := row.Scan(
		&movie.ID,
		&movie.Title,
		&movie.Description,
		&movie.Year,
		&movie.ReleaseDate,
		&movie.Runtime,
		&movie.Rating,
		&movie.MPAARating,
		&movie.CreatedAt,
		&movie.UpdatedAt,
	)
	if err != nil {
		return nil, err
	}

	// 指定したmovie_idのgenresを取得するクエリ
	query = `select
						mg.id, mg.movie_id, mg.genre_id, g.genre_name
					from
						movies_genres mg
						left join genres g on (g.id = mg.genre_id)
					where
						mg.movie_id = $1
	`

	// 指定したmovie_idのgenresを取得する(複数行)
	rows, _ := m.DB.QueryContext(ctx, query, id)
	defer rows.Close()

	genres := make(map[int]string)
	for rows.Next() {
		var mg MovieGenre
		err := rows.Scan(
			&mg.ID,
			&mg.MovieID,
			&mg.GenreID,
			&mg.Genre.GenreName,
		)
		if err != nil {
			return nil, err
		}
		genres[mg.ID] = mg.Genre.GenreName
	}

	movie.MovieGenre = genres

	return &movie, nil
}
```

##utilities.goの作成
`movie-handlers.go`では、DBから取得したデータやエラーを取得したあとに、レスポンスをJSONで返します。
`writeJSON()`や`errorJSON`といったメソッドを`utilities.go`に記述します。

```go:utilities.go
package main

import (
	"encoding/json"
	"net/http"
)

func (app *application) writeJSON(w http.ResponseWriter, status int, data interface{}, wrap string) error {
	// メモリを割り当てる
	wrapper := make(map[string]interface{})

	// wrapキーの構造体にdataを代入する
	wrapper[wrap] = data

	// 構造体をJSONに変換する
	js, err := json.Marshal(wrapper)
	if err != nil {
		return err
	}
	
	// ヘッダーにContent-Typeを付加する
	w.Header().Set("Content-Type", "application/json")
	// ステータスコードとともにHTTP応答ヘッダーを返す
	w.WriteHeader(status)
	// ボディを返す
	w.Write(js)

	return nil
}

func (app *application) errorJSON(w http.ResponseWriter, err error) {
	type jsonError struct {
		Message string `json:"message"`
	}

	theError := jsonError {
		Message: err.Error(),
	}

	app.writeJSON(w, http.StatusBadRequest, theError, "error")
}
```

`json.Marshal()`を行う際、構造体タグを利用することで以下のように結果を制御することができます。

```go
type Genre struct {
	ID int `json:"-"` // Marshal時に省略される
	GenreName string `json:"genre_name"` // Marshal時にjsonのキーが'genre_name'になる
	CreatedAt time.Time `json:"-"`
	UpdatedAt time.Time `json:"-"`
}
```

##動作確認
`go run cmd/api/*.go`もしくは`go run ./cmd/api`を実行します。
`http://localhost:4000/v1/movie/1`にアクセスすると、以下のように結果が表示されます。
![スクリーンショット 2022-01-08 10.19.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/7056de79-6c8f-2694-47e6-91cfe6fb57a6.png)

##CORS(Cross-Origin Resource Sharing)
別のオリジン(ドメイン、プロトコル、ポート番号)で提供されているフロントエンドのプロジェクト(React, Vue)からGoのAPIサーバにリクエストを行うためには、CORS(オリジン間リソース共有)を行う必要があります。
CORSを行うことによって、追加のHTTPヘッダーを使用して、あるオリジンで動作しているウェブアプリケーションに、異なるオリジンにあるリソースへのアクセス権を与えるようブラウザに指示することができます。

HTTPヘッダーに`Access-Control-Allow-Origin`を付加するようなメソッド`enableCORS()`を`middleware.go`に作成して、`routes.go`に追記します。

```go:middleware.go
package main

import (
	"net/http"
)

func (app *application) enableCORS(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Access-Control-Allow-Origin", "*")

		next.ServeHTTP(w, r)
	})
}
```

```routes.go
func (app *application) routes() http.Handler {
	router := httprouter.New()

	router.HandlerFunc(http.MethodGet, "/v1/movie/:id", app.getOneMovie)

	return app.enableCORS(router)
}
```

##コード全文
以下においてあります。

https://github.com/suzuki0430/backend-go-movies

#参考資料

https://www.udemy.com/share/104HTy3@IK146mNsO0KSpfeaq9dZ4m4MAaNqDxkGjjM4OLg0nUB4pscXOkswXVwXn6Jpvrov/

https://blog.framinal.life/entry/2021/04/11/013819

https://zenn.dev/optimisuke/articles/105feac3f8e726830f8c

https://qiita.com/mocotan/items/b1889ca798d5331a23c2

https://qiita.com/TsuyoshiUshio@github/items/34b63b663ffd56125c07

https://sourjp.github.io/posts/go-db/

https://free-engineer.life/golang-net-http-simple/

https://www.asobou.co.jp/blog/web/marshal-unmarshal

https://tech.librastudio.co.jp/entry/index.php/2018/08/24/post-1937/

https://please-sleep.cou929.nu/go-sql-db-connection-pool.html

https://developer.mozilla.org/ja/docs/Web/HTTP/CORS