0
0

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

Last updated at Posted at 2024-06-08

この記事の内容

  • 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"
            }
        }
    ]
}

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

全体の処理(コード汚くてごめんなさい笑)

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))
}


まとめ

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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0