こんにちは、ヤマウチです!
2020/11/14に弊社の福岡支店にてデブキャンを行いました。
泊りがけでなんかしてそうな名前ですが、1年半くらい前から最低月1以上継続して行っているものの、未だに日帰りしかありません。。。
まあそれは置いとくとして、今回作ったツールを紹介していこうと思います!
ちなみにGo言語に関してはスーパー初学者でして、ツールみたいなのを作ったのも初めてなので、コードは色々と問題があると思いますw
今回のツールをざっくり紹介
今回のツールですが、APIとそれを叩く側を別で作成しました。
これがリポジトリです。
API:https://github.com/y-keisuke/pokemon
叩く側:https://github.com/y-keisuke/pokemon_command
レビューしてやるかってくらいの気持ちで見てくださいw
本当は一つのリポジトリにまとめたかったんですが、それが可能なのかもよくわからずにリポジトリを分けたので、あとから使ったコマンドの方だけリポジトリにcommandってついてます。っていうどうでもいい話。
ということでざっくり紹介ということで動きとしてはこんな感じです↓
(動きがもっさりしてて申し訳、計4回叩いてます)
すごいざっくりですが、こんな感じです。
API側の紹介
まずAPI側ですが、こちら↓のリポジトリからjsonを拝借しました。
https://github.com/fanzeyi/pokemon.json/blob/master/pokedex.json
db.go
本当はDBを用意したかったんですが、面倒だったのでjsonをそのまま使いました←
それがdb.go
です。
package db
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
)
type PokemonCollection struct {
Pokemons []PokemonData
}
type PokemonData struct {
Id int `json:"id"`
Name PokemonName `json:"name"`
Type []string `json:"type"`
Base PokemonBase `json:"base"`
}
type PokemonName struct {
English string `json:"english"`
Japanese string `json:"japanese"`
Chinese string `json:"chinese"`
French string `json:"french"`
}
type PokemonBase struct {
HP int `json:"hp"`
Attack int `json:"attack"`
Defense int `json:"defense"`
SpAttack int `json:"spattack"`
SpDefense int `json:"spdefense"`
Speed int `json:"speed"`
}
func GetPokemonCollection() PokemonCollection {
raw, err := ioutil.ReadFile("./pokedex.json")
if err != nil {
fmt.Println(err.Error())
os.Exit(1)
}
var pokemonCollection PokemonCollection
json.Unmarshal(raw, &pokemonCollection)
return pokemonCollection
}
上でいくつか定義している構造体はjsonの構造とまったく同じにしています。
拝借したjsonから少しだけ構造は変わってます。
ここで定義しているGetPokemonCollection()
でまるっと構造体にjsonのデータをブチ込んでいます。
具体的には
json.Unmarshal(raw, &pokemonCollection)
この部分で構造体に詰めています。
この構造体をなんちゃってDBとして使います。
main.go
続いてmain.go
です。
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
pokemon2 "pokemon/pokemon"
)
func pokemonToJson(w http.ResponseWriter, r *http.Request) {
name := r.FormValue("name")
pokemon, err := pokemon2.GetPokemonBy(name)
// ここでいい感じにエラー情報のjsonを返す方法がわからなかった
if err != nil {
log.Writer()
http.Error(w, fmt.Sprintf("{\"err\":\"%s\"}", err), 200)
return
}
pokemonJson, _ := json.Marshal(pokemon)
fmt.Fprint(w, fmt.Sprintf("%+v", string(pokemonJson)))
}
func handleRequests() {
http.HandleFunc("/", pokemonToJson)
log.Fatal(http.ListenAndServe(":18888", nil))
}
func main() {
handleRequests()
}
ここでやっていることは、「ポート18888でアクセスを待ち受けて、そのアクセスのパラメータを見て、ポケモンのデータを取得し、jsonで種族値を返す」ということです。
まずポート18888で待ち受けている部分
// ポート18888をlisten
http.ListenAndServe(":18888", nil)
続いて、そのアクセスのパラメータを見て
// nameというkeyのパラメータを取得
name := r.FormValue("name")
さらに、ポケモンのデータを取得し
// pokemon2(詳細後述)のGetPokemonByでポケモンを取得
pokemon, err := pokemon2.GetPokemonBy(name)
最後に、jsonで種族値を返す
// 構造体をjsonに変換してから返す
pokemonJson, _ := json.Marshal(pokemon)
fmt.Fprint(w, fmt.Sprintf("%+v", string(pokemonJson)))
Go?なにそれ食えんの?な人から「返さずプリントしてるやん!」っていう声も聞こえそうなので補足しておくと、Fprintというのが書き込み先を指定できる関数で、今回はw(http.ResponseWriter)に書き込んでいるので、結果レスポンスとして返るって感じです。
pokemon.go
pokemon2と書いてあったのがこいつです。
プロジェクト名と被せちゃったので2です。
かなりNGな命名です。
golandがよしなにしてくれたんで…
package pokemon
import (
"errors"
"pokemon/db"
)
type Pokemon struct {
Name string `json:"name"`
HP int `json:"hp"`
Attack int `json:"attack"`
Defense int `json:"defense"`
SpAttack int `json:"sp_attack"`
SpDefense int `json:"sp_defense"`
Speed int `json:"speed"`
}
func GetPokemonBy(name string) (*Pokemon, error) {
pokemonCollection := getPokemonCollection()
for _, pokemon := range pokemonCollection.Pokemons {
if pokemon.Name.Japanese == name {
return getPokemonStruct(pokemon), nil
}
}
return nil, errors.New("ポケモンが見つかりません")
}
func getPokemonCollection() db.PokemonCollection {
return db.GetPokemonCollection()
}
func getPokemonStruct(pokemon db.PokemonData) *Pokemon {
return &Pokemon{
Name: pokemon.Name.Japanese,
HP: pokemon.Base.HP,
Attack: pokemon.Base.Attack,
Defense: pokemon.Base.Defense,
SpAttack: pokemon.Base.SpAttack,
SpDefense: pokemon.Base.SpDefense,
Speed: pokemon.Base.Speed}
}
とりあえずmain.go
から呼び出していたGetPokemonByという関数の説明だけします。
// なんちゃってDBを取得している
pokemonCollection := getPokemonCollection()
なんかプライペート関数経由して取得してますが、なんでこうしたのかは覚えていません。
for _, pokemon := range pokemonCollection.Pokemons {
if pokemon.Name.Japanese == name {
return getPokemonStruct(pokemon), nil
}
}
取得したDBのポケモンの名前と、パラメータで受け取ったポケモンの名前が一致した場合は、そのポケモンを新しい構造体に詰めて返しています。
ざっくりですが、API側はこんな感じです。
叩く側
次にAPIを叩く側です。
main.go
とりあえずソースコードを。
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"pokemon_command/input"
"pokemon_command/pokemon"
)
func main() {
url := input.CreateUrl()
resp, _ := http.Get(url)
defer resp.Body.Close()
byteArray, _ := ioutil.ReadAll(resp.Body)
var errCheck map[string]string
json.Unmarshal(byteArray, &errCheck)
if val, ok := errCheck["err"]; ok {
fmt.Println(val)
return
}
pokemonStruct := pokemon.JsonToPokemon(byteArray)
pokemon.PrintPokemon(pokemonStruct)
}
main関数の中身の説明ですが、
// inputの詳細は後述
url := input.CreateUrl()
ここで標準入力を受け取って、それをもとにAPIを叩くためのURLを生成しています。
resp, _ := http.Get(url)
defer resp.Body.Close()
次に生成したURLにアクセスして、その結果を受け取ります。
byteArray, _ := ioutil.ReadAll(resp.Body)
受け取ったレスポンスのBodyを取得します。
var errCheck map[string]string
json.Unmarshal(byteArray, &errCheck)
if val, ok := errCheck["err"]; ok {
fmt.Println(val)
return
}
ここではエラーハンドリングをしていますが、最適解がわからず。。。
// pokemonは後述
pokemonStruct := pokemon.JsonToPokemon(byteArray)
pokemon.PrintPokemon(pokemonStruct)
最後に受け取ったjsonを構造体に格納してから出力しています。
main.go
はこんな感じです。
input.go
package input
import (
"bufio"
"fmt"
"os"
)
func CreateUrl() string {
fmt.Print("Please enter the name of the Pokemon.\n>> ")
scanner := bufio.NewScanner(os.Stdin)
scanner.Scan()
return "http://localhost:18888/?name=" + scanner.Text()
}
ここは大したことはなく標準入力を受け付けて、その入力をもとにURLを生成しています。
fmt.Print("Please enter the name of the Pokemon.\n>> ")
scanner := bufio.NewScanner(os.Stdin)
scanner.Scan()
ここ3行が入力を促す文章を表示して、bufioパッケージの関数にて標準入力を取得しています。
return "http://localhost:8081/?name=" + scanner.Text()
取得した文字列を結合してURLを生成しています。
なんかもうちょっといい感じに書きたい。
pokemon.go
package pokemon
import (
"encoding/json"
"fmt"
"log"
)
type Pokemon struct {
Name string `json:"name"`
HP int `json:"hp"`
Attack int `json:"attack"`
Defense int `json:"defense"`
SpAttack int `json:"sp_attack"`
SpDefense int `json:"sp_defense"`
Speed int `json:"speed"`
}
func JsonToPokemon(pokemonJson []byte) *Pokemon {
pokemon := new(Pokemon)
err := json.Unmarshal(pokemonJson, pokemon)
if err != nil {
log.Fatal(err)
}
return pokemon
}
func PrintPokemon(pokemon *Pokemon) {
fmt.Println("なまえ : ", pokemon.Name)
fmt.Println("HP : ", pokemon.HP)
fmt.Println("こうげき : ", pokemon.Attack)
fmt.Println("ぼうぎょ : ", pokemon.Defense)
fmt.Println("とくこう : ", pokemon.SpAttack)
fmt.Println("とくぼう : ", pokemon.SpDefense)
fmt.Println("すばやさ : ", pokemon.Speed)
}
ここでも大したことはしておらず、受け取ったjsonを構造体に詰める関数と、その構造体をもとに出力する関数があるのみです。
ほんとにたいしたことは書いてないので、詳細は割愛ですw
感想
かなり書きなぐった感じの記事になりましたが←
GOで何か作ってみるということで、今回は「標準入力で入力を受け付ける」ということをしたいと思っていたので、とりあえずとしては、そんな感じで実装できたので良しとします。
理想はpokemonコマンドみたいなものを作って、オプションによって表示が変わる。
みたいなものを作りたいですが、それはまた別の機会でがんばります。
ローカルだけじゃなくて、いつでもどこでもAPI叩きたいなぁと思って、さくらVPSを契約してそこにAPIをおいたのでw、今度その辺も記事に書こうと思います。
ということで以上です!