この記事の内容
- GoのAPI実装でクエリパラメータを扱う方法
前回の記事はこちら
この続きでやっていきたいと思います。
実装すること
- クエリパラメータでポケモンのレベル、努力値、個体値を受け取る
- 受け取ったクエリパラメータからステータスを計算し、レスポンスを返す
実装の流れ
- クエリパラメータの取得
- 受け取ったパラメータでステータスを計算
- 計算後のステータスをレスポンスに含め返す
クエリパラメータの取得
クエリパラメータとは?
URIの一部で、Webサーバーに特定のデータを渡すために使用される。
URLのパスの後に「?」記号に続いて配置され、キーと値のペアで構成される。各ペアは&記号で区切られる。
以下のようなものです。
https://example.com/hoge?name=hoge&age=99
?の後に続く
name がキーで=の後のhogeが値になります。
上記の例では
キー:name 値:hoge
キー:age 値:99
という形になっています。
今回はこのクエリパラメータにレベル、努力値、個体値の3つのデータを渡し、ハンドラ内では受け取ったデータを元にステータスを計算し、レスポンスを返すようにしていきます。
Goでクエリパラメータから値を取得するには
リクエスト構造体のURLフィールドが持つURIパッケージのQueryメソッドを使用します。
GetPokeDataHandler := func(w http.ResponseWriter, req *http.Request) {
//省略
query := req.URL.Query()
//省略
}
ハンドラーの第2引数のreq構造体のURLフィールドよりQueryメソッドを呼び出します。
クエリパラメータの情報はURLフィールドのRawQueryが持っています。
URLパッケージのソースより抜粋
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型の値を返します。
func (u *URL) Query() Values {
v, _ := ParseQuery(u.RawQuery)
return v
}
Values型の定義は以下のようになっており、Keyとvalueのmapになっています。
// 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メソッドを使います。
//クエリパラメータからレベル、努力値、個体値を取得
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メソッドを使用します。
クエリパラメータがなかった場合、エラーになるので、空チェックを入れてます。
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}×せいかく補正
今回はせいかく補正は割愛します。
ステータス計算の処理
努力値、個体値、種族値、レベルを引数に計算します。
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
}
計算後のステータスをレスポンスに含め返す
計算後のステータスをレスポンスに含めるため、構造体にフィールドを追加します。
//ポケモンのデータを格納する構造体
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フィールドの配列をループし、全てのステータスを計算し、セット
// レベル、努力値、個体値を元にステータスを計算
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
結果
{
"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"
}
}
]
}
きちんと計算ができていそうです。
全体の処理(コード汚くてごめんなさい笑)
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))
}
まとめ
以上になります。
次は、ゴルーチンを使って、対象のポケモン、画像を平行処理で取得し、返すようにしていきたいと考えています。
その際にエラー処理を共通化したり処理後とにパッケージにも切り分けていくこともやってみたいと思います。