【Go】PokeAPIで学ぶGo言語 ~クエリパラメータから値を取得し、ステータスを計算~

# この記事の内容
* GoのAPI実装でクエリパラメータを扱う方法

前回の記事はこちら

https://qiita.com/rikuya98/items/a67aa61ce7bb463b4b4b

この続きでやっていきたいと思います。
# 実装すること
* クエリパラメータでポケモンのレベル、努力値、個体値を受け取る
* 受け取ったクエリパラメータからステータスを計算し、レスポンスを返す

# 実装の流れ
* クエリパラメータの取得
* 受け取ったパラメータでステータスを計算
* 計算後のステータスをレスポンスに含め返す


# クエリパラメータの取得

**クエリパラメータとは？**
>URIの一部で、Webサーバーに特定のデータを渡すために使用される。
URLのパスの後に「?」記号に続いて配置され、キーと値のペアで構成される。各ペアは&記号で区切られる。
以下のようなものです。
```
https://example.com/hoge?name=hoge&age=99
```
?の後に続く
name がキーで=の後のhogeが値になります。
上記の例では
キー:name 値:hoge
キー:age 値:99
という形になっています。

今回はこのクエリパラメータにレベル、努力値、個体値の３つのデータを渡し、ハンドラ内では受け取ったデータを元にステータスを計算し、レスポンスを返すようにしていきます。

Goでクエリパラメータから値を取得するには
リクエスト構造体のURLフィールドが持つURIパッケージのQueryメソッドを使用します。
```go
	GetPokeDataHandler := func(w http.ResponseWriter, req *http.Request) {
    //省略
		query := req.URL.Query()
    //省略
  }
```
ハンドラーの第２引数のreq構造体のURLフィールドよりQueryメソッドを呼び出します。

クエリパラメータの情報はURLフィールドのRawQueryが持っています。

>URLパッケージのソースより抜粋
```go
type URL struct {
	Scheme      string
	Opaque      string    // encoded opaque data
	User        *Userinfo // username and password information
	Host        string    // host or host:port (see Hostname and Port methods)
	Path        string    // path (relative paths may omit leading slash)
	RawPath     string    // encoded path hint (see EscapedPath method)
	OmitHost    bool      // do not emit empty host (authority)
	ForceQuery  bool      // append a query ('?') even if RawQuery is empty
 　　//ここにあるRowQueryフィールドが持っている。
	RawQuery    string    // encoded query values, without '?'
	Fragment    string    // fragment for references, without '#'
	RawFragment string    // encoded fragment hint (see EscapedFragment method)
}
```
エンコードされたクエリの値と書かれていますね。

QueryメソッドはRawQueryフィールドに含まれる、クエリパスを解析し、Values型の値を返します。
```go
func (u *URL) Query() Values {
	v, _ := ParseQuery(u.RawQuery)
	return v
}
```

Values型の定義は以下のようになっており、Keyとvalueのmapになっています。
```go
// Values maps a string key to a list of values.
// It is typically used for query parameters and form values.
// Unlike in the http.Header map, the keys in a Values map
// are case-sensitive.
type Values map[string][]string
```

これにより、解析したクエリパラメータがkeyとvalueで簡単に取り出せるようになります。

keyから値を取り出すにはGetメソッドを使います。
```go
		//クエリパラメータからレベル、努力値、個体値を取得
		query := req.URL.Query()
		pathLevel := query.Get("lv")
		pathEffortVal := query.Get("ef")
		pathIndividualVal := query.Get("in")
```

上記のリクエスト時のパスの例に出すと以下のようになります。
```
http://localhost:8080/pokemon/1?lv=50&ef=1&in=1
```
lv(レベル)が50
ef(努力値)が1
in(個体値)が1
の値を取り出します。

### 取り出した値を数値に変換
気をつけないといけないのは、取り出したばかりの値はstring型になっています。
今回はステータスの計算をしたいので、数値型に変換する必要があります。

文字列から数値型に変換する処理はstrconvパッケージのAtoiメソッドを使用します。
クエリパラメータがなかった場合、エラーになるので、空チェックを入れてます。

```go
		if pathLevel != "" && pathEffortVal != "" && pathIndividualVal != "" {
			level, err = strconv.Atoi(pathLevel)
			if err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}
			effortVal, err = strconv.Atoi(pathEffortVal)
			if err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}
			indvidualVal, err = strconv.Atoi(pathIndividualVal)
			if err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}
		}
```

これでクエリパラメータから値を取り出し、数値型にすることができました。

## 受け取ったパラメータでステータスを計算
ポケモンのステータスの計算式は以下のようになっているそうです。
>最大HP
(種族値×2+個体値+努力値÷4)×レベル÷100+レベル+10
こうげき・ぼうぎょ・とくこう・とくぼう・すばやさ
{(種族値×2+個体値+努力値÷4)×レベル÷100+5}×せいかく補正
今回はせいかく補正は割愛します。

### ステータス計算の処理
努力値、個体値、種族値、レベルを引数に計算します。
```go
	CalculateHP := func(baseStat int, individualVal int, effortVal int, level int) int {
		calStatus := (baseStat*2+individualVal+effortVal/4)*level/100 + level + 10
		return calStatus
	}
	CalculateOtherStat := func(baseStat int, individualVal int, effortVal int, level int) int {
		calStatus := (baseStat*2+individualVal+effortVal/4)*level/100 + 5
		return calStatus
	}
```

#  計算後のステータスをレスポンスに含め返す
計算後のステータスをレスポンスに含めるため、構造体にフィールドを追加します。
```go
	//ポケモンのデータを格納する構造体
	type PokeData struct {
		Stats []struct {
			BaseStat int `json:"base_stat"`
			CalStat  int `json:"cal_stat"` //追加
			Stat     struct {
				Name string `json:"name"`
			} `json:"stat"`
		} `json:"stats"`
	}
```

あとは呼び出し側でCalStatにセットします。
Statsフィールドの配列をループし、全てのステータスを計算し、セット
```go
		// レベル、努力値、個体値を元にステータスを計算
		for i, stat := range PokeData.Stats {
			switch stat.Stat.Name {
			case "hp":
				PokeData.Stats[i].CalStat = CalculateHP(stat.BaseStat, indvidualVal, effortVal, level)
			default:
				PokeData.Stats[i].CalStat = CalculateOtherStat(stat.BaseStat, indvidualVal, effortVal, level)
			}
		}
```

# 挙動確認
```
curl http://localhost:8080/pokemon/1?lv=50&ef=1&in=1
```

結果
```json
{
    "stats": [
        {
            "base_stat": 45,
            "cal_stat": 105,
            "stat": {
                "name": "hp"
            }
        },
        {
            "base_stat": 49,
            "cal_stat": 54,
            "stat": {
                "name": "attack"
            }
        },
        {
            "base_stat": 49,
            "cal_stat": 54,
            "stat": {
                "name": "defense"
            }
        },
        {
            "base_stat": 65,
            "cal_stat": 70,
            "stat": {
                "name": "special-attack"
            }
        },
        {
            "base_stat": 65,
            "cal_stat": 70,
            "stat": {
                "name": "special-defense"
            }
        },
        {
            "base_stat": 45,
            "cal_stat": 50,
            "stat": {
                "name": "speed"
            }
        }
    ]
}
```

きちんと計算ができていそうです。

## 全体の処理(コード汚くてごめんなさい笑)
```main.go
package main

import (
	"encoding/json"
	"github.com/gorilla/mux"
	"log"
	"net/http"
	"strconv"
)

func main() {
	//ポケモンのデータを格納する構造体
	type PokeData struct {
		Stats []struct {
			BaseStat int `json:"base_stat"`
			CalStat  int `json:"cal_stat"`
			Stat     struct {
				Name string `json:"name"`
			} `json:"stat"`
		} `json:"stats"`
	}

	//ステータスを計算する関数
	CalculateHP := func(baseStat int, individualVal int, effortVal int, level int) int {
		calStatus := (baseStat*2+individualVal+effortVal/4)*level/100 + level + 10
		return calStatus
	}
	CalculateOtherStat := func(baseStat int, individualVal int, effortVal int, level int) int {
		calStatus := (baseStat*2+individualVal+effortVal/4)*level/100 + 5
		return calStatus
	}

	//ポケモンのデータを取得するhandler
	GetPokeDataHandler := func(w http.ResponseWriter, req *http.Request) {
		var err error
		var level, effortVal, indvidualVal int

		//クエリパラメータからレベル、努力値、個体値を取得
		query := req.URL.Query()
		pathLevel := query.Get("lv")
		pathEffortVal := query.Get("ef")
		pathIndividualVal := query.Get("in")

		if pathLevel != "" && pathEffortVal != "" && pathIndividualVal != "" {
			level, err = strconv.Atoi(pathLevel)
			if err != nil {
				http.Error(w, err.Error(), http.StatusBadRequest)
				return
			}
			effortVal, err = strconv.Atoi(pathEffortVal)
			if err != nil {
				http.Error(w, err.Error(), http.StatusBadRequest)
				return
			}
			indvidualVal, err = strconv.Atoi(pathIndividualVal)
			if err != nil {
				http.Error(w, err.Error(), http.StatusBadRequest)
				return
			}
		}

		//パスパラメータを元にポケモンのデータを取得
		vars := mux.Vars(req)
		res, err := http.Get("https://pokeapi.co/api/v2/pokemon/" + vars["id"])
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		defer res.Body.Close()

		//レスポンスを構造体に変換
		var PokeData PokeData
		if err := json.NewDecoder(res.Body).Decode(&PokeData); err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}

		// レベル、努力値、個体値を元にステータスを計算
		for i, stat := range PokeData.Stats {
			switch stat.Stat.Name {
			case "hp":
				PokeData.Stats[i].CalStat = CalculateHP(stat.BaseStat, indvidualVal, effortVal, level)
			default:
				PokeData.Stats[i].CalStat = CalculateOtherStat(stat.BaseStat, indvidualVal, effortVal, level)
			}
		}

		//レスポンスをjson形式で返す
		w.Header().Set("Content-Type", "application/json")
		if err := json.NewEncoder(w).Encode(PokeData); err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
	}

	r := mux.NewRouter()
	r.HandleFunc("/pokemon/{id:[0-9]+}", GetPokeDataHandler).Methods(http.MethodGet)
	http.Handle("/", r)
	log.Fatal(http.ListenAndServe(":8080", nil))
}


```

# まとめ
以上になります。
次は、ゴルーチンを使って、対象のポケモン、画像を平行処理で取得し、返すようにしていきたいと考えています。
その際にエラー処理を共通化したり処理後とにパッケージにも切り分けていくこともやってみたいと思います。