🔨

GoでOSごとにビルド結果を変える(ただどのOSでもビルドできる)

2023/09/23に公開
  • 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.goGreet()、macOSでビルドした際にはdarwin.goGreet()が呼ばれるようになりました。

最後に実行用の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
脚注
  1. 正式名称はBuild constraints ↩︎

  2. https://pkg.go.dev/cmd/go#hdr-Build_constraints ↩︎

Discussion