go言語(echo)APIサーバーの構築手順(+reactとの接続設定手順)

# 概要
dockerを使用してGo言語でJSONを返すAPIサーバーを構築
(+ローカルで起動させたreactからAPIを呼び出しJSONを受け取る)

※dockerとgolangの動作環境が準備済みである事、reactの環境構築については割愛する

# 目次
[1. dockerコンテナとgo言語環境ソースファイルのファイル構成](#1-dockerコンテナとgo言語環境ソースファイルのファイル構成)
[2. dockerコンテナの準備](#2-dockerコンテナの準備)
[3. APIサーバーの構築](#3-apiサーバーの構築)
[4. react側の記述](#4-react側の記述)


# 1. dockerコンテナとgo言語環境ソースファイルのファイル構成

go言語の環境構築については下記記事参照
[windowsでGo言語を使ったWEBアプリケーション作成の環境を構築する](https://qiita.com/kubota_ndatacom/items/84e98d84e5859d602102)

```ファイル構成
docker
+---golang-api
    |   docker-compose.yml
    +---images
        +---golang-api
                DockerFile
go
+---sample-app
    |   go.mod
    |   go.sum
    |   main.go
    +---sql
            sql.go
```

ローカルで作成したgo言語環境をdockerコンテナにバインドさせてAPIサーバーを作成する


# 2. dockerコンテナの準備

docker-compose.ymlとDockerFileを下記のように記述する
dockerのベースイメージはgolang公式の1.19.3-alpineを使用
docker-compose.ymlのあるディレクトリで「docker-compose up --build -d」を実行すればコンテナ起動する

```yml:docker-compose.yml
version: '3'

services:
  # サービス名
  golang-api:
    # コンテナ名
    container_name: golang-api-container_test
    # Dockerfileのあるディレクトリを指定
    build: /images/golang-api
    # ローカルのフォルダをコンテナのフォルダと同期させる
    volumes:
      - type: bind
        source: ../../go/sample-app
        target: /go/src/app
    # ホストの127.0.0.1:1323からコンテナの1323に繋ぐ
    ports:
      - "127.0.0.1:1323:1323"
```

```Dockerfile:Dockerfile
FROM golang:1.19.3-alpine

# ROOTフォルダ及びWORKDIR設定
ENV ROOT=/go/src/app
RUN mkdir ${ROOT}
WORKDIR ${ROOT}

# パッケージの更新とビルドツールパッケージのインストール
RUN apk update && apk add git alpine-sdk

# 環境変数設定
ENV CGO_ENABLED=0 \
    GOOS=linux \
    GOARCH=amd64
# port設定
EXPOSE 8080
# コンテナ起動時にgoを起動
CMD ["go", "run", "main.go"]
```




# 3. APIサーバーの構築


main.goを下記のように記述する
middleware.CORSWithConfigでCORS(オリジン間リソース共有)の設定を行う
AllowOrigins:の記述でlocalhost:3000で動作するフロントエンドのreactサーバーからの接続を許可している(全て許可する場合は「"*"」にする)


```go:main.go
package main

import (
    /* sql実行用のsqlパッケージをインポート */
	"sample-app/sql"
    /* サーバー起動用のパッケージ */
	"net/http"
    /* ミドルウェア用パッケージ */
	"github.com/labstack/echo/middleware"
    /* メインのフレームワークにechoを使用 */
	"github.com/labstack/echo"
)

func main() {
    /* echoのインスタンス作成 */
	e := echo.New()

	/* ミドルウェア類 */
	e.Use(middleware.Logger())
	e.Use(middleware.Recover())

    /* CORS(オリジン間リソース共有)の設定 */
	e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
        /* http://localhost:3000からの接続を許可する */
		AllowOrigins: []string{"http://localhost:3000"},
		AllowMethods: []string{
			http.MethodGet,
			http.MethodPut,
			http.MethodPost,
			http.MethodDelete,
		},
	}))

    /* パスでのリクエストに対する処理 */
	e.GET("/path/:no", sql.GetFromPath)
    /* クエリでのリクエストに対する処理 */
	e.GET("/query", sql.GetFromQuery)
    /* JSONでのリクエストに対する処理 */
	e.POST("/json", sql.PostFromJson)

    /* サーバー起動 */
	e.Logger.Fatal(e.Start(":1323"))
}
```

DBへの接続及び処理をsql.goに記述する(ドライバはlib/pqを使用)
sql.Openのhost(IP),port,user,password,dbnameは各自設定する事

```go:sql.go
package sql

import (
    /* データベース操作用パッケージ */
	"database/sql"
    /* サーバー起動用のパッケージ */
	"net/http"
    /* 文字列型と数値型・論理型の相互変換用パッケージ */
	"strconv"

    /* メインのフレームワークにechoを使用 */
	"github.com/labstack/echo"
    /* データベース接続用のドライバ */
	_ "github.com/lib/pq"
)

/* パスでのリクエストに対してのreturn用json */
type ReturnFromPath struct {
	Result int    `json:"result"`
	Name   string `json:"name"`
}

/* クエリでのリクエストに対してのreturn用json */
type ReturnFromQuery struct {
	Result int    `json:"result"`
	No     int    `json:"no"`
	DtlNo  int    `json:"dtl_no"`
	Name   string `json:"name"`
}

/* jsonでのリクエストに対してのreturn用json */
type ReturnFromJson struct {
	Result int    `json:"result"`
	No     int    `json:"no"`
	DtlNo  int    `json:"dtl_no"`
	Name   string `json:"name"`
}

/* データベースへのselectに対する結果を取得する用 */
type ResultTestTable struct {
	No    int
	DtlNo int
	Name  string
}

/* JSONでのリクエストを受け取る用 */
type PostTest struct {
	No    int `json:"no"`
	DtlNo int `json:"dtl_no"`
}

var Db *sql.DB

/* 初期化処理、initは他のfuncが呼び出された際も最初に実行される */
func init() {
	var err error
    /* DB接続設定 */
	Db, err = sql.Open("postgres", "host=**** port=**** user=**** password=**** dbname=**** sslmode=disable")
	if err != nil {
		panic(err)
	}
}

/* パスでのリクエストに対する処理 */
func GetFromPath(c echo.Context) error {
    /* Paramでパスの値を取得する */
	no := c.Param("no")
    /* selectの結果取得用 */
	result := ResultTestTable{}
    /* return用のJSON */
	returnJson := ReturnFromPath{}
    /* select実行 */
	if err := Db.QueryRow("select no, name from api_test_table where no = $1", no).Scan(&result.No, &result.Name); err != nil {
		returnJson.Result = 2
	} else {
		returnJson.Result = 1
		returnJson.Name = result.Name
	}
    /* jsonを返す */
	return c.JSON(http.StatusCreated, returnJson)
}

/* クエリでのリクエストに対する処理 */
func GetFromQuery(c echo.Context) error {
    /* QueryParamでクエリの値を取得する */
	no := c.QueryParam("no")
	dtl_no := c.QueryParam("dtl_no")
    /* selectの結果取得用 */
	result := ResultTestTable{}
    /* return用のJSON */
	returnJson := ReturnFromQuery{}
    /* select実行 */
	if err := Db.QueryRow("select no, dtl_no, name from api_test_table where no = $1 and dtl_no = $2", no, dtl_no).Scan(&result.No, &result.DtlNo, &result.Name); err != nil {
		returnJson.Result = 2
	} else {
		returnJson.Result = 1
		returnJson.No = result.No
		returnJson.DtlNo = result.DtlNo
		returnJson.Name = result.Name
	}
    /* jsonを返す */
	return c.JSON(http.StatusCreated, returnJson)
}

/* jsonでのリクエストに対する処理 */
func PostFromJson(c echo.Context) error {
    /* データ受け取り用のjsonを定義 */
	post := new(PostTest)
    /* Bindでデータを受け取る */
	if err := c.Bind(post); err != nil {
		return err
	}
    /* selectの結果取得用 */
	result := ResultTestTable{}
    /* return用のJSON */
	returnJson := ReturnFromJson{}
    /* select実行 */
	if err := Db.QueryRow("select no, dtl_no, name from api_test_table where no = $1 and dtl_no = $2", strconv.Itoa(post.No), strconv.Itoa(post.DtlNo)).Scan(&result.No, &result.DtlNo, &result.Name); err != nil {
		returnJson.Result = 2
	} else {
		returnJson.Result = 1
		returnJson.No = result.No
		returnJson.DtlNo = result.DtlNo
		returnJson.Name = result.Name
	}
    /* jsonを返す */
	return c.JSON(http.StatusCreated, returnJson)
}
```


GetFromPathはパス指定でのGETリクエストに対する処理を行っている
(http://127.0.0.1:1323/path/1での呼び出し)
GetFromQueryはクエリでのGETリクエストに対する処理を行っている
(http://127.0.0.1:1323/query?no=2&del_no=1での呼び出し)
PostFromJsonはJSONでのPOSTリクエストに対する処理を行っている
(http://127.0.0.1:1323/jsonでの呼び出し)


# 4. react側の記述

reactサーバーの構築については割愛
参照：[Node.js+React+Herokuでアプリ開発環境を構築する](https://qiita.com/kubota_ndatacom/items/c62109ad6e570085af88)

パス、クエリ、JSONでのfetchを使用した呼び出し方法は下記の通り
requestOptionsでmode: 'cors'を指定する必要があるので注意

```js:App.js(パス指定でのGETリクエスト)
    /* API接続設定 */
    const requestOptions = {
        mode: 'cors',
        method: 'GET'
    };
    /* 外部APIへ接続 */
    fetch('http://127.0.0.1:1323/path/1', requestOptions)
      /* APIからのデータをJSONに変換 */
      .then((res) => res.json())
      .then(
        (data) => {
          /* API正常終了 */
          if(data.result===1) {
            alert(data.name);
            setLoading(false);
          /* APIエラー */
          }else if(data.result===2) {
            alert('エラー。');
            setLoading(false);
          }
        }
      );
```

```js:App.js(クエリでのGETリクエスト)
    /* API接続設定 */
    const requestOptions = {
        mode: 'cors',
        method: 'GET'
    };
    /* 外部APIへ接続 */
    fetch('http://127.0.0.1:1323/query?no=2&dtl_no=1', requestOptions)
      /* APIからのデータをJSONに変換 */
      .then((res) => res.json())
      .then(
        (data) => {
          /* API正常終了 */
          if(data.result===1) {
            alert(data.name);
            setLoading(false);
          /* APIエラー */
          }else if(data.result===2) {
            alert('エラー。');
            setLoading(false);
          }
        }
      );
```

```js:App.js(JSONでのPOSTリクエスト)
    /* API接続設定 */
    const sendData = {
      no: 2,
      dtl_no: 2
    }
    const requestOptions = {
        mode: 'cors',
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(sendData)
    };
    /* 外部APIへ接続 */
    fetch('http://127.0.0.1:1323/json', requestOptions)
      /* APIからのデータをJSONに変換 */
      .then((res) => res.json())
      .then(
        (data) => {
          /* API正常終了 */
          if(data.result===1) {
            alert(data.name);
            setLoading(false);
          /* APIエラー */
          }else if(data.result===2) {
            alert('エラー。');
            setLoading(false);
          }
        }
      );
```