Golang中为什么不能使用`go run <非main包>`?

Golang中为什么不能使用go run <非main包>? 目前,要使 go run <arg> 正常工作,<arg> 必须包含 package main 和一个 func main。如果它只有后者而没有前者,你会得到错误:

package command-line-arguments is not a main package

为什么不修改 go run 以允许它运行任何包含 func main 的包呢? 这需要对 go run <arg> 命令进行两处更改:

  1. 如果 <arg> 解析为一个包路径并且该包包含一个 func main,则运行它。
  2. 否则(当前行为),在 <arg> 中寻找一个包含 func mainmain 包并运行它,如果未找到,则给出错误。错误信息应从:
package command-line-arguments is not a main package

改为

package command-line-arguments does not have a main function and is not a main package

(或类似意思的表述)

优点:

  • 使 Go 工具链更加规范,并且我认为更简单。移除了一个看似任意的限制(“func main 仅在它位于 package main 中时才表示程序入口点”)。
  • 这是对 API 的一个向后兼容的扩展。不会破坏任何先前能正常工作的程序。

缺点:

  • 可能存在一些现有的命令行项目,它们已经在非 main 包中定义了 main() 函数。此提案不会破坏任何调用此类项目的现有方式(因为它们都通过 package main)。但可以想象,它可能导致某个测试失败,例如,项目原本有一个针对 go run <non-main-package> 返回错误的显式测试。因此,如果此提案被实施,项目可能需要进行代码更改,仅仅是为了保持所有测试通过的状态。

以下是我的用例: 我有一系列文档示例,每个示例都是一个简短、独立的程序(意味着读者可以通过 go run 命令运行它)。我希望在一个单一的项目(即一个单一的模块)中维护这些源代码。我应该如何组织项目结构,以便能够运行任何选定的示例?

  1. 我可以将每个示例放在一个单独的包中,但包含一个 func main,然后实现一个顶层的驱动程序,该程序知道如何调用每个单独的代码示例包。调用方式类似于 go run driver <sample>。 这样做存在冗余:必须提供这个驱动程序,并且由于每个示例程序都在单独的包中,还必须将每个示例程序放在单独的子目录中。

  2. (我目前作为变通方案正在做的)我将每个示例程序维护在一个单独的源文件中(位于项目根目录)。每个文件都包含 package mainfunc main。 我可以通过 go run <sample>.go 来调用选定的示例(注意这是一个文件路径)。我无法对该模块进行 go testgo build(因为存在重复的 func main 定义),但目前我暂时接受这一点。


更多关于Golang中为什么不能使用`go run <非main包>`?的实战教程也可以访问 https://www.itying.com/category-94-b0.html

4 回复

你好。为什么不使用构建标签,这样你就能拥有任意多个 main 包/函数?

更多关于Golang中为什么不能使用`go run <非main包>`?的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


我的代码示例几乎就是一个独立的程序,实际上是为核心代码片段提供的最小可运行包装。在这种情况下,通过命令行界面分发或在其上运行测试对我来说并没有真正的好处。

我想问的是:保留go run只能运行“main”包这一限制,对Go语言及其生态系统有什么优势?我并不是在问我们是否应该废除main包的定义,我能理解当有多个入口点可供选择时,它仍然可以是默认的入口点。

你好 @BobHy,以下是我的建议:

  1. 类似于方案1,但每个包中不包含 func main()。相反,使用像 urfave/cliCobra 这样的命令调度器来编写一个 func main(),将每个包作为一个子命令提供。
  2. 类似于方案2,但将文件移动到单独的子文件夹中,以便进行测试和构建。

这是一个很好的问题,触及了Go工具链设计的核心原则。go run 命令要求 package main 并非任意的限制,而是Go语言设计哲学的直接体现:包是程序组织的基本单元,而 main 包是程序入口的明确标识

为什么必须使用 package main

  1. 语义清晰性package main 是一个特殊的、具有明确语义的包名。它告诉Go工具链(go run, go build, go install):“这个包包含程序的入口点”。这种设计使得代码的意图一目了然,无需解析包内容来推断其用途。

  2. 工具链的简化与效率:Go工具链可以快速确定一个包是否为可执行程序,只需检查包名是否为 main。如果允许任何包包含 main 函数,工具链必须扫描每个包的AST来寻找 main 函数,这会增加复杂性和开销。

  3. 避免歧义:一个模块中可能有多个包都包含 main 函数(例如,用于测试或示例)。如果允许非 main 包运行,工具链将面临选择哪个 main 函数的歧义。

你的用例解决方案

对于你的文档示例场景,有几种符合Go惯例的解决方案:

方案1:使用 example 子目录(推荐)

这是Go标准库和许多项目采用的标准模式。

project/
├── go.mod
├── example/
│   ├── hello/
│   │   └── main.go      // package main
│   ├── server/
│   │   └── main.go      // package main
│   └── worker/
│       └── main.go      // package main
└── internal/
    └── pkg/
        └── ...          // 共享代码

运行示例:

go run ./example/hello
go run ./example/server

方案2:使用构建标签(build tags)

如果你希望将所有示例放在根目录但避免冲突:

// hello.go
//go:build example_hello
// +build example_hello

package main

func main() {
    println("Hello example")
}
// server.go  
//go:build example_server
// +build example_server

package main

func main() {
    println("Server example")
}

运行示例:

go run -tags example_hello .
go run -tags example_server .

方案3:使用 //go:run 指令(Go 1.21+)

从Go 1.21开始,可以使用 //go:run 指令创建可运行的示例:

// example_test.go
package pkg_test

import "testing"

//go:run
func ExampleHello() {
    println("This can be run with: go test -run=ExampleHello")
}

为什么你的提案不可行

虽然你的提案在理论上可行,但它违反了Go的“显式优于隐式”原则。考虑这个例子:

// pkg/helper.go
package helper

// 这是一个工具函数,不是程序入口
func main() {
    // 清理临时文件
}

// pkg/program.go  
package helper

// 这才是真正的入口
func Main() {
    // 实际程序逻辑
}

如果允许非 main 包运行,工具链如何区分辅助函数 main() 和程序入口 Main()

结论

go run 要求 package main 不是工具链的缺陷,而是经过深思熟虑的设计决策。它确保了:

  • 代码意图明确
  • 工具链简单高效
  • 项目结构清晰

对于你的文档示例,建议采用 example/ 子目录模式,这是Go社区广泛接受的最佳实践。

回到顶部