迫在眉睫的交付期限,不切实际的进度计划,无休止的按时开发并发布应用程序的压力,并且还要保证质量。是不是很熟悉的感觉?对于嵌入式开发团队来说,尽快实现嵌入式应用程序的发布是一个很重要的事项。那么,有没有一个捷径既能快速交付应用程序,同时又能优先确保应用质量和安全性呢?在这一思路下,OTA软件更新管理软件开发团队 Mender.io,就Mender的嵌入式客户端和服务器端部分的开发,做了一个最佳编程软件评估,Go、C和C++入围。最终,Go被选中。下面,通过评估过程中优劣判断对比,分析一下Go能够胜出的原因。
尽管这种选择带有一定的主观性,但Go确实是一种非常高效的嵌入式开发语言,尤其是涉及到网络编程时。网络编程在某些时候是遍布每个连接的设备或应用程序的。Go语言由Google创建,起初是为了满足Google开发人员的需求,用于面对其生态系统中爆炸式增长的复杂性。因此,GO语言自面世就遵循了高效编译、高效执行和易于编程这三个原则,这是它较之其他主流语言的优势。
但是,需要强调的是,Go并不能被视为C语言的替代品。很多时候C语言仍是无可替代的,例如实时操作系统和设备驱动程序的开发。
嵌入式开发的严格要求
建立架构后,Mender产品工程团队开始评估哪种语言最适合开发 Mender应用程序。该系统由两部分组成:一是在嵌入式设备上运行的客户端,二是连接各客户端的中心服务器。因此,对该语言有以下几个要求:
- 客户端应用程序运行于嵌入式设备之上,要求编译的二进制文件尽可能地小;
- 能够与Linux的嵌入式发行版本Yocto兼容。
- 客户端要易于安装,不需要依赖外部项和库。
- 由于需要在不同的设备上运行,故该语言须具备跨体系结构编译能力。
- Mender客户端应用程序运行的设备会是IoT或网络设备,因此该语言需要具备访问网络库能力。
此外还有一些非功能性要求:
- 此语言能被部门大多数程序员理解掌握;
- 尽可能无难度地实现与现有的C语言编写的应用程序之间共享和重用已有代码,客户端和服务器应用程序之间亦可重用代码。
- 还要考虑开发速度——团队时刻面临着快速添加新功能的需求和压力。
Go、C 和 C++的比较如图 1 所示。Go之所以被选择,主要是由于它支持缓冲区溢出保护、自动内存管理、使用标准数据容器,以及对JSON、HTTP和SSL/TLS库的集成支持。
图 1:Go、C 和 C++的功能表比较
评估团队使用Yocto创建一个完善的定制Linux映像,比较了原始的映像体积较之具有网络堆栈的映像体积,以及最终应用程序写入映像的方式。在这一评估过程中,Go是可以与C,C++和C++ / Qt相媲美的。由于Go应用程序可以静态编译为单个二进制文件,因此它并不需要任何额外的虚拟环境(比如Java除了二进制文件之外还需要虚拟机),Go代码在设备上运行也不需要依赖其他项。
图 2:Go 与其他编程语言进行比较
比较GO和C
选中Go语言进行开发,还源于它极其丰富的标准库。这一优势有助于开发进程的加快,特别是与C语言相比。Go语言从C、C++和Python继承了许多元素,包括表达式语句,控制流语句,数据结构,接口,指针,引用传递概念,参数解析语法,字符串处理和垃圾回收机制。凭借其优化的编译器,Go 可在嵌入式设备上实现代码本地运行。
当然,Go语言不可避免的会有一些缺点,后面也将提及。但瑕不掩瑜,这并不影响Go语言明显的优势,比如在开发速度和语言构建方面的便利性。这些对于评估团队的选择也有所影响,下面逐一来看。
Go和C代码体积比较
在代码体积方面,Go不如C轻便,这是它为数不多的缺点之一。以最简单的应用程序“hello world”为例,在Go使用内置printIn函数编写,去除调试符号后,它的大小为刚刚超过600KB(代码段1)。如果将fmt包及其依赖项包括在内,则大小将增至 1.5 MB(代码段 2)。
与之相比较,使用C语言的话,如果是构建一个动态链接库,则它的大小只有8KB。但如果使用静态链接库,则大小将增加到800 KB以上,比较令人意外的是它比Go二进制文件(代码段3)还要大。
代码段1:最基本的 Go语言 “hello world” 代码:
package main
func main() {
println("hello world")
}
$ go build
> 938K
$ go build -ldflags ‘-s -w’
> 682K
$ go build & strip
> 623K
代码段2:标准的Go语言“hello world” 代码:
package main
import "fmt"
func main() {
fmt.Println("hello world")
}
$ go build
> 1,5M
代码段3:C语言“hello world” 代码:
#include <stdio.h>
int main(void)
{
printf("hello world\n");
return 0;
}
$ gcc main.c
> 8,5K
$ ldd a.out
> linux-vdso.so.1
> libc.so.6
> /lib64/ld-linux-x86-64.so.2
$ gcc -static main.c
> 892K
$ gcc -static main.c & strip
> 821K
Go与C的运行速度比较
编译后的Go代码运行起来通常会比C语言的可执行文件慢。Go是具备完全垃圾收集机制的,生来就影响了运行速度。使用C语言时可以精确地指定为变量分配的内存的位置,可以具体到该变量是在栈上还是在堆上;而使用Go时,编译器会自动处理将变量分配在何处。程序员可以看到变量的分配位置 (go build -gcflags -m),但不能强制编译器仅使用摸个位置比如栈。
但是,速度不是仅限于上述,还要考虑到编译速度和开发速度。Go提供了极快的编译速度。比如,15,000行代码Go客户端仅需1.4秒就能编译完毕。并且,Go是为并发执行(goroutines和channels)而设计的,前面阐述过的丰富的标准库能够涵盖大部分基本需求,因此有利于程序员开发速度加快。
编译和交叉编译
有两种编译器可供程序员选择使用:原始编译器叫做gc ,默认安装程序自带,由Google编写和维护。第二种叫做gccgo,是GCC的前端。gccgo使用起来编译速度极快,大型模块通常几秒钟就可以编译完毕。如前所述,Go默认是以静态库编译,这样就可以无需额外依赖项或虚拟机创建单个二进制文件。可以使用-linkshared标志创建和使用共享库。通常,Go不需要构建文件,如需构建则可通过使用简单的“go build”命令执行,高级复杂的构建可以使用Makefile。这里是在Mender使用的Makefile。
除此之外,Go在交叉编译方面支持众多操作系统和架构。图 3 示例了 Go 支持的各种操作系统和平台。
图3 Go 支持的各种操作系统和平台
在 Go 中调试和测试
许多开发人员使用GDB监视程序执行情况。对于高并发Go应用程序,使用GDB调试时会出现一些问题。万幸的是,一个名为 Delve 的专用Go调试器是可以完美使用的,确保了即使是只会使用GDB的开发人员大多数情形下也能轻松调试。
Go代码中添加测试和单元测试十分简单,测试是内置在Go语言中的——只需创建一个带有“test”后缀的文件,为函数添加测试前缀,导入测试包,然后运行“go test”。所有测试就将自动从源中获取并相应执行。
并发支持
Go语言对并发支持特别友好。它有两个内置调度机制:goroutines和channels。goroutines是轻量级线程,仅仅2 kB。创建非常容易:只需要在函数前面添加“go”关键字,它就会同时执行。Go有自己的内部调度器,可以根据需要将goroutines多路复用到OS线程中。channels是用于在 goroutine 之间交换消息的“管道”,可以是阻塞的,也可以是通行的。
静态和动态库:Go语言中的的 C 代码
Go有许多鲜为人知的功能,允许程序员使用特殊标志将指令发送给编译器、连接器和工具链的其他部分。其中包括-buildmode和-linkshared标志,二者皆可用于为Go和C代码段创建并使用静态和动态库。通过特定标志的组合,程序员可以使用生成的C语言头文件创建静态或动态库,这些头文件以后可由C代码调用。
是的,没错!使用Go,可以调用C代码并拥有两全其美的功能。Go发行版包含一个工具名为cgo,可以执行C代码,这使得重用程序员自己的或系统的C库成为可能。实际上,Cgo以及C系统库的间接引用是十分常见的。某些情况下,标准C库例程就是由Go语言构造语法本身从Go实例中选取。
例如,在Unix系统上使用网络包时,很有可能会选择基于cgo的解析器。它会调用C库例程 ( getaddrinfo和 getnameinfo)来解析无法使用本机Go解析器的名称(例如在 OS X上直接 DNS 调用被禁用时);另一种常见情况则是使用系统/用户程序包时。如果想跳过构建cgo部件,程序员可以通过osusergo和netgo标记 (go build -tags osusergo,netgo)或使用CGO_ENABLED=0变量完全禁用 cgo。
将C和Go混合在一起时,还需要注意一些比较棘手的事情。由于Go是有垃圾回收机制的而 C不是,所以如果在Go代码中使用了任何分配在C代码堆栈上的C变量,都必须在 Go 中声明释放。
通过使用接口生成工具(SWIG)来实现,C++也可以在Go代码中使用。SWIGC能够将Go与C++以及Python等语言融合在一起。
Go的优点大于缺陷
从以往的经验可以得出Go既有优点也有缺陷。就负面因素来讲,社区中有很多外部库,但良莠不齐,需要非常小心辨别使用。随着Go语言的成熟和广泛采用,这种情况正在迅速改善。
此外,当Go代码中存在一些C绑定时,事情通常会变得更为复杂,尤其是交叉编译,如果不导入整个C交叉编译基础结构,就难以完成。
总体而言,使用Go进行嵌入式开发还是有很多优点的。从C或Python(或二者兼有)过渡到Go并在短期内使用是快速简单而可行的。Go语言提供了非常优秀的工具和一个包含100多个软件包的标准库。程序员可以轻松控制和设置运行参数,例如,要使用的操作系统线程数(GOMAXPROCS),打开或关闭垃圾回收 (GOGC=off)等。库以及代码可以在服务器和客户端开发团队之间轻松共享,就像文中提及的实例那样。
这样说可能会引起争论,但一致的编码标准事实上加快了开发速度。无论程序员要检查的代码是什么,它看起来是一致的。无需耗费时间在思考使用哪种编码标准上。
工程师们开发了一些嵌入式Go应用程序的演示,可以应用在新兴设备上(例如Beaglebone Black或Raspberry Pi):其中一个名为“恒温器”,基于Beaglebone硬件,使用红外距离、温度和湿度传感器,并可以通过 Web界面读取所有传感器读数;还有一个是基于PRi的机器人汽车,带有摄像头,可以通过Web界面进行控制。喜欢就去享受吧!
译者介绍
张哲刚,51CTO社区编辑,系统运维工程师,国内较早一批硬件评测及互联网从业者,曾入职阿里巴巴。十余年IT项目管理经验,具备复合知识技能,曾参与多个网站架构设计、电子政务系统开发,主导过某地市级招生考试管理平台运维工作。
原文标题:Comparing Go vs. C in embedded applications,作者:Marcin Pasinski