1
Help us understand the problem. What are the problem?

posted at

updated at

Organization

【Go】Goで配列を爆速で処理してくれるライブラリが素敵だった【koazee】

はじめに

Goで配列(SliceでもArrayでも)を操作したいと思っていたときに見つけた素敵ライブラリを、備忘録として記録。
JSとかSQLとか使っているとよく使うメソッド群をだいたい使えるようにしてくれる、とても素敵ライブラリ。
全部GoogleAdsQueryBuilderにgroup byが無いのが悪い。

koazeeの公式github のサンプルを元に、基本的な機能やキモについては
@nqdior様 https://qiita.com/nqdior/items/e225eae820d6157dc05b
に詳しいので、こちらもご一読いただけるといいかもしれないです。 むしろ必読です。

今回は、ポケモンの最新版仕様のデータをjsonでまとめられているgithubを見つけたので
初代151匹のポケモンのjsonを引っ張ってきて遊んでみることにします。

今回やってみるのは3つ。

タイプごとに並び替えたい
SQLとかでおなじみのgroup byをやってみます
特定のタイプのポケモンだけ抽出したい
JSでお世話になりがちのfilterを使います
努力値を2倍にしてみる
同じくJSのmapみたいなことにチャレンジです

開発環境 と 設定ファイルたち

macOS Catalina 10.15.7
少し古いOSなので、大体のmac環境では動くでしょう。Windowsは知らん。

とりあえずの動作確認がしたかったので、Docker内でGo環境を構築しました。

Dockerfile
# ベースとなるDockerイメージ指定
FROM golang:1.18.3-alpine3.16

# コンテナ内に作業ディレクトリを作成
RUN mkdir /go/src/work

# コンテナログイン時のディレクトリ指定
WORKDIR /go/src/work

# ホストのファイルをコンテナの作業ディレクトリに移行
ADD sources /go/src/work

# downするたびに外部ライブラリが消えるので、build時にgo get
RUN go get github.com/wesovilabs/koazee
docker-compose.yml
version: '3' # composeファイルのバーション指定
services:
  app: # service名
    build: . # ビルドに使用するDockerfileがあるディレクトリ指定
    tty: true # コンテナの起動永続化
    volumes:
      - ./sources:/go/src/work # マウントディレクトリ指定

パッケージ構成

最小構成かつ適当です。

<projectroot>
┝ Dockerfile
┝ docker-compose.yml
└ sources
  ┝ main.go
  ┝ go.mod
  ┝ go.sum
  └ pokemon.json

go.mod / go.sum については、初回build時にコンテナの中に入って
go mod init <モジュール名> として、go.modを作りました。
Dockerfileで書きたかったけどやり方分からなかった。次回までの課題。

いったんpokemon.json を素の状態で出力

実行結果との差異を確認したいだけなので、不要なら飛ばしてください。
main.go
func main() {
	// pokemon.json ファイルの読み込み
	src, err := ioutil.ReadFile("./pokemon.json")
	if err != nil {
		panic(err)
	}

	// 読み込んだpokemon.json をPokemon構造体に格納
	pokemonTable := []Pokemon{}
	json.Unmarshal([]byte(src), &pokemonTable)
    // (番号順で並んでいるので)タイプ1 / 名前 / 努力値 だけ出力
	for _, v := range pokemonTable {
		fmt.Printf("Type1: %s Name: %s EvYield: %v\n", v.Types[0], v.Name, v.EvYield)
	}
}
Type1: くさ Name: フシギダネ EvYield: [0 0 0 1 0 0]
Type1: くさ Name: フシギソウ EvYield: [0 0 0 1 1 0]
Type1: くさ Name: フシギバナ EvYield: [0 0 0 2 1 0]
Type1: くさ Name: フシギバナ-1 EvYield: [0 0 0 2 1 0]
Type1: ほのお Name: ヒトカゲ EvYield: [0 0 0 0 0 1]
Type1: ほのお Name: リザード EvYield: [0 0 0 1 0 1]
Type1: ほのお Name: リザードン EvYield: [0 0 0 3 0 0]
Type1: ほのお Name: リザードン-1 EvYield: [0 0 0 3 0 0]
Type1: ほのお Name: リザードン-2 EvYield: [0 0 0 3 0 0]
Type1: みず Name: ゼニガメ EvYield: [0 0 1 0 0 0]
Type1: みず Name: カメール EvYield: [0 0 1 0 1 0]
Type1: みず Name: カメックス EvYield: [0 0 0 0 3 0]
Type1: みず Name: カメックス-1 EvYield: [0 0 0 0 3 0]
Type1: むし Name: キャタピー EvYield: [1 0 0 0 0 0]
Type1: むし Name: トランセル EvYield: [0 0 2 0 0 0]
Type1: むし Name: バタフリー EvYield: [0 0 0 2 1 0]
Type1: むし Name: ビードル EvYield: [0 0 0 0 0 1]
Type1: むし Name: コクーン EvYield: [0 0 2 0 0 0]
Type1: むし Name: スピアー EvYield: [0 2 0 0 1 0]
Type1: むし Name: スピアー-1 EvYield: [0 2 0 0 1 0]
Type1: ノーマル Name: ポッポ EvYield: [0 0 0 0 0 1]
Type1: ノーマル Name: ピジョン EvYield: [0 0 0 0 0 2]
Type1: ノーマル Name: ピジョット EvYield: [0 0 0 0 0 3]
Type1: ノーマル Name: ピジョット-1 EvYield: [0 0 0 0 0 3]
Type1: ノーマル Name: コラッタ EvYield: [0 0 0 0 0 1]
Type1: あく Name: コラッタ-1 EvYield: [0 0 0 0 0 1]
Type1: ノーマル Name: ラッタ EvYield: [0 0 0 0 0 2]
Type1: あく Name: ラッタ-1 EvYield: [0 0 0 0 0 2]
Type1: ノーマル Name: オニスズメ EvYield: [0 0 0 0 0 1]
Type1: ノーマル Name: オニドリル EvYield: [0 0 0 0 0 2]
Type1: どく Name: アーボ EvYield: [0 1 0 0 0 0]
Type1: どく Name: アーボック EvYield: [0 2 0 0 0 0]
Type1: でんき Name: ピカチュウ EvYield: [0 0 0 0 0 2]
Type1: でんき Name: ピカチュウ-1 EvYield: [0 0 0 0 0 2]
Type1: でんき Name: ピカチュウ-2 EvYield: [0 0 0 0 0 2]
Type1: でんき Name: ピカチュウ-3 EvYield: [0 0 0 0 0 2]
Type1: でんき Name: ピカチュウ-4 EvYield: [0 0 0 0 0 2]
Type1: でんき Name: ピカチュウ-5 EvYield: [0 0 0 0 0 2]
Type1: でんき Name: ピカチュウ-6 EvYield: [0 0 0 0 0 2]
Type1: でんき Name: ピカチュウ-7 EvYield: [0 0 0 0 0 2]
Type1: でんき Name: ピカチュウ-8 EvYield: [0 0 0 0 0 2]
Type1: でんき Name: ライチュウ EvYield: [0 0 0 0 0 3]
Type1: でんき Name: ライチュウ-1 EvYield: [0 0 0 0 0 3]
--- 以下略 ---

図鑑番号順で並んでます。
ピカチュウ-7 とかは、おそらくフォルム違いなので気にしないことにします。

ソースコードその1

やってること
まずはio/ioutilパッケージでpokemon.jsonを読み込む。
encoding/jsonパッケージのUnmarshalメソッドでPokemon構造体に変換することでpokemonTableを生成。
pokemonTablekoazee.StreamOfメソッドでkoazee形式に変換して各々の関数に渡す、という流れです。

main.go
package main

import (
	"encoding/json"
	"fmt"
	"io/ioutil"

	"github.com/wesovilabs/koazee"
	"github.com/wesovilabs/koazee/stream"
)

// pokemon.json から欲しい項目だけ抜き出した構造体
type Pokemon struct {
	Name    string   `json:"name"`     // ポケモンの名前
	Types   []string `json:"types"`    // ポケモンのタイプ(タイプ1、タイプ2があるので、[]string)
	EvYield []int    `json:"ev_yield"` // 倒した時にもらえる努力値(H,A,B,C,D,S の6要素あるので []int)
}

func main() {
	// pokemon.json ファイルの読み込み
	src, err := ioutil.ReadFile("./pokemon.json")
	if err != nil {
		panic(err)
	}

	// 読み込んだpokemon.json をPokemon構造体に格納
	pokemonTable := []Pokemon{}
	json.Unmarshal([]byte(src), &pokemonTable)

	// koazee.StreamOfメソッドを使用して配列をKoazee形式で格納する。
	streamPokemonTable := koazee.StreamOf(pokemonTable)

	fmt.Println("----- groupByPokemonType1 -----")
	groupByPokemonType1(&streamPokemonTable)
	fmt.Println("----- filterByDragonType -----")
	filterByDragonType(&streamPokemonTable)
	fmt.Println("----- doubleEvYields -----")
	doubleEvYields(&streamPokemonTable)
}

// ポケモンのタイプ1でグループ化して順番に出力する
func groupByPokemonType1(in *stream.Stream) {
	typeBasedPokemonTable, err := in.GroupBy(
		func(p Pokemon) string {
			return p.Types[0]
		})
	if err != nil {
		panic(err)
	}

	// reflect.Value型が使いづらいので、interface型を経由してmap[string][]Pokemon型に型アサーション
	var mapTypeBasedPokemonTable = map[string][]Pokemon{}
	typeBasedPokemonTableToInterface := typeBasedPokemonTable.Interface()
	switch typeBasedPokemonTableToInterface.(type) {
	case map[string][]Pokemon:
		mapTypeBasedPokemonTable = typeBasedPokemonTableToInterface.(map[string][]Pokemon)
	}

	// map型は順番を保証してくれないので、いつも同じ出力結果になるようにPokemonTypesの順番で並び替える
	orderedTypeBasedPokemonTable := []Pokemon{}
	for _, v := range PokemonTypes {
		orderedTypeBasedPokemonTable = append(
			orderedTypeBasedPokemonTable,
			mapTypeBasedPokemonTable[v]...,
		)
	}

	for _, pokemon := range orderedTypeBasedPokemonTable {
		fmt.Printf("Type1: %s Name: %s\n", pokemon.Types[0], pokemon.Name)
	}
}

// 特定のタイプ(今回はドラゴンタイプ)を持つポケモンだけのスライスを、filterメソッドで生成
func filterByDragonType(in *stream.Stream) {
	dragonTypePokemonTable := in.Filter(
		func(p Pokemon) bool {
			return p.Types[0] == "ドラゴン"
		}).Out().Val()

	// stream.Filter はinterface{}型が返ってくるので、[]pokemon型に型アサーション
	var sliceDragonTypePokemonTable []Pokemon
	switch dragonTypePokemonTable.(type) {
	case []Pokemon:
		sliceDragonTypePokemonTable = dragonTypePokemonTable.([]Pokemon)
	}
	for _, pokemon := range sliceDragonTypePokemonTable {
		fmt.Printf("Type1: %s Name: %s\n", pokemon.Types[0], pokemon.Name)
	}
}

// EvYieldsを2倍して新たにスライスを作る関数
func doubleEvYields(in *stream.Stream) {
	doubleEvYieldsPokemonTable := in.Map(
		func(p Pokemon) Pokemon {
			for k, v := range p.EvYield {
				p.EvYield[k] = v * 2
			}
			return p
		}).Out().Val()

	// interface{}型を[]Pokemon 型に型アサーション
	var sliceDoubleEvYieldsPokemonTable []Pokemon
	switch doubleEvYieldsPokemonTable.(type) {
	case []Pokemon:
		sliceDoubleEvYieldsPokemonTable = doubleEvYieldsPokemonTable.([]Pokemon)
	}
	for _, pokemon := range sliceDoubleEvYieldsPokemonTable {
		fmt.Printf("Name: %s EvYield: %v\n", pokemon.Name, pokemon.EvYield)
	}
}

出力結果その1

長いので格納
----- groupByPokemonType1 -----
Type1: ノーマル Name: ポッポ
Type1: ノーマル Name: ピジョン
Type1: ノーマル Name: ピジョット
Type1: ノーマル Name: ピジョット-1
Type1: ノーマル Name: コラッタ
Type1: ノーマル Name: ラッタ
Type1: ノーマル Name: オニスズメ
Type1: ノーマル Name: オニドリル
Type1: ノーマル Name: プリン
Type1: ノーマル Name: プクリン
Type1: ノーマル Name: ニャース
Type1: ノーマル Name: ペルシアン
Type1: ノーマル Name: カモネギ
Type1: ノーマル Name: ドードー
Type1: ノーマル Name: ドードリオ
Type1: ノーマル Name: ベロリンガ
Type1: ノーマル Name: ラッキー
Type1: ノーマル Name: ガルーラ
Type1: ノーマル Name: ガルーラ-1
Type1: ノーマル Name: ケンタロス
Type1: ノーマル Name: メタモン
Type1: ノーマル Name: イーブイ
Type1: ノーマル Name: イーブイ-1
Type1: ノーマル Name: ポリゴン
Type1: ノーマル Name: カビゴン
Type1: ほのお Name: ヒトカゲ
Type1: ほのお Name: リザード
Type1: ほのお Name: リザードン
Type1: ほのお Name: リザードン-1
Type1: ほのお Name: リザードン-2
Type1: ほのお Name: ロコン
Type1: ほのお Name: キュウコン
Type1: ほのお Name: ガーディ
Type1: ほのお Name: ウインディ
Type1: ほのお Name: ポニータ
Type1: ほのお Name: ギャロップ
Type1: ほのお Name: ガラガラ-1
Type1: ほのお Name: ブーバー
Type1: ほのお Name: ブースター
Type1: ほのお Name: ファイヤー
Type1: みず Name: ゼニガメ
Type1: みず Name: カメール
Type1: みず Name: カメックス
Type1: みず Name: カメックス-1
--- 以下略 ---

----- filterByDragonType -----
Type1: ドラゴン Name: ミニリュウ
Type1: ドラゴン Name: ハクリュー
Type1: ドラゴン Name: カイリュー

----- doubleEvYields -----
Name: フシギダネ EvYield: [0 0 0 2 0 0]
Name: フシギソウ EvYield: [0 0 0 2 2 0]
Name: フシギバナ EvYield: [0 0 0 4 2 0]
Name: フシギバナ-1 EvYield: [0 0 0 4 2 0]
Name: ヒトカゲ EvYield: [0 0 0 0 0 2]
Name: リザード EvYield: [0 0 0 2 0 2]
Name: リザードン EvYield: [0 0 0 6 0 0]
Name: リザードン-1 EvYield: [0 0 0 6 0 0]
Name: リザードン-2 EvYield: [0 0 0 6 0 0]
Name: ゼニガメ EvYield: [0 0 2 0 0 0]
Name: カメール EvYield: [0 0 2 0 2 0]
Name: カメックス EvYield: [0 0 0 0 6 0]
Name: カメックス-1 EvYield: [0 0 0 0 6 0]
Name: キャタピー EvYield: [2 0 0 0 0 0]
Name: トランセル EvYield: [0 0 4 0 0 0]
Name: バタフリー EvYield: [0 0 0 4 2 0]
Name: ビードル EvYield: [0 0 0 0 0 2]
Name: コクーン EvYield: [0 0 4 0 0 0]
Name: スピアー EvYield: [0 4 0 0 2 0]
Name: スピアー-1 EvYield: [0 4 0 0 2 0]
--- 以下略 ---

groupByPokemonType1 では、タイプ1ごとに出力できました。
orderedTypeBasedPokemonTable の箇所では、常に出力順を同じにしたかったので
PokemonTypes というstring型のスライスを用意して、その順番にmapTypeBasedPokemonTableを並び替えてます。
Goではmapの順番を保証してくれないため、この処理を入れないと実行する度に出力順が変わります。
(ある時は「むし」→「ほのお」… またある時は「みず」→「ドラゴン」… のようにランダムで出力されて気持ち悪い)

filterByDragonType では、タイプ1にドラゴンを持つポケモンが出力できました。
初代では、ミニリュウ、ハクリュー、カイリューの3体のみです。

doubleEvYields では、EvYieldの値がそれぞれ2倍になって出力されました。
(ポケルスに感染したよ。やったね。)

ちょっと修正

各関数で、streamの型をそれぞれ任意の型に型アサーションしている処理がありますが
意図した型に変換できなかったときのエラーが怖いので (エラー怖いpanic嫌だ…)
今回は配列を操作したものを出力するだけの関数なので、出力の部分もkoazeeの仕組みを使ってみました。

ただ、私の調べる限りでは、どうしてもGroupByにはメソッドチェーンができなかったので
filterByDragonTypedoubleEvYields だけやってみました。要研究。

ソースコードその2(変更箇所のみ)

// 特定のタイプ(今回はドラゴンタイプ)を持つポケモンだけのスライスを、filterメソッドで生成
func filterByDragonType(in *stream.Stream) stream.Stream {
	dragonTypePokemonTable := in.Filter(
		func(p Pokemon) bool {
			return p.Types[0] == "ドラゴン"
		}).ForEach(
		func(p Pokemon) {
			fmt.Printf("Type1: %s Name: %s\n", p.Types[0], p.Name)
		}).Do()
	return dragonTypePokemonTable
}

// EvYieldsを2倍して新たにスライスを作る関数
func doubleEvYields(in *stream.Stream) stream.Stream {
	doubleEvYieldsPokemonTable := in.Map(
		func(p Pokemon) Pokemon {
			for k, v := range p.EvYield {
				p.EvYield[k] = v * 2
			}
			return p
		}).Foreach(
		func(p Pokemon) {
			fmt.Printf("Name: %s EvYield: %v\n", p.Name, p.EvYield)
		}).Do()

	return doubleEvYieldsPokemonTable
}

それぞれ、Filterメソッド / Mapメソッド のあとに ForEachメソッド をメソッドチェーンすることで
型アサーションを無くして、koazeeの仕組みから出ることなく出力するように変更しました。

最後のDo() はkoazeeのキモのひとつで、Do() が実行されたタイミングで初めて式が評価される仕様のようです。
Do() を入れずに実行すると、それぞれのfmt.Printf で何も出力されません。
やはり詳しい説明は@nqdior様 の記事にお任せします。

出力結果その2

----- filterByDragonType -----
Type1: ドラゴン Name: ミニリュウ
Type1: ドラゴン Name: ハクリュー
Type1: ドラゴン Name: カイリュー
----- doubleEvYields -----
Name: フシギダネ EvYield: [0 0 0 2 0 0]
Name: フシギソウ EvYield: [0 0 0 2 2 0]
Name: フシギバナ EvYield: [0 0 0 4 2 0]
Name: フシギバナ-1 EvYield: [0 0 0 4 2 0]
Name: ヒトカゲ EvYield: [0 0 0 0 0 2]
Name: リザード EvYield: [0 0 0 2 0 2]
Name: リザードン EvYield: [0 0 0 6 0 0]
Name: リザードン-1 EvYield: [0 0 0 6 0 0]
Name: リザードン-2 EvYield: [0 0 0 6 0 0]
Name: ゼニガメ EvYield: [0 0 2 0 0 0]
Name: カメール EvYield: [0 0 2 0 2 0]
Name: カメックス EvYield: [0 0 0 0 6 0]
Name: カメックス-1 EvYield: [0 0 0 0 6 0]
Name: キャタピー EvYield: [2 0 0 0 0 0]
Name: トランセル EvYield: [0 0 4 0 0 0]
Name: バタフリー EvYield: [0 0 0 4 2 0]
Name: ビードル EvYield: [0 0 0 0 0 2]
Name: コクーン EvYield: [0 0 4 0 0 0]
Name: スピアー EvYield: [0 4 0 0 2 0]
Name: スピアー-1 EvYield: [0 4 0 0 2 0]
--- 以下略 ---

型アサーションする作業を無くして同じ結果を出力することができました。
余計なエラー要因を潰せるので、こっちの方が具合が良さそうです。

おわりに

GoogleAdsQueryBuilderにgroup byが無いお陰で
自分で外部ライブラリを見つけて触ってみる経験ができたのは大きい収穫でした。
他にも、IndexOfとかReduceなど他言語の配列操作系でよく見るメソッドが多く収録されているので
Goで配列操作したいってなったら是非koazeeを試してみてください。(更新が2019年で止まっていることだけが懸念点?)

koazee以外にも配列操作系のライブラリはいくつかあるみたいなので、いずれそれらも触ってみたいなと思う今日この頃。

コラッタLv.2を倒すのも一苦労なレベルの初心者が「動けっっ!」って思いながら書いたものですので
もっと美しい書き方や間違い等ございましたら、ご指摘いただけますと大変嬉しく思います。

参考リンク

koazee公式github
https://github.com/wesovilabs/koazee

【Golang】コアラのように怠惰でチンパンジーのように賢い、高速配列操作ライブラリ「Koazee」使ってみた
@nqdior様 https://qiita.com/nqdior/items/e225eae820d6157dc05b

DockerでGoの開発環境を構築する
@uji_(ACALL株式会社)様 https://qiita.com/uji_/items/8c9eda89526abe0ba900

GoでJSONから構造体に変換するときに気をつけること
@NaoyukiSato様 https://qiita.com/NaoyukiSato/items/6dcd26725d01d0b6b2fa

[Golang] ファイル読み込みサンプル
@tchnkmr様 https://qiita.com/tchnkmr/items/b686adc4a7e144d48755

8世代対応全ポケモンの.jsonファイルを作ってみた
@dayu_282_様 https://qiita.com/dayu_282_/items/f03edc0f4266c5d67d69

Goでmapをキー順にソートする
@ryuken様 https://qiita.com/ryuken/items/6ef4a33fa5ad4819af85

Register as a new user and use Qiita more conveniently

  1. You can follow users and tags
  2. you can stock useful information
  3. You can make editorial suggestions for articles
What you can do with signing up
1
Help us understand the problem. What are the problem?