🔨
GoでOSごとにビルド結果を変える(ただどのOSでもビルドできる)
- Goで特定のOSによって挙動を変えたい
- 最終ビルドにいらないOS部分を載せたくない(無駄なimportを減らしたい)
- どのOSでも正常にビルドしたい(パッケージとして出すなど)
こんなときありますよね?
[1]でとりあえずやってみる
ビルドタグGoでは、ソースファイルの上部に//go:build windows
のようなコメントを付加することで、特定のOSでのみビルドされるよう設定することができます[2]。
今回は例として、OSごとに挨拶を変えるosgreet
パッケージを作ってみます(user
は適宜変えてください)。
mkdir osgreet
cd osgreet
go mod init github.com/user/osgreet
次に、次のようなファイル構成にします
.
├── darwin.go // 追加
├── go.mod
└── win.go // 追加
win.go
//go:build windows
package osgreet
import "fmt"
func Greet() {
fmt.Println("Hello from Windows!")
}
darwin.go
//go:build darwin
package osgreet
import "fmt"
func Greet() {
fmt.Println("Hello from macOS!")
}
ファイル全体
これで、Windowsでビルドした際にはwin.go
のGreet()
、macOSでビルドした際にはdarwin.go
のGreet()
が呼ばれるようになりました。
最後に実行用のcmd/main.go
を作ります(検証用なのでパッケージ制作に必ずしも必要ではありません)。
cmd/main.go
package main
import "github.com/user/osgreet"
func main(){
osgreet.Greet()
}
macOSで実行
❯ go build -o main cmd/main.go
❯ ./main
Hello from macOS!
完成!
こいつの問題点
今回、自分はこれをパッケージとして公開したいと考えています。そういったとき、別のプロジェクトがこのパッケージをimportしたとき、osgreetが対応していないOSでそのプロジェクトがビルドできなくなります...
Linuxでビルド...
❯ go build -o main cmd/main.go
package command-line-arguments
imports github.com/user/osgreet: build constraints exclude all Go files in /Users/bonychops/Documents/osgreet
できればビルドが通るようにしておいて、非対応OS向けにビルドした際はエラーを吐いて呼び出し元のプロジェクトにハンドリングさせたほうが汎用が効きそうですよね。
じゃあどうするか
client.go
を作って、実行用のClient(OSGreet
)構造体を定義します。
client.go
package osgreet
import "errors"
type Greet interface {
IsImplemented() bool // 実装されているかの判定
Greet() error // 実装するメソッド
}
// オーバーライド(風なこと)をするため、親に当たる構造体を作る
// client.goでのみ用いる
type OSGreetBase struct{}
// 実際の実装にはこちらを用いる
type OSGreet struct {
OSGreetBase // OSGreetBaseを継承(風なことをする)
}
func NewOSGreet() (Greet, error) {
var greet Greet = &OSGreet{}
if ok := greet.IsImplemented(); !ok {
// 実装されていない = このOSではサポートされていない場合
return nil, errors.New("this os is not supported")
}
return greet, nil
}
// 実装されているかの判定につかう
// 親構造体をfalseにしておいて、実装した際にはtrueを返すメソッドをオーバーライド(風なことを)する
func (*OSGreetBase) IsImplemented() bool {
return false
}
// ダミーのメソッド
func (*OSGreetBase) Greet() error {
return errors.New("this os is not supported")
}
各ファイルを書き換えて、Greet()
をオーバーライド(風なことを)します。
win.go
//go:build windows
package osgreet
import "fmt"
+ func (*OSGreet) IsImplemented() bool {
+ return true
+ }
- func Greet() {
+ func (*OSGreet) Greet() error {
fmt.Println("Hello from Windows!")
+ return nil
}
darwin.go
//go:build darwin
package osgreet
import "fmt"
+ func (*OSGreet) IsImplemented() bool {
+ return true
+ }
- func Greet() {
+ func (*OSGreet) Greet() error {
fmt.Println("Hello from macOS!")
+ return nil
}
Greet()
を直接呼び出していたので、Client(OSGreet
)経由で呼び出すよう変えます。
cmd/main.go
package main
+ import "fmt"
import "github.com/user/osgreet"
func main() {
- osgreet.Greet()
+ client, err := osgreet.NewOSGreet()
+ if err != nil {
+ fmt.Println(`main.go (this package is seems to be not supported this os...)`)
+ return
+ }
+
+ err = client.Greet()
+ if err != nil {
+ // 適切に処理されていれば、そもそもここは呼ばれない
+ // そういう意味では`Greet()`の実装はerrorのreturnなしのpanic()でも良いのかも
+ fmt.Println(`main.go (this package is seems to be not supported this os...)`)
+ return
+ }
}
ファイル全体
panicでの実装案
client.go
package osgreet
import "errors"
type Greet interface {
IsImplemented() bool // 実装されているかの判定
- Greet() error // 実装するメソッド
+ Greet() // 実装するメソッド
}
// オーバーライド(風なこと)をするため、親に当たる構造体を作る
// client.goでのみ用いる
type OSGreetBase struct{}
// 実際の実装にはこちらを用いる
type OSGreet struct {
OSGreetBase // OSGreetBaseを継承(風なことをする)
}
func NewOSGreet() (Greet, error) {
var greet Greet = &OSGreet{}
if ok := greet.IsImplemented(); !ok {
// 実装されていない = このOSではサポートされていない場合
return nil, errors.New("this os is not supported")
}
return greet, nil
}
// 実装されているかの判定につかう
// 親構造体をfalseにしておいて、実装した際にはtrueを返すメソッドをオーバーライド(風なことを)する
func (*OSGreetBase) IsImplemented() bool {
return false
}
// ダミーのメソッド
- func (*OSGreetBase) Greet() error {
+ func (*OSGreetBase) Greet() {
- return errors.New("this os is not supported")
+ panic("this os is not supported")
}
win.go
//go:build windows
package osgreet
import "fmt"
func (*OSGreet) IsImplemented() bool {
return true
}
- func (*OSGreet) Greet() error {
+ func (*OSGreet) Greet() {
fmt.Println("Hello from Windows!")
- return nil
}
darwin.go
//go:build darwin
package osgreet
import "fmt"
func (*OSGreet) IsImplemented() bool {
return true
}
- func (*OSGreet) Greet() error {
+ func (*OSGreet) Greet() {
fmt.Println("Hello from macOS!")
- return nil
}
cmd/main.go
package main
- import "fmt"
import "github.com/user/osgreet"
func main() {
client, err := osgreet.NewOSGreet()
if err != nil {
fmt.Println(`main.go (this package is seems to be not supported this os...)`)
return
}
- err = client.Greet()
+ client.Greet()
- if err != nil {
- // 適切に処理されていれば、そもそもここは呼ばれない
- // そういう意味では`Greet()`の実装はerrorのreturnなしのpanic()でも良いのかも
- fmt.Println(`main.go (this package is seems to be not supported this os...)`)
- return
- }
}
実行
対応OS(macOS)
❯ go build -o main cmd/main.go
❯ ./main
Hello from macOS!
非対応OS(Linux)
❯ go build -o main cmd/main.go # ビルドが成功する
❯ ./main
main.go (this package is seems to be not supported this os...)
main関数でうまくエラーハンドリングできているようですね。
バイナリサイズも若干変わっています。
macOS
❯ l main
-rwxr-xr-x@ 1 bonychops staff 1.9M 9 23 01:01 main
Linux
❯ l main
-rwxr-xr-x@ 1 bonychops staff 1.2M 9 23 01:01 main
-
正式名称はBuild constraints ↩︎
Discussion