画像、音楽、フォント、シナリオなど、プログラムの外部にあるファイルを読み込む機会は大変多いです。画像を読み込んで表示しようでは便利関数 ebitenutil.NewImageFromFile
を使って画像を読み込みましたが、いつもこの手の便利関数があるとは限りません。ebitenutil
を使わず自力でファイルを読み込む方法も抑えておいた方がよいでしょう。
ebitenutil を使わず画像を読み込む
bg.jpg を読み込んで bg *ebiten.Image
に代入するまでのプログラムを、ebitenutil
を使わない形に書き換えるとこのようになります。
- // 画像を読み込む
- // img, _, err := ebitenutil.NewImageFromFile("bg.jpg")
- // if err != nil {
- // return nil, err
- // }
+ f, err := os.Open("bg.jpg")
+ if err != nil {
+ return nil, err
+ }
+ defer f.Close()
+ i, _, err := image.Decode(f)
+ if err != nil {
+ return nil, err
+ }
+ img := ebiten.NewImageFromImage(i)
+ g.bg = img
実行したら以前と同じように背景が描画されているはずです。順を追って見ていきましょう。
ファイルを読み込むには、まず最初に os.Open
関数でファイルを「開く」必要があります。ファイルが存在しないなど、開けなかった場合はエラーを返します。
f, err := os.Open("bg.jpg")
if err != nil {
return nil, err
}
defer f.Close()
開いたファイルは使い終わったら必ず Close
メソッドで閉じましょう。ファイルは一度に開いておける数に上限があったり、開いているファイルは他のプログラムから開けなくなったりするので、後片付けは大事です。
defer
は関数の終わりに処理を実行するよう予約する機能です。つまり f.Close()
はここではなくnewGame関数の終了時に実行されるようになります。これで後片付けはOKです。
ファイルを開いたら、image
パッケージの Decode
関数を使い、圧縮されたファイルのデータを元の画像にデコード(復号)します。返り値にはデコードした結果の image.Image
と、"png" や "jpeg" などの画像形式を表す文字列(今回使わない)と、エラーが返ります。
i, _, err := image.Decode(f)
if err != nil {
return nil, err
}
image.Decode
関数は画像形式を自動判別してくれますが、そのためには事前に import _ "image/png"
のようにデコードに必要なパッケージをブランクインポートする必要があります。
最後に、ebiten.NewImageFromImage
関数を使って画像を GPU に転送します。これで画像を表示する準備が整いました。
img := ebiten.NewImageFromImage(i)
以前も述べた通り、ファイルを開き、画像をデコードし、GPUに転送する一連の処理はかなり重い処理なので、必ず一度読み込んだ *ebiten.Image
は使いまわしましょう。
以上が便利関数なしで画像を読み込む方法でした。
io.Reader
実は image.Decode
関数の引数は io.Reader
というインターフェース型です。
func Decode(r io.Reader) (Image, string, error)
具体的にはこんなインターフェースです。
type Reader interface {
Read(p []byte) (n int, err error)
}
Read
メソッドの引数と返り値の意味はかなり深いのでちゃんと説明するのは難しいのですが、とにかく、io.Reader
はファイルのように読み込めるもの全てに共通するインターフェースであることが重要で、実に様々なファイル的なものをこのインターフェースを通じて扱うことができます。実際にやってみて体感しましょう。
embed に対応する
ゲームを配布して遊んでもらおう / Goのビルドと配布で紹介した、embed
で実行ファイルに埋め込む方法を利用してみます。
//go:embed bg.jpg
var fsys embed.FS
- // f, err := os.Open("bg.jpg")
+ f, err := fsys.Open("bg.jpg")
if err != nil {
return nil, err
}
defer f.Close()
i, _, err := image.Decode(f)
if err != nil {
return nil, err
}
img := ebiten.NewImageFromImage(i)
g.bg = img
なんと、os.Open
の代わりに fsys.Open
を使うところしか変更がありません。これは os.Open
の返り値も fsys.Open
の返り値も io.Reader
インターフェースを満たしており、どちらも image.Decode
に渡せるからです。
インターネットからのダウンロードに対応する
この調子でさらにいろんな読み込み方法に対応してみましょう。Goならインターネット上から画像を読み込むのも簡単です。net/http
パッケージの http.Get
関数を使います。
+ resp, err := http.Get("https://eihigh.pages.dev/gopher.png")
if err != nil {
return nil, err
}
+ defer resp.Body.Close()
+ i, _, err := image.Decode(resp.Body)
if err != nil {
return nil, err
}
img := ebiten.NewImageFromImage(i)
g.bg = img
インターネット上からデータを読み込むには、応答/responseのうち Body
(本体)フィールドを使って読み込みますが、それ以外はやはり同じ流れですね。インターネットにアクセスするのが簡単すぎて夢が広がります。
外部とのデータの入出力をI/O(Input/Outputの略)と言いますが、GoはこのI/O関連がかなり気合を入れて整備されておりかなり便利です。深いところに興味のある方はGoならわかるシステムプログラミングなど読んでみると面白いでしょう。
まとめ
Goで頻繁に登場するファイルを読み込む処理について学びました。おさらいしましょう。
-
os.Open
関数でファイルを開く。 - ファイルは使い終わったら
f.Close()
で必ず閉じる。defer
で関数の最後に実行するように予約する。 -
io.Reader
インターフェースを経由して実に様々なファイル的なものを同じように扱える。
これも何度となく出てくる系のプログラムなので、書いているうちに馴染んでくると思います。
詳しい解説
for文中のdeferにご用心
defer
は「関数の最後に」処理を実行するよう予約する機能なので、for文とうっかり組み合わせると好ましくない状況になります。
for _, name := range tooManyNames {
f, err := os.Open(name)
if err != nil {
return nil, err
}
defer f.Close()
// ...
}
このプログラムでは、ファイル f
はループ内でしか使わないにも関わらず、tooManyNames
の数分のファイルが開かれたままになり、関数が終わる時にまとめて f.Close()
されてしまいます。この数が少ないうちは特に問題にならないかもしれませんが、大量になった時初めて問題が起きうるというのもまた厄介です。
この問題を回避するには、defer
を使わず、必要なタイミングで普通に f.Close()
するよう書き換えるか、あるいは ebitenutil.NewImageFromFile
のようにファイル一個を使う処理を関数に切り出して、その中で defer
を使って書くか、になります。どっちを選ぶかは状況にも好みにもよるのでなんとも言えませんが、とにかく for
の中での defer
は避けましょう。