この記事の内容
- Go言語で外部APIを叩く方法
- 取得した結果から特定の項目だけ取り出し、扱う方法
Goのチュートリアル、入門書を終えたのでPokeAPIを使い手を動かしながら学ぶ記事です。
実現すること
PokeAPIを叩いて得たデータから種族値の値のみ抜き出してレスポンスを返す。
PokeAPI
ポケモンに関する様々なデータを取得できるAPI
種族値とは?
ポケモンの種類ごとに各『のうりょく』別に能力値の基準となる、種族値(しゅぞくち)という隠し数値を持つ
後々、努力値、レベル、個体値などからステータス算出する処理など実装しても勉強になりそうなのでやりたいと考えています。
実装の流れ
以下の順番で行います。
- 新しいGoプロジェクトを作成
- ハンドラーの作成、ルーティングの設定
- PokeAPIを叩き、種族値をレスポンスとして返すハンドラーを実装
新しいGoプロジェクトを作成
Goで新しいプロジェクトを作成する時は「go mod」コマンドを使用します。
go mod init github.com/[アカウント名]/[リポジトリめい]
これでgo mod initを実行したディレクトリ配下をGoモジュールとして扱えるようになります。
モジュールとは、パッケージの集まりです。
パッケージとは同じディレクトリの中にまとめられた、変数や定数、関数定義の集合です。
Goでは様々なパッケージを利用し、作成しながら開発をしていきます。
go mod init
コマンドを実行するとgo.modというファイルが作成されます。
これにより依存関係整理(追加、更新、削除)を行えるようになります。
ハンドラーの作成、ルーティングの設定
cmdディレクトリを作成し、その配下にmain.goファイルを作成します。
.
├── cmd
│ └── main.go
├── go.mod
このmain.goに処理を書いていきます。
gorilla/muxをインストール
ルーティングには今回 gorilla/muxパッケージを使用します。
HTTPメソッドのバリデーション、リクエストパスの取得等簡単に行えます。
ミドルウェアを追加する際なども便利な機能がたくさん用意されています。
go get -u github.com/gorilla/mux
ルーティングとサーバの起動は以下のように行います。
//ポケモンの種族値を取得するhandler
GetPokeDataHandler := func(w http.ResponseWriter, req *http.Request) {
//この中にPokeAPIを叩いて、種族値のデータのみ取得する処理を書く
}
//gorilla/muxパッケージのルータインスタンスを作成
r := mux.NewRouter()
//ルータインスタンスに対して、ルーティングを設定
//HandeFunc関数の第1引数にパス、第2引数にそのパスにリクエストが来た際に実行するハンドラ-を渡す。
//Methodsを使用することで引数で指定したHTTPメソッドのみ許可するようにできます。
r :
r.HandleFunc("/pokemon/{id:[0-9]+}", GetPokeDataHandler).Methods(http.MethodGet)
//handleメソッドの第2引数にルーティングを設定したインスタンスを渡す
//ListenAndServeでサーバ起動
http.Handle("/", r)
log.Fatal(http.ListenAndServe(":8080", nil))
PokeAPIを叩き、種族値をレスポンスとして返すハンドラーを実装
ルーティングの設定ができたので、GetPokeDataHandlerの処理を書いていきます。
最終的にこうなります
package main
import (
"encoding/json"
"github.com/gorilla/mux"
"log"
"net/http"
)
func main() {
//ポケモンのデータを格納する構造体
type PokeData struct {
Stats []struct {
BaseStat int `json:"base_stat"`
Stat struct {
Name string `json:"name"`
} `json:"stat"`
} `json:"stats"`
}
//ポケモンのデータを取得するhandler
GetPokeDataHandler := func(w http.ResponseWriter, req *http.Request) {
vars := mux.Vars(req)
//パスパラメータを数値に変換
var PokeData PokeData
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()
//レスポンスを構造体に変換
if err := json.NewDecoder(res.Body).Decode(&PokeData); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
//レスポンスを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))
}
1つずつ解説していきます。
GoでAPIを叩く
今回叩くエンドポイントはこちら
https://pokeapi.co/api/v2/pokemon/[id]
idに紐づくポケモンの名前、画像等様々なデータを返してくれます。
GoでAPIを叩く時は、httpパッケージのGetメソッドを使います。
res, err := http.Get("https://pokeapi.co/api/v2/pokemon/" + vars["id"])
Getメソッドはレスポンス型の構造体とエラーを戻り値として返します。
type Response struct {
Status string
StatusCode int
Proto string
ProtoMajor int
ProtoMinor int
Header Header
Body io.ReadCloser
ContentLength int64
TransferEncoding []string
Close bool
Uncompressed bool
Trailer Header
Request *Request
TLS *tls.ConnectionState
}
このレスポンス型の構造体の各フィールドにデータが格納されます。
リクエストURLからパスを取り出す。
ポケモンのデータを得るにはIDを指定する必要があります。
IDはリクエストパラメータから取得するようにします。
現在パスはこのようになっています。
r.HandleFunc("/pokemon/{id:[0-9]+}", GetPokeDataHandler).Methods(http.MethodGet)
id:[0-9]+ と書くことで
pokemon/以降のパラメータが1つ以上の数値で構成されていることを期待するようになります。
localhost:8080/pokemon/123
のような形式ということですね。
id:と名前をつけることで 123
などの数値をidという名前で扱えるようになります。
こうすることでハンドラ内で以下のようにパスを取り出すことができます
GetPokeDataHandler := func(w http.ResponseWriter, req *http.Request) {
vars := mux.Vars(req)
res, err := http.Get("https://pokeapi.co/api/v2/pokemon/" + vars["id"])
//以下省略
リクエストの情報は
第2引数であるreq つまりhttpパッケージのRequest構造体が保持しています。
gorilla/muxパッケージのVarsメソッドにこの構造体を渡してあげると簡単に取り出すことができます。
Varsメソッドは戻り値としてmap型を返します。
map[string]string
先ほど名前をつけたidをkeyにパスの値を取得することができます。
http.Get("https://pokeapi.co/api/v2/pokemon/" + vars["id"])
これをAPIのURLの末尾につけることで受け取ったパラメータをセットすることができました。
取得したレスポンスから必要なフィールドだけ抜き出す。
これでAPIを叩きレスポンスを得ることができました。
ただ、このままでは必要のない情報もたくさん含まれています。
このままの状態のレスポンスを見てみましょう
{"abilities":[{"ability":{"name":"overgrow","url":"https://pokeapi.co/api/v2/ability/65/"},"is_hidden":false,"slot":1},{"ability":{"name":"chlorophyll","url":"https://pokeapi.co/api/v2/ability/34/"},"is_hidden":true,"slot":3}],"base_experience":64,"cries":{"latest":"https://raw.githubusercontent.com/PokeAPI/cries/main/cries/pokemon/latest/1.ogg","legacy":"https://raw.githubusercontent.com/PokeAPI/cries/main/cries/pokemon/legacy/1.ogg"},"forms":[{"name":"bulbasaur","url":"https://pokeapi.co/api/v2/pokemon-form/1/"}],"game_indices":[{"game_index":153,"version":{"name":"red","url":"https://pokeapi.co/api/v2/version/1/"}},{"game_index":153,"version":{"name":"blue","url":"https://pokeapi.co/api/v2/version/2/"}},{"game_index":153,"version":{"name":"yellow","url":"https://pokeapi.co/api/v2/version/3/"}},{"game_index":1,"version":{"name":"gold","url":"https://pokeapi.co/api/v2/version/4/"}},{"game_index":1,"version":{"name":"silver","url":"https://pokeapi.co/api/v2/version/5/"}},{"game_index":1,"version":{"name":"crystal","url":"https://pokeapi.co/api/v2/version/6/"}},{"game_index":1,"version":{"name":"ruby","url":"https://pokeapi.co/api/v2/version/7/"}},{"game_index":1,"version":{"name":"sapphire","url":"https://pokeapi.co/api/v2/version/8/"}},{"game_index":1,"version":{"name":"emerald","url":"https://pokeapi.co/api/v2/version/9/"}},{"game_index":1,"version":{"name":"firered","url":"https://pokeapi.co/api/v2/version/10/"}},{"game_index":1,"version":{"name":"leafgreen","url":"https://pokeapi.co/api/v2/version/11/"}},{"game_index":1,"version":{"name":"diamond","url":"https://pokeapi.co/api/v2/version/12/"}},{"game_index":1,"version":{"name":"pearl","url":"https://pokeapi.co/api/v2/version/13/"}},{"game_index":1,"version":{"name":"platinum","url":"https://pokeapi.co/api/v2/version/14/"}},{"game_index":1,"version":{"name":"heartgold","url":"https://pokeapi.co/api/v2/version/15/"}},{"game_index":1,"version":{"name":"soulsilver","url":"https://pokeapi.co/api/v2/version/16/"}},{"game_index":1,"version":{"name":"black","url":"https://pokeapi.co/api/v2/version/17/"}},{"game_index":1,"version":{"name":"white","url":"https://pokeapi.co/api/v2/version/18/"}},{"game_index":1,"version":{"name":"black-2","url":"https://pokeapi.co/api/v2/version/21/"}},{"game_index":1,"version":{"name":"white-2","url":"https://pokeapi.co/api/v2/version/22/"}}],"height":7,"held_items":[],"id":1,"is_default":true,"location_area_encounters":"https://pokeapi.co/api/v2/pokemon/1/encounters","moves":[{"move":{"name":"razor-wind","url":"https://pokeapi.co/api/v2/move/13/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"egg","url":"https://pokeapi.co/api/v2/move-learn-method/2/"},"version_group":{"name":"gold-silver","url":"https://pokeapi.co/api/v2/version-group/3/"}},{"level_learned_at":0,"move_learn_method":{"name":"egg","url":"https://pokeapi.co/api/v2/move-learn-method/2/"},"version_group":{"name":"crystal","url":"https://pokeapi.co/api/v2/version-group/4/"}}]},{"move":{"name":"swords-
//省略
すごい量の情報ですね。
このままでは扱いにくいですよね。
JSON形式に変換して、欲しい情報を1つずつ取り出してとしても良いのですが、Goにはとても便利な機能があります。
それが
json.NewDecoderメソッドとDecodeメソッドです。
json.NewDecoder(res.Body).Decode(&PokeData);
NewDecoderメソッドの引数にレスポンスのボディを
Decodeメソッドに構造体を渡します。
ただ、何も考えずに行うときちんと変換できずエラーになるので以下のことを意識する必要があります
- 構造体のフィールド名にjsonのフィールド名に対応するjsonタグをつける
- レスポンスのjsonのフィールドの構造とGoの構造体を同じ形式にする
具体的に解説します。
PokeAPIで今回欲しいデータの構造は以下のようになっています。
"stats": [
{
"base_stat": 45,
"effort": 0,
"stat": {
"name": "hp",
"url": "https://pokeapi.co/api/v2/stat/1/"
}
},
{
"base_stat": 49,
"effort": 0,
"stat": {
"name": "attack",
"url": "https://pokeapi.co/api/v2/stat/2/"
}
},
{
"base_stat": 49,
"effort": 0,
"stat": {
"name": "defense",
"url": "https://pokeapi.co/api/v2/stat/3/"
}
},
{
"base_stat": 65,
"effort": 1,
"stat": {
"name": "special-attack",
"url": "https://pokeapi.co/api/v2/stat/4/"
}
},
{
"base_stat": 65,
"effort": 0,
"stat": {
"name": "special-defense",
"url": "https://pokeapi.co/api/v2/stat/5/"
}
},
{
"base_stat": 45,
"effort": 0,
"stat": {
"name": "speed",
"url": "https://pokeapi.co/api/v2/stat/6/"
}
}
],
APIを叩いてどんな結果を得られるのか確認するのには「Postman」がおすすめです。
statsという配列のなあに
base_statというのが種族値です。
その中にあるstatのnameが種族値に対応する名前です。
全てのステータスのnameとbase_statが今回欲しいデータです。
対応するGo構造体を作る
上記のデータを取り出すための構造体は以下の通りです。
type PokeData struct {
Stats []struct {
BaseStat int `json:"base_stat"`
Stat struct {
Name string `json:"name"`
} `json:"stat"`
} `json:"stats"`
}
ポイントとなるのがこの部分です
`json:"stats"`
これがタグです。
jsonパッケージのDecodeメソッドでは構造体の中のこのタグを見て同じ名前のものをマッピングします。
タグがなくてもフィールド名が同じであれば問題ないのですが
複数の単語を含める場合、Goはキャメルケースで命名することになっています。
しかしjsonはスネークケースです。
base_statなどGoの構造体のフィールドでは表現できない名前もあります。
そういったときにタグを使うことで、マッピングすることができます。
再びJSONに変換して返す
//レスポンスを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
構造体からまたJSON形式に戻したい場合はEncodeメソッドを使います。
結果を確認
idが1のポケモンの種族値を取得してみます。
ちなみに1はフシギダネです笑
curl http://localhost:8080/pokemon/1
{
"stats": [
{
"base_stat": 45,
"stat": {
"name": "hp"
}
},
{
"base_stat": 49,
"stat": {
"name": "attack"
}
},
{
"base_stat": 49,
"stat": {
"name": "defense"
}
},
{
"base_stat": 65,
"stat": {
"name": "special-attack"
}
},
{
"base_stat": 65,
"stat": {
"name": "special-defense"
}
},
{
"base_stat": 45,
"stat": {
"name": "speed"
}
}
]
}
無事欲しい、種族値だけ取得することができました。
まとめ
以上、基本的な内容にはなりましたが、Goで外部APIを叩く方法、特定のデータだけ抽出する方法でした。
取得した種族値からレベル、個体値、種族値などによって計算する処理など追加することで、構造体に変換し扱いやすいようにするメリットも感じられると思うので、次はそのあたりをやってみようかと思います。
画像データ、日本語のポケモン名などは、別のエンドポイントになるので、ゴルーチンなど使ってみたいですね。