Golang新手关于包和库的使用问题

Golang新手关于包和库的使用问题 请原谅我的无知,我围绕这个主题已经搜索了大约一天,但至今仍未找到答案,因此我在这里发帖,希望有人能对此有所启发。

作为一名C语言程序员,为多个应用程序使用的函数创建一个库是很常见的,例如静态库 myutils.a 或共享库 myutils.so。这类库通常会被放在一个与链接它们的各种应用程序同级的 lib 目录中。所以一个典型的目录结构可能如下所示:

/app1/somefile.c /app2/otherfile.c /app3/morefile.c /lib/file1.c, file2.c, file3.c 编译成 myutils.so 或 myutils.a

然后,通过设置 LD_PATH,任何一个或所有的应用程序都可以链接到 lib 目录中的 myutils.a 或 myutils.so。我发现这是我多年来在几家公司用C和CPP构建应用程序时常用的方法。同时,我也发现这是一个非常舒适的应用程序编译开发模型,因此我试图在Golang中模仿这种方法,但发现它相当成问题。特别是,从包中创建实际的库似乎有点困难。我阅读了一些文档,但无济于事。我确实找到了一些关于链接共享包文件的文档,但这些链接不准确且无法使用。有人能告诉我这是否可能吗?如果可能,有人能给我指出一个相关的链接或文档吗?我非常感谢您能提供的任何帮助。


更多关于Golang新手关于包和库的使用问题的实战教程也可以访问 https://www.itying.com/category-94-b0.html

11 回复

Ashish,感谢你提供的信息和链接,这很可能提供了很大的帮助。我认为这可能已经解决了一半的问题。

更多关于Golang新手关于包和库的使用问题的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


查尔斯,非常感谢你的帮助,我会研究一下这些内容,看看能有什么收获。

确实,GOPATH 可以包含多个路径。但这并不是重点。相较于旧的 GOPATH 系统,Go 模块系统提供了如此多的优势,因此我不会建议任何人继续使用 GOPATH。

没有必要回归到老旧且有缺陷的 GOPATH 单工作区模式。GOPATH 在内部依然存在,并缓存着你项目导入的所有包。

在本地保留源代码包的 Go Modules 方式是,在 go.mod 中使用 replace 指令。

GOPATH 对我来说从来不是一个“单一”的工作区。我会让它引用一系列本地工作区——至少包括我正在工作的那个,以及一个公共工作区,我在那里存放 Git 包并推广我开发的实用工具包。(当我切换回去时,该配置在 Go 1.16 中仍然有效。)

在 Go v1.11 版本之前,include(...) 代码块中标识的包会使用 GOBIN 和 GOPATH 环境变量自动定位。你仍然可以通过设置 GO111MODULE=off 来使用这种方法。(无需在 go build 命令中命名包含的包。)请查阅关于环境变量的文档。

我可能完全误解了您想要实现的目标,但如果您想为嵌入式设备编译,主流的Go二进制文件并不是合适的选择(无意双关),主要是因为它会附带运行时环境。我建议您看看 TinyGo,它基于LLVM进行交叉编译,能生成体积合理的二进制文件。

你好 @apglaser

欢迎来到这个论坛!

Go 语言并不是为创建或使用共享(或动态)库而设计的。相反,你更可能通过公共或公司内部的代码仓库,以 Go 模块的形式共享源代码本身。其他人就可以从你的模块中导入包,并编译出一个单一的、静态的、自包含的二进制文件。

优点:

  • 没有共享库的版本地狱。(“糟糕,我的应用需要 v1.2.3 或更高版本,但系统安装的是 v1.0.7…”)
  • 静态二进制文件没有依赖项,因此部署起来极其简单
  • 共享库绑定在它们编译时所针对的特定平台(即操作系统和架构)。要支持多个平台,你需要为每个平台创建和维护一个额外的库。X 个操作系统乘以 Y 种架构再乘以 Z 个库版本 = 让人喘不过气来。而源代码包则没有这个问题。

例如,可以参阅 https://golang.org/doc/tutorial/getting-started#call 了解其工作原理。

我理解你的观点。

对于服务器端应用或典型的命令行工具(这是Go的两大主要应用领域),二进制文件的大小并不是一个大问题。(Docker、Kubernetes、Caddy Web服务器、Traefik、CockroachDB,以及所有很酷的Hashicorp项目——它们都是用Go编写的。)

如果对二进制文件大小有严格要求,那么Go可能不是合适的选择。

可以看看,例如:

  • TinyGo,“适用于小型场景的Go编译器”
  • 任何系统编程语言(这类语言通常没有垃圾回收器或庞大的运行时),例如ZigRust

关于在本地保留已编译的库以便构建应用程序,Go已经自动为你处理了这一点。

每当你import一个包时,Go会自动从远程仓库获取包含该包的模块,在本地缓存它,甚至为你预编译。你无需手动管理任何这些步骤。

(执行ls $(go env GOPATH)/pkg命令,并检查以你当前操作系统和架构组合命名的目录下的文件,例如"darwin_amd64"。那里存放着你所有import过的包,它们已被缓存和预编译。)

你不再需要自己管理库的本地编译。这就是为什么你没有找到手动操作的简单方法——在Go中,本意就不是那样做的。

Christoph,感谢您的回复,我非常理解您的出发点。我自己也花了多年时间编写并为 Arm7 交叉编译 C 和 C++,所以对那些担忧再熟悉不过了。最近几个月我开始接触 Golang,不久前我构建了一个相对小巧简单的应用程序,并为 Orange Pi Zero 进行了静态编译。当我通过 scp 将二进制文件传输到目标设备时,看到它的大小,我几乎要晕过去了。我还没有尝试过对最终二进制文件进行剥离(strip),所以我猜这可能是我下一步在对抗臃肿体积时要做的。

从项目管理的角度来看,我更习惯于将 Go 中所谓的包(package)放在它们各自的小型本地目录中,并将其构建成某种库——要么是静态构建的 “.a” 文件,要么是动态构建的 “.so” 文件。在我搜索的过程中,我发现了一些关于实现这一目标的最佳方式相互矛盾的报道。我尝试了 “go install”,但遇到了问题,因为它似乎将包安装到了一个我没有写入权限的系统目录中。然后我尝试强制它在本地构建,但似乎也没有成功。我尝试过 “go mod” 和 “go tidy”,但它们的工作方式并不符合我的期望。

无论是静态链接还是动态链接,我想做的是:在一个目录中存放一组包文件,并在该目录中将其编译成一个小型库。然后在另一个目录中,存放用于创建主程序(main)的文件,并从我的库中导入函数。尽管我在网上找到了一些这样的例子,但很少能成功让它运行起来。我觉得这很奇怪,因为在 C 和 C++ 中,这是一个非常非常直接的过程。我记得 Ken Thompson 和 Robert Pike 是 Golang 的主要推动者之一,所以我原以为这种事情应该是轻而易举的。显然并非如此。

尽管如此,Christoph,我仍然非常感谢您花时间回复,我会继续努力的。

在Go中实现类似C语言的库模式是完全可行的,但Go的包管理方式与C有本质区别。Go使用模块(module)作为代码组织和版本控制的基本单位,而不是直接操作静态库或共享库文件。

核心概念:Go模块

在Go中,你应该创建一个独立的模块作为共享库,然后在其他应用中导入它。以下是一个完整的示例:

1. 创建共享库模块

首先,在你的lib目录中创建Go模块:

# 创建lib目录
mkdir -p /path/to/project/lib
cd /path/to/project/lib

# 初始化模块
go mod init github.com/yourname/myutils

# 创建包文件
mkdir -p myutils

/path/to/project/lib/myutils/utils.go:

package myutils

import "fmt"

// 导出的函数必须以大写字母开头
func Greet(name string) string {
    return fmt.Sprintf("Hello, %s!", name)
}

func Add(a, b int) int {
    return a + b
}

// 未导出的函数(小写开头)只能在包内部使用
func internalHelper() {
    // 内部实现
}

/path/to/project/lib/go.mod:

module github.com/yourname/myutils

go 1.21

2. 在应用中使用共享库

现在,在你的应用中导入这个模块:

/path/to/project/app1/main.go:

package main

import (
    "fmt"
    "github.com/yourname/myutils/myutils"
)

func main() {
    result := myutils.Greet("World")
    fmt.Println(result) // 输出: Hello, World!
    
    sum := myutils.Add(5, 3)
    fmt.Printf("5 + 3 = %d\n", sum) // 输出: 5 + 3 = 8
}

/path/to/project/app1/go.mod:

module app1

go 1.21

require github.com/yourname/myutils v0.0.0

replace github.com/yourname/myutils => ../lib

3. 本地开发时的替换指令

在开发阶段,使用replace指令指向本地路径:

module app1

go 1.21

require github.com/yourname/myutils v0.0.0

// 关键:将模块替换为本地路径
replace github.com/yourname/myutils => ../lib

4. 构建和依赖管理

初始化应用模块并获取依赖:

cd /path/to/project/app1
go mod init app1
go mod tidy

5. 替代方案:使用工作区(Go 1.18+)

对于本地开发,Go 1.18引入了工作区功能,更适合多模块项目:

在项目根目录创建go.work:

go 1.21

use (
    ./app1
    ./app2
    ./app3
    ./lib
)

然后各个应用的go.mod中可以直接引用:

// app1/go.mod
module app1

go 1.21

require github.com/yourname/myutils v0.0.0
// 不再需要replace指令

6. 发布为版本化模块

当库稳定后,可以发布到版本控制系统:

cd /path/to/project/lib
git tag v1.0.0
git push origin v1.0.0

然后在应用中引用具体版本:

// app1/go.mod
module app1

go 1.21

require github.com/yourname/myutils v1.0.0

关键差异说明

  1. 没有显式的.a.so文件:Go在构建时自动处理依赖编译
  2. 包路径即导入路径import "github.com/yourname/myutils/myutils"
  3. 版本管理内置:通过go.mod管理确切的版本
  4. 本地开发灵活:使用replace指令或工作区进行本地开发

这种设计虽然与C的传统方式不同,但提供了更好的依赖管理和版本控制。对于从C转来的开发者,适应这种模式后会发现它在大型项目中更加可靠和可维护。

回到顶部