- 投稿日:2020-09-12T23:54:16+09:00
echoでより高速なJSONを返す
はじめに
goのwebフレームワークであるEchoにてデフォルトのJSONよりも高速なJSONを返したいことがあると思います。その例としてeasyjsonで作ったJSONを返そうとしたときに少し詰まりどころがあったので書き留めておきたいと思います。
まずはデフォルトから
main.gopackage main import ( "github.com/masibw/echo_json/model" "net/http" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" ) func main() { // Echo instance e := echo.New() // Middleware e.Use(middleware.Logger()) e.Use(middleware.Recover()) // Routes e.GET("/", getUser) // Start server e.Logger.Fatal(e.Start(":1323")) } // Handler func getUser(c echo.Context) error { user := model.User{Name:"John",Age:18} return c.JSON(http.StatusOK, user) }という感じで
return c.JSON(http.StatusOK, user)と返せば良いです。
easyjsonで作ったJSONを返す
詳しいパフォーマンス測定は行っていませんが こちらの記事によるとencoding/jsonよりも2倍くらいは速そうです。
echoにて既にencoding済みのJSONを返すには
func(c echo.Context) error { encodedJSON := []byte{} // Encoded JSON from external source return c.JSONBlob(http.StatusOK, encodedJSON) }という感じで渡してあげれば良いです。ということでencodedJSONのところにeasyjsonで作ったJSONを渡しましょう。
ディレクトリ構成はこんな感じで作業します。
. ├── go.mod ├── go.sum ├── main.go └── model └── model.gomodel.gopackage model type User struct { Name string `json:"name"` Age int `json:"age"` }easyjsonのインストール
go get -u github.com/mailru/easyjson/...した後にmodelが記載されているディレクトリに移動して
easyjson -all <file>.goする必要があります。
すると<file>_easyjson.goというファイルが作成されるはずです。
あとはプログラム内でencodedJSON, err := easyjson.Marshal(user)のようにJSONへencodeすることができます。
これをechoで返す様にすれば良いです。全体像がこちらmain.gopackage main import ( "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" "github.com/mailru/easyjson" "github.com/masibw/echo_json/model" "log" "net/http" ) func main() { // Echo instance e := echo.New() // Middleware e.Use(middleware.Logger()) e.Use(middleware.Recover()) // Routes e.GET("/", getUser) // Start server e.Logger.Fatal(e.Start(":1323")) } // Handler func getUser(c echo.Context) error { user := model.User{Name: "John", Age: 18} encodedJSON, err := easyjson.Marshal(user) if err != nil { log.Fatal("json marshal error") } return c.JSONBlob(http.StatusOK, encodedJSON) }つまりどころ
mainパッケージに対してコマンドが実行できない
easyjson -all <file>.go該当Issue?
なのでmodelパッケージのように別パッケージを用意してあげましょう。
それに加えて厳密な条件は調べていませんが構造体定義以外が対象ファイルに記載されているとエラーが返ってきてしまうことがあるようです。
その場合は一旦構造体のみ別パッケージに移すなりしてeasyjsonのコマンドを実行し,自動生成されたファイルをコピーして同じパッケージにすれば動きます(構造体定義が変わるたびに実行する必要があるのでめんどくさいですが)
- 投稿日:2020-09-12T23:18:18+09:00
A Tour of Go メモ【2】1日目
A Tour of Go
時折、コードを変更したり、関連記事を調べたりしながら、学習しました。for
func main() { sum := 0 for i :=0; i < 10; i++ { sum += i fmt.Println(i) fmt.Println(sum) } } #省略可能 for ; sum < 100; { sum += sum } #さらに省略可能 for sum < 100 { sum += 100 } # 無限ループ for { }if
func judge(x int) string { if x > 0 { return "マイナス" } return fmt.Println("プラス") } func main() { fmt.Println(judge(−100)) } >> "マイナス" #注意 returnどちらか一つ if文内の returnに到達したら、 if文外の return の戻り値は返ってこない条件の前に変数を定義できる
条件の前に定義した変数はifのスコープ内のみ使用可能
もちろん、elseブロッック内でも使用可能func pow(x, n, lim float64) float64 { if v := math.pow(x, n); v < lim { return v } else { return v+10 } return lim } func main() { fmt.Println( pow(3, 2, 10) pow(3, 3, 20) ) } >> 9 >> 127 #スコープ外で使うとエラー if v := 10: v < 100 { return "100以下" } fmt.Println(v) >> undefined: vSwitch
breakはいらない
caseの条件が一致すれば、自動的にbreakするruntime.GOOSで実行しているOSを判定できる
func main() { fmt.Printl("Go runs on") switch os := runtime.GOOS; os { case "darwin": fmt.Println("OS x.") case "linux": fmt.Println("Linux.") default: fmt.Printf("%s.\n", os) } } switch { t := time.now() case t.Hour() < 12: fmt.Println("Good morning") case t.Hour() < 17: fmt.Println("Good afternoon") default: fmt.Println("Good evening") }Defer
呼び出し元の関数が終わるまで、deferに渡した関数の実行を遅らせる
deferに渡した関数の引数はすぐに評価される
Go言語のdeferを正しく理解する | How defer in Golang worksfunc main() { world := "world" defer fmt.Println(world) world = "new world" fmt.Println("hello") } > hello # deferにより、main()関数が終わった後に実行される # しかし、worldはすぐに評価されるので、deferの後に"new world" と変更しても、反映されない > world
- 投稿日:2020-09-12T23:16:01+09:00
A Tour of Go メモ【1】1日目
goのチュートリアル
A Tour of GO
時折、コードをいじって、どんなエラーが出るかなどを試しながら、学習しました。出力
packages.gopackage main # パッケージをインポート import ( # 出力のためのパッケージ "fmt" ) func main() { # 出力 fmt.Println("Hello World")) }パッケージのインポート
packages.gopackage main #複数行で書くこともできる import "fmt" import "math" #こっちの方がスマート import ( "fmt" "math" )関数と引数
function.gopackage main import "fmt" #変数の後ろに型名を指定する必要がある func add(x int, y int) int { return x + y } func main() { fmt.Pringln(add(20, 30)) } // 50 #関数の引数が同じ型なら、省略可能 func add(x, y int) int { return x + y }複数の戻り値
func swap(x, y string) (string, string) { return y, x } func main() { a, b := swap("hello", "world") fmt.Println(a,b) } // "world" "hello" # Namec return values * 以下は戻り値の変数が定義されないので、エラーになる func split(sum int) (int, int) { a = sum / 2 b = sum / 3 return a, b } * 戻り値の変数に名前をつける必要がある func split2(sum int) (a, b int) { a = sum / 2 b = sum / 3 return a, b }変数
var n int var c func main () { fmt.Println(n) fmt.Println(c) } # int を指定し、何も値を指定しないと初期値は0 // 0 # 型を指定しなかった c はエラーになる // syntax error: unexpected newline, expecting type変数の初期値
var i, j int = 1, 2 # 初期値を同時に指定すると、型を宣言しなくて良い var a, b = 10, 11 var c, python, java = true, false, "no" func main() { fmt.Println(i, j) fmt.Println(a, b) fmt.Println(c, python, java) } // 1 2 // 10 11 // true false no変数の定義と代入
func main() { a := "hello" k := 3 fmt.Println(a) fmt.Println(k) } // hello // 3 # func main()の外で := を使って、代入するとエラー # ok var a strint = "hello" # ng b := "hello" func main() { fmt.Println(a) fmt.Println(b) } # a は OK // hello # b はエラーになる // syntax error: non-declaration statement outside function body各変数の初期値
func main() { var i int // 0 var f float64 // 0 var b bool // false var s string // "" ... }型の変換
var i int = 32 var f float64 = float64(i) var u uint = uint(f) i := 32 f := float64(i) u := uint(f)型の確認
func main() { v := 100 fmt.Printf("%T\n", v) s := "hello" fmt.Printf("%T\n", s) } // v > int // s > string定数の宣言
func main() { const Say = "hello" fmt.Println(Say) #再代入できない Say = "Hey" // cannot assign to Say } > "hello"
- 投稿日:2020-09-12T23:05:35+09:00
レイヤードアーキテクチャ+DDDでAPI開発[入門]
前書き
- 著者の理解が甘く間違っている箇所もあるかもしれません。その際は優しく教えていただけると幸いですmm
- レイヤードアーキテクチャは具体的なディレクトリ構成等を示すものではなく、あくまで概念的な指標です。正解はありません。
なるべく初学者にもわかりやすい様に書きました。
始めは実装コード等理解しづらいと思いますが、概念的な理解の足掛かりにしていただけると嬉しいです☺️レイヤードアーキテクチャって何?
概要
ソフトウェアの関心事をレイヤー毎に分離するための設計方法の一つです。
他にもクリーンアーキテクチャ・オニオンアーキテクチャなどが有名ですが、どれも責務・依存関係を明確化するというゴールは変わりません。メリット
アーキテクチャを導入する主なメリットは以下の二つです。
- DBやUI・ビジネスロジックなど、仕様変更の際にコードの修正箇所が明確・軽微
- 各レイヤーにおいてテストする目的が明確・処理をモックしやすいことからテストしやすい
仕様変更に強くテストもしやすいことから、保守運用しやすいということで人気なんですね?
レイヤー解説
※矢印は依存方向です。依存とは、関数や変数を参照することを指します。
レイヤードアーキテクチャ+DDDの基本はこの図です。
純正レイヤードアーキテクチャではdomain->infrastructure層
という依存関係でしたが、「ビジネスの中核をなすdomainが、使用DB(infra)などに依存する・影響されるっておかしくない?」と言う事で、DDDではdomainが依存の最高位に君臨しています。これを依存関係逆転の原則(DIP)っていうみたいです?domain層: ビジネスルールの中核の定義を担当 infrastructure層: DB通信・DBデータ更新を担当 usecase層: アプリケーション固有のビジネスルールを担う処理を担当 interface層: ユーザーからのリクエスト受け取りや、表示に関することを担当大まかにはこんな役割です。このままではわかりづらいので以下で詳しく説明します!
domain層
ここでは「ドメインモデル」と「リポジトリ」を定義します。
ドメインモデル
一言でいうとビジネスルールの中核・対象の存在です。
ex)「物を売り買いする」というビジネスルールでは、「ユーザー」「商品」などがドメインモデルとして定義される。SQLのテーブルとよく似ていますが、そのビジネスルールで使用しないデータの場合はdomainとして扱わないという点で異なります。
ex) SQLには解析用にcreated_atが保存されているが、サービス上は使用しないためドメインモデルには持たせないなど。以下、ユーザーのドメインモデルの例です。
path/to/domain/model/user/model.gopackage user type User struct { ID int32 Name string }リポジトリ
ドメインモデルになんらかの変更をする処理を定義する所です。
注意したいのは、ここで定義されているモノは「ドメインモデルこうやって変更するよ!」と抽象的に宣言しているだけで、具体的な処理を記述したものではないという事です。「ドメイン層は、他の仕様変更(使用DBやUI)に影響されない(依存しない)」という思想の元、リポジトリには具体的な処理を記述せず、抽象的な宣言に止まります。
具体的には、Interfaceを用いて抽象的なドメインの更新処理を表現しています。ここら辺難しいですよねorz
イメージは、リポジトリという上司が「ドメインモデルに対してこんなことしたい!」と算段なく抽象的言っている感じで、DB更新などの具体的な処理はinfra層などの部下がリポジトリの意図(Interface)を汲み取って行います。以下、リポジトリの例です。ユーザーというドメインモデルに対して、「こんな引数・戻り値で名前を更新するぞ!」という抽象的な宣言がなされています。
path/to/domain/repository/user/repository.gopackage user import "path/to/domain/model/user" type Repository interface { UpdateName(record *user.User, name string) error }infrastructure層
ここでは実際にDBと通信し、リポジトリに記載された「抽象的なドメインモデルの更新処理」を実現します。
リポジトリで宣言されたInterface(抽象的な更新処理)を継承し、infra層での実際の更新処理をメソッド化しています。
そうすることによって、リポジトリで定義された抽象的な更新処理がinfra層の具体的な更新処理と紐づけられています。(go#interfaceの仕様上、整合性が取れないとエラーを吐くためです。もっと噛み砕けば、リポジトリで宣言された抽象的な更新処理が、infra層に実装されていなければエラーを吐くということです。)ユーザーの名前変更を例にコードをみてみましょう。
- server.goでinfra層に使用DBを渡す
- リポジトリで定義したInterface(抽象的な更新処理)をinfra層(repositoryImpl)に継承
- UpdateNameメソッドで実際のDB更新処理を記述
を行っています。(使用DBはinfra層にベタ書きでもいいのですが、sqlmockでテストしやすいためserver.goから渡しています。)
server.gopackage server import ( ur "path/to/infrastructure/repositoryimpl/user" ) ... // mysql.Conn = sqlへのコネクション。sql.Open("mysql"... ur.NewRepositoryImpl(mysql.Conn) ...path/to/infrastructure/repositoryimpl/user/repositoryimpl.gopackage user import ( um "path/to/domain/model/user" ur "path/to/domain/repository/user" "database/sql" ) type repositoryImpl struct { db *sql.DB } // NewRepositoryImpl Userリポジトリで定義したinterfaceを継承 func NewRepositoryImpl(db *sql.DB)ur.Repository { return &repositoryImpl{ db, } } // UpdateName repositoryImplのメソッドとして、実際のDB更新。 func (impl repositoryImpl) UpdateName(record *um.User, newName string) error { stmt, err := impl.db.Prepare("UPDATE user SET name = ? WHERE id = ?") if err != nil { return err } _, err = stmt.Exec(newName, record.ID) return err }usecase層
ここではアプリケーション固有のビジネスルールを記述します。
ビジネスルールってなんだよ!って感じですよね笑例として、「一定額以上の買い物をした人に対して、ある還元率でポイントを付与する」というケースで考えてみましょう。おさらいですが、実際にUserというドメインモデルの所持ポイントを更新するのは(抽象的に)リポジトリの役目ですよね?
しかし、いくら以上の支払いでポイントを付与するか・ポイント還元率は何%か
などは、実際の更新というよりも、更新に使うデータの選定・整形に近いと思います。これが「アプリケーション固有のビジネスルール」と言われている部分です。それらを記述するのがusecase層です。interface層(handler)から渡されたデータを整形し、必要に応じてそのデータをリポジトリに渡します。
コードをみてみましょう。例示が行き来して申し訳ないですが、名前更新処理です。
- 先ほど作成した実際のデータ更新処理が記載されたinfra層(userRepoImpl)をユースケースに(リポジトリを介して間接的に)継承
- interface層から渡されたnameから更新処理を目論む。データ更新の際は、継承されたrepositoryImplを(リポジトリを介して間接的に)叩く。
server.gopackage server import ( ur "path/to/infrastructure/repositoryimpl/user", uu "path/to/usecase/user" ) ... userRepoImpl := ur.NewRepositoryImpl(mysql.Conn) uu.NewUseCase(userRepoImpl) ...path/to/usecase/user/usecase.gopackage user import ( um "path/to/domain/model/user" ur "path/to/domain/repository/user" ) // interface層で参照するためInterfaceで切り出し type UseCase interface { UpdateName(name string) error } type useCase struct { repository ur.Repository } // NewUseCase Userリポジトリ(domain層)を継承したrepositoryImpl(infra層)を間接的に参照する func NewUseCase(userRepo ur.Repository) UseCase { return &useCase{ repository: userRepo, } } // UpdateName ユーザーの名前を更新するユースケース func (ur useCase) UpdateName(name string) error { ...変数userにcontextからユーザーデータを代入する処理... err := ur.repository.UpdateName(user, name) return nil }interface層
ここではrequest/responseの整形・バリデーションを行います。
ユーザーへの表示をメインとしたこの層では、具体的なビジネスロジックは書かないことが重要です。APIではリクエストが不正値かどうかを確認した後ユースケースを呼び出し、返り値を整形してレスポンスすることだけが役目です。例えば名前更新処理では、長すぎる文字列列クエストはこの層でrejectします。
また
interface
はGo言語の予約語なので、ディレクトリ作成時はinterfaces/
とかにしましょう。以下、名前更新の例です。
- 先ほど作ったuserのusecaseを継承
- リクエストのバリデーションを行い、そのデータをusecaseに渡す(呼び出す)。
- 返り値を整形してレスポンス
server.goimport ( ur "path/to/infrastructure/repositoryimpl/user", uu "path/to/usecase/user", uh "path/to/interfaces/api/handler/user" "net/http" ) func Serve(addr string) { userRepoImpl := ur.NewRepositoryImpl(mysql.Conn) userUsecase := uu.NewUseCase(userRepoImpl) userHandler := uh.NewHandler(userUsecase) // contextにユーザー情報を入れるAuthenticateや、postメソッドは各々でお願いします。 http.HandleFunc("/user/update", post(Authenticate(userHandler.HandleUpdate()))) ...サーバー起動処理... }path/to/interfaces/api/handler/user.gopackage user import ( "path/to/usecase/user" "encoding/json" "http/net" ) // Handler UserにおけるHandlerのインターフェース type Handler interface { HandleUpdate() http.HandlerFunc } type handler struct { useCase user.UseCase } // NewHandler : User データに関する Handler を生成 func NewHandler(userUseCase user.UseCase) Handler { return &handler{ useCase: userUseCase, } // HandleUpdate ユーザ情報更新処理 func (uh handler) HandleUpdate() http.HandlerFunc { return func(writer http.ResponseWriter, request *http.Request) { // リクエストBodyからNameを取得 var requestBody UpdateRequest err := json.NewDecoder(request.Body).Decode(&requestBody) if err != nil { ...リクエストエラー... return } // リクエストのバリデーション(名前の長さは適切か) if len(requestBody.Name) < 名前の最短値 || 名前の最長値 < len(requestBody.Name) { ...バリデーションエラー... return } // 名前を更新するユースケースを呼び出す。 err = uh.useCase.UpdateName(user, requestBody.Name) if err != nil { ...ユースケースのエラー処理... return } // いい感じのresponse response.Success(writer, nil) } } // UpdateRequest ユーザーの名前更新リクエスト type UpdateRequest struct { Name string `json:"name"` }まとめ
以上がレイヤードアーキテクチャ+DDDの構成例解説です。
稚拙な文章を最後まで読んでくださりありがとうございました。
ご指摘・ご感想あればコメントか、twitterにまで連絡していただけると幸いですmm参考にした記事は以下ですmm
①今すぐ「レイヤードアーキテクチャ+DDD」を理解しよう。(golang)
②【Golang + レイヤードアーキテクチャ】DDD を意識して Web API を実装してみるそれではみなさん、楽しい開発ライフを!!
- 投稿日:2020-09-12T20:29:02+09:00
production-ready なGo製2Dゲームライブラリ Ebiten の紹介
はじめに
Ebiten は、Goで書かれた非常にシンプルな2Dゲームライブラリです。(公式サイト: https://ebiten.org)
圧倒的な安定性を誇り、今2Dゲームを作るなら非常にオススメ! というわけで、改めて紹介記事を書いてみます。
ねずみバスターズ、インドとブラジルに数万人規模でリリースしたんですが、報告されたクラッシュが30件程度なので驚愕の安定性...
— Daigo (@daigo) September 11, 2020特徴
- クロスプラットフォームで、非常に安定
- Windows
- macOS (Metalを使用)
- Linux
- FreeBSD
- Android
- iOS
- web
- WebAssembly (, GopherJS)
- 非常にシンプルなAPI (参照: Go のゲームライブラリ Ebiten における API 設計思想)
- 豊富なサンプル(ここですぐに触れます)
- 高いパフォーマンス
- 今もアクティブに開発中で、バグはすぐに修正され、新機能も追加されている
はじめる
(ここではGoのインストールおよび、Goの基礎知識に関しては割愛します。)
Windowsおよび、macOSで Ebiten を使い始めるのは非常に簡単です!
まずは、
go mod init <モジュール名>
で適当なGo Moduleを作り、以下のコードを書いてみましょう。
(https://ebiten.org/tour/hello_world.html の転載)main.gopackage main import ( "log" "github.com/hajimehoshi/ebiten" "github.com/hajimehoshi/ebiten/ebitenutil" ) type Game struct{} func (g *Game) Update(screen *ebiten.Image) error { return nil } func (g *Game) Draw(screen *ebiten.Image) { ebitenutil.DebugPrint(screen, "Hello, World!") } func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) { return 320, 240 } func main() { ebiten.SetWindowSize(640, 480) ebiten.SetWindowTitle("Hello, World!") if err := ebiten.RunGame(&Game{}); err != nil { log.Fatal(err) } }できたら、
go run main.go
を実行します。初回だけは各種ビルドに時間がかかるので待ちましょう。画像のような、いい感じに味のあるHello Worldが表示されれば成功です!
Windows, macOS以外のプラットフォームでのビルドについては、ドキュメント https://ebiten.org/documents/ を参照してください。
できること
ざっくりと Ebiten の機能を列挙してみます。詳しい解説は Ebitenの個人的まとめ に譲りますので、実際に扱う際はそちらを参照してください。
- 画像の描画
- 変形:移動、回転、拡大縮小、スキュー
- 色の変化:色相、スケールなど
- 各種アルファブレンディング
- 音声の再生 (mp3, wav, ogg)
- テキスト描画
- フラグメントシェーダーと、その記述のための独自言語 Kage
- 入力の取得
- キーボード、マウス、ゲームパッド、タッチ
- 入力まわりを補助してくれるパッケージもある。
入力まわりを除けば全ての機能が、Webやモバイルを含む各種プラットフォームで同じように利用できます。(対応状況)
できないこと
Ebiten は意図的にシンプルさを保っているため、フレームワーク的な部分は備えていないのでご留意ください。
- ゲームオブジェクトの管理機能
- カメラ機能
- 当たり判定
- HTML Canvas レベルの高度なベクター描画
- などなど...
より深く
最後に各種リソースへのリンクをご紹介するので、学習に役立ててください!
サンプル
Webサイト には、たくさんの今すぐ触れるサンプルが掲載されています。
ソースは、リポジトリの
examples/
ディレクトリ以下にあります。Webよりもさらに数が多いです。(GitHubのリンク)ソースは以下のように、
-tags=example
をつけて実行します(インストール時に examples もバイナリとして入ってきてしまう問題への対策)。# go run -tags=example ソースへのパス go run -tags=example examples/rotate/main.goドキュメント
- 公式サイト
- リファレンス(godoc)
- Ebitenの個人的まとめ
- Qiitaの詳細な解説記事。
- Go のゲームライブラリ Ebiten における API 設計思想
コミュニティ
- Gophers Slack の #ebiten チャンネル
- 参加は この招待リンク から。
- 英語メインですが、比較的アクティブで、最新情報の告知もここでされることが多いです。
- すぐ反応してもらえるので、非常にサポートが手厚い(重要)
- vim-jp slack の #lang-go #gamedev チャンネル
- Vim のコミュニティですが、なぜか日本で一番盛り上がっている技術コミュニティでもあります。Ebiten の話題もすぐ反応してもらえるので、こちらの方が便利かも。
- 作者ツイッター
- Reddit の r/golang
- たまに Ebiten に関連する話題が投下されることがある。
- Reddit の r/ebiten
- まだ作りたてだけど、これから盛り上がってほしい。
- 投稿日:2020-09-12T20:29:02+09:00
production-ready なGo製2Dゲームライブラリ Ebiten の紹介 & リンク集
はじめに
Ebiten は、Goで書かれた非常にシンプルな2Dゲームライブラリです。(公式サイト: https://ebiten.org )
圧倒的な安定性を誇り、今2Dゲームを作るなら非常にオススメ! というわけで、改めて紹介記事を書いてみます。
ねずみバスターズ、インドとブラジルに数万人規模でリリースしたんですが、報告されたクラッシュが30件程度なので驚愕の安定性...
— Daigo (@daigo) September 11, 2020特徴
- クロスプラットフォームで、非常に安定
- Windows
- macOS (Metalを使用)
- Linux
- FreeBSD
- Android
- iOS
- web
- WebAssembly (, GopherJS)
- 非常にシンプルなAPI (参照: Go のゲームライブラリ Ebiten における API 設計思想)
- 豊富なサンプル(ここですぐに触れます)
- 高いパフォーマンス
- 今もアクティブに開発中で、バグはすぐに修正され、新機能も追加されている
はじめる
(ここではGoのインストールおよび、Goの基礎知識に関しては割愛します。)
Windowsおよび、macOSで Ebiten を使い始めるのは非常に簡単です!
まずは、
go mod init <モジュール名>
で適当なGo Moduleを作り、以下のコードを書いてみましょう。
(https://ebiten.org/tour/hello_world.html の転載)main.gopackage main import ( "log" "github.com/hajimehoshi/ebiten" "github.com/hajimehoshi/ebiten/ebitenutil" ) type Game struct{} func (g *Game) Update(screen *ebiten.Image) error { return nil } func (g *Game) Draw(screen *ebiten.Image) { ebitenutil.DebugPrint(screen, "Hello, World!") } func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) { return 320, 240 } func main() { ebiten.SetWindowSize(640, 480) ebiten.SetWindowTitle("Hello, World!") if err := ebiten.RunGame(&Game{}); err != nil { log.Fatal(err) } }できたら、
go run main.go
を実行します。初回だけは各種ビルドに時間がかかるので待ちましょう。画像のような、いい感じに味のあるHello Worldが表示されれば成功です!
Windows, macOS以外のプラットフォームでのビルドについては、ドキュメント https://ebiten.org/documents/ を参照してください。
できること
ざっくりと Ebiten の機能を列挙してみます。詳しい解説は Ebitenの個人的まとめ に譲りますので、実際に扱う際はそちらを参照してください。
- 画像の描画
- 変形:移動、回転、拡大縮小、切り抜き、スキュー
- 色の変化:色相、スケールなど
- 各種アルファブレンディング
- 音声の再生 (mp3, wav, ogg)
- テキスト描画
- フラグメントシェーダーと、その記述のための独自言語 Kage
- 入力の取得
- キーボード、マウス、ゲームパッド、タッチ
- 入力まわりを補助してくれるパッケージもある。
入力まわりを除けば全ての機能が、Webやモバイルを含む各種プラットフォームで同じように利用できます。(対応状況)
できないこと
Ebiten は意図的にシンプルさを保っているため、フレームワーク的な部分は備えていないのでご留意ください。
- ゲームオブジェクトの管理機能
- カメラ機能
- 当たり判定
- HTML Canvas レベルの高度なベクター描画
- などなど...
より深く
最後に各種リソースへのリンクをご紹介するので、学習に役立ててください!
サンプル
Webサイト には、たくさんの今すぐ触れるサンプルが掲載されています。
ソースは、リポジトリの
examples/
ディレクトリ以下にあります。Webよりもさらに数が多いです。(GitHubのリンク)ソースは以下のように、
-tags=example
をつけて実行します(インストール時に examples もバイナリとして入ってきてしまう問題への対策)。# go run -tags=example ソースへのパス go run -tags=example examples/rotate/main.goドキュメント
- 公式サイト
- リファレンス(godoc)
- Ebitenの個人的まとめ
- Qiitaの詳細な解説記事。
- Go のゲームライブラリ Ebiten における API 設計思想
コミュニティ
- Gophers Slack の #ebiten チャンネル
- 参加は この招待リンク から。
- 英語メインですが、比較的アクティブで、最新情報の告知もここでされることが多いです。
- すぐ反応してもらえるので、非常にサポートが手厚い(重要)
- vim-jp slack の #lang-go #gamedev チャンネル
- Vim のコミュニティですが、なぜか日本で一番盛り上がっている技術コミュニティでもあります。Ebiten の話題もすぐ反応してもらえるので、こちらの方が便利かも。
- 作者ツイッター
- Reddit の r/golang
- たまに Ebiten に関連する話題が投下されることがある。
- Reddit の r/ebiten
- まだ作りたてだけど、これから盛り上がってほしい。
- 投稿日:2020-09-12T13:59:45+09:00
sqlfile : GoでSQLファイルを簡単に実行する
GoでSQLファイルを簡単に実行するためのライブラリを作ったので,その紹介をします.
https://github.com/tanimutomo/sqlfile
sqlfileの簡単な使い方
詳しい使い方については,本記事の下,もしくは,上記のGitHubをご参照ください.
main.goimport ( "database/sql" "github.com/tanimutomo/sqlfile" ) db, err := sql.Open("DBMS", "CONNECTION") s := sqlfile.New() err := s.File("example.sql") res, err := s.Exec(db)はじめに
Goで複数のクエリが書かれたSQLファイルを実行できるライブラリがなかったので,作りました. sqlfile
作成の背景
Goで,DAO (data access object) パッケージのユニットテストの際にseed data挿入が必要になりました.
RailsのFixtureのような,seed dataを作成するためのパッケージとして,testfixtures という便利なライブラリがあります.(RailsのFixturesほどの高価な機能はないです.)
しかし,以下のような点で使い勝手が悪かったです.
(もしかしたら,testfixturesの設定で解決できるのかもしれませんが,見つけられませんでした)
- テーブルごとにファイルを作成する必要がある
- テストごとにレコードが削除されるわけではないため,レコードを削除したいテーブルは,
table_name.yml
というファイルを作成し,中身は[]
を書いておく必要がある- 空のymlファイルがあるとエラーになる
これらのことから,
DBのデータに変更を加える関数をテストする際に,テストケースごとにseed dataを用意しようとすると,testdata/
に大量のyamlファイルを保持しておく必要があり,管理が面倒でした(以下,例).tree. └── testdata/ ├── func1/ │ ├── success_login_user/ │ │ ├── users.yml │ │ ├── articles.yml │ │ ├── tag.yml │ │ └── article_tags.yml │ ├── fail_not_found_user/ │ │ ├── users.yml │ │ ├── articles.yml │ │ ├── tag.yml │ │ └── article_tags.yml │ └── ... ├── func2/ │ └── ... └── ...
特に,空のファイルが許容されてないので,
- 不要になったテーブルは,ファイルごと削除する必要がある
- seed data間の関係をみる際に,ファイルを跨ぐ必要がある.
以上の理由と,これらの問題に関して,testfixturesの挙動を調べるのが面倒だったので,SQLファイルを直接書いて実行したくなりました.
SQLファイルを簡単に実行できる方法がない
ということで,testfixturesを諦めて,SQLファイルを書こうと思ったわけですが,
ここで,GoにはSQLファイルを簡単に実行できる方法がないことに気づきました.ここでやりたいのは,以下のような複数のクエリが書かれたSQLファイルを読み込んで,実行することです.
DELETE FROM users; -- delete all records from users table INSERT INTO users ( -- new user id, name, created_at, updated_at ) VALUES ( 1, 'foo', NOW(), NOW() );単一のSQLのクエリであれば,
database/sql
を使えばできますが,一度に複数のクエリを一度に実行できません.
また,SQLファイルを1行ずつ読み込んで,database/sql
で,実行することも可能ですが,以下のような問題に対処する必要があります.
- 一つのクエリが,複数行にまたがっている
- コメントアウト部分を取り除く
そこで,これらの問題に対処して,適切にクエリを読み込み,実行するためのライブラリを作りました.
sqlfile
長くなってしまいましたが,ここからが作成した sqlfile というライブラリの紹介になります.
本記事を執筆した時点の最新版
v1.0.0
では,ざっくり以下の2機能しかありません.
- SQLファイルの読み込み
- 読み込んだクエリの実行
GitHubの方に,Usageを載せてありますが,ここでも簡単に説明をします.
Installation
go get github.com/tanimutomo/sqlfileUsage
SQLファイルを準備
注意! : 各クエリの最後に,必ず;
をつけてください.example.sqlINSERT INTO users ( -- users table id, name, email, created_at, updated_at ) VALUES ( 1, 'user1', 'user1@example.com', now(), now() ); INSERT INTO articles ( -- articles table id, user_id, title, content, created_at, updated_at ) VALUES ( 1, 1, 'title1', "-- About -- \n I'm sqlfile.", now(), now() -- post1 ), ( 2, 1, 'title2', '- About - \n I''m sqlfile.', now(), now() -- post2 );SQLファイルの読み込みと,実行
File
で読み込んで,Exec
で実行です.
Exec
の中では,内部でトランザクションを発行しているため,どこかのクエリでエラーが起こったら,全てがRollbackされます.main.goimport ( "database/sql" "github.com/tanimutomo/sqlfile" ) // Get a database handler db, err := sql.Open("DBMS", "CONNECTION") // Initialize SqlFile s := sqlfile.New() // Load input file and store queries written in the file err := s.File("example.sql") // Load input files and store queries written in the files err := s.Files("example.sql", "example2.sql") // Load files in the input directory and store queries written in the files err := s.Directory("./examples") // Execute the stored queries // transaction is used to execute queries in Exec() res, err := s.Exec(db)まとめ
Goでdaoのwriterオブジェクトをテストする際のベストプラクティスに関しては,模索中です.
開発中に,SQLファイルを実行したいタイミングがあれば,是非使ってみてください.
Goはまだ書き始めて4ヶ月とかなので,もし間違っている箇所や改善案などがあれば,ぜひコメントお願いします.
- 投稿日:2020-09-12T04:08:01+09:00
GoのBenchmarkに出てくる関数名の後ろの数字
Goのベンチマークをするときに出てくる
関数名の後ろの数値ってなんだっけとど忘れしたのでメモしておきます。関数名-4 <-この"-4"の事ですね。
これはベンチマークに使ったCPU数です。
なのでgo test -bench . -cpu 2のようにCPU数を指定すると
ちゃんと変わります。
普段からGo使ってる人にとっては当たり前の事かもしれませんが
使ってないと知らなかったり忘れたりするので。
- 投稿日:2020-09-12T01:56:57+09:00
Go言語のチュートリアル A Tour of Go のチートシート その1 - Basics
Go言語入門の定番、チュートリアル A Tour of Go をやったので、復習を兼ねて内容をまとめておきます。
また、チュートリアル中、何度も前のページに戻って確認しなければならなかったので、これからやろうという方にはチートシートとしてお使いいただけます。
チュートリアルをやる代わりに、さらっと眺める用途にもどうぞ。
Packages, variables and functions
パッケージとインポート・エクスポート
- プログラムはmainパッケージから実行される。
- importでパッケージを読み込む。
- 大文字で始まる名前はエクスポートされる。
// ファイルの冒頭にpackage名を宣言する package main // パッケージのインポートをグループ化できる // factored import statementと呼ばれる import ( "fmt" "math" ) func main() { // Printf / Sqrt はそれぞれのパッケージからエクスポートされている fmt.Printf("Sqrt(9) is %g\n", math.Sqrt(9)) // 小文字で始まる名前は外部に公開されないので、エラーが発生する fmt.Println(math.pi) }関数
- 関数はfuncで定義する。
- 引数の型は変数の後ろに書く。
// 2つのintを受け取り、intを返す関数 func add(x int, y int) int { return x + y } // 型が同じ場合は、最後の型以外は省略できる func addThree(x, y, z int) int { return x + y + z } // 複数の戻り値を返すことができる func swap(x, y string) (string, string) { return y, x } // 戻り値に名前をつけることができる func div(x, y int) (result int) { // result変数はこの時点で定義済み result = x / y // この場合、何も書かずにreturnできる(naked return) return }変数・定数
変数の宣言はパッケージまたは関数の中で利用できる
// 変数はvarで宣言する var x, y, z int var s string // initializerで初期化可能、型を省略できる var i, j, b = 1, 2, true // 定数はconstで宣言する const K int = 10000 const Truth = true func main() { // 関数内限定でvarの代わりに:=を使うことができる foo := "bar" c := 2 + 0.5i // 複素数型 }基本型(組み込み型)
- bool
- string
- int int8 int16 int32 int64
- uint uint8 uint16 uint32 uint64
- byte(uint8の別名)
- rune(int32の別名)
- float32 float64
- complex64 complex128 (複素数型)
ゼロ値
初期値なしで宣言すると、ゼロ値が入る
- 数値型: 0
- bool: false
- string: ""(空文字列)
型変換
明示的な型変換が必要
var i int = 60 var f float64 = float64(i) var u uint = uint(i)Flow control statements
forループ
- C言語などと同じで、初期化; 条件式; 後処理を記述する
- 多言語のwhileループの代わりにGoではforを利用する
sum := 0 // カッコは不要 for i := 0; i < 10; i++ { sum += i } // 初期化と後処理は省略可能 for ;sum < 1000; { sum += sum } // セミコロンも省略可能 -> whileループ for sum < 10000 { sum += sum } // 条件式も省略可能 -> 無限ループ for { if sum > 100000 { break } }if
- forループと同じでカッコは不要
- 条件判定用の変数を初期化することができる
// カッコ不要 if i > 3 { return i } // xのスコープはif-elseブロック内のみ if x := i * j + 3; x < 100 { return x } else { return x - 100 } // compile error return x + 100switch
- ひとつのcaseが実行されたら、残りのcaseは実行されないので、caseごとのbreakは不要
- ifと同じように条件判定用の変数を初期化することができる
- caseは定数である必要はない
// os変数を初期化 switch os := runtime.GOOS; os { case "darwin": fmt.Println("OS X.") case "linux": fmt.Println("Linux.") default: // freebsd, openbsd, plan9, windows... fmt.Printf("%s.\n", os) } // 条件のないswitchも可能(switch trueと同じ) t := time.Now() switch { case t.Hour() < 12: fmt.Println("Good morning!") case t.Hour() < 17: fmt.Println("Good afternoon.") default: fmt.Println("Good evening.") }defer
- deferに関数の実行を渡すと、関数の終わりまで評価が遅延される
- 最後に渡したdeferから順番に実行される(last-in-first-out)
func hello() { defer fmt.Println("world") fmt.Println("hello") } func countDown() { for i := 0; i < 10; i++ { defer fmt.Print(i) } // 9876543210と表示される }More types: structs, slices, and maps
ポインタ
- ポインタは値のメモリアドレスを指す
- ポインタのゼロ値は
nil
// int型のポインタは *int型 var p *int // &オペレータでポインタを引き出す var i int = 50 p = &I // *オペレータでポインタの指す変数を示す j := *p // pからiの値を読みだす *p = 40 // pを通してiに値を代入するstruct
- struct(構造体)はフィールド(field)の集まり
type Vertex struct { X int Y int } func test() { // Vertexリテラル: field順に値を渡す v := Vertex{1, 2} v.X = 5 // ポインタを使う p := &v // ポインタを通してフィールドにアクセスする (*p).X = 6 // 省略してp.Xとも書ける p.X = 6 } // 初期化時にフィールドの名前を使うことができる var ( v1 = Vertex{Y: 2, X: 1} // フィールドの順番は関係なし v2 = Vertex{Y: 2} // Xはゼロ値(0)をとる v3 = Vertex{} // X: 0, Y: 0 p = &Vertex{1, 2} // *Vertex型 )array
- 配列は長さを指定して宣言する
- 配列の長さは型の一部分なので、配列のサイズを変えることはできない
// int型の要素を持つ長さ10の配列の宣言 var a [10]int // リテラル primes := [6]int{2, 3, 5, 7, 11, 13} var s [2]string s[0] = "Hello" s[1] = "World"slices
- 配列が柔軟になった可変長の型
a[1:4]
などの記法で配列からスライスを作ることができる- スライスはデータを格納せず、元の配列の部分列を指し示す
primes := [6]int{2, 3, 5, 7, 11, 13} // int型の要素を持つスライスの宣言 var s1 []int // primesの要素のうち、1番目から3番目の要素を含むスライス s1 = primes[1:4] // {3, 5, 7} // 下限と上限の値を省略するとデフォルトの値が使用される // デフォルト値は下限: 0, 上限: スライスの長さ // 以下は等価 primes[0:6] primes[:6] primes[0:] primes[:] // 同じ元となる配列を共有している場合、変更が反映される s2 := primes[1:3] // {3, 5} s2[0] = 59 // -> primes[1]とs1[0]も59になる // リテラル // [3]{true, true, false}の配列を作成して、それを参照する b := []{true, true, false}長さ(length)と容量(capacity)
- 長さ:スライスに含まれる要素数、len関数で取得可能
- 容量:スライスの最初の要素から数えた元となる配列の要素数、cap関数で取得可能
a := [6]int{0, 1, 2, 3, 4, 5} s1 := a[0:0] // len(s1) -> 0, cap(s1) -> 6 s2 := a[0:4] // len(s2) -> 4, cap(s2) -> 6 s3 := a[2:6] // len(s3) -> 2, cap(s3) -> 4 // 容量が十分な場合、スライスを再度スライスすると長さが伸びる s4 := s1[0:4] // len(s4) -> 4, cap(s4) -> 6 // スライスのゼロ値はnil、長さと容量は0になる var s5 []int // len(s5) -> 0, cap(s5) -> 0make関数
組み込みのmake関数を使用してスライスを作成することができる
// 型と長さを引数にとる a := make([]int, 2) // len(a) -> 2, cap(a) -> 2, a: [0 0] // 容量も引数に渡すことができる b := make([]int, 0, 3) // len(b) -> 0, cap(b) -> 3, b: []多次元スライス
スライスは任意の型を含むことができるので、スライスを含むスライスを作ることができる
board := [][]int{ []int{1, 2, 3}, []int{4, 5, 6}, []int{7, 8, 9}, }append関数
- append関数:
func append(s []T, vs ...T) []T
- スライスへ新しい要素を追加することができる
- appendしたときに、スライスの参照先の配列の容量を超えてしまう場合は、戻り値となるスライスはより大きいサイズの配列を作って参照する
var s[]int // len(s) -> 0, cap(s) -> 0, s: [] s = append(s, 0) // len(s) -> 1, cap(s) -> 1, s: [0] s = append(s, 1) // len(s) -> 2, cap(s) -> 2, s: [0 1] s = append(s, 2, 3, 4) // len(s) -> 5, cap(s) -> 6, s: [0 1 2 3 4]map
- キーと値を関連づける
- ゼロ値はnil
var m map[string]int m = make(map[string]int) m["likes"] = 10 m["stocks"] = 5 // リテラル m = map[string]int{ "likes": 10, "stocks": 5, } // 追加・更新 m["foo"] = 1 // 取得 likes := m["likes] // 削除 delete(m, "likes") // 存在確認、二つ目の戻り値は、値が存在すればtrue、存在しなければfalse stocks, ok := m["stocks"]range
- slicesやmapをループするときに使うことができる
var pow = []int{1, 2, 4, 8, 16, 32, 64, 128} // ループごとにindexとvalueを返してくれる for i, v := range pow { fmt.Printf("2**%d = %d\n", i, v) } // 不要な場合はアンダースコアで捨てることができる for i, _ := range pow for _, value := range pow // indexだけが欲しい場合は2つ目を省略しても良い for i := range powクロージャ
- 関数はクロージャ
- 関数は自身の外部から変数を参照できる
func adder() func(int) int { sum := 0 // sumへの参照を保持した無名関数 return func(x int) int { sum += x return sum } }続きは次回!
次回の内容は……
- Methods and interfaces
- Concurrency
参考
- 投稿日:2020-09-12T00:37:28+09:00
[Go] Cobraをgo getでインストールしようとするとcommand not foundになる問題の解決
問題
cobraをインストールしてCLIを書いてみようと思って、cobra公式のインストールコマンドを実行した。
go get -u github.com/spf13/cobra/cobraその後cobraがインストールされたか確認しようとしたら
command not found
と表示された。cobra --help command not foundちゃんと
$GOROOT
や$GOPATH
、$GO111MODULE
などの環境変数の設定もしているので謎だった。環境
- MacOS Catalina 10.15.6
- go version go1.15 darwin/amd64
- goenv 2.0.0beta11
解決策
よく見るとインストールコマンドを実行した時のメッセージは以下のようになっていた。
go: github.com/spf13/cobra/cobra upgrade => v0.0.0-20200909172742-8a63648dd905 go get github.com/spf13/cobra/cobra: ambiguous import: found package github.com/spf13/cobra/cobra in multiple modules: github.com/spf13/cobra v1.0.0 (/Users/hoge/go/1.15.0/pkg/mod/github.com/spf13/cobra@v1.0.0/cobra) github.com/spf13/cobra/cobra v0.0.0-20200909172742-8a63648dd905 (/Users/hoge/go/1.15.0/pkg/mod/github.com/spf13/cobra/cobra@v0.0.0-20200909172742-8a63648dd905)これについてググってみるとcobra公式のIssune#1215に解決策があった。
インストールするcobraのバージョンの指定が必要だったらしい。A workaround I found was to "go get" a specific version:
go get -u github.com/spf13/cobra/cobra@v1.0.0これでok.
cobra --help Cobra is a CLI library for Go that empowers applications. This application is a tool to generate the needed files to quickly create a Cobra application. Usage: cobra [command] Available Commands: add Add a command to a Cobra Application help Help about any command init Initialize a Cobra Application Flags: -a, --author string author name for copyright attribution (default "YOUR NAME") --config string config file (default is $HOME/.cobra.yaml) -h, --help help for cobra -l, --license string name of license for the project --viper use Viper for configuration (default true) Use "cobra [command] --help" for more information about a command.
- 投稿日:2020-09-12T00:16:27+09:00
Goでスライスを逆順にする
ドキュメントをみたら、
sort.Reverse()
という関数があった。お、これはPythonの順番を逆転する例のやつかな、と思って使ってみた。a: l = []string{"a", "b", "c"} // lはLess, Len, Swapが定義されたインタフェース sort.Reverse(a) log.Println(a) // "a", "b", "c" // 変わってない!え、何も仕事してないじゃん、と思ってコードを見てみたら・・・
つまり、
sort.Interface
を実装してからソートしていたもうはるか思い出せないぐらい過去の時代のためのコードで、単にLess()
メソッドのtrue/falseを逆転させた別のインタフェースを作っているだけで、ここからソートしてあげないとソートはされないa: l = []string{"a", "b", "c"} sort.Sort(sort.Reverse(a)) log.Println(a) // "c", "b", "a" // 変わった!まあ、今時なら
sort.Slice()
使えばイチコロですね。添字だけみてソートすればOK。こちらの方が簡単ですね。sort.Slice(a, func(i, j int) bool { return i > j }) // "c", "b", "a" // 変わった!