掘金 后端 ( ) • 2024-05-06 11:04

Go 提供了丰富的 CLI 命令工具,使我们不仅可以编写常规代码,还可以编写供其他 Go 程序调用的插件,以及供其他语言调用的库。本文主要从三个方面学习如何利用 Go CLI 命令构建共享库 (shared libraries):

  • 如何创建 Go 插件 (用于 Go 代码之间,不支持 Windows)
  • 如何创建通用库 (用于与其它语言,如 C,Python)
    • 静态库 (static)
    • 共享库 (shared)

1. Go 中如何创建插件

Go 的插件(plugin)是一种机制,允许在运行时动态加载和执行编译好的代码。插件的目的是为了实现应用程序的可扩展性和灵活性。通过使用插件,开发人员可以将应用程序的功能模块化,使其能够在不重新编译整个应用程序的情况下,动态地加载新的功能或模块。

这个理论看起来挺唬人,但要说写个 demo 也简单:

package main

import "fmt"

func ThingsToDo() {
  fmt.Println("Code in plugin...")
}

上面的代码就可以作为一个 Go 插件,要成为插件,有几点需要说明:

  • 包名需要是 main
  • 不需要有 main() 函数,但需要有导出函数 (即大写字母开头) 可供调用
  • 需要使用构建标志将其构建为插件代码
    • go help build 有一个标志 -buildmode,其介绍为:build mode to use. See 'go help buildmode' for more.
    • go help buildmode 有介绍 -buildmode=pluginBuild the listed main packages, plus all packages that they import, into a Go plugin. Packages not named main are ignored.

所以 上面的代码 可以使用 go build -buildmode=plugin ./plugin.go 编译为插件,结果是在同一目录内生成一个 plugin.so 的文件。

创建插件之后看一下该如何使用,我们创建另一个 demo 程序,代码如下:

func main() {
  path := flag.String("plugin", "", "plugin to execute")
  flag.Parse()

  if *path == "" {
    log.Fatal("path to plugin not specified")
  }

  p, _err := plugin.Open(*path)

  symbol, _err := p.Lookup("ThingsToDo")

  thingsToDo, ok := symbol.(func())
  if !ok {
    log.Fatalf("could not find function 'ThingsToDo' in plugin")
  }

  thingsToDo()
  log.Println("Did the things")
}

上面的代码并不复杂:

  • 运行程序时传入插件路径
  • 打开插件,查找函数
  • 类型断言成功后,执行函数

使用 go run . -plugin ../plugin/plugin.so 运行上面的代码,得到输出:

Code in plugin...
2024/05/05 21:36:02 Did the things

2. Go 中如何创建静态库

静态库和共享库是两种常见的库文件形式。先了解一下什么是静态库:

  • 静态库是在编译时被链接到目标程序中的库文件。
  • 静态库的代码在编译时被复制到目标程序中,因此目标程序在运行时不再依赖于静态库。
  • 静态库的文件扩展名通常为 .a(在 Windows 上为 .lib)。
  • 静态库的优点是在程序运行时不需要外部依赖,但缺点是会增加目标程序的体积。

这个理论看起来挺唬人,但要说写个 demo 也简单:

package main

import (
  "C"
  "fmt"
)

func main() {}

//export Hello
func Hello() {
  fmt.Println("Hello from Go Static Library")
}

上面的代码有几点需要说明:

  • import "C" 必须要有
  • //export Hello 也是必须的:固定格式是 //exportHello 是要导出的函数名
  • 因为我们不需要 main 函数,但没有它又无法编译,所以保留一个空的 main 函数

代码目录内 使用 go build -buildmode c-archive hello.go 编译代码,会生成两个文件: hello.ahello.h

在同一目录内写个简单的 C 代码:

// hello.c
#include "hello.h"

int main(void) {
  Hello();
  return 0;
}

运行 gcc hello.c ./hello.a -lpthread 编译 C 文件,生成一个 a.out 可执行文件,执行这个文件得到输出:

./a.out
# Hello from Go Static Library

由于我们生成的是静态库,此时删除 hello.ahello.h 文件,再次执行 a.out 也依然没有什么问题。

3. Go 中如何创建共享库

共享库也称动态链接库,了解一下:

  • 共享库是在程序运行时被动态加载到内存中的库文件。
  • 共享库的代码在程序运行时被共享,多个程序可以共享同一个共享库的实例。
  • 共享库的文件扩展名通常为 .so(在 Windows 上为 .dll)。
  • 共享库的优点是可以减小目标程序的体积,但缺点是在运行时需要确保共享库的可用性。

静态库在编译时被链接到目标程序中,而共享库在程序运行时被动态加载。

这个理论看起来挺唬人,但要说写个 demo 也简单:

package main

import (
  "C"
  "fmt"
)

func main() {}

//export Hello
func Hello() {
  fmt.Println("Hello from Go Shared Library")
}

这个代码和上面的代码没有什么区别 (唯一的区别是 fmt.PrintlnStatic 改成了 Shared),使用 go build -buildmode c-shared hello.go 编译代码,会生成两个文件:hellohello.h

依然是同样的 C 代码,使用 gcc hello.c ./hello 编译,会生成一个 a.out 文件,执行此文件得到输出:

./a.out
# Hello from Go Shared Library

但由于这次我们构建的是共享库,删除 hello 文件之后再次执行 a.out 则会报错。

4. 总结

本文的目的只是指引作用,也许我们永远不会用到这些知识,但当某一天需要时,知道这个知识点的存在再根据需求进一步探索,应该会更轻松一些。