Go 语言怎么编写 Windows DLL 动态链接库

周叙
周叙
发布时间:2026-08-25 | 15人浏览过
原创

前言

在需要把 Go 能力嵌入 Windows 程序,或暴露给 C/C++、Python、C# 调用时,DLL 是一条很直接的交付方式。难点不在单纯跑通编译,而在于导出函数的写法、C 兼容类型选择,以及字符串返回后的内存释放责任;这篇文章会按实际开发顺序把这些关键点梳理清楚。

在需要和 Windows 系统深度集成,或给 C/C++、Python、C# 这类语言提供可调用模块时,把 Go 代码编译成 DLL 是一条很实用的路径。关键不只是跑通 go build -buildmode=c-shared,还要弄清楚导出函数怎么写、哪些类型能安全暴露、字符串内存该由谁释放。下面按“环境检查、最小示例、规则约束、字符串案例”的顺序展开,便于你快速判断自己的项目是否适合这样做。

先把环境准备齐:Go 和 GCC 都要可用

在 Windows 下用 Go 生成 DLL,先决条件并不复杂,但少任何一项都会直接卡在编译阶段。

首先需要安装 Go 开发环境;其次要有 GCC 编译器。Go 导出 C 可调用符号时依赖 C 编译工具链,常见选择是 MinGW-w64。安装完成后,还要把 GCC 所在目录加入系统的 PATH 环境变量。

可以先用下面两条命令做最基本的检查:

go version
gcc --version

如果两条命令都能正常输出版本信息,说明构建 DLL 所需的基础环境已经就绪。对于刚接触这一流程的读者来说,这一步的意义在于尽早排除工具链问题,避免把编译报错误判成 Go 代码本身的问题。

第一个 Go DLL:最小示例怎么写

Go 提供了 buildmode 选项来支持构建动态链接库。在实际使用中,最常见的模式就是 c-shared,它会生成一个可供其他语言调用的 Windows DLL。

Go 生成 Windows DLL 的最小流程与产物示意
Go DLL 最小构建流程用纸面卡片展示 Go DLL 的最小写法、构建命令与产物关系,帮助读者快速建立编译流程认知。

下面用一个简单的数学库示例演示完整写法。先创建 mathutil 目录,再写入 math.go:

package main

import "C"

import (
    "math"
)

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

//export Multiply
func Multiply(a, b float64) float64 {
    return a * b
}

//export SquareRoot
func SquareRoot(n float64) float64 {
    return math.Sqrt(n)
}

func main() {}

这个例子里有几个容易忽略、但必须满足的点。

为什么必须有 import "C"

Go 的 DLL 导出能力建立在 cgo 机制之上,因此文件中必须出现 import "C"。没有这一句,后面的导出注释和 c-shared 构建模式就不会按预期工作。

//export 注释的作用

导出函数不是靠函数名首字母大写完成的,而是依赖 //export 注释。只有被明确标记的函数,才会出现在最终生成的 DLL 导出符号中,供外部程序调用。

Go DLL 字符串处理时的内存归属与释放路径示意
字符串接口的内存边界把字符串参数转换、结果返回和释放责任画成调用链,强调跨语言接口里最容易出问题的内存边界。

为什么还要保留空的 main 函数

这里的包名必须是 main,同时还要保留一个空的 main 函数作为入口。它本身不会导出给外部使用,但缺少它通常会导致构建形式不符合 c-shared 的要求。

代码写好后,直接执行构建命令:

go build -buildmode=c-shared -o mathutil.dll math.go

编译完成后,通常会得到两个关键文件:

  • mathutil.dll:真正的动态链接库文件;
  • mathutil.h:配套生成的 C 头文件,里面包含导出函数声明。

这也是 Go DLL 和普通可执行文件最大的区别之一:除了二进制本体,还会顺带给出一个供 C/C++ 集成时使用的头文件。对于需要在其他语言项目中接入 Go 能力的场景,这个头文件就是最直接的接口说明。

导出函数有哪些硬规则

能成功编译出 DLL,只说明第一步完成了;想让其他语言稳定调用,还得遵守一组更重要的接口规则。这里最需要关心的就是类型兼容、内存管理和并发安全。

返回值和参数尽量使用 C 兼容类型

外部语言并不了解 Go 运行时,也不能自然处理 Go 的复杂类型。因此,导出函数最好只暴露 C 兼容的基础类型,比如 int、float64、*C.char 这类可以清晰映射到 C 接口的形式。

如果接口里直接混入 Go 专有结构,调用端往往很难正确处理,轻则调用麻烦,重则直接造成崩溃或未定义行为。尤其是字符串场景,更适合通过 C 字符串指针来传递结果,而不是试图把 Go 字符串原样暴露出去。

Go 分配的对象不能随意交给 C 侧长期使用

内存问题是 DLL 场景里最常见的坑之一。由 Go 管理的字符串或其他对象,不能简单返回给 C 代码后让对方长期持有。更稳妥的做法,是在 Go 中分配 C 可管理的内存,或者让调用方先分配缓冲区再传入。

这条规则的本质是:接口边界一旦跨到 C 世界,内存归属必须说清楚。谁申请,谁释放;在哪个运行时下分配,就尽量在哪个运行时下回收。否则,泄漏和非法访问都很容易出现。

多线程调用时要考虑并发安全

如果 DLL 可能被多个线程同时调用,导出的 Go 函数就必须保证线程安全。尤其当函数内部读写共享状态时,更要明确同步策略。

原文里还特别提醒了一点:更建议在 DLL 初始化阶段创建 Goroutine,而不是在每个导出函数里临时创建。这类约束并不是语法层面的硬错误,但在真实项目中会明显影响稳定性和调用行为,值得在接口设计阶段就提前考虑。

更实用的例子:字符串处理 DLL

数学计算示例更适合说明构建流程,而真正进入跨语言调用时,字符串处理往往更能暴露接口设计的细节。下面这个例子展示了一个更贴近实际的字符串工具库写法:

package main

/*
#include <stdlib.h>
*/
import "C"

import (
    "strings"
    "unsafe"
)

//export ToUpperCase
func ToUpperCase(s *C.char) *C.char {
    return C.CString(strings.ToUpper(C.GoString(s)))
}

//export FreeMemory
func FreeMemory(p unsafe.Pointer) {
    C.free(p)
}

// ReverseString、ConcatStrings 等函数的实现类似
// 使用 C.GoString 转换输入,C.CString 返回结果

func main() {}

这个案例虽然不长,但把 Go DLL 最关键的字符串处理逻辑基本都带出来了。

输入参数:先把 C 字符串转成 Go 字符串

ToUpperCase 接收的是 *C.char,进入函数后先通过 C.GoString(s) 转成 Go 可处理的字符串,再交给 strings.ToUpper 做业务逻辑。这是跨语言字符串输入最常见的一步。

返回结果:用 C.CString 构造可交给外部的字符串

处理结果不能直接把 Go 字符串返回出去,而是要通过 C.CString 生成 C 侧可识别的字符串指针。这样调用方拿到的是标准 C 风格字符串,更容易在 C/C++、Python 或 C# 封装层中处理。

为什么必须提供 FreeMemory

一旦使用 C.CString 创建返回值,就意味着这块内存需要显式释放。因此示例里额外导出了 FreeMemory:

func FreeMemory(p unsafe.Pointer) {
    C.free(p)
}

这一点很关键。调用方在使用完返回的字符串后,需要主动调用 FreeMemory 回收内存,否则很容易造成泄漏。对于字符串反转、拼接、大小写转换这类频繁调用的接口,如果不把释放约定设计清楚,问题通常会在运行一段时间后才暴露出来。

这个字符串 DLL 的编译方式与前面的数学示例一致:

go build -buildmode=c-shared -o stringutil.dll main.go

也就是说,真正变化的不是构建命令,而是导出函数的签名设计和内存处理方式。很多项目第一次做 Go DLL 时,编译阶段并不难,难的是把接口收敛成一个外部语言容易接、又不容易出错的 C 风格边界。

适合哪些场景,关键判断点是什么

把 Go 编译成 Windows DLL,核心方法并不复杂:使用 //export 标记导出函数,保持接口类型尽量 C 兼容,再通过 go build -buildmode=c-shared 生成 DLL 和对应头文件。

这条路线特别适合几类场景:一是给 Python、C++、C# 等现有项目补充高性能模块;二是把 Go 中已经写好的业务逻辑封装成可复用组件;三是在保留原有系统主体不变的前提下,用 DLL 方式逐步引入 Go 的实现能力。

但在真正落地前,最好先判断三件事:你的接口能否收敛成基础 C 类型、字符串或缓冲区的内存归属能否说清楚、调用方是否存在多线程并发访问。如果这三点都能设计明白,Go 写 Windows DLL 通常是可行且成本不高的方案。

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表