- 第 1 章 语言基础
- 1.1 Go语言创世纪
- 1.2 Hello, World 的革命
- 1.3 数组、字符串和切片
- 1.4 函数、方法和接口
- 1.5 面向并发的内存模型
- 1.6 常见的并发模式
- 1.7 错误和异常
- 1.8 补充说明
- 第 2 章 CGO 编程
- 2.1 快速入门
- 2.2 CGO基础
- 2.3 类型转换
- 2.4 函数调用
- 2.5 内部机制
- 2.6 实战: 封装qsort
- 2.7 CGO内存模型
- 2.8 C++类包装
- 2.9 静态库和动态库
- 2.10 编译和链接参数
- 2.11 补充说明
- 第3章 汇编语言
- 3.1 快速入门
- 3.2 计算机结构
- 3.3 常量和全局变量
- 3.4 函数
- 3.5 控制流
- 3.6 再论函数
- 3.7 汇编语言的威力
- 3.8 例子:Goroutine ID
- 3.9 Delve调试器
- 3.10 补充说明
- 第4章 RPC和Protobuf
- 4.1 RPC入门
- 4.2 Protobuf
- 4.3 玩转RPC
- 4.4 gRPC入门
- 4.5 gRPC进阶
- 4.6 gRPC和Protobuf扩展
- 4.7 pbgo: 基于Protobuf的框架
- 4.8 grpcurl工具
- 4.9 补充说明
- 第5章 Go和Web
- 5.1 Web开发简介
- 5.2 请求路由
- 5.3 中间件
- 5.4 请求校验
- 5.5 和数据库打交道
- 5.6 服务流量限制
- 5.7 大型Web项目分层
- 5.8 接口和表驱动开发
- 5.9 灰度发布和A/B测试
- 5.10 补充说明
- 第6章 分布式系统
- 6.1 分布式 id 生成器
- 6.2 分布式锁
- 6.3 延时任务系统
- 6.4 分布式搜索引擎
- 6.5 负载均衡
- 6.6 分布式配置管理
- 6.7 分布式爬虫
- 6.8 补充说明
- 附录
- 附录A: Go语言常见坑
- 附录B: 有趣的代码片段
- 附录C: 作者简介
2.4 函数调用
函数是C语言编程的核心,通过CGO技术我们不仅仅可以在Go语言中调用C语言函数,也可以将Go语言函数导出为C语言函数。
2.4.1 Go调用C函数
对于一个启用CGO特性的程序,CGO会构造一个虚拟的C包。通过这个虚拟的C包可以调用C语言函数。
/*
static int add(int a, int b) {
return a+b;
}
*/
import "C"
func main() {
C.add(1, 1)
}
以上的CGO代码首先定义了一个当前文件内可见的add函数,然后通过C.add
。
2.4.2 C函数的返回值
对于有返回值的C函数,我们可以正常获取返回值。
/*
static int div(int a, int b) {
return a/b;
}
*/
import "C"
import "fmt"
func main() {
v := C.div(6, 3)
fmt.Println(v)
}
上面的div函数实现了一个整数除法的运算,然后通过返回值返回除法的结果。
不过对于除数为0的情形并没有做特殊处理。如果希望在除数为0的时候返回一个错误,其他时候返回正常的结果。因为C语言不支持返回多个结果,因此<errno.h>
标准库提供了一个errno
宏用于返回错误状态。我们可以近似地将errno
看成一个线程安全的全局变量,可以用于记录最近一次错误的状态码。
改进后的div函数实现如下:
#include <errno.h>
int div(int a, int b) {
if(b == 0) {
errno = EINVAL;
return 0;
}
return a/b;
}
CGO也针对<errno.h>
标准库的errno
宏做的特殊支持:在CGO调用C函数时如果有两个返回值,那么第二个返回值将对应errno
错误状态。
/*
#include <errno.h>
static int div(int a, int b) {
if(b == 0) {
errno = EINVAL;
return 0;
}
return a/b;
}
*/
import "C"
import "fmt"
func main() {
v0, err0 := C.div(2, 1)
fmt.Println(v0, err0)
v1, err1 := C.div(1, 0)
fmt.Println(v1, err1)
}
运行这个代码将会产生以下输出:
2 <nil>
0 invalid argument
我们可以近似地将div函数看作为以下类型的函数:
func C.div(a, b C.int) (C.int, [error])
第二个返回值是可忽略的error接口类型,底层对应 syscall.Errno
错误类型。
2.4.3 void函数的返回值
C语言函数还有一种没有返回值类型的函数,用void表示返回值类型。一般情况下,我们无法获取void类型函数的返回值,因为没有返回值可以获取。前面的例子中提到,cgo对errno做了特殊处理,可以通过第二个返回值来获取C语言的错误状态。对于void类型函数,这个特性依然有效。
以下的代码是获取没有返回值函数的错误状态码:
//static void noreturn() {}
import "C"
import "fmt"
func main() {
_, err := C.noreturn()
fmt.Println(err)
}
此时,我们忽略了第一个返回值,只获取第二个返回值对应的错误码。
我们也可以尝试获取第一个返回值,它对应的是C语言的void对应的Go语言类型:
//static void noreturn() {}
import "C"
import "fmt"
func main() {
v, _ := C.noreturn()
fmt.Printf("%#v", v)
}
运行这个代码将会产生以下输出:
main._Ctype_void{}
我们可以看出C语言的void类型对应的是当前的main包中的_Ctype_void
类型。其实也将C语言的noreturn函数看作是返回_Ctype_void
类型的函数,这样就可以直接获取void类型函数的返回值:
//static void noreturn() {}
import "C"
import "fmt"
func main() {
fmt.Println(C.noreturn())
}
运行这个代码将会产生以下输出:
[]
其实在CGO生成的代码中,_Ctype_void
类型对应一个0长的数组类型[0]byte
,因此fmt.Println
输出的是一个表示空数值的方括弧。
以上有效特性虽然看似有些无聊,但是通过这些例子我们可以精确掌握CGO代码的边界,可以从更深层次的设计的角度来思考产生这些奇怪特性的原因。
2.4.4 C调用Go导出函数
CGO还有一个强大的特性:将Go函数导出为C语言函数。这样的话我们可以定义好C语言接口,然后通过Go语言实现。在本章的第一节快速入门部分我们已经展示过Go语言导出C语言函数的例子。
下面是用Go语言重新实现本节开始的add函数:
import "C"
//export add
func add(a, b C.int) C.int {
return a+b
}
add函数名以小写字母开头,对于Go语言来说是包内的私有函数。但是从C语言角度来看,导出的add函数是一个可全局访问的C语言函数。如果在两个不同的Go语言包内,都存在一个同名的要导出为C语言函数的add函数,那么在最终的链接阶段将会出现符号重名的问题。
CGO生成的 _cgo_export.h
文件回包含导出后的C语言函数的声明。我们可以在纯C源文件中包含 _cgo_export.h
文件来引用导出的add函数。如果希望在当前的CGO文件中马上使用导出的C语言add函数,则无法引用 _cgo_export.h
文件。因为_cgo_export.h
文件的生成需要依赖当前文件可以正常构建,而如果当前文件内部循环依赖还未生成的_cgo_export.h
文件将会导致cgo命令错误。
#include "_cgo_export.h"
void foo() {
add(1, 1);
}
当导出C语言接口时,需要保证函数的参数和返回值类型都是C语言友好的类型,同时返回值不得直接或间接包含Go语言内存空间的指针。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论