20190915のGoに関する記事は7件です。

【Go】文字列をハッシュ化

関連

サンプルコード

package main

import (
    "crypto/md5"
    "crypto/sha1"
    "crypto/sha256"
    "crypto/sha512"
    "encoding/hex"
    "fmt"

    "golang.org/x/crypto/ripemd160"
)

func main() {
    s := "password"
    b := []byte(s)

    md5 := md5.Sum(b)
    sha1 := sha1.Sum(b)
    sha256 := sha256.Sum256(b)
    sha384 := sha512.Sum384(b)
    sha512 := sha512.Sum512(b)
    ripemd160 := ripemd160.New().Sum(b)

    // 出力したいだけなら`fmt.Printf("%x\n", md5)`とすればよい
    // 文字列として取得したい場合は`hex.EncodeToString`を使用する
    fmt.Println("md5:       " + hex.EncodeToString(md5[:]))
    fmt.Println("sha1:      " + hex.EncodeToString(sha1[:]))
    fmt.Println("sha256:    " + hex.EncodeToString(sha256[:]))
    fmt.Println("sha384:    " + hex.EncodeToString(sha384[:]))
    fmt.Println("sha512:    " + hex.EncodeToString(sha512[:]))
    fmt.Println("ripemd160: " + hex.EncodeToString(ripemd160[:]))

    // => md5:       5f4dcc3b5aa765d61d8327deb882cf99
    //    sha1:      5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8
    //    sha256:    5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8
    //    sha384:    a8b64babd0aca91a59bdbb7761b421d4f2bb38280d3a75ba0f21f2bebc45583d446c598660c94ce680c47d19c30783a7
    //    sha512:    b109f3bbbc244eb82441917ed06d618b9008dd09b3befd1b5e07394c706a8bb980b1d7785e5976ec049b46df5f1326af5a2ea6d103fd07c95385ffab0cacbc86
    //    ripemd160: 70617373776f72649c1185a5c5e9fc54612808977ee8f548b2258d31
}

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Golang - ポリモーフィズム

概要

インターフェースを使ったポリモーフィズムについてまとめる

必要な前提知識は以下の二つ(もし分からなければこちらの記事の上の方を参照ください)
・インターフェースの定義の仕方
・インターフェースを実装する方法

メリットは
ロジック内にはインターフェースしか現れないこと

インターフェース とは

インターフェースは言葉通り、境界面のイメージ
つまり、外から見た時に表面に何があるかを表している

Golangではインターフェースはメソッドの一覧で定義されているので
外からはこのメソッド一覧が見えて、そのメソッド一覧を使う事ができる

ポリモーフィズム とは

ある一つのインターフェースを実装した複数のオブジェクトに対して、そのインターフェースで公開されたメソッドで共通の処理をできること
開発者はインターフェースに命令することを意識すれば良い
どの構造体がこのインターフェースを実装したかは考えなくて良い

下のコードでは、猫か犬のどちらが来たかを判断して名前を判定している

変数の型指定やメソッドを呼ぶ時には、インターフェースのIAnimalしか意識していない事が分かる
具体的なDogとCatを意識するのは、それらを生成するときだけである

package main

import (
    "fmt"
)

type IAnimal interface {
    GetName() string
}

type Dog struct {
    name string
}

func (d Dog) GetName() string {
    return d.name
}

type Cat struct {
    name string
}

func (c Cat) GetName() string {
    return c.name
}

func main() {
    var animals []IAnimal = []IAnimal{
        Dog{"ポチ"},
        Cat{"ミケ"},
        Cat{"タマ"},
    }

    for _, animal := range animals {
        fmt.Println(animal.GetName())
    }
}

まとめ

インターフェースを実装する事で、ロジック内にインターフェースしか現れない事が分かった

補足1
今回はインターフェースを実装した場合でポリモーフィズムを考えたが、継承に関してもポリモーフィズムは適用できる
仮に構造体PoodleをDogから継承させた場合もPoodle型はインターフェースであるIAnimalとして扱う事ができる

補足2
今回は作らなかったがICat、IDog、IPoodleなどのインターフェースを作る事で
ICatを実装もしくは継承したオブジェクトだけが使える機能をICatで公開することもできる
インターフェースが役割を決め、それを実装や継承したものはその役割を持つという事が考えられる

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Protocol Buffers / Go Generated Code(和訳)

このページは Protocol Buffers 公式リファレンス Go Generated Codeの和訳です。原文はCreative Commons Attribution 4.0 Licenseで公開されており、ソースコードはApache Licenseで公開されています。この訳文もそれにならいます。

Go Generated Code

このページでは、プロトコルバッファーコンパイラが、任意のプロトコル定義に対し。どのようなGoのコードを生成するかについて、正確に述べています。proto2とprot3の違いは明示されています(これらの違いはこのドキュメントに書かれているように生成されたコードにあり、ベースAPIにはありません。API上は二つのバージョンに違いはありません)。このドキュメントの前にproto2/proto3の言語仕様ガイドを読むことをお勧めします。

コンパイラ呼び出し

プロトコルバッファーコンパイラがGoのコードを生成するにはプラグインが必要です。 次のようにインストールすると、

$ go get github.com/golang/protobuf/protoc-gen-go

protocにコマンドラインフラグとして-go_outを渡して実行した際に使われるprotoc-gen-goのバイナリが入ります。--go_outフラグで、コンパイラにGoのソースファイルを書き込む場所を指定します。 コンパイラは各.protoファイルの入力に対して、単一のソースファイルを作成します。

出力されるファイル名は.protoファイルの名前と以下の二つの変更によって決定されます。

  • .proto拡張子は.pb.goに置き換えられます。例えば、player_record.protoというファイルはplayer_record.pb.goという名前になります。
  • --proto_path、もしくは-Iで指定された)protoパスは、(--go_outで指定された)ソース出力先に置き換えられます。

次のようにプロトコンパイラを実行すると

protoc --proto_path=src --go_out=build/gen src/foo.proto src/bar/baz.proto

コンパイラはsrc/foo.protosrc/bar/baz.protoから、build/gen/foo.pb.gobuild/gen/bar/baz.pb.goを生成します。

必要であれば、コンパイラはbuild/gen/barディレクトリを自動生成しますが、buildbuild/genは生成しません。これらは予め作成しておく必要があります。

パッケージ

.protoファイルがパッケージ宣言を含んでいる場合、生成されたコードはprotoにあるpackageをGoのパッケージ名として使います。その際まず._に変換します。例えば、protoのパッケージ名がexample.high_scoreであれば、Goのパッケージ名はexample_high_scoreになります。

.protoファイル内にgo_package optionを入れることで、特定の.protoに生成されるデフォルトのパッケージ名を上書きすることができます。例えば、.protoファイルに次のように書かれているとすると、

package example.high_score;
option go_package = "hs";

hsというGoのパッケージ名が生成されます。

他にありうるケースとして、.protoファイルにパッケージ宣言が含まれていない場合、生成されたコードはファイル名(から拡張子を除いたもの)をGoのパッケージ名にします。その際、まず._に変換されます。例えば、パッケージ宣言のないhigh.score.protoという名前のprotoパッケージは、high.score.pb.goというファイルのhigh_scoreというパッケージで出力されます。

メッセージ型

次のようなシンプルなメッセージ宣言があるとします。

message Foo {}

プロトコルバッファコンパイラはFooという構造体を生成します。*FooMessageインターフェースを実装します。詳細についてはインラインコメントで説明します。

type Foo struct {
}

// Reset はprotoの状態をデフォルト値にします。
func (m *Foo) Reset()         { *m = Foo{} }

// String は、protoを文字列表現にして返します。
func (m *Foo) String() string { return proto.CompactTextString(m) }

// ProtoMessage はprotoのMessageであることを明示的に示すために付加されており、
// 誰かが誤ってproto.Messageインターフェースを実装しないようにします
func (*Foo) ProtoMessage()    {}

これらのメンバメソッドは常に存在します。また、optimize_for オプションをつけても、Goでは生成コードは変わりません(訳注:Java/C++では最適化されます)。

ネストされた型

メッセージ型は他のメッセージ型の中に宣言することもできます。例を示します。

message Foo {
  message Bar {
  }
}

この場合、コンパイラは二つの構造体を生成します。FooFoo_Barです。

Well-known types

プロトコルバッファには、 well-known types (WKT)と呼ばれる、一連の定義済みメッセージが付属しています。これの型は、他のサービスとの相互運用性のためにも、あるいは単純によくある有用なパターンを簡潔に表現するためにも使えます。例えば、Structメッセージ型は任意のC言語風の構造体を表します。

WKT用に事前生成されたGoのコードが、Go Protobuf ライブラリ の一部として配布されており、WKTを使うと、あなたが定義したメッセージ型から生成されたコードからも参照されます。例えば、このようにメッセージ型を定義したとします。

import "google/protobuf/struct.proto"
import "google/protobuf/timestamp.proto"

message NamedStruct {
  string name = 1;
  google.protobuf.Struct definition = 2;
  google.protobuf.Timestamp last_modified = 3;
}

生成されるGoのコードは次のようになるでしょう。

import google_protobuf "github.com/golang/protobuf/ptypes/struct"
import google_protobuf1 "github.com/golang/protobuf/ptypes/timestamp"

...

type NamedStruct struct {
   Name         string
   Definition   *google_protobuf.Struct
   LastModified *google_protobuf1.Timestamp
}

大抵の場合、コード内でこれらの型を直接インポートする必要はないでしょう。ただ、もし直接これらの型を参照する必要があれば、単純にgithub.com/golang/protobuf/ptypes/[TYPE]パッケージをインポートして、普通に使うだけです。

フィールド

プロトコルバッファコンパイラはメッセージ型の中に定義されたそれぞれのフィールドに対応する構造体のフィールドを生成します。その性質が実際にどのようになるかは、フィールドの型や単数、配列、マップ、oneofのうちの何であるかによって異なります。

Goのフィールド名は必ずキャメルケースで生成されます。.protoファイル内でフィールド名がこのようにスネークケースでで記述されていても同じです。大文字と小文字の変換は次のような流れで機能します。

  1. 先頭文字はエクスポートするため大文字になります。先頭文字がアンダースコアの場合は削除され、代わりに大文字のXが前置されます。

  2. 先頭文字以外でアンダースコアが小文字に続く場合、アンダースコアは削除され、それに続く小文字は大文字に変換されます。

したがって、foo_bar_bazというprotoのフィールドはFooBarBazとなり、_my_field_name_2XMyFieldName_2となります。

単数スカラー値フィールド (proto2)

下のフィールド定義のどちらの場合にも

optional int32 foo = 1;
required int32 foo = 1;

コンパイラはFooという名前の*int32型のフィールドと、GetFoo()というメソッドを持った構造体を生成します。GetFoo()int32値か、フィールドに値が設定されていない場合はデフォルト値を返します。デフォルト値が明示的に設定されていない場合は、その型のゼロ値を返します(数値であれば0、文字列であれば空文字になります)。

(boolbytesstringといった)他のスカラー型のフィールドでは、上記の*int32の部分はスカラー値型テーブル上で対応するGoの型に置き換えられます。

単数形スカラー値フィールド (proto3)

このフィールド定義の場合

int32 foo = 1;

コンパイラはFooという名前の*int32型のフィールドと、GetFoo()というメソッドを持った構造体を生成します。GetFoo()int32値か、フィールドに値が設定されていない場合はデフォルト値を返します。デフォルト値が明示的に設定されていない場合は、その型のゼロ値を返します(数値であれば0、文字列であれば空文字になります)。

(boolbytesstringといった)他のスカラー型のフィールドでは、上記の*int32の部分はスカラー値型テーブル上で対応するGoの型に置き換えられます。proto内で未設定の値は、その型のゼロ値として表現されます(数値であれば0、文字列であれば空文字になります)。

単数形メッセージ型フィールド

あるメッセージ型が定義され、

message Bar {}

Barフィールドを持ったメッセージ型に対しては、

// proto2
message Baz {
  optional Bar foo = 1;
  // optionalでなくrequiredでも生成コードは同じ
}

// proto3
message Baz {
  Bar foo = 1;
}

コンパイラは次のようなGoの構造体を生成します。

type Baz struct {
        Foo *Bar
}

メッセージ型フィールドはnilの場合があります。この場合フィールドは設定されておらず、実際は空の値です。これは空のメッセージ型となる構造体の空のインスタンスを値に設定した場合とは異なります。

またコンパイラはヘルパー関数としてfunc (m *Baz) GetFoo() *Barを生成します。このおかげで、途中のnilチェックなしに参照呼び出しを連鎖させることができます。

repeatedフィールド

repeatedフィールドは、Goの構造体内の該当するフィールドの要素型がTであるとき、フィールドにTのスライスを生成します。

repeatedフィールドを持ったこのメッセージの場合、

message Baz {
  repeated Bar foo = 1;
}

コンパイラは以下のような構造体を生成します。

type Baz struct {
        Foo  []*Bar
}

同様にフィールド定義が、repeated bytes foo = 1;であった場合、コンパイラはFooという名前の[][]byteフィールドを持ったGoの構造体を生成します。repeated enumの場合、repeated MyEnum bar = 2;に対して、コンパイラはBarという名前の[]MyEnumフィールドを持った構造体を生成します。

mapフィールド

各mapフィールドにおいて、TKeyがフィールドのキーとなる型で、TValueがフィールドの値となる型である場合、コンパイラはGoの構造体に、map[TKey]TValue型のフィールドを生成します。

mapフィールドを持ったこのメッセージの場合、

message Bar {}

message Baz {
  map<string, Bar> foo = 1;
}

コンパイラは次のようなGo構造体を生成します。

type Baz struct {
        Foo map[string]*Bar
}

oneof フィールド

oneof フィールドの場合、プロトコルバッファコンパイラはisMessageName_MyFieldというインターフェースのフィールドをひとつ生成します。またoneofの中のそれぞれの単数フィールドに相当する構造体を生成します。これらは全てこのisMessageName_MyFieldインターフェースを実装します。

oneofフィールドを持ったこのメッセージの場合、

package account;
message Profile {
  oneof avatar {
    string image_url = 1;
    bytes image_data = 2;
  }
}

コンパイラはt次の構造体を生成します。

type Profile struct {
        // Avatarに代入できる型:
        //      *Profile_ImageUrl
        //      *Profile_ImageData
        Avatar isProfile_Avatar `protobuf_oneof:"avatar"`
}

type Profile_ImageUrl struct {
        ImageUrl string
}
type Profile_ImageData struct {
        ImageData []byte
}

*Profile_ImageUrl*Profile_ImageDataのどちらも、空のisProfile_Avatar()メソッドを提供することで、isProfile_Avatarを満たしています。

次の例でどのようにしてフィールドに値を入れるかを示します。

p1 := &account.Profile{
  Avatar: &account.Profile_ImageUrl{"http://example.com/image.png"},
}

// imageDataは[]byteです
imageData := getImageData()
p2 := &account.Profile{
  Avatar: &account.Profile_ImageData{imageData},
}

フィールドにアクセスするには、値の型によるswitch文を使うことで、メッセージ型ごとに処理することができます。

switch x := m.Avatar.(type) {
case *account.Profile_ImageUrl:
        // x.ImageUrlを使ってURLからくProfile画像を読み込む
case *account.Profile_ImageData:
        // x.ImageDataを使ってbyte配列からProfile画像を読み込む
case nil:
        // フィールドがない
default:
        return fmt.Errorf("Profile.Avatarに未知の型が割り当てられています %T", x)
}

またコンパイラはfunc (m *Profile) GetImageUrl() stringfunc (m *Profile) GetImageData() []byteというメソッドも生成します。それぞれのget関数はフィールドの値か、もし未設定であればゼロ値を返します。

列挙型

次のような列挙型が与えられた場合、

message SearchRequest {
  enum Corpus {
    UNIVERSAL = 0;
    WEB = 1;
    IMAGES = 2;
    LOCAL = 3;
    NEWS = 4;
    PRODUCTS = 5;
    VIDEO = 6;
  }
  Corpus corpus = 1;
  ...
}

プロトコルバッファコンパイラは型とその型の一連の定数を生成します。

(上記のような)メッセージ型の中に列挙型がある場合、型の名前はメッセージ型の名前から始まります。

type SearchRequest_Corpus int32

パッケージレベルの列挙型の場合、

enum Foo {
  DEFAULT_BAR = 0;
  BAR_BELLS = 1;
  BAR_B_CUE = 2;
}

Goの型の名前はprotoのenumの名前と同じです。

type Foo int32

この型にはその値のラベルを返すString()メソッドがあります。

Enum()メソッド1は、その値に割り当てられたメモリを初期化し、対応するポインタを返します。

func (Foo) Enum() *Foo

プロトコルバッファコンパイラはそれぞれの列挙型の値に対して定数を生成します。メッセージ内に列挙型がある場合、定数名の頭に親のメッセージ型の名が付きます。

const (
        SearchRequest_UNIVERSAL SearchRequest_Corpus = 0
        SearchRequest_WEB       SearchRequest_Corpus = 1
        SearchRequest_IMAGES    SearchRequest_Corpus = 2
        SearchRequest_LOCAL     SearchRequest_Corpus = 3
        SearchRequest_NEWS      SearchRequest_Corpus = 4
        SearchRequest_PRODUCTS  SearchRequest_Corpus = 5
        SearchRequest_VIDEO     SearchRequest_Corpus = 6
)

パッケージレベルの列挙型では、定数は代わりに列挙型の名前から始まります。

const (
        Foo_DEFAULT_BAR Foo = 0
        Foo_BAR_BELLS   Foo = 1
        Foo_BAR_B_CUE   Foo = 2
)

プロトコルバッファコンパイラは、整数値をキーにラベルの文字列の名前を値にしたmapと、ラベルの文字列をキーに整数値を値にしたmapを生成します。

var Foo_name = map[int32]string{
        0: "DEFAULT_BAR",
        1: "BAR_BELLS",
        2: "BAR_B_CUE",
}
var Foo_value = map[string]int32{
        "DEFAULT_BAR": 0,
        "BAR_BELLS":   1,
        "BAR_B_CUE":   2,
}

.protoの記法では、複数の列挙型のラベルが同じ数の値を持つことができます。同じ値を持つラベルは同義語です。Goではこれらはきっかり同じように表現されます。すなわち同じ数値に複数の名前が対応するようになります。数値からラベルを逆引きするmapでは.protoで最初に現れるラベルだけが使われます。

拡張機能 (proto2)

拡張機能はproto2にのみ存在します。 proto2拡張のGo生成コードAPIのドキュメントについては、protoパッケージのドキュメントを参照してください。

Service

GoのコードジェネレータはデフォルトではServiceを生成しません。 gRPCプラグインを有効にすると(gRPC Goクイックスタートガイドを参照)、gRPCをサポートするコードが生成されます。


  1. v1.3.2現在、列挙型で生成されるコードにEnum()メソッドが存在しない 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GolangのWebサーバで階層型アーキテクチャで行く場合、panicを使うと良いと思う

Don't panic!!

この言葉があることは十分に承知しています。

しかし、Golang標準ライブラリでも使っている箇所があります。

しかも、1ヶ所、2ヶ所では無いです。

(一番下にリンクした記事では、ざっくりgrepで、4000ヶ所らしいです。テストが含まれている気がして、正確性には疑問がありますが。。)

おそらく使うべき状況というものが存在するのだと思います。

(そもそも、絶対ダメなら言語仕様に無いはずです。)

なので、十分に見切って使う分には、使ったほうが良いと思うのです。

余談ですが、僕のことを好意的に思っているエンジニアでも、この話をすると引かれることが多いです。。。

使っちゃ駄目なパターン


Golang の defer 文と panic/recover 機構について

上記記事の引用:

  • 外部に公開する API はエラーを伝える手段として panic() を使ってはいけない
    • 多値の返り値 w/ error インターフェースを使うこと
    • panic() はパッケージをまたいで伝搬させることがないようにする
  • panic/recover 機構はスタックが深くなるような呼び出しをする際に有用
    • 使用することで可読性を向上させることができる

つまり、外部にエラーを伝える手段としては error インターフェースを使うのが正式なやり方で、panic/recover はあくまで内部的に用いるべきもの、ということらしい。

Golangは全体的にエラーを返すやり方で記述されています。

なので、とあるライブラリだけ、panicを使って外部にエラーを返すのは、迷惑すぎるということだと思います。

逆に言えば、内部で使用する分には問題ないということです。

タイトルのWebサーバで階層型アーキテクチャの場合、階層分の if error が無くなって良いと思うのです。

メリットについて


色々とメリットが多いです。

  • エラー時にstacktraceの詳細なものが、必ず取れる
    • Golang書いてびっくりしたのが、stacktrace頑張らないと出ないんですよね。。
  • if err != null の記述が大幅に無くなるので、見通しが良くなる
  • メソッドチェインが出来るようになる
  • エラーのreturn忘れが無くなる
  • 1値返すメソッドか?、2値返すメソッドか?、あまり考えなくて良くなる
  • 考えることが減るのと、コーディング量が減るので開発が速くなる
  • どんなエラーが発生しても、必ず捉えられ後処理を実行できる
    • データベースのTransaction、Rollbackとか

エラーのreturn忘れですが、知り合いのエンジニアからLinter使うでしょ?的な話がありました。

staticcheck などのLinterを使えば指摘してくれます)

ですが、Linter無いと危なくて書けないとか、言語として間違っていると思います。

Golangはおそらく、Webサーバ用に作ってないのでしょうが、panic使えば回避出来るとか、例外的状況にも対応出来る言語だなぁと思います。

システム開発時に重要だと思うこと


僕はシステムは、結果を出してなんぼだと思っています。

大きな会社に居た時に、すごいメンバーがサービスを作って、売上出ないからクローズというのを、よく見かけました。

さらに最近の世の中は、変化が速く、変化よりも速い速度で製品を出せないと、出す頃には古くなっているという自体が起きます。

なので、システム開発に重要なのは、下記の2点になると思います。

* 正しく動作すること
* 速く開発が出来ること

これを考えると、

  • Golang
  • Webサーバ
  • 階層型アーキテクチャ
  • panic解禁

というのは、かなり良い構成じゃないかなぁと。

人手不足で納期も短い世の中とあっては、プラクティスは気にしつつも、

結果が出るほうを優先するのが良いと思いますが、どうですかね?

開発に銀の弾丸は無い


よく銀の弾丸は無いと言われます。

そうであれば、

ベストプラクティスも存在しないのでは?

と思います。

現時点で書いたコードは、一年後にエンジニアとしてレベルが上がっているのであれば、レガシーコードに映るはずです。

どうせレガシーになるなら、現時点でチームが納得のいく、ベストを尽くすまでなのかなぁと思います。

(駄目だった場合の責任取るのもチームですし。)

完全に余談ですが、コードレビューの時に自分のコードが指摘されると、腹が立つという人が居るみたいです。

チームのレギュレーションに沿って、機械的にレビューするなら、そんなことにはならないと思うのですが、どうなんですかね?

Javaとかのtry-catch機構に近い


悪手も状況が違えば、善手になりうると思います。

Javaのtry catchは、強制されるのがイケてないだけで、

コードがシンプルになるという意味では、エラー機構として優れていると思います。

そして、それはGolangでも出来ます。

Webサーバのような、DBなどの外部APIの待ち時間が支配的な状況下においては、実行速度の低下も気にもなりませんし。

まとめ


韓国のエンジニアに見せてもらったGolangの本では、堂々と使おうと書いてあるみたいです。

日本の記事を見てみると書いている人がいないので、書いてみました。

(もしかしたら、日本の場合、強烈なマサカリが飛んでくるので怖くて書けないだけかもしれませんが。。)

システム開発は、トレードオフの連続で、記述的に正しいよりは、結果が出るのかどうか?を軸に考えたほうが良いと思います。

2人しかいないチームで実際にpanic使って書いていますが、かなり良くなったと思っています。

もしかしたら後で後悔することになるかもしれないですが、今の所、その兆候は全く無いですし、そんときはそんときで書き直せば良いのだと思います。

ちゃんとテスト書いているから、なんとかなります。

最後に、英語の記事ですが、下記はサンプルコードもありますので、時間があるときにでも見てみてください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ebitenの個人的まとめ

Ebiten

EbitenとはGo言語のシンプルな2Dゲームライブラリです。
自動テクスチャアトラス化、自動バッチ処理などで大量のスプライトを高速に描画できます。
WindowsではCgoが不要。

テスト環境

Go 1.12.5
Ebiten 1.9.3
Windows10

ウィンドウ

ウィンドウを表示させる

main.go
package main

import (
    "log"

    "github.com/hajimehoshi/ebiten"
    "github.com/hajimehoshi/ebiten/ebitenutil"
)

func update(screen *ebiten.Image) error {
    if ebiten.IsDrawingSkipped() {
        return nil
    }
    ebitenutil.DebugPrint(screen, "Hello, World!")
    return nil
}

func main() {
    if err := ebiten.Run(update, 320, 240, 2, "Hello, World!"); err != nil {
        log.Fatal(err)
    }
}

func ebiten.Run

コールバック関数とウィンドウの幅、高さ、スケーリング、タイトルを設定している。
1秒間にあらかじめ設定されている回数update関数が呼び出されることになる。

ebiten.Run(update, 320, 240, 2, "Hello, World!")

第1引数のupdate関数はレシーバでもいい。

type Game struct {
    hoge Hoge
}

func (g *Game) update(screen *ebiten.Image) error {
    if ebiten.IsDrawingSkipped() {
        return nil
    }
    return nil
}

var game *Game

func main() {
    if err := ebiten.Run(game.update, 320, 240, 2, "Game"); err != nil {
        log.Fatal(err)
    }
}

func ebiten.IsDrawingSkipped

この関数より上に更新の度にしたい計算等の処理を書き。この関数より下に描画関係の処理を書きます。

    if ebiten.IsDrawingSkipped() {
        return nil
    }

更新処理が間に合わなくなると、描画をスキップして次回の更新に間に合わせようとする為にあるようです。

func ebitenutil.DebugPrint

デバッグ用テキスト表示。途中で改行もできる。日本語不可。

TPSとFPS

現在のTPSとFPSを表示させる。

func update(screen *ebiten.Image) error {
    if ebiten.IsDrawingSkipped() {
        return nil
    }

    msg := fmt.Sprintf("TPS = %0.2f\nFPS = %0.2f", ebiten.CurrentTPS(), ebiten.CurrentFPS())
    ebitenutil.DebugPrint(screen, msg)

    return nil
}

func ebiten.CurrentTPS

現在1秒間にupdate関数が呼ばれる回数を取得する。
TPS (ticks per second)

func ebiten.CurrentFPS

現在1秒間に描画されている回数を取得する。
FPS (frames per second)

例えば小さい画像を数万個描画すると、TPS=60, FPS=30という表示になり、計算2回に描画1回という感じで描画回数が少なくなります。

この描画スキップにより安定してupdate関数が呼び出されることで、固定フレームレートを前提とした計算をしても、ほぼほぼ問題は起こらないように思います。

ウィンドウアイコン

ウィンドウのアイコンを変更する。ウィンドウのアイコンはWindowsにしかない。Alt+Tabのウィンドウ切り替え時の表示や、タスクバーのアイコンも一緒に変更される。

main.go
package main

import (
    "image"
    _ "image/png"
    "log"
    "os"

    "github.com/hajimehoshi/ebiten"
)

var icon image.Image

func init() {
    fp, err := os.Open("ebiten.png")
    if err != nil {
        log.Fatal(err)
    }
    defer fp.Close()

    icon, _, err = image.Decode(fp)
    if err != nil {
        log.Fatal(err)
    }
}

func update(screen *ebiten.Image) error {
    if ebiten.IsDrawingSkipped() {
        return nil
    }
    return nil
}

func main() {
    ebiten.SetWindowIcon([]image.Image{icon})

    if err := ebiten.Run(update, 320, 240, 2, "Window Icon"); err != nil {
        log.Fatal(err)
    }
}

画像の読み込み

pngファイルを読み込むので、インポートに_ "image/png"を記述する。

import (
    "image"
    _ "image/png"
    "log"
    "os"

    "github.com/hajimehoshi/ebiten"
)

os.Open()からのimage.Decode()image.Image構造体を取得。
ここまではEbitenに限らずgoで画像ファイルを扱う時と同じ。

func ebiten.SetWindowIcon

引数にはimage.Image構造体のスライスを渡す。ebiten.Imageではないことに気を付ける。
update内でも使用できるが1度実行すればいいようなのでmain関数内で実行している。
画像のサイズは16x16, 32x32, 64x64であることが望ましい。

ウィンドウ非アクティブ時の動作

デフォルトではウィンドウが非アクティブになるとプログラムが一時停止する。

動作を停止させない
ebiten.SetRunnableInBackground(true)

引数にtrueを指定すると非アクティブ状態でも動作し続ける。デフォルトはfalse
1度実行するだけでいい。
ブラウザでは意味ない。

マウスカーソルを非表示に

1度実行するだけでいい。

カーソル非表示
ebiten.SetCursorVisible(false)

グラフィック

座標の原点は左上で、右下方向に数字が大きくなる。

画像の描画

main.go
package main

import (
    "image"
    "image/color"
    _ "image/png"
    "log"
    "os"

    "github.com/hajimehoshi/ebiten"
)

var (
    uma *ebiten.Image
)

func init() {
    fp, err := os.Open("uma.png")
    if err != nil {
        log.Fatal(err)
    }

    img, _, err := image.Decode(fp)
    if err != nil {
        log.Fatal(err)
    }

    uma, err = ebiten.NewImageFromImage(img, ebiten.FilterDefault)
    if err != nil {
        log.Fatal(err)
    }
}

func update(screen *ebiten.Image) error {
    if ebiten.IsDrawingSkipped() {
        return nil
    }

    screen.Fill(color.RGBA{0x37, 0x94, 0x6E, 0xFF})

    op := &ebiten.DrawImageOptions{}
    op.GeoM.Translate(64, 64)
    screen.DrawImage(uma, op)

    return nil
}

func main() {
    if err := ebiten.Run(update, 320, 240, 2, "Image"); err != nil {
        log.Fatal(err)
    }
}

func ebiten.NewImageFromImage

画像から*ebiten.Imageを作成する。
引数にimage.Image構造体とフィルタータイプを指定する。

func Fill

画像を指定した色で塗りつぶす関数。今回は画面全体を塗りつぶしている。
image/colorを使う。

単色のテクスチャを用意したいときにも使える。

img, _ := ebiten.NewImage(16, 16, ebiten.FilterDefault)
img.Fill(color.White)

フェードインアウト表現にも使える。

type ebiten.DrawImageOptions

描画の際の座標変換や色変換、描画モード等を設定するもの。

    op := &ebiten.DrawImageOptions{}
    op.GeoM.Translate(64, 64)
    screen.DrawImage(uma, op)

op.GeoM.Translate(64, 64)とはxとyを64移動させているということ。
ほかにScaleRotateもある。詳しくはGoDocを見る。

func DrawImage

画像に対してほかの画像を描画する関数。
今回はスクリーン画像に対してumaを描画している。

ポリゴン

3角形を描画する。自由な変形が可能。

main.go
package main

import (
    "image"
    "image/color"
    _ "image/png"
    "log"
    "os"

    "github.com/hajimehoshi/ebiten"
)

var (
    uma       *ebiten.Image
    vertecies []ebiten.Vertex
    indices   []uint16
)

func init() {
    fp, err := os.Open("uma.png")
    if err != nil {
        log.Fatal(err)
    }

    img, _, err := image.Decode(fp)
    if err != nil {
        log.Fatal(err)
    }

    uma, err = ebiten.NewImageFromImage(img, ebiten.FilterDefault)
    if err != nil {
        log.Fatal(err)
    }
}

func init() {
    const (
        x = 160
        y = 64
        w = 32
        h = 32
    )
    vertecies = []ebiten.Vertex{
        {
            DstX:   x - 16,
            DstY:   y,
            SrcX:   0,
            SrcY:   0,
            ColorR: 1,
            ColorG: 1,
            ColorB: 1,
            ColorA: 1,
        },
        {
            DstX:   x + w - 16,
            DstY:   y,
            SrcX:   0 + w,
            SrcY:   0,
            ColorR: 1,
            ColorG: 1,
            ColorB: 1,
            ColorA: 1,
        },
        {
            DstX:   x + w + 16,
            DstY:   y + h,
            SrcX:   0 + w,
            SrcY:   0 + h,
            ColorR: 1,
            ColorG: 1,
            ColorB: 1,
            ColorA: 1,
        },
        {
            DstX:   x + 16,
            DstY:   y + h,
            SrcX:   0,
            SrcY:   0 + h,
            ColorR: 1,
            ColorG: 1,
            ColorB: 1,
            ColorA: 1,
        },
    }
    indices = []uint16{0, 1, 2, 0, 2, 3}
}

func update(screen *ebiten.Image) error {
    if ebiten.IsDrawingSkipped() {
        return nil
    }

    screen.Fill(color.RGBA{0x37, 0x94, 0x6E, 0xFF})

    op := &ebiten.DrawImageOptions{}
    op.GeoM.Translate(64, 64)
    screen.DrawImage(uma, op)

    screen.DrawTriangles(vertecies, indices, uma, nil)

    return nil
}

func main() {
    if err := ebiten.Run(update, 320, 240, 2, "Polygon"); err != nil {
        log.Fatal(err)
    }
}

右側がポリゴンで変形させて描画したもの。

type ebiten.Vertex

頂点座標、テクスチャ座標(画像の座標であってuvではない)、頂点カラー(maxが1)を設定する構造体。
これのスライスを使用する。

func DrawTriangles

画像に対してポリゴンを描画する。
引数に、頂点スライス、インデックススライス、テクスチャ、最後にtype DrawTrianglesOptionsなんだが、今回は不要なのでnilを渡している。

公式サンプルexamples/shapesでは、DrawTrianglesを使用してラインや矩形を描画している。ラインの場合は線の太さを求める処理が必要なようだ。

アニメーション

キャラクターアニメーション。

main.go
package main

import (
    "fmt"
    "image"
    "image/color"
    _ "image/png"
    "log"
    "os"

    "github.com/hajimehoshi/ebiten"
    "github.com/hajimehoshi/ebiten/ebitenutil"
)

var (
    umas  *ebiten.Image
    count int
    frame int
)

const duration = 10

func init() {
    fp, err := os.Open("uma_sheet.png")
    if err != nil {
        log.Fatal(err)
    }

    img, _, err := image.Decode(fp)
    if err != nil {
        log.Fatal(err)
    }

    umas, err = ebiten.NewImageFromImage(img, ebiten.FilterDefault)
    if err != nil {
        log.Fatal(err)
    }
}

func update(screen *ebiten.Image) error {
    count++
    if count > duration {
        count = 0
        frame++
        if frame >= 5 {
            frame = 0
        }
    }

    if ebiten.IsDrawingSkipped() {
        return nil
    }

    screen.Fill(color.RGBA{0x37, 0x94, 0x6E, 0xFF})

    x := frame * 32
    uma := umas.SubImage(image.Rect(x, 0, x+32, 32)).(*ebiten.Image)
    op := &ebiten.DrawImageOptions{}
    op.GeoM.Translate(64, 64)
    screen.DrawImage(uma, op)

    msg := fmt.Sprintf("frame = %d", frame)
    ebitenutil.DebugPrint(screen, msg)

    return nil
}

func main() {
    if err := ebiten.Run(update, 320, 240, 2, "Animation"); err != nil {
        log.Fatal(err)
    }
}

カウンタ計算はebiten.IsDrawingSkipped()より上に書く。

func SubImage

画像からimage.Rectangle構造体の範囲の画像を取得する。
ピクセルをコピーではなく共有している。

uma := umas.SubImage(image.Rect(x, 0, x+32, 32)).(*ebiten.Image)

上記はSubImageの戻り値を*ebiten.Imageに変換している。
型アサーション。

タイリング

SubImageを使ったタイルマップ表現。

main.go
package main

import (
    "image"
    _ "image/png"
    "log"
    "os"

    "github.com/hajimehoshi/ebiten"
)

var (
    tiles *ebiten.Image
)

var tilemap = [][]int{
    {0, 0, 0, 0, 3, 0, 0, 2, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0},
    {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 1, 0},
    {0, 0, 2, 0, 0, 1, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0},
    {0, 0, 0, 1, 0, 0, 4, 0, 0, 0, 0, 4, 0, 5, 0, 0, 1, 0, 0, 0},
    {0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 2, 3, 0, 0, 2},
    {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
    {0, 1, 0, 0, 2, 0, 0, 0, 0, 0, 0, 2, 0, 1, 0, 0, 0, 1, 2, 0},
    {0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0},
    {0, 0, 0, 0, 0, 0, 0, 2, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 5, 0},
    {0, 4, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
    {2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0},
    {0, 0, 0, 0, 1, 0, 4, 0, 3, 0, 0, 0, 2, 0, 0, 0, 0, 4, 2, 0},
    {0, 0, 0, 0, 0, 0, 2, 0, 1, 0, 0, 0, 0, 0, 5, 0, 1, 0, 0, 0},
    {1, 0, 0, 2, 0, 0, 0, 0, 0, 0, 2, 0, 0, 1, 0, 0, 0, 0, 0, 0},
    {0, 2, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0},
}

func init() {
    fp, err := os.Open("grass_tiles.png")
    if err != nil {
        log.Fatal(err)
    }

    img, _, err := image.Decode(fp)
    if err != nil {
        log.Fatal(err)
    }

    tiles, err = ebiten.NewImageFromImage(img, ebiten.FilterDefault)
    if err != nil {
        log.Fatal(err)
    }
}

func update(screen *ebiten.Image) error {
    if ebiten.IsDrawingSkipped() {
        return nil
    }

    for col := 0; col < 15; col++ {
        for row := 0; row < 20; row++ {
            n := tilemap[col][row]
            x := (n % 4) * 16
            y := n / 4 * 16
            tile := tiles.SubImage(image.Rect(x, y, x+16, y+16)).(*ebiten.Image)
            op := &ebiten.DrawImageOptions{}
            op.GeoM.Translate(float64(row*16), float64(col*16))
            screen.DrawImage(tile, op)
        }
    }

    return nil
}

func main() {
    if err := ebiten.Run(update, 320, 240, 2, "Tilemap"); err != nil {
        log.Fatal(err)
    }
}

Geomety Matrix

行列を使った図形の変換。

移動してから回転することと、回転してから移動する事では結果が違うことに気を付ける。
順番が大事。
主に使うのは以下の3つだと思う。他はGoDoc見る。

func (g *GeoM) Rotate(theta float64)
func (g *GeoM) Scale(x, y float64)
func (g *GeoM) Translate(tx, ty float64)

Rotate

引数にはRadianを指定する。

ラジアンの求め方
r := float64(角度) * math.Pi / 180
op.GeoM.Rotate(r)

Scale

xとy軸で拡大縮小。

画像を左右反転させる場合にもこれを使う。

左右反転
op.GeoM.Scale(-1, 1)

反転時の中心は画像の原点なので、あらかじめTranslateで移動させるか、反転後に移動させるかすること。

Color Matrix

行列を使った色の変換。
公式サンプルexamples/flood, examples/hsv, examples/hueあたりを見る。

描画する画像を塗りつぶして点滅させる。

main.go
package main

import (
    "image"
    "image/color"
    _ "image/png"
    "log"
    "os"

    "github.com/hajimehoshi/ebiten"
)

var (
    uma   *ebiten.Image
    count int
    flush bool
)

func init() {
    fp, err := os.Open("uma.png")
    if err != nil {
        log.Fatal(err)
    }

    img, _, err := image.Decode(fp)
    if err != nil {
        log.Fatal(err)
    }

    uma, err = ebiten.NewImageFromImage(img, ebiten.FilterDefault)
    if err != nil {
        log.Fatal(err)
    }
}

func update(screen *ebiten.Image) error {
    count++
    if count > 10 {
        count = 0
        flush = !flush
    }
    if ebiten.IsDrawingSkipped() {
        return nil
    }

    screen.Fill(color.RGBA{0x37, 0x94, 0x6E, 0xFF})

    op := &ebiten.DrawImageOptions{}
    op.GeoM.Translate(64, 64)
    if flush {
        op.ColorM.Scale(0, 0, 0, 1)
        op.ColorM.Translate(1, 1, 0.8, 0)
    }
    screen.DrawImage(uma, op)

    return nil
}

func main() {
    if err := ebiten.Run(update, 320, 240, 2, "Flush"); err != nil {
        log.Fatal(err)
    }
}

透明度以外を操作

op.ColorM.Scale(0, 0, 0, 1)
op.ColorM.Translate(1, 1, 0.8, 0)

Scaleでアルファ以外の色要素を0にしてから、Translateで塗りつぶしている。
Translateで指定する値は0~1。

半透明に描画したいとき

透明度を半分に
op.ColorM.Scale(1, 1, 1, 0.5)

描画モード

CompositeMode描画方法を設定する。デフォルトはアルファブレンディング。
使い方はexample/additive, example/maskingを見る。

加算モード
op = &ebiten.DrawImageOptions{}
op.CompositeMode = ebiten.CompositeModeLighter
screen.DrawImage(img, op)

キーボード入力

キーが押されているか、押した瞬間、離した瞬間、押している長さを調べることができる。

サンプルではスペースキーの状態を調べている。

main.go
package main

import (
    "fmt"
    "log"

    "github.com/hajimehoshi/ebiten"
    "github.com/hajimehoshi/ebiten/ebitenutil"
    "github.com/hajimehoshi/ebiten/inpututil"
)

func update(screen *ebiten.Image) error {
    pressed := ebiten.IsKeyPressed(ebiten.KeySpace)
    justpressed := inpututil.IsKeyJustPressed(ebiten.KeySpace)
    justreleased := inpututil.IsKeyJustReleased(ebiten.KeySpace)
    duration := inpututil.KeyPressDuration(ebiten.KeySpace)

    if ebiten.IsDrawingSkipped() {
        return nil
    }

    if pressed {
        ebitenutil.DebugPrintAt(screen, "Space key pressed.", 0, 0)
    }
    if justpressed {
        ebitenutil.DebugPrintAt(screen, "Space key just pressed.", 0, 16)
    }
    if justreleased {
        ebitenutil.DebugPrintAt(screen, "Space key just pressed.", 0, 32)
    }
    msg := fmt.Sprintf("Space key duration = %d", duration)
    ebitenutil.DebugPrintAt(screen, msg, 0, 48)

    return nil
}

func main() {
    if err := ebiten.Run(update, 320, 240, 2, "Keyboard"); err != nil {
        log.Fatal(err)
    }
}

キーの状態判定はebiten.IsDrawingSkipped()より上に書くこと。

func ebiten.IsKeyPressed

引数に指定したキーが押されている状態かを返す。
引数にebiten.Key型の定数を指定する。戻り値はbool
定義されているキーはGoDocを見る。

ebiten.Key型は元はintなので型キャストできる。

k := ebiten.Key(0)

func inpututil.IsKeyJustPressed

キーが押された瞬間かどうかを返す。

func inpututil.IsKeyJustReleased

キーが離された瞬間かどうかを返す。

func inpututil.KeyPressDuration

キーの押されている時間を返す。戻り値はint

マウス入力

マウスカーソル座標、マウスホイールの移動量、各種ボタンの状態を調べることができる。

サンプルではカーソル座標、ホイール、マウス左ボタンの状態を見ている。

main.go
package main

import (
    "fmt"
    "log"

    "github.com/hajimehoshi/ebiten"
    "github.com/hajimehoshi/ebiten/ebitenutil"
)

var wheelx, wheely float64

func update(screen *ebiten.Image) error {
    x, y := ebiten.CursorPosition()
    xoff, yoff := ebiten.Wheel()
    wheelx += xoff
    wheely += yoff

    leftpressed := ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft)

    if ebiten.IsDrawingSkipped() {
        return nil
    }

    msg := fmt.Sprintf("Cursor x = %d, y = %d\nWheel x = %0.2f, y = %0.2f", x, y, wheelx, wheely)
    ebitenutil.DebugPrint(screen, msg)

    if leftpressed {
        ebitenutil.DebugPrintAt(screen, "Left mousebutton pressed.", 0, 32)
    }

    return nil
}

func main() {
    if err := ebiten.Run(update, 320, 240, 2, "Mouse"); err != nil {
        log.Fatal(err)
    }
}

func ebiten.CursorPosition

戻り値にマウスカーソル座標x,yが返る。
この座標にはウィンドウのスケールもかかる。

func ebiten.Wheel

戻り値にマウスホイールの移動した値が返る。

func ebiten.IsMouseButtonPressed

マウスのボタンが押されているか調べる。
引数には定数を指定する。MouseButtonLeft, MouseButtonRight, MouseButtonMiddleのいずれか。

func inpututil.IsMouseButtonJustPressed

押された瞬間かどうか。

func inpututil.IsMouseButtonJustReleased

離した瞬間かどうか。

func inpututil.MouseButtonPressDuration

押し続けている時間を取得。

ゲームパッド

テキスト

用意したフォントを使ってテキストを描画する。
日本語可。

main.go
package main

import (
    "image/color"
    "io/ioutil"
    "log"

    "github.com/golang/freetype/truetype"
    "golang.org/x/image/font"

    "github.com/hajimehoshi/ebiten"
    "github.com/hajimehoshi/ebiten/text"
)

var (
    mplusfont font.Face
    pixelfont font.Face
)

func init() {
    data, err := ioutil.ReadFile("mplus-1p-regular.ttf")
    if err != nil {
        log.Fatal(err)
    }

    ttf, err := truetype.Parse(data)
    if err != nil {
        log.Fatal(err)
    }

    op := truetype.Options{Size: 24, DPI: 72, Hinting: font.HintingFull}
    mplusfont = truetype.NewFace(ttf, &op)
}

func init() {
    data, err := ioutil.ReadFile("PixelMplus12-Regular.ttf")
    if err != nil {
        log.Fatal(err)
    }

    ttf, err := truetype.Parse(data)
    if err != nil {
        log.Fatal(err)
    }

    op := truetype.Options{Size: 12, DPI: 72, Hinting: font.HintingNone, SubPixelsX: 16}
    pixelfont = truetype.NewFace(ttf, &op)
}

func update(screen *ebiten.Image) error {
    if ebiten.IsDrawingSkipped() {
        return nil
    }
    screen.Fill(color.RGBA{0x37, 0x94, 0x6E, 0xff})

    text.Draw(screen, "はじめてのEbiten。", mplusfont, 32, 32, color.White)
    text.Draw(screen, "はじめてのEbiten。", pixelfont, 32, 128, color.White)

    return nil
}

func main() {
    if err := ebiten.Run(update, 320, 240, 2, "Text"); err != nil {
        log.Fatal(err)
    }
}

下は等倍表示。

こちらのフォントを使用させてもらいました。
M+FONTS
PixelMplus

フォント読み込み

インポートを追加
import (
    "github.com/golang/freetype/truetype"
    "golang.org/x/image/font"
)
ttfファイルからbyteデータを取得
    data, err := ioutil.ReadFile("mplus-1p-regular.ttf")
Fontに展開
    ttf, err := truetype.Parse(data)
オプションを設定してfont.Faceを作成
    op := truetype.Options{Size: 24, DPI: 72, Hinting: font.HintingFull}
    mplusfont = truetype.NewFace(ttf, &op)

truetype.Options構造体

Size: 0なら12が使われる
DPI: 0なら72が使われる
Hinting: 0なら無し
GlyphCacheEntries: 0なら512
SubPixelsX: 0なら4が使われる
SubPixelsY: 0なら1が使われる

詳細はfreetype/truetype/face.goを見る。

テキスト描画

指定する座標はテキストの左下。

func text.Draw

    text.Draw(screen, "はじめてのEbiten。", mplusfont, 32, 32, color.White)
    text.Draw(screen, "はじめてのEbiten。", pixelfont, 32, 128, color.White)

文字の縁取り

1ドットずつ上下左右にずらして、重ねて描画すると縁取りしたように見せられる。
同じ文字を複数回描画することは、コストの高い処理ではない。

下は等倍表示。

文字の幅を調べる

文章の折り返し位置を調べるために。

1文字ずつ文字の幅を調べていく。

advance, ok := mplusfont.GlyphAdvance(rune('あ'))
fmt.Println(advance.Floor())

戻り値が固定小数点なので、整数に変換する。

オーディオ

.wav, .ogg, .mp3形式の音声データを再生する。

基本的な形

main.go
package main

import (
    "log"
    "os"

    "github.com/hajimehoshi/ebiten"
    "github.com/hajimehoshi/ebiten/audio"
    "github.com/hajimehoshi/ebiten/audio/vorbis"
    "github.com/hajimehoshi/ebiten/ebitenutil"
)

const (
    sampleRate = 44100
)

var (
    audioContext *audio.Context
)

func init() {
    var err error
    audioContext, err = audio.NewContext(sampleRate)
    if err != nil {
        log.Fatal(err)
    }

    f, err := os.Open("audio_xxx.ogg")
    if err != nil {
        log.Fatal(err)
    }

    s, err := vorbis.Decode(audioContext, f)
    if err != nil {
        log.Fatal(err)
    }

    p, err := audio.NewPlayer(audioContext, s)
    if err != nil {
        log.Fatal(err)
    }
    p.Play()
}

func update(screen *ebiten.Image) error {
    if ebiten.IsDrawingSkipped() {
        return nil
    }
    if !audioContext.IsReady() {
        ebitenutil.DebugPrint(screen, "audio context not ready")
    }
    return nil
}

func main() {
    if err := ebiten.Run(update, 320, 240, 2, "Audio play"); err != nil {
        log.Fatal(err)
    }
}

func audio.NewContext

オーディオコンテキストを作成する。
指定するサンプルレートは44100または48000がよい。

Windows10ではプレイヤーを再生するまで、IsReady()falseを返す。

audioContext.IsReady()

func vorbis.Decode

oggから再生可能なストリームソースに変換する。
2ch16bit指定したサンプルレートに変換されるので、再生するファイルのサンプルレート等が違っても問題ない。

os.Close()でファイルを閉じてはいけない。GCに回収されるときに閉じられる。

func audio.NewPlayer

ストリームソースからプレイヤーを作成。
Play()で再生する。他にPause(), SetVolume()などがある。
1つのソースを複数のプレイヤーで共有できない。

ループ再生

通常との違う部分だけ抜粋。

    s, err := vorbis.Decode(audioContext, f)
    l := audio.NewInfiniteLoop(s, s.Length())
    p, err := audio.NewPlayer(audioContext, l)

func audio.NewInfiniteLoop

ソースから無限ループストリームを作成。
長さはバイト数を指定している。

もう一つイントロ部分を設定できるNewInfiniteLoopWithIntroというのもある。

同じソースを重複再生

短い効果音を重複して再生したいときは、NewPlayerFromBytesでプレイヤーを作成する。

スペースキーを連打すると、音が重なっているのがわかると思う。

main.go
package main

import (
    "io/ioutil"
    "log"
    "os"

    "github.com/hajimehoshi/ebiten"
    "github.com/hajimehoshi/ebiten/audio"
    "github.com/hajimehoshi/ebiten/audio/wav"
    "github.com/hajimehoshi/ebiten/ebitenutil"
    "github.com/hajimehoshi/ebiten/inpututil"
)

const (
    sampleRate = 44100
)

var (
    audioContext *audio.Context
    byteSource   []byte
)

func init() {
    var err error
    audioContext, err = audio.NewContext(sampleRate)
    if err != nil {
        log.Fatal(err)
    }

    f, err := os.Open("jab.wav")
    if err != nil {
        log.Fatal(err)
    }

    s, err := wav.Decode(audioContext, f)
    if err != nil {
        log.Fatal(err)
    }

    byteSource, err = ioutil.ReadAll(s)
    if err != nil {
        log.Fatal(err)
    }
}

func update(screen *ebiten.Image) error {
    if inpututil.IsKeyJustPressed(ebiten.KeySpace) {
        p, err := audio.NewPlayerFromBytes(audioContext, byteSource)
        if err != nil {
            return err
        }
        p.SetVolume(0.5)
        p.Play()
    }

    if ebiten.IsDrawingSkipped() {
        return nil
    }

    if !audioContext.IsReady() {
        ebitenutil.DebugPrint(screen, "audio context not ready")
    }

    return nil
}

func main() {
    if err := ebiten.Run(update, 320, 240, 2, "Audio byte source"); err != nil {
        log.Fatal(err)
    }
}

func audio.NewPlayerFromBytes

バイトソースからプレイヤーを作成する。
同じバイトソースを複数のプレイヤーで共有できる。

examples

audio, audioinfiniteloop, pcm, piano, sinewave, wavを見る。

リソースファイルの埋め込み

Ebitenのexamplesでは画像や音声ファイルを.goファイルに埋め込んでいるようです。

埋め込み方法

file2byteslice
こちらのプログラムを使います。
go getしてからgo installするとbinに配置してくれるので楽です。
使い方はexamples/resources/generate.goを見ると、go generateを使ってまとめて処理しているようです。

画像

埋め込んだbyteスライスからebiten.Imageを作成。

変更部分だけ抜粋
import "my/resources/images"

    img, _, err := image.Decode(bytes.NewReader(images.Hoge_png))
    hogeImage, _ = ebiten.NewImageFromImage(img, ebiten.FilterDefault)

変換した.goファイルのパッケージをインポートする。
Hoge_png(グローバル変数)というbyteスライスをリーダーに変換してからデコードする。

フォント

もともとbyteスライスをパースしているのであまり違いが無い。

変更部分だけ抜粋
import "my/resources/fonts"

    ttf, err := truetype.Parse(fonts.Hoge_ttf)

オーディオ

byteスライスをaudio.BytesReadSeekCloserに渡してからデコードする。

変更部分だけ抜粋
import raudio "my/resources/audio"

    s, err := wav.Decode(audioContext, audio.BytesReadSeekCloser(raudio.Hoge_wav))

ウェブブラウザー

GopherJS

WebAssembly

その他

スクリーンショット

撮影するシュートカットキーを設定する。
一時的に環境変数を設定する。
コマンドプロンプトだと1行で書けない。

エスケープキーで撮影
set EBITEN_SCREENSHOT_KEY=escape
go run main.go

メインのフォルダに保存される。windowスケール1倍で保存される。

os.Setenv関数で設定しても大丈夫だった。

一時的な環境変数
func init() {
    err := os.Setenv("EBITEN_SCREENSHOT_KEY", "escape")
    if err != nil {
        log.Fatal(err)
    }
}

内部イメージをダンプ

環境変数EBITEN_INTERNAL_IMAGES_KEYにキーを設定すると、内部イメージをすべてダンプすることができる。
これはビルドタグにebitendebugを設定している場合にのみ有効。

コマンドプロンプト
set EBITEN_INTERNAL_IMAGES_KEY=escape
go run -tags=ebitendebug main.go

コマンドプロンプトを非表示に

Windowsで起動時に一瞬だけ表示されるのを出てこないようにする。

go build -ldflags -H=windowsgui

Ebitenリンク

Ebiten
GitHub
Wiki
GoDoc

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Go】GitHub Markdown APIを叩いてみる

はじめに

GitHub Markdown APIの使い方に関しては、以下の記事を参照。

サンプルコード

package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
    "strings"
)

func main() {
    apiURL := "https://api.github.com/markdown/raw"
    markdown := "# Hello World"

    // リクエストを生成
    req, err := http.NewRequest("POST", apiURL, strings.NewReader(markdown))
    if err != nil {
        panic(err)
    }
    req.Header.Add("Content-Type", "text/plain")

    // POSTリクエストを送信
    client := &http.Client{}
    res, err := client.Do(req)
    if err != nil {
        panic(err)
    }
    defer res.Body.Close()

    // レスポンスを読み取り
    bytes, err := ioutil.ReadAll(res.Body)
    if err != nil {
        panic(err)
    }

    fmt.Println(string(bytes))
    // => <h1>
    //    <a id="user-content-hello-world" class="anchor" href="#hello-world" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Hello World</h1>
}

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

怖くないよね?マルチステージビルド

会社の本番環境をAWSのEKSに移行することになりその際にgolangのアプリを配布する際に使用したマルチステージビルドについて備忘録を交えながら記事を書いていきます。

先に言うんですけど

すみません僕Docker自体初めてです

今まで特に自分でDockerなどで環境構築をしたことがなかったのでこれが初めてのDocker作成です。
ほとんど分かっていないのでこいつダメやなって思いながら見てください

マルチステージビルドについて
https://github.com/moby/moby/pull/31257

簡単に言うと、fromでから作られた2つの複数のFROMイメージ間のファイルを COPY --from で直接を参照できるようになったのと AS で中間イメージに名前が付けられるようになったと言うものです。

golangってクロスコンパイルなどでシングルバイナリさえあればgolangの実行環境不要ですもんね
(golangにいては割愛します。)

早速やりましょうか

基本的なファイル構成はこちら

├build(ここにbuildの成果物が格納される)
│ └ appname(実行バイナリ)
├cmd(実行ディレクトリ)
│ └ main.go
├configs(設定ファイル関連)
├bitbucket-pipelines.yml
├.env(環境変数設定ファイル)
├docker-compose.yml
├Dockerfile
├go.mod
├go.sum
├Makefile(タスクランナー実行ファイル)

Makefile

基本的には今回は go build なども含めて全てMakefile側に任せています。

makefileに関しての設定はここで書きませんがちゃんとbuild配下にバイナリが入るように以下のように設定しました

build: 

@go build -ldflags="-w -s" -o build/appname ./cmd/main.go

これでmake buildのコマンドをターミナルで実行するとbuild配下にappnameと言うシングルバイナリが作成されます

Dockerfile

FROM golang:1.12.9 as builder

ADD . /appname/

WORKDIR /appname/

ENV GO111MODULE=on

COPY go.mod .
COPY go.sum .

RUN go mod download
COPY . .

RUN  CGO_ENABLED=0 GOOS=linux GOARCH=amd64 make build 

FROM alpine

RUN apk update \
  && apk add --no-cache

COPY --from=builder /appname/build /app/build

ENTRYPOINT [ "/app/build/appname" ]

まずポイントを解説すると

FROM golang:1.12.9 as builder

これでgolangのbaseimageを作成しますこの as builderと言うのがこの後にこのimageを渡すためのエイリアスになります

ENV GO111MODULE=on

COPY go.mod .
COPY go.sum .

RUN go mod download
COPY . .

go.modはgolangのモジュール関連の設定ファイルです(jsのpackage.jsonみたいなもの)
https://qiita.com/propella/items/e49bccc88f3cc2407745

ENV GO111MODULE=onはおまじないみたいなもので現行のgo1.13の環境では必要ないと思う(動かなかったらいれてください)

RUN  CGO_ENABLED=0 GOOS=linux GOARCH=amd64 make build 

ここでgoのbuildを行います make buildコマンドは先ほど作成した Makefileのトリガーとして buildを設定していたのでこのようなコマンドでタスクの実行が可能です

クロスコンパイルについてはこちらを参照してください
https://qiita.com/xshirade/items/abeb0d595be27cb6326e

FROM alpine

こちらはDockerの公式が提供している軽量なLinuxのイメージです。
https://alpinelinux.org/

COPY --from=builder /appname/build /app/build

ここで先ほどbuildした実行バイナリの格納ファイル等をこちらにCOPYしています
これでマルチステージビルドの完成です

補足

これでducker build などでもいいのですが開発環境等でmysqlに接続できる等などの確認が必要だと思うのでdokcer-compose.ymlで環境の構築を行います

docker-compose.yml
version: "3"

services:
  mysql:
    image: mysql:5.7.26
    container_name: db
    env_file:
      - .env
    ports:
      - "3306:3306"
    volumes:
      - db_data:/var/lib/mysql
      - ./mysql/scripts/init:/docker-entrypoint-initdb.d
      - ./mysql/config:/etc/mysql/conf.d
  api:
    build: .
    depends_on:
      - mysql
    env_file:
      - .env
    ports:
      - "8080:8080"

volumes:
  db_data:

これで

$ docker-composer build && docker-compose up -d

こちらで接続ができているかなどの確認(go側にmysqlに接続などのプログラムは書いてくださいね)

最後に

僕自身初めてのDockerだったので探り探りでの実装だったのですがもし他にこれの方がいいよってものがありましたら是非教えて頂けますと幸いです。
最後まで読んで頂いてありがとうございました。

次にbitbucket-pipelines.ymlを使用したECRへのPUSHをしたいと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む