深入学习CGO

article/2025/8/30 1:31:14

深入学习CGO

  • 快速入门
  • 基础知识
    • import "C" 语句
    • `#cgo`语句
  • GO与C的类型转换
  • CGO函数调用
  • CGO内部机制
  • CGO内存模型
  • C++类封装成C API
  • CGO调用在go runtime 层面的处理
  • CGO的静态/动态库封装以及编译链接参数
  • CGO定位内存泄露
  • CGO性能
  • CGO最佳使用场景总结
  • 参考文献:

很多场景下我们希望在Go中利用好已有的C/C++库,Go语言通过自带的一个叫CGO的工具来支持C语言函数调用。

本文主要focus在 Go调用C函数的场景。

快速入门

CGO 一般面向C 的接口编程,下面给出一个最基本的,通过CGO在GO中调用CGO的函数打印字符串:

这里我们定义一个 SayHello 的C函数来实现打印,然后从Go语言环境中调用这个SayHello函数:

package main/*
#include <stdio.h>
static void SayHello(const char* s) {puts(s);
}*/
import "C"func main() {C.SayHello(C.CString("Hello, World\n"))
}
  1. import "C" 这个是必须导入的,表示启用CGO
  2. import "C" 上面的注释是内嵌的C代码(也可以通过.c和.h文件封装)
  3. C.CString 函数可以将go中字符串通过拷贝的形式转换成C中的char*。这里实际上是通过C的malloc函数申请的内存,所以需要在Go中手动free掉。这里没有做free是因为程序退出会自动清理进程所有资源。

基础知识

import “C” 语句

要使用CGO特性,需要安装C/C++构建工具链,在macOS和Linux下是要安装GCC,在windows下是需要安装MinGW工具。同时需要保证环境变量CGO_ENABLED被设置为1,这表示CGO是被启用的状态。

在Go代码中出现了import "C"语句则表示使用了CGO特性,紧跟在这行语句前面的注释是一种特殊语法,里面包含的是正常的C语言代码。当确保CGO启用的情况下,还可以在当前目录中包含C/C++对应的源文件。

需要注意的是,import “C”导入语句需要单独一行,不能与其他包一同import。向C函数传递参数也很简单,就直接转化成对应C语言类型传递就可以。

#cgo语句

在import "C"语句前的注释中可以通过#cgo语句设置编译阶段和链接阶段的相关参数。编译阶段的参数主要用于定义相关宏和指定头文件检索路径。链接阶段的参数主要是指定库文件检索路径和要链接的库文件。

// #cgo CFLAGS: -DPNG_DEBUG=1 -I./include
// #cgo LDFLAGS: -L/usr/local/lib -lpng
// #include <png.h>
import "C"

上面的代码中,CFLAGS部分,-D部分定义了宏PNG_DEBUG,值为1;-I定义了头文件包含的检索目录。LDFLAGS部分,-L指定了链接时库文件检索目录,-l指定了链接时需要链接png库。

因为C/C++遗留的问题,C头文件检索目录可以是相对目录,但是库文件检索目录则需要绝对路径。

#cgo语句主要影响CFLAGS、CPPFLAGS、CXXFLAGS、FFLAGS和LDFLAGS几个编译器环境变量。LDFLAGS用于设置链接时的参数,除此之外的几个变量用于改变编译阶段的构建参数(CFLAGS用于针对C语言代码设置编译参数)。

GO与C的类型转换

在这里插入图片描述
为了提高C语言的可移植性,在<stdint.h>文件中,不但每个数值类型都提供了明确内存大小,而且和Go语言的类型命名更加一致。
在这里插入图片描述
CGO的C虚拟包提供了以下一组函数,用于Go语言和C语言之间数组和字符串的双向转换:

// Go string to C string
// The C string is allocated in the C heap using malloc.
// It is the caller's responsibility to arrange for it to be
// freed, such as by calling C.free (be sure to include stdlib.h
// if C.free is needed).
func C.CString(string) *C.char
// Go []byte slice to C array
// The C array is allocated in the C heap using malloc.
// It is the caller's responsibility to arrange for it to be
// freed, such as by calling C.free (be sure to include stdlib.h
// if C.free is needed).
func C.CBytes([]byte) unsafe.Pointer
// C string to Go string
func C.GoString(*C.char) string
// C data with explicit length to Go string
func C.GoStringN(*C.char, C.int) string
// C data with explicit length to Go []byte
func C.GoBytes(unsafe.Pointer, C.int) []byte

其中C.CString针对输入的Go字符串,克隆一个C语言格式的字符串;返回的字符串由C语言的malloc函数分配,不使用时需要通过C语言的free函数释放。C.CBytes函数的功能和C.CString类似,用于从输入的Go语言字节切片克隆一个C语言版本的字节数组,同样返回的数组需要在合适的时候释放。C.GoString用于将从NULL结尾的C语言字符串克隆一个Go语言字符串。C.GoStringN是另一个字符数组克隆函数。C.GoBytes用于从C语言数组,克隆一个Go语言字节切片。

该组辅助函数都是以克隆的方式运行。当Go语言字符串和切片向C语言转换时,克隆的内存由C语言的malloc函数分配,最终可以通过free函数释放。当C语言字符串或数组向Go语言转换时,克隆的内存由Go语言分配管理。通过该组转换函数,转换前和转换后的内存依然在各自的语言环境中,它们并没有跨越Go语言和C语言。克隆方式实现转换的优点是接口和内存管理都很简单,缺点是克隆需要分配新的内存和复制操作都会导致额外的开销。

CGO函数调用

这里给出一个例子,展示Go通过接口调用模块化的C函数:

我们可以抽象一个名为hello的模块,模块的全部接口函数都在hello.h头文件定义:

//hello.h
void SayHello(const char* s);

其中只有一个SayHello函数的声明。但是作为hello模块的用户来说,就可以放心地使用SayHello函数,而无需关心函数的具体实现。而作为SayHello函数的实现者来说,函数的实现只要满足头文件中函数的声明的规范即可。下面是SayHello函数的C语言实现,对应hello.c文件:

//hello.c
#include <stdio.h>
#include "hello.h"void SayHello(const char* s) {puts(s);
}

在hello.c文件的开头,实现者通过#include "hello.h"语句包含SayHello函数的声明,这样可以保证函数的实现满足模块对外公开的接口。

接口文件hello.h是hello模块的实现者和使用者共同的约定,但是该约定并没有要求必须使用C语言来实现SayHello函数。我们也可以用C++语言来重新实现这个C语言函数:

#include <iostream>//通过extern "C"语句指示该函数的链接符号遵循C语言的规则。
extern "C" {#include "hello.h"
}void SayHello(const char* s) {std::cout << s;
}

在C++版本的SayHello函数实现中,我们通过C++特有的std::cout输出流输出字符串。不过为了保证C++语言实现的SayHello函数满足C语言头文件hello.h定义的函数规范,我们需要通过extern "C"语句指示该函数的链接符号遵循C语言的规则。

最后就是运行CGO的main函数:

package main//#include "hello.h"
import "C"func main() {C.SayHello(C.CString("Hello, World\n"))
}

CGO内部机制

TODO

CGO内存模型

CGO是架接Go语言和C语言的桥梁,它使二者在二进制接口层面实现了互通,但是我们要注意因两种语言的内存模型的差异而可能引起的问题。

这里有一些关键点我们需要注意:

  1. C语言的内存在分配之后就是稳定的,只要不是被人为提前释放,那么在Go语言空间可以放心大胆地使用。
  2. Go语言的栈始终是可以动态伸缩的(动态栈)。
  3. GC 导致 Go语言内存生命周期不固定。
  4. cgo调用的C函数返回前, 传入的Go内存有效。
  5. cgo调用的C函数返回后, Go内存对C语言失效。
  6. CGO的的调用类似于系统调用,会阻塞原协程。并且C函数的执行会切换到g0,也就是C函数是在系统线程执行的,也就是内核线程栈。
  7. C中栈内存不能返回(函数调用返回就被回收)。

借助C语言内存稳定的特性,在C语言空间先开辟同样大小的内存,然后将Go的内存填充到C的内存空间;返回的内存也是如此处理。下面的例子是这种思路的具体实现:

package main/*
#include <stdio.h>
#include <stdlib.h>
void printString(const char* s) {printf("%s", s);
}
*/
import "C"
import "unsafe"func printString(s string) {var cs *C.char  = C.CString(s)C.printString(cs)C.free(unsafe.Pointer(cs))
}
func main() {s := "hello"printString(s)
}

在需要将Go的字符串传入C语言时,先通过C.CString将Go语言字符串对应的内存数据复制到新创建的C语言内存空间上。上面例子的处理思路虽然是安全的,但是效率极其低下(因为要多次分配内存并逐个复制元素),同时也极其繁琐。

为了简化并高效处理此种向C语言传入Go语言内存的问题,cgo针对该场景定义了专门的规则:在CGO调用的C语言函数返回前,cgo保证传入的Go语言内存在此期间不会发生移动,C语言函数可以大胆地使用Go语言的内存!

C++类封装成C API

TODO
参考:C++ 类包装

CGO调用在go runtime 层面的处理

CGO调用的入口在runtime.cgocall函数: cgocall.go(1.14)

这里先简单翻译下该文件里面的一些注释(只针对Go调用C的场景):

要从Go中调用C函数,cgo生成的代码会调用 runtime.cgocall(_cgo_Cfunc_f, frame),其中_cgo_Cfunc_f对应的C函数。

runtime.cgocall会调用 entersyscall 进入系统调用以避免阻塞其余协程的调度或则垃圾回收器。然后调用 runtime.asmcgocall(_cgo_Cfunc_f, frame)

runtime.asmcgocall 是汇编实现的,该函数会切换内核线程的 g0 栈(也就是操作系统分配的堆栈),因此可以安全的运行gcc编译的代码以及调用_cgo_Cfunc_f

_cgo_Cfunc_f会调用实际的C函数,并拿到执行的结果,然后返回给runtime.asmcgocall

等当前协程重新获取控制后,runtime.asmcgocall 会切换回原来的go协程的栈,并返回到runtime.cgocall.

等当前协程重新获取控制后, runtime.cgocall会调用exitsyscall,该函数会阻塞直到m能够运行当前协程。

这里粘贴出部分源码:

func cgocall(fn, arg unsafe.Pointer) int32 {......mp := getg().mmp.ncgocall++mp.ncgo++// Reset traceback.mp.cgoCallers[0] = 0// 宣布正在进入系统调用,从而调度器会创建另一个 M 来运行 goroutineentersyscall()// Tell asynchronous preemption that we're entering external// code. We do this after entersyscall because this may block// and cause an async preemption to fail, but at this point a// sync preemption will succeed (though this is not a matter// of correctness).osPreemptExtEnter(mp)mp.incgo = true// asmcgocall 是汇编实现, 它会切换到m的g0栈,然后调用_cgo_Cfunc_main函数errno := asmcgocall(fn, arg)// Update accounting before exitsyscall because exitsyscall may// reschedule us on to a different M.mp.incgo = falsemp.ncgo--osPreemptExtExit(mp)// 宣告退出系统调用,等待runtime调度器重新M去执行exitsyscall()// Note that raceacquire must be called only after exitsyscall has// wired this M to a P.if raceenabled {raceacquire(unsafe.Pointer(&racecgosync))}// 从垃圾回收器的角度来看,时间可以按照上面的顺序向后移动。// 如果对 Go 代码进行回调,GC 将在调用 asmcgocall 时能看到此函数。// 当 Go 调用稍后返回到 C 时,系统调用 PC/SP 将被回滚并且 GC 在调用// enteryscall 时看到此函数。通常情况下,fn 和 arg 将在 enteryscall 上运行// 并在 asmcgocall 处死亡,因此如果时间向后移动,GC 会将这些参数视为已死,// 然后生效。通过强制它们在这个时间中保持活跃来防止这些未死亡的参数崩溃KeepAlive(fn)KeepAlive(arg)KeepAlive(mp)return errno
}

CGO的静态/动态库封装以及编译链接参数

这块这里不准备细说,基本是一些编译静/动态库的参数,以及编译链接的参数的一些注意事项:
参考:
静态库和动态库
编译和链接参数

CGO定位内存泄露

valgrind 能够很方便的定位C/C++中的内存泄漏问题。对于CGO的场景,valgrind能够很快定位C函数中的内存泄漏;但是valgrind对Go代码中的内存泄漏(比如Go中调用C.CString函数不手动free),检测能力有限,只能提示内存泄漏大概位置,没法精准定位。

对于Go中的pprof工具,是没法定位CGO的内存泄漏问题,猜测是因为:Go的pprof只会检测Go垃圾回收器申请和释放的内存,C.CString以及c代码中的内存申请都没有经过gc,所以无法监测。

参考:Go语言使用cgo时的内存管理笔记

CGO性能

我们使用CGO一般有几个场景考虑(个人观点:)

  1. 继承C/C++历史积累的优秀库;
  2. 历史遗留项目的改造。

从网上各路文章中能够回到,CGO通过go去调用C是有比较大的性能开销的。造成性能开销原因有很多:

  1. 必须切换go的协程栈到系统线程的主栈去执行C函数
  2. 涉及到系统调用以及协程的调度。
  3. 由于需要同时保留C/C++的运行时,CGO需要在两个运行时和两个ABI(抽象二进制接口)之间做翻译和协调。这就带来了很大的开销。

这里我做了个测试,通过CGO调用一个空的C函数以及Go调用原生的空函数的性能损耗:

package main/*
//#include <stdio.h>
//#include <stdlib.h>
void printString() {}
*/
import "C"
import ("fmt""time"
)func main() {s := time.Now().UnixNano()for i := 0; i < 100000000; i++ {C.printString()}e := time.Now().UnixNano()fmt.Println("cgo:", e-s, "ns")s = time.Now().UnixNano()for i := 0; i < 100000000; i++ {empty()}e = time.Now().UnixNano()fmt.Println("go:", e-s, "ns")
}func empty() {}

测试机器:15款小MacPro;
这里的测试不是非常精准,因为涉及到go里面的循环,但是大概能够说明问题,测试结果显示:

cgo: 7765102000 ns
go:       52018000 ns

可以看到性能差距是非常明显的,每次CGO调用性能损耗在77ns左右。

所以CGO适用场景是有限制的,并不适合与高性能,高频调用场景。

CGO最佳使用场景总结

先说一下使用CGO的一些缺点:

1. 内存隔离
2. C函数执行切换到g0(系统线程)
3. 收到GOMAXPROC线程限制
4. CGO空调用的性能损耗(50+ns)
5. 编译损耗(CGO其实是有个中间层)

CGO 适合的场景:

1. C 函数是个大计算任务(不在乎CGO调用性能损耗)
2. C 函数调用不频繁
3. C 函数中不存在阻塞IO
4. C 函数中不存在新建线程(与go里面协程调度由潜在可能互相影响)
5. 不在乎编译以及部署的复杂性

参考文献:

CGO 和 CGO 性能之谜
why-cgos-performance-is-so-slow-is-there-something-wrong-with-my-testing-code
Go语言使用cgo时的内存管理笔记
如何把Go调用C的性能提升10倍?
Go原本-CGO
深入CGO编程


http://chatgpt.dhexx.cn/article/sBwJ57PK.shtml

相关文章

快速上手 CGO,掌握在 Go 里写 C!

大家好&#xff0c;最近因为各种奇怪的原因&#xff0c;接触到了 Go 特色之一 CGO。这方面的相关内容也相对少一些&#xff0c;给大家抛砖引玉&#xff0c;有经验的大佬欢迎补充。 图片来源于 marlin 毕竟很多跨语言调用&#xff0c;还是会依赖 CGO 这个特性。希望大家在真正要…

DevOps学习心得总结

流程步骤&#xff1a; 1、PLAN 制定计划 &#xff08;牢记交付给用户的目标&#xff09; 2、CODE 开始编码 &#xff08;使用相同的代码&#xff0c;不同版本的代码存储到仓库中&#xff0c;借助Git等工具在需要时合并【版本控制】&#xff09; 3、BUILD 构建阶段…

DevOps工具链

DevOps是敏捷研发中持续构建&#xff08;Continuous Build&#xff0c;CB&#xff09;、持续集成&#xff08;Continuous Integration&#xff0c;CI&#xff09;、持续交付&#xff08;Continuous Delivery&#xff0c;CD&#xff09;的自然延伸&#xff0c;从研发周期向右扩展…

DevOps及DevOps常用的工具介绍

目录 1. 什么是 DevOps2. DevOps 概念的起源2.1. 单体架构 瀑布模式2.2. 分布式架构 敏捷开发模式2.2.1. 多人协同开发问题2.2.2. 多机器问题2.2.3. 开发和运维角色的天生对立问题 2.3. 微服务架构 DevOps 3. DevOps 到底是什么4. DevOps 常用的工具4.1. Jenkins4.2. Kubern…

DevOps 简史

【注】本文节译自&#xff1a;https://www.bmc.com/blogs/devops-history/   IT 行业的当前状态受技术进步在整个历史中所产生的连锁效应所影响。不时出现的新技术极大地改变了世界运转的方式。最近&#xff0c;技术进步似乎开始以惊人的速度出现。自从互联网出现以来&#…

DevOps 学习

目录 一、概述 1、CI/CD简介 二、Git简介 三、Jenkins简介 一、概述 DevOps是Development和Operations的组合&#xff0c;也就是开发和运维的简写。 DevOps集文化理念、实践与工具于一身&#xff0c;可以提高组织高速交付应用程序和服务的能力&#xff0c;与使用传统软件…

DevOps实践

数字化时代&#xff0c;技术的交付速度和质量&#xff0c;直接关系业务的发展和创新。IT 技术交付和运行的效率&#xff0c;成为决定数字化转型成败的关键&#xff0c;而 DevOps 要解决的问题正在于此&#xff0c;DevOps 成为数字化转型的重要一环。 能力构建 随着云原生技术的…

DevOps的前世今生

导语 DevOps诞生已经13年了&#xff0c;你理解他吗&#xff1f; 为什么相伴了13年&#xff0c;你仍然对他不甚了了呢&#xff1f; 你真的以为DevOps是一个筐&#xff0c;什么东西都可以往里装吗&#xff1f; 你以为DevOps落地就是找一个JIRA&#xff08;敏捷管理工具&#…

Learning DevOps

什么是 DevOps DevOps&#xff08;Development & Operations&#xff09;/de’vps/ 是一组过程、方法与系统的统称&#xff0c;用于促进开发 (Dev)、技术运营 (Ops)和质量保障&#xff08;QA&#xff09;部门之间的沟通、协作与整合。 DevOps 的开发流程 软件从零开始到…

DevOps思想

什么是DevOps&#xff1f; DevOps是一种思想或方法论&#xff0c;它涵盖了开发、测试、运维的整个过程&#xff01;DevOps强调开发、测试、运维、质检(QA)部门之间的有效沟通与协作。强调通过自动化的方法管理软件变更、软件集成。使软件从构建到测试、发布更加快捷、可靠&…

DevOps的发展史

公众号关注 「奇妙的 Linux 世界」 设为「星标」&#xff0c;每天带你玩转 Linux &#xff01; — 1 — 可操作的概述 多亏了云计算和开源&#xff0c;软件开发的速度从几年缩短到几个月。每家公司都在向一个软件公司转变。DevOps 已迅速成为公司大规模开发和部署软件的最有效方…

DevOps——简析

节选自百度等资料 知乎解析连接 一、DevOps的目的 只有一个&#xff1a;提高开发到运维发布版本的效率。 1.初级应用&#xff1a;开发运维一体化 2.最高阶的应用&#xff1a;端到端的概念。 DevOps 的三大支柱之中&#xff0c;即人&#xff08;People&#xff09;、流程&…

DevOps推广实践总结

中大型团队在敏捷DevOps转型过程中常见的实践总结 目录 1、聘用外部DevOps顾问 2、建立DevOps共识 3、采用“DevOps改进”而非“DevOps转型” 4、构建“比学赶超”的组织氛围 5、规范化DevOps实践 1、聘用外部DevOps顾问 小型团队可以不用聘用昂贵的外部教练&#xff0c;因…

DevOps

DevOps 一、DevOps的由来和概念1. 由来2. DevOps概念解析&#xff08;1&#xff09;来自不同渠道和来源的定义&#xff1a;&#xff08;2&#xff09;其他摘录 二、DevOps 工作流程1. DevOps的好处与价值2. DevOps能力环 三、devops流程工具四、DevOps发展现状哪些互联网公司采…

Devops的概念

1、什么是DevOps&#xff1f; 答&#xff1a;DevOps是产品开发过程中开发&#xff08;Dev&#xff09;和运营&#xff08;Ops&#xff09;团队之间的灰色区域。DevOps是一种在产品开发周期中强调沟通&#xff0c;集成和协作的文化。因此&#xff0c;它消除了软件开发团队和运营…

DevOps—基本概念

DevOps—基本概念 1. DevOps2. CI/CD 1. DevOps 维基百科定义&#xff1a; DevOps是一组过程、方法与系统的统称&#xff0c;用于促进 开发、技术运营 和 质量保障&#xff08;QA&#xff09; 部门之间的沟通、协作与整合。我理解DevOps是一种软件管理思维模式。 为什么会有D…

DevOps简介

一、DevOps定义&#xff1a;Development和Operations的组合&#xff0c;突出重视软件开发人员与运维人员的沟通合作&#xff0c;通过自动化流程使得软件构建、测试、发布更加快捷、频繁和可靠。 它是一个完整的面向IT运维的工作流&#xff0c;以 IT 自动化以及持续集成&#xf…

什么是 DevOps?看这一篇就够了!

文章目录 一、前因二、记忆三、他们说……3.1、Atlassian 回答“什么是 DevOps&#xff1f;”3.2、微软回答“什么是 DevOps&#xff1f;”3.3、AWS 回答“什么是 DevOps&#xff1f;” 四、DevOps 文化4.1、什么是文化&#xff1f;4.2、什么是 DevOps 文化&#xff1f;4.3、领…

Devops基本概念和原理

一、什么是DevOps 1、 DevOps概述 DevOps&#xff0c;即Development and Operations&#xff0c;是一组过程、方法与系统的统称&#xff0c;用于促进软件开发、运维和质量保障部门之间的沟通、协作与整合。DevOps的出现是由于软件行业日益清晰的认识到&#xff1a;为了按时交…

DevOps概念及搭建全过程(Jenkins、Harbor、SonarQube、K8s)

DevOps入门及过程搭建 在如今互联网的格局下&#xff0c;抢占市场变得尤为重要&#xff0c;因此敏捷开发越来越被大家所推崇。于是&#xff0c;慢慢的有了DevOps这个概念&#xff0c;大致意思是开发-运维一体化。 1 DevOps概念 1.1 基本概念 可以看到上图是一个无穷大的一个符…