パッケージをうまく駆使すると、大規模なプログラムを見通しよく整理できます。今回はランチャー(プログラムを起動するためのプログラム)を作りながら、パッケージを作る方法について学びます。
ファイル分割
パッケージを作る前に、ファイル分割について知っておきましょう。
Goはディレクトリの中でファイルを自由に分割できます。試しに main.go
の内容を以下だけ残して、
package main
import "github.com/eihigh/miniten"
func main() {
p.y = ground - 38 // 初期値=地上
miniten.Run(draw)
}
game.go
というファイルを新しく作り、package main
とインポート文を書いた後、残り全てを game.go
に移してしまいましょう。
package main
import "github.com/eihigh/miniten"
type player struct {
x float64
y float64
vy float64
inAir bool
}
var (
ground = 300.0
p player
)
func draw() {
// 地面描画
miniten.DrawRect(0, int(ground)-10, 640, 360)
// ...以下略...
package main
とインポート文だけはファイルを分割するごとにまた必要になりますが、残りのプログラムは main.go
にあったものをそのまま移してきただけです。この状態で go run .
して問題なく今まで通り動作するのを確かめてみましょう。
ファイル分割は本当に自由です。センスの見せ所ですね。ちなみに今やった分割はセンスが無いので参考にしないでください。
ランチャー
プログラムを起動するためのプログラムをランチャーと呼びます。今回は、今まで作ってきたあなたの力作を集めた至高のゲームランチャーを作ってみます。
まずはHello, World! のときに説明したように、新たなディレクトリ(名前は自由ですが launcher
がいいかな)を作り、モジュールを初期化(go mod init launcher
)してください。
launcher
ディレクトリにはいつも通り main.go
を作ります。今回はそれに加えて、flappy
ディレクトリを作り、その中にはねるgopherくんゲームの main.go
をコピーして持ってきます。rungame
ディレクトリも作って同じように走るgopherくんゲームのプログラムをコピーしてきます。全体はこのような構造になります。
launcher
|-- flappy
| `-- main.go
|-- rungame
| |-- game.go
| `-- main.go
|-- go.mod
`-- main.go
launcher直下の main.go
はこんな感じで、ターミナルに入力された数字を元に起動するゲームを変えるようにしましょう。
package main
import (
"fmt"
)
func main() {
fmt.Println("起動したいゲームの数字を入力してね")
fmt.Println("0) はねるgopherくんゲーム")
fmt.Println("1) 走るgopherくんゲーム")
input := 0
fmt.Scan(&input)
switch input {
case 0:
// はねるgopherくんゲームを起動
case 1:
// 走るgopherくんゲームを起動
}
}
さてどうやったら各ゲームを起動できるかがこのページの肝です。
mainパッケージとそれ以外のパッケージ
Goのパッケージはディレクトリごとに存在します。今回だとlauncher自体と、flappy
ディレクトリと、rungame
ディレクトリがあるので、計3つのパッケージが今あることになります。パッケージをインポートすればそのパッケージが利用できるようになるので、main.go
から flappy
と rungame
をインポートして呼び出したいところですが、それには今のままだとダメで、若干改修が必要です。
まず、main
パッケージはプログラムの起点となる特殊なパッケージなので、よそのパッケージから利用することはできません。コピーしてきたゲームのパッケージ宣言を main
から flappy
, rungame
に書き換えましょう。
-package main
+package flappy
import "github.com/eihigh/miniten"
var (
y = 0.0
vy = 0.0 // Velocity of y(速度のy成分)の略
// ...以下略...
-package main
+package rungame
import "github.com/eihigh/miniten"
func main() {
p.y = ground - 38 // 初期値=地上
miniten.Run(draw)
}
-package main
+package rungame
import "github.com/eihigh/miniten"
type player struct {
x float64
y float64
// ...以下略...
これでランチャーから flappy
, rungame
パッケージがインポートできるようになりました。ですがもう一手間必要です。
パッケージは大文字から始まる関数や変数、型だけを公開します。小文字のものは非公開であり、インポートしたとて呼び出せません。というわけで、flappy, rungame の main
関数を大文字から始まる Main
関数に書き換えます。これでインポートすれば呼び出せるようになります。
package flappy
import "github.com/eihigh/miniten"
var (
y = 0.0
vy = 0.0 // Velocity of y(速度のy成分)の略
frames = 0 // 経過フレーム数
wallXs = []int{} // 壁のX座標
wallYs = []int{} // 壁のY座標
)
-func main() {
+func Main() {
miniten.Run(draw)
}
package rungame
import "github.com/eihigh/miniten"
-func main() {
+func Main() {
p.y = ground - 38 // 初期値=地上
miniten.Run(draw)
}
そういえば、新たなモジュールを作ったので、go mod tidy
がまた必要になるかもしれません。適宜実行してください。
$ go mod tidy
そしたら、ランチャーから呼び出してみましょう!インポートパス=パッケージの場所は "モジュール名/flappy"
のようになることに注意してください。詳しくは次のページで解説します。
package main
import (
"fmt"
+ "launcher/flappy"
+ "launcher/rungame"
)
func main() {
fmt.Println("起動したいゲームの数字を入力してね")
fmt.Println("0) はねるgopherくんゲーム")
fmt.Println("1) 走るgopherくんゲーム")
input := 0
fmt.Scan(&input)
switch input {
case 0:
// はねるgopherくんゲーム
+ flappy.Main()
case 1:
// 走るgopherくんゲーム
+ rungame.Main()
}
}
$ go run .
起動したいゲームの数字を入力してね
0) はねるgopherくんゲーム
1) 走るgopherくんゲーム
0
(はねるgopherくんゲームが起動する...)
数字を入力して対応するゲームが起動すれば、成功です!🎉
パッケージの使い所
ランチャー以外にも、プログラムの一部分をパッケージとして分けて、必要なものだけ公開することで、大規模なプログラムの見通しをよくすることができます。
が、個人でゲームを作っている間はほぼパッケージを分ける機会は無いかもしれません。むしろ分けることで見るべきところが増えて面倒になるというデメリットも...。
次のページで解説するモジュールを学んで、インターネット上に自分のパッケージを公開して使ってもらう経験を踏めば、ある程度勘所がわかってくるかもしれません。気長にやりましょう!
まとめ
- 同じディレクトリの中でファイルは自由に分割できる。
- ディレクトリごとにパッケージが存在する。
-
main
パッケージはプログラムの起点となる特殊なパッケージで、よそのパッケージからインポートできない。 - パッケージをインポートすると、公開された関数・変数・型に限って使えるようになる。
- パッケージのインポートパス=パッケージの場所は
"モジュール名"
から始まる文字列になる。
そんなものがあるんだな、程度でサラッと把握したらOKです!
詳しい解説
mainパッケージ
Goで実行できるのはmainパッケージだけです。試しに flappy
や rungame
ディレクトリを開いて go run .
するとエラーになるのが確かめられるはずです。
$ go run .
package launcher/flappy is not a main package
mainパッケージの高度な使い方として、モジュールの根っこではなく子ディレクトリにmainパッケージを置いたり、一つのモジュールの中に多数のmainパッケージを作るなどがあります。Ebitengineのexamples配下にはたくさんのmainパッケージがあるのでわかりやすいかと思います。
go run の引数
launcher
を開いて、go run ./flappy
のように ./
から始まるパスを引数として渡すことで、子ディレクトリを実行できます。
$ go run ./flappy
package launcher/flappy is not a main package
./
を忘れると思ったような挙動にならないので注意してください(詳しく言うと、GOROOT
からパッケージを探そうとする)。
init関数
initという名前の関数は特殊で、プログラムの起動時、mainパッケージのmain関数より先に自動で実行されます。主にmain以外のパッケージが変数の初期化を行うときに使います。
package rungame
import "github.com/eihigh/miniten"
+func init() {
+ p.y = ground - 38
+}
func Main() {
- // p.y = ground - 38 // 初期値=地上
miniten.Run(draw)
}
うまく使えば便利なのですが、濫用すると「呼んでないのに実行された(init関数は起動時に自動で実行されるので)」処理が増えて、プログラムの流れを追いづらくなってしまいます。用法用量を守ってお使いください。
循環参照の禁止
パッケージを使い込もうとした人がよくぶつかるのがこの循環参照の禁止ルールです。循環参照とはパッケージAからBをインポートし、BからもAをインポートすることです。AとBの間に別のパッケージがあって間接的に循環していても同じく禁止です。これはむしろメリットと捉えて欲しくて、一つはこのルールのおかげで内部的なビルド(リンク)の仕組みがすごく単純になること、もう一つは強制的に適切な分割ポイントを見極めるよう促してくれていること、これらが循環参照の禁止によってもたらされると考えてください。
結局インターフェースと考え方は同じで、プログラムを分割したところで、やりとりするものの多さ(インターフェースならメソッドの数、パッケージなら公開しているものの数)が多ければ多いほど、分割されたもの同士がお互いをよく知らないといけないことを意味し、そうなると分割した意味はどんどん薄れ、しまいには分割しない方がましだったとなってしまうからです。
循環参照はその最たるもので、パッケージ同士が密接に連携していることを意味し、お互いを完全に把握していないと正しく動作しないので、分割することの意味は非常に薄くなってしまいます。そういうことなのでいっそ最初から禁止してくれた方がありがたいというわけです。
循環参照は「そもそもパッケージを分けるべきではなかった」合図です。迷ったら分けないの精神でパッケージとうまく付き合っていきましょう!