了解如何通过 CGO 在 Go 中使用 C 库,那么让我们使用 C 库吧!

2025-06-11

了解如何通过 CGO 在 Go 中使用 C 库

因此让我们使用 C 库!

你可能听说过 CGO,并且知道 Go 可以使用系统中的共享库来发挥 C 语言的强大功能。但是具体怎么做呢?我会解释这个过程,你会发现它其实并不复杂——事实上,它相当简单。

本教程是为初学者准备的,但您需要具备一些 C 编程知识。

我写这篇教程的原因很简单,因为我觉得官方文档以及我读过的教程都有些过于简陋。就我而言,我需要从一个非常简单的情况开始,一个真实的“Hello World”,然后慢慢过渡到使用共享库。所以,这就是我的愿景,我看待事物的方式,我在这里分享给大家。

什么是共享库?

共享库(在 Windows 上也称为动态链接库 (DLL),在 Linux 等基于 Unix 的系统上也称为共享对象 (SO))是包含可同时供多个程序使用的编译代码的文件。这些代码存储在共享库中,无需在每个需要执行该任务的程序中重复执行该任务的代码。这样,程序就可以使用这些库中的函数和过程,而无需将代码直接包含在文件中。

Go 可以利用这一点。当然,就像 Python 或 Rust 一样。

CGO 是啥?

它是一个允许在 Go 代码中调用 C 函数和使用 C 库的工具。它充当 Go 和 C 语言之间的桥梁,使 Go 程序能够整合现有的 C 库并利用现有的 C 代码库。

实际上,它可以这样做:

  • 将 Go 的功能传递给 C,但这不是我们今天将要看到的
  • 直接从语句上方的“注释”调用 C 函数import "C"。或者从.c项目内部的文件中调用。
  • 使用 C 库来使用共享库中已编译的函数和类型。

CGO 随 Go 提供,但您需要一个 C 编译器,例如 Ming 上的 GCC(适用于 Windows)。

CGO 将使用“C”包,其中所有 C 函数和变量均可访问。然后,它将使用 C 编译器链接到您的 Go 项目。

让我们尝试在 Go 中使用 C!

首先,为了理解 CGO 的作用,我们将调用我们创建的 C 函数。

创建一个项目,例如在~/Projects/testCGO此目录中:

go mod init testcgo
Enter fullscreen mode Exit fullscreen mode

然后创建一个main.go文件并写入以下内容:

package main

// #include <stdio.h>
// void hello() {
//    printf("Hello from C")
//}
import "C"

func main(){
    // let's call it
    C.hello()
}
Enter fullscreen mode Exit fullscreen mode

在终端中输入go run .并查看结果。没错,它显示“Hello from C”。

但是,这里的 CGO 是什么?

实际上,CGO 已经使用了语句上方的注释import "C",并且共享了“C”包中的函数。

这意味着hello()用 C 开发的函数可以像C.hello()在 Go 中一样访问。“C”包就像一个命名空间,可以在其中访问 C 变量和函数。

但是,我们可以做得更漂亮一些。如果 C 代码行数不多,注释会很有用,但当项目变得庞大时,注释很快就会变得有点烦人。所以,让我们使用真正的 C 源文件。

在同一目录中,创建hello.c文件:

#include <stdio.h>

void hello() {
    printf("Hello from C in another file")
}
Enter fullscreen mode Exit fullscreen mode

并且,hello.h声明该函数的文件:

void hello();
Enter fullscreen mode Exit fullscreen mode

然后,在您的main.go文件中,替换内容以获得以下内容:

package main

// #include "hello.h"
import "C"

func main(){
    // let's call it
    C.hello()
}
Enter fullscreen mode Exit fullscreen mode

这样,我们就可以使用includeC 包含语法了。CGO 不会有问题:

go run main.go
Hello from C in another file
Enter fullscreen mode Exit fullscreen mode

再一次,CGO 接受了上面的注释import "C",因为该#include语句是一个有效的 C 调用,所以它毫无问题地编译了 C 文件。

我们刚刚了解了基础。一边是 C 代码,另一边是 Go 项目,现在我们知道该如何连接两者了。当然,接下来还会有更多限制性的东西需要管理。

如何使用 C 类型?

让我们更改hello.c文件以接受参数并向某人打招呼。

#include <stdio.h>

// say hello to the name
void hello(char* name) {
    printf("Hello %s\n", name);
}
Enter fullscreen mode Exit fullscreen mode

当然,更改头文件以声明该函数:

void hello(char*);
Enter fullscreen mode Exit fullscreen mode

然后,将main.go文件更改为现在尝试向约翰打招呼:

package main

// #include "hello.h"
import "C"

func main() {
    C.hello("John")
}
Enter fullscreen mode Exit fullscreen mode

这将失败...

./main.go:7:10: cannot use "John" (untyped string constant) as *_Ctype_char value in argument to (_Cfunc_hello)
Enter fullscreen mode Exit fullscreen mode

记住这一点非常重要,我们需要将变量从 Go 转换为 C。

Go 简化了多种类型的操作,例如数组、指针和字符串。当你需要从 C 语言发送或接收变量时,它们的类型并不完全相同。因此,我们需要“强制转换”类型。不过,不用担心,过一段时间你就会自然而然地做到。

那么,如何解决这个问题?

我们需要将 Go 修改stringchar*类型。我们可以使用C.char类型,但需要手动分配内存。相反,有一种C.CString类型更容易使用。

func main() {
    name := C.CString("Gopher")
    C.hello(name)
}
Enter fullscreen mode Exit fullscreen mode

现在,它起作用了!

这是使用 CGO 的“复杂”之处,你需要转换、强制类型转换以及操作变量类型以确保其正常工作。
而且由于它是 C 语言,C 变量没有垃圾回收器,所以你需要在需要时释放内存(使用C.free())。

因此让我们使用 C 库!

现在我们已经了解了 CGO 如何编译 C 代码,让我们尝试告诉它链接共享库。

为了举例,我将使用非常简单的“libuuid”。

您需要安装该库的 devel 包才能获取头文件。在 Fedora 上,这是一个简单的sudo dnf install libuuid-devel命令行。

为了能够生成 UUID,您需要阅读库的文档(是的...RTFM...) - 当然,我已经这样做了,我可以解释如何生成 UUID。

// In C:
// we need a uuid_t variable to initalize
uuid uuid_t;
// then we generate the random string. I use the random form
// but you can use other generate methods.
uuid_generate_random(uuid);
// To get a uuid string, we need to "unparse"
char uuid_str[37];
uuid_unparse_lower(uuid, uuid_str);
// In the uuid_str char*, there is a uuid
Enter fullscreen mode Exit fullscreen mode

好的,那我们尝试一下。

我们将包含CGO 能够找到的 Linuxuuid/uuid.h常见文件/usr/include。然后我们将使用这个头文件中的类型和函数。

package main

// #include <uuid/uuid.h>
import "C"
import "fmt"

func main() {
    var uuid C.uuid_t
    var uuid_str *C.char
    uuid_str = (*C.char)(C.malloc(37))
    C.uuid_generate_random(uuid)
    C.uuid_unparse(uuid, uuid_str)
    fmt.Println(C.GoString(uuid_str))
}
Enter fullscreen mode Exit fullscreen mode

这将失败...

这里的第一个问题是 不起作用typdedef。我们需要阅读错误信息才能理解它实际上uuid_t是一个uchar*。但又不完全是……实际上,阅读头文件你会发现它是一个char[16]

记住,在 C 语言中, achar*就像 a char[](我在这里过于简化了)。
但是,libuuid 声明了uuid_t的大小为 16 个字符,这意味着,使用指针形式,我们需要用 来分配内存malloc

因此让我们将该行改为:

var uuid *C.uchar
uuid = (*C.uchar)(C.malloc(16))
Enter fullscreen mode Exit fullscreen mode

你需要了解一些 C 语言才能开始工作。这里,我做的是一个简单的 C 语言uuid = (uchar*)malloc(16)转置,并将其与 Go 中的“C”包进行转换。

我们再跑一次,然后……

/usr/lib/golang/pkg/tool/linux_amd64/link: running gcc failed: exit status 1
/usr/bin/ld: /tmp/go-link-645695464/000001.o: in function `_cgo_b1eb6bfc450b_Cfunc_uuid_generate_random':
/tmp/go-build/cgo-gcc-prolog:49: undefined reference to `uuid_generate_random'
/usr/bin/ld: /tmp/go-link-645695464/000001.o: in function `_cgo_b1eb6bfc450b_Cfunc_uuid_unparse':
/tmp/go-build/cgo-gcc-prolog:62: undefined reference to `uuid_unparse'
collect2: error: ld returned 1 exit status
Enter fullscreen mode Exit fullscreen mode

现在该使用“链接器”了。当然,我们需要告诉编译器使用libuuid.so共享库。

理解问题所在。之前,我们使用了用 CGO 编译的 C 源文件。但现在,我们想使用“已编译”的源文件来生成“.so”库(或.dll用于 Windows)。头文件的作用仅仅是提供函数声明(名称、参数和返回类型)。

这在 C/C++ 中很常见——这使得编译非常智能和快速,因为我们不需要编译库。我们只需“链接”它们。

为了通知 CGO 链接共享库,我们可以向编译器添加特定的标志。因为libuuid它很简单-luuid(可以理解-l uuid)。这将链接libuuid.so到我们的二进制文件。Go 建议在注释中以#cgo:语句的形式指定这些参数。

在头文件的包含上面,仅附加一条特殊说明:

// #cgo LDFLAGS: -luuid
// #include <uuid/uuid.h>
import "C"

Enter fullscreen mode Exit fullscreen mode

现在好了

 go run  .
78137255-35a3-4f61-af7c-e04bf9eb513a
Enter fullscreen mode Exit fullscreen mode

就是这样,您有一个由 C 共享库生成的 UUID。

整个源代码是:

package main

// #cgo LDFLAGS: -luuid
// #include <uuid/uuid.h>
import "C"
import "fmt"

func main() {
    var uuid *C.uchar
    var uuid_str *C.char
    uuid = (*C.uchar)(C.malloc(16))
    uuid_str = (*C.char)(C.malloc(37))
    C.uuid_generate_random(uuid)
    C.uuid_unparse(uuid, uuid_str)
    fmt.Println(C.GoString(uuid_str))
}
Enter fullscreen mode Exit fullscreen mode

效果确实不错,但实用吗?不……

让它变得更好

通过动态类型转换来调用 C 函数是不切实际的、没有吸引力的,而且根本不容易维护。

使用 C 语言,函数使用起来更加简单:

uuid_t uuid;
uuid_generate_random(uuid);
char *str = malloc(37); // because 36 chars + \0
uuid_unparse_lower(uuid, str);
// and I can return "str" variable
// that contains a UUID
Enter fullscreen mode Exit fullscreen mode

然后...

我们真正想要的是什么?一个返回 UUID 的函数。所以我们要做一件非常实际的事情:

用 C 编写一个函数,使我们的工作更轻松,并确保我们可以在 Go 程序中访问它。

好的,尝试一下:

package main

// #cgo LDFLAGS: -luuid
//
// #include <uuid/uuid.h>
// #include <stdlib.h>
//
// // create a uuid function in C to return a uuid char*
// char* _go_uuid() {
//   uuid_t uuid;
//   uuid_generate_random(uuid);
//   char *str = malloc(37);
//   uuid_unparse_lower(uuid, str);
//   return str;
// }
import "C"
import "fmt"

// uuid generates a UUID using the C shared library.
// It returns a Go string.
func uuid() string {
    return C.GoString(C._go_uuid())
}

func main() {
    // and now it's simple to use
    myuuid := uuid() // this is a go string now
    fmt.Println(myuuid)
}
Enter fullscreen mode Exit fullscreen mode

当然,我们可以_go_uuid()在 C 源文件中创建该函数,并创建一个.h文件来声明我们的函数。然后,包含它go_uuid.h

当我们想要将共享库绑定到 Go 时,我们在这里所做的非常常见。我们创建了一些辅助函数来转换类型并调用 C 函数,而无需用户C自己使用包本身。

这就是https://github.com/go-gst/go-gsthttps://github.com/go-gl/glfw,甚至https://fyne.io/如何使用系统库来提出许多功能。

提醒

因此,当您想要使用共享库时需要记住以下几点:

  • “C”包可以访问 C 函数、类型和变量
  • 您可以使用“C”包导入上方的注释来包含头文件”
  • 您可以使用注释中的语句向编译器LDFLAGS提供CFLAGS#cgo
  • 你经常需要将类型转换为 Go 类型,或者将 Go 类型转换为 C
  • 您可以在 C 语言中创建注释帮助程序,以简化库的使用

结论

C 并非唯一允许生成共享库的语言。当然,你也可以用 Go、Rust、Python 等语言生成它们。但迄今为止,C 语言仍然是生成库最广泛的语言。

可以使用 C 或从共享库调用 C 函数,从而实现 Go 的多种强大功能。

显然,我们更倾向于完全用 Go 开发的库。这样可以避免对用户必须在系统上安装或与用户共享库(以.so或 的形式.dll)的依赖。由于 Go 通常使用静态编译,因此强制使用静态编译有点“可惜”。但这在实践中非常有用。例如,功能强大的 Gstreamer 库完全用 Go 重新创建会非常复杂。它是用 C 语言创建的,并且在许多平台上运行良好。因此,依赖这个库是将音频和视频流转换为 Go 语言的绝佳解决方案。

你需要一些 C 语言知识才能在 Go 中链接库。但你不必是这方面的专家。你只需要找到正确的变量类型转换,并适时创建一个辅助函数即可。

无论如何,我希望我的文章能为您开辟道路,消除一些误解,并且您能够在 Go 中使用共享库!

鏂囩珷鏉ユ簮锛�https://dev.to/metal3d/understand-how-to-use-c-libraries-in-go-with-cgo-3dbn
PREV
开始使用 AWS、无服务器和 TypeScript
NEXT
使用 Ansible 自动化您的编码环境,并仅使用 bash 脚本为其创建一个简单的 GUI 介绍 Ansible 安装 Ansible 定义主机 Playbook 和角色示例角色和 Playbook 从 CLI 运行 Ansible 为 Ansible 制作一个简单的 GUI 结论