使用不同的包和构建标签构建多个二进制文件

huangapple go评论80阅读模式
英文:

Building multiple binaries using different packages and build tags

问题

我有一个应用程序,根据目标操作系统需要使用不同的软件包,然后生成一个可执行文件。核心软件包有一个接口,根据使用的软件包需要填充该接口。

我最近发现实现这一目标的最佳方法是使用构建标签(build tags)。但我遇到的问题是如何使用正确的构建标签加载软件包并填充接口。或者也许有更好的替代方法。

这是我设想的实现方式的示意图:
使用不同的包和构建标签构建多个二进制文件

英文:

I have an application that needs to use different packages depending on the target operating system and then generate an executable. The core package has an interface that needs to be populated depending on the package that is used.

I've recently found out the best way to achieve this is by using build tags. But what I'm struggling with is getting the interface populated by the loaded package with the correct build tag(s). Or perhaps there is a better alternative approach.

Here is a visual of how I imagined this to look like:
使用不同的包和构建标签构建多个二进制文件

答案1

得分: 4

无论您选择哪种构建约束,您都可以通过接口和使用New()构造函数来实现此目标。每个特殊文件都将具有您所需的特殊包,每个文件都是独立的。这种方法还通过强制您仅拆分实现每个体系结构特定的原始部分来实现良好的解耦。

我个人更喜欢使用文件后缀而不是构建标签,因为通过查看文件名,可以非常容易地知道哪个文件与哪个体系结构绑定。一个重要的优点是您不必处理任何构建标签,它将自动工作。因此,下面的示例将使用文件后缀。具体格式如下:

*_GOOS
*_GOARCH
*_GOOS_GOARCH

例如,renderer_windows_amd64.gorenderer_windows_amd64_test.gorenderer_linux.gorenderer_linux_test.go等。您可以在此处找到Go支持的所有GOOS和GOARCH

编辑:在孩子的笔记本电脑上验证了代码(调整了构建错误)。;) 请注意,您不能调用go run main.go,因为无法确定体系结构。您需要使用go build && ./mybinary在本地执行它进行测试。

main.go

package main

import (
	"fmt"
	"os"
)

func main() {
	r, err := NewRenderer()
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}

	// 调用特定goarch-goos的Render()方法。
	if err := r.Render(); err != nil {
		fmt.Println(err)
	}
}

renderer.go

这是一个简单的文件,仅定义了接口和一些常见的枚举。

package main

// Renderer执行特定于平台的渲染。
type Renderer interface {
    Render() error
}

// 或者,您可以在下面的每个文件中定义一个全局renderer结构,
// 如果它们始终相同。通常不是这样,因为您希望在每个结构中保留特定体系结构的状态。
// type renderer struct {
//     ...
// }
//
// func (r *renderer) Render() error {
// 	...
// }

renderer_windows.go

包括32位和64位构建。如果要针对特定的64位编译的DLL仅目标64位,则可以使用renderer_windows_amd64.go更具体地进行目标设置。

package main

import (
	"fmt"
	// "WindowsDLLPackage" 用于Windows的特定包
)

// renderer实现Renderer接口。
type renderer struct {
	// 您可以在此处包含一些特定于Windows版本的有状态信息,
	// 以将其保持在全局堆之外。
    GOOS         string
    WindowsRules bool
}

// NewRenderer实例化Windows版本。
func NewRenderer() (Renderer, error) {
    return &renderer{
    	GOOS:         "Windows",
	    WindowsRules: true,
    }, nil
}

// Render渲染Windows版本。
func (r *renderer) Render() error {

	// 使用WindowsDLLPackage.NewSomething()

	fmt.Println(r.GOOS, r.WindowsRules)
	return nil
}

renderer_linux.go

Linux不包括Android(也不包括darwin,即macOS)构建。

package main

import (
	"fmt"
	// "LinuxPackage" 用于Linux的特定包
)

// renderer实现Renderer接口。
type renderer struct {
	// 您可以在此处包含一些特定于Linux版本的有状态信息,
	// 以将其保持在全局堆之外。
    GOOS       string
    LinuxRules bool
}

// NewRenderer实例化Linux版本。
func NewRenderer() (Renderer, error) {
    return &renderer{
    	GOOS:       "Linux",
    	LinuxRules: true,
    }, nil
}

// Render渲染Linux版本。
func (r *renderer) Render() error {

	// 使用LinuxPackage.NewSomething()

	fmt.Println(r.GOOS, r.LinuxRules)
	return nil
}

renderer_android.go

仅适用于Android的特定版本。

package main

import (
	"fmt"
	// "AndroidPackage" 用于Android的特定包
)

// renderer实现Renderer接口。
type renderer struct {
	// 您可以在此处包含一些特定于Android版本的有状态信息,
	// 以将其保持在全局堆之外。
    GOOS         string
    AndroidRules bool
}

// NewRenderer实例化Android版本。
func NewRenderer() (Renderer, error) {
    return &renderer{
    	GOOS:         "Linux",
    	AndroidRules: true,
    }, nil
}

// Render渲染Android版本。
func (r *renderer) Render() error {

	// 使用AndroidPackage.NewSomething()

	fmt.Println(r.GOOS, r.AndroidRules)
	return nil
}

生成不同的二进制文件

剩下的就是交叉编译:

$ GOOS=windows GOARCH=amd64 go build -o mybinary.exe
$ GOOS=linux GOARCH=amd64 go build -o mybinary_linux
$ GOOS=darwin GOARCH=amd64 go build -o mybinary_macos
# 以及获取iOS/Android构建的其他操作...

请注意,上面的所有文件都属于同一个package main,并且它们都存在于同一个目录中。这是因为编译器只会选择与GOOS(windows、linux或android - 还可以使用darwin、freebsd等)对应的文件后缀。在编译过程中,编译器只使用一个文件实现NewRenderer()。这也是您可以针对每个文件使用特定包的方式。

还请注意,func NewRenderer() (Renderer, error)返回Renderer接口,而不是renderer结构类型。

type renderer struct对于包的其余部分来说是完全无关的,可以通过保存所需的任何状态来用于任何体系结构的目的。

还请注意,这里没有全局变量。我经常在高并发应用程序中使用这种模式,使用goroutineschannels - 没有互斥锁的瓶颈。避免将事物放在堆上是避免互斥锁的关键。您可以轻松地执行go r.Render()并让它生成一个goroutine。或者,调用它数百万次。

最后,请注意上面的所有文件名都很容易区分它们所针对的平台。

不要与构建标签对抗,让工具为您工作。

上面的编码提示:

  • 我将接口Renderer导出,因为所有这些内容都可以很容易地移动到main之外的包中。您不希望导出结构版本。但是,您可能希望导出NewRenderer()初始化方法。

  • Renderer遵循GoLang Effective Go指南,使用具有单个函数的简单接口:Render。这些函数成为接口的名称,后缀为“er” - 是的,即使名称以“er”结尾,我们也会在末尾添加“er”,它变成了type Renderer interface。也就是说,它不应该被称为RenderEngine,而应该称为具有您正在操作的单个方法的Renderer。这清楚地定义了工具和代码的单一焦点。即,“Go的方式”。

英文:

Whichever Build Constraints you chose, you can achieve this with interfaces and implementing the interface with New() constructors. And each of those special files will have the special packages you seek, on a per file basis. This approach also enforces good decoupling by forcing you to break off only the raw parts you need to implement specific to each architecture.

I am a personal fan of file suffixes, instead of build tags, as it makes it extremely easy to know which file binds to what architecture - just by looking at the filename. A big plus is you don't have to mess with any build tags and it will JustWork™. So my examples below will use file file suffixes. Specifically, the format is:

*_GOOS
*_GOARCH
*_GOOS_GOARCH

For example, renderer_windows_amd64.go, renderer_windows_amd64_test.go, renderer_linux.go, renderer_linux_test.go, etc. You can find all the GOOS and GOARCH that Go supports here.

EDIT: Validated code on kiddo's laptop (tweaking a build error). 使用不同的包和构建标签构建多个二进制文件 Note though, you can't call go run main.go as the architecture isn't known. You'll have to go build && ./mybinary to execute it locally to test.

main.go

package main

import (
	"fmt"
	"os"
)

func main() {
	r, err := NewRenderer()
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}

	// call the Render() method for the specific goarch-goos.
	if err := r.Render(); err != nil {
		fmt.Println(err)
	}
}

renderer.go

This is a simple file that only defines the interface. And maybe some common enums.

package main

// Renderer renders performs the platform-specific rendering.
type Renderer interface {
    Render() error
}

// alternatively, you could define a global renderer struct
// here to use in each of hte files below if they are always
// the same.  often not though, as you want to keep states of
// of specific architectures within each struct.
// type renderer struct {
//     ...
// }
//
// func (r *renderer) Render() error {
// 	...
// }

renderer_windows.go

Includes 32 and 64 bit builds. If you want to target, say 64 bit only for specific 64bit compiled DLLs, then you can target more specifically with renderer_windows_amd64.go.

package main

import (
	"fmt"
	// "WindowsDLLPackage"  specific package to import for Windows
)

// renderer implements Renderer interface.
type renderer struct {
	// you can include some stateful info here for Windows versions,
	// to keep it out of the global heap.
    GOOS         string
    WindowsRules bool
}

// NewRenderer instantiates a Windows version.
func NewRenderer() (Renderer, error) {
    return &renderer{
    	GOOS:         "Windows",
	    WindowsRules: true,
    }, nil
}

// Render renders the Windows version.
func (r *renderer) Render() error {

	// use WindowsDLLPackage.NewSomething()

	fmt.Println(r.GOOS, r.WindowsRules)
	return nil
}

renderer_linux.go

Linux does not include Android (nor darwin, aka macOS) builds.

package main

import (
	"fmt"
	// "LinuxPackage"  specific package to import for Linux
)

// renderer implements Renderer interface.
type renderer struct {
	// you can include some stateful info here for Linux versions,
	// to keep it out of the global heap.
    GOOS       string
    LinuxRules bool
}

// NewRenderer instantiates a Linux version.
func NewRenderer() (Renderer, error) {
    return &renderer{
    	GOOS:       "Linux",
    	LinuxRules: true,
    }, nil
}

// Render renders the Linux version.
func (r *renderer) Render() error {

	// use LinuxPackage.NewSomething()

	fmt.Println(r.GOOS, r.LinuxRules)
	return nil
}

renderer_android.go

Android only specific version.

package main

import (
	"fmt"
	// "AndroidPackage"  specific package to import for Android
)

// renderer implements Renderer interface.
type renderer struct {
	// you can include some stateful info here for Android versions,
	// to keep it out of the global heap.
    GOOS         string
    AndroidRules bool
}

// NewRenderer instantiates a Android version.
func NewRenderer() (Renderer, error) {
    return &renderer{
    	GOOS:         "Linux",
    	AndroidRules: true,
    }, nil
}

// Render renders the Android version.
func (r *renderer) Render() error {

	// use AndroidPackage.NewSomething()

	fmt.Println(r.GOOS, r.AndroidRules)
	return nil
}

generate different binaries

All that's left is to cross-compile:

$ GOOS=windows GOARCH=amd64 go build -o mybinary.exe
$ GOOS=linux GOARCH=amd64 go build -o mybinary_linux
$ GOOS=darwin GOARCH=amd64 go build -o mybinary_macos
# and whatever u do to get ios/android builds...

Notice how all of the files above are part of the single package main and they exist all in the same directory? This works because the compiler only picks the one file suffix for the GOOS (windows, linux or android - you can do darwin, freebsd and a lot more). During compilation, the compiler only implements NewRenderer() once by using that one file. This is also how you can use specific packages per file.

Also notice how func NewRenderer() (Renderer, error) returns the Renderer interface, not the renderer struct type.

The type renderer struct is completely agnostic to the rest of the package, and can be used for any architecture's means by holding any state you need.

Also note that there aren't any global variables here. I've often use this pattern with goroutines and channels for highly concurrent applications - with no mutex locking bottlenecks. Keeping things off the heap is critical to avoid mutex locking. You could easily do go r.Render() and let it spawn a goroutine. Or, call it a few million times.

Finally, notice how all of the filenames above are easily distinguishable of what platform they target?

Don't fight the tooling with build tags, let the tools work for you.

Coding tips above:

  • I exported the interface, Renderer, as all of this could be moved to a package outside of main quite easily. You don't want to export the struct versions. But, you may want to export the NewRenderer() init method.

  • Renderer follows GoLang Effective Go guidelines in using simple interfaces with a single function: Render. And those functions become the name of the interface with the suffix of "er" - yes, even if the name ends in 'er', we add 'er' to the end and it becomes type Renderer interface. IOW: it shouldn't be called RenderEngine and instead it should be called Renderer with a single method you are manipulating: Render(). This clearly defines a single focus for the tooling and code. Aka, "the Go Way."

答案2

得分: 3

创建两个类似以下的文件:

// +build myBuildFlag

package mypackage

import package1

var important = package1.Foo

另一个文件:

// +build !myBuildFlag

package mypackage

import package2

var important = package2.Foo

现在,无论何时使用important,它都会根据你的构建标志而有所不同。

英文:

Create two files somewhat like these:

// +build myBuildFlag

package mypackage

import package1

var important = package1.Foo

Other one:

// +build !myBuildFlag

package mypackage

import package2

var important = package2.Foo

Now whenever you use important it something different depending on your build flag.

答案3

得分: 0

请看一下Dave Chaney的文章,他很清楚地解释了如何处理针对特定平台/架构构建的问题。文章链接:https://dave.cheney.net/2013/10/12/how-to-use-conditional-compilation-with-the-go-build-tool

英文:

Take a look at Dave Chaney's post which explains how to deal with building for specific platforms/architectures quite clearly: https://dave.cheney.net/2013/10/12/how-to-use-conditional-compilation-with-the-go-build-tool

huangapple
  • 本文由 发表于 2017年4月5日 03:07:26
  • 转载请务必保留本文链接:https://go.coder-hub.com/43215655.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定