标签 汇编 下的文章

Go 1.5中值得关注的几个变化

GopherCon2015开幕之 际,Google Go Team终于放出了Go 1.5Beta1版本的安装包。在go 1.5Beta1的发布说明中,Go Team也诚恳地承认Go 1.5将打破之前6个月一个版本的发布周期,这是因为Go 1.5变动太大,需要更多时间来准备这次发布(fix bug, Write doc)。关于Go 1.5的变化,之前Go Team staff在各种golang技术会议的slide  中暴露不少,包括:

- 编译器和运行时由C改为Go(及少量汇编语言)重写,实现了Go的self Bootstrap(自举)
- Garbage Collector优化,大幅降低GC延迟(Stop The World),实现Gc在单独的goroutine中与其他user goroutine并行运行。
- 标准库变更以及一些go tools的引入。

每项变动都会让gopher激动不已。但之前也只是激动,这次beta1出来后,我们可以实际体会一下这些变动带来的“快感”了。Go 1.5beta1的发布文档目前还不全,有些地方还有“待补充”字样,可能与最终go 1.5发布时的版本有一定差异,不过大体内容应该是固定不变的了。这篇文章就想和大家一起浅显地体验一下go 1.5都给gophers们带来了哪些变化吧。

一、语言

【map literal】

go 1.5依旧兼容Go 1 language specification,但修正了之前的一个“小疏忽”。

Go 1.4及之前版本中,我们只能这么来写代码:

//testmapliteral.go
package main

import (
    "fmt"
)

type Point struct {
    x int
    y int
}

func main() {
    var sl = []Point{{3, 4}, {5, 6}}
    var m = map[Point]string{
        Point{3,4}:"foo1",
        Point{5,6}:"foo2",
    }
    fmt.Println(sl)
    fmt.Println(m)
}

可以看到,对于Point这个struct来说,在初始化一个slice时,slice value literal中无需显式的带上元素类型Point,即

var sl = []Point{{3, 4}, {5, 6}}

而不是

var sl = []Point{Point{3, 4}, Point{5, 6}}

但当Point作为map类型的key类型时,初始化map时则要显式带上元素类型Point。Go team承认这是当初的一个疏忽,在本次Go 1.5中将该问题fix掉了。也就是说,下面的代码在Go 1.5中可以顺利编译通过:

func main() {
    var sl = []Point{{3, 4}, {5, 6}}
    var m = map[Point]string{
        {3,4}:"foo1",
        {5,6}:"foo2",
    }
    fmt.Println(sl)
    fmt.Println(m)
}

【GOMAXPROCS】

就像这次GopherCon2015上现任Google Go project Tech Lead的Russ Cox的开幕Keynote中所说的那样:Go目标定位于高度并发的云环境。Go 1.5中将标识并发系统线程个数的GOMAXPROCS的初始值由1改为了运行环境的CPU核数。

// testgomaxprocs.go
package main

import (
    "fmt"
    "runtime"
)

func main() {
    fmt.Println(runtime.GOMAXPROCS(-1))
    fmt.Println(runtime.NumGoroutine())
}

这个代码在Go 1.4下(Mac OS X 4核)运行结果是:

$go run testgomaxprocs.go
1
4

而在go 1.5beta1下,结果为:

$go run testgomaxprocs.go
4
4

二、编译

【简化跨平台编译】

1.5之前的版本要想实现跨平台编译,需要到$GOROOT/src下重新执行一遍make.bash,执行前设置好目标环境的环境变量(GOOS和 GOARCH),Go 1.5大大简化这个过程,使得跨平台编译几乎与普通编译一样简单。下面是一个简单的例子:

//testcrosscompile.go
package main

import (
    "fmt"
    "runtime"
)

func main() {
    fmt.Println(runtime.GOOS)
}

在我的Mac上,本地编译执行:
$go build -o testcrosscompile_darwin testcrosscompile.go
$testcrosscompile_darwin
darwin

跨平台编译linux amd64上的目标程序:

$GOOS=linux GOARCH=amd64 go build -o testcrosscompile_linux testcrosscompile.go

上传testcrosscompile_linux到ubuntu 14.04上执行:
$testcrosscompile_linux
linux

虽然从用户角度跨平台编译命令很简单,但事实是go替你做了很多事情,我们可以通过build -x -v选项来输出编译的详细过程,你会发现go会先进入到$GOROOT/src重新编译runtime.a以及一些平台相关的包。编译输出的信息 太多,这里就不贴出来了。但在1.5中这个过程非常快(10秒以内),与1.4之前版本的跨平台编译相比,完全不是一个级别,这也许就是编译器用Go重写完的好处之一吧。

除了直接使用go build,我们还可以使用go tool compile和go tool link来编译程序,实际上go build也是调用这两个工具完成编译过程的。

$go tool compile testcrosscompile.go
testcrosscompile.o
$go tool link testcrosscompile.o
a.out
$a.out
darwin

go 1.5移除了以前的6a,6l之类的编译连接工具,将这些工具整合到go tool中。并且go tool compile的输出默认改为.o文件,链接器输出默认改为了a.out。

【动态共享库】

个人不是很赞同Go语言增加对动态共享库的支持,.so和.dll这类十多年前的技术在如今内存、磁盘空间都“非常大”的前提下,似乎已经失去了以往的魅 力。并且动态共享库所带来的弊端:"DLL hell"会让程序后续的运维痛苦不已。Docker等轻量级容器的兴起,面向不变性的架构(immutable architecture)受到更多的关注。人们更多地会在container这一层进行操作,一个纯static link的应用在部署和维护方面将会有天然优势,.so只会增加复杂性。如果单纯从与c等其他语言互操作的角度,似乎用途也不会很广泛(但游戏或ui领域 可能会用到)。不过go 1.5还是增加了对动态链接库的支持,不过从go tool compile和link的doc说明来看,目前似乎还处于实验阶段。

既然go 1.5已经支持了shared library,我们就来实验一下。我们先规划一下测试repository的目录结构:

$GOPATH
    /src
        /testsharedlib
            /shlib
                – lib.go
        /app
            /main.go

lib.go中的代码很简单:

//lib.go
package shlib

import "fmt"

// export Method1
func Method1() {
    fmt.Println("shlib -Method1")
}

对于希望导出的方法,采用export标记。

我们来将这个lib.go编译成shared lib,注意目前似乎只有linux平台支持编译go shared library:

$ go build -buildmode=shared testsharedlib/shlib
# /tmp/go-build709704006/libtestsharedlib-shlib.so
warning: unable to find runtime/cgo.a

编译ok,那个warning是何含义不是很理解。

要想.so被其他go程序使用,需要将.so安装到相关目录下。我们install一下试试:

$ go install -buildmode=shared testsharedlib/shlib
multiple roots /home1/tonybai/test/go/go15/pkg/linux_amd64_dynlink & /home1/tonybai/.bin/go15beta1/go/pkg/linux_amd64_dynlink

go工具居然纠结了,不知道选择放在哪里,一个是$GOPATH/pkg/linux_amd64_dynlink,另外一个则是$GOROOT/pkg/linux_amd64_dynlink,我不清楚这是不是一个bug。

在Google了之后,我尝试了网上的一个解决方法,先编译出runtime的动态共享库:

$go install -buildmode=shared runtime sync/atomic

编译安装后,你就会在$GOROOT/pkg下面看到多出来一个目录:linux_amd64_dynlink。这个目录下的结构如下:

$ ls -R
.:
libruntime,sync-atomic.so  runtime  runtime.a  runtime.shlibname  sync

./runtime:
cgo.a  cgo.shlibname

./sync:
atomic.a  atomic.shlibname

这里看到了之前warning提到的runtime/cgo.a,我们再来重新执行一下build,看看能不能消除warning:

$ go build -buildmode=shared testsharedlib/shlib
# /tmp/go-build086398801/libtestsharedlib-shlib.so
/home1/tonybai/.bin/go15beta1/go/pkg/tool/linux_amd64/link: cannot implicitly include runtime/cgo in a shared library

这回连warnning都没有了,直接是一个error。这里提示:无法在一个共享库中隐式包含runtime/cgo。也就是说我们在构建 testshared/shlib这个动态共享库时,还需要显式的link到runtime/cgo,这里就需要另外一个命令行标志:- linkshared。我们再来试试:

$ go build  -linkshared -buildmode=shared testsharedlib/shlib

这回build成功!我们再来试试install:

$ go install  -linkshared -buildmode=shared testsharedlib/shlib

同样成功了。并且我们在$GOPATH/pkg/linux_amd64_dynlink下发现了共享库:

$ ls -R
.:
libtestsharedlib-shlib.so  testsharedlib

./testsharedlib:
shlib.a  shlib.shlibname

$ ldd libtestsharedlib-shlib.so
    linux-vdso.so.1 =>  (0x00007fff93983000)
    libruntime,sync-atomic.so => /home1/tonybai/.bin/go15beta1/go/pkg/linux_amd64_dynlink/libruntime,sync-atomic.so (0x00007fa150f1b000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fa150b3f000)
    libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fa150921000)
    /lib64/ld-linux-x86-64.so.2 (0x00007fa1517a7000)

好了,既然共享库编译出来了。我们就来用一下这个共享库。

//app/main.go

package main

import (
    "testsharedlib/shlib"
)

func main() {
    shlib.Method1()
}

$ go build -linkshared main.go
$ ldd main
    linux-vdso.so.1 =>  (0x00007fff579f7000)
    libruntime,sync-atomic.so => /home1/tonybai/.bin/go15beta1/go/pkg/linux_amd64_dynlink/libruntime,sync-atomic.so (0x00007fa8d6df2000)
    libtestsharedlib-shlib.so => /home1/tonybai/test/go/go15/pkg/linux_amd64_dynlink/libtestsharedlib-shlib.so (0x00007fa8d6962000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fa8d6586000)
    libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fa8d6369000)
    /lib64/ld-linux-x86-64.so.2 (0x00007fa8d71ef000)

$ main
shlib -Method1

编译执行ok。从输出结果来看,我们可以清晰看到main依赖的.so以及so的路径。我们再来试试,如果将testsharedlib源码目录移除后,是否还能编译ok:

$ go build -linkshared main.go
main.go:4:2: cannot find package "testsharedlib/shlib" in any of:
    /home1/tonybai/.bin/go15beta1/go/src/testsharedlib/shlib (from $GOROOT)
    /home1/tonybai/test/go/go15/src/testsharedlib/shlib (from $GOPATH)

go编译器无法找到shlib,也就说即便是动态链接,我们也要有动态共享库的源码,应用才能编译通过。

internal package

internal包不是go 1.5的原创,在go 1.4中就已经提出对internal package的支持了。但go 1.4发布时,internal package只能用于GOROOT下的go core核心包,用户层面GOPATH不支持internal package。按原计划,go 1.5中会将internal包机制工作范围全面扩大到所有repository的。我原以为1.5beta1以及将internal package机制生效了,但实际结果呢,我们来看看示例代码:

测试目录结构如下:

testinternal/src
    mypkg/
        /internal
            /foo
                foo.go
        /pkg1
            main.go

    otherpkg/
            main.go

按照internal包的原理,预期mypkg/pkg1下的代码是可以import "mypkg/internal/foo"的,otherpkg/下的代码是不能import "mypkg/internal/foo"的。

//foo.go
package foo

import "fmt"

func Foo() {
    fmt.Println("mypkg/internal/foo")
}

//main.go
package main

import "mypkg/internal/foo"

func main() {
    foo.Foo()
}

在pkg1和otherpkg下分别run main.go:

mypkg/pkg1$ go run main.go
mypkg/internal/foo

otherpkg$ go run main.go
mypkg/internal/foo

可以看到在otherpkg下执行时,并没有任何build error出现。看来internal机制并未生效。

我们再来试试import $GOROOT下某些internal包,看看是否可以成功:

package main

import (
    "fmt"
    "image/internal/imageutil"
)

func main() {
    fmt.Println(imageutil.DrawYCbCr)
}

我们run这个代码:

$go run main.go
0x6b7f0

同样没有出现任何error。

不是很清楚为何在1.5beta1中internal依旧无效。难道非要等最终1.5 release版么?

【Vendor】
Vendor机制是go team为了解决go第三方包依赖和管理而引入的实验性技术。你执行以下go env:

$go env
GOARCH="amd64"
GOBIN="/Users/tony/.bin/go15beta1/go/bin"
GOEXE=""
GOHOSTARCH="amd64"
GOHOSTOS="darwin"
GOOS="darwin"
GOPATH="/Users/tony/Test/GoToolsProjects"
GORACE=""
GOROOT="/Users/tony/.bin/go15beta1/go"
GOTOOLDIR="/Users/tony/.bin/go15beta1/go/pkg/tool/darwin_amd64"
GO15VENDOREXPERIMENT=""
CC="clang"
GOGCCFLAGS="-fPIC -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fno-common"
CXX="clang++"
CGO_ENABLED="1"

从结果中你会看到新增一个GO15VENDOREXPERIMENT变量,这个就是用来控制vendor机制是否开启的环境变量,默认不开启。若要开启,可以在环境变量文件中设置或export GO15VENDOREXPERIMENT=1临时设置。

vendor机制是在go 1.5beta1发布前不长时间临时决定加入到go 1.5中的,Russ Cox在Keith Rarick之前的一个Proposal的基础上重新做了设计而成,大致机制内容:

If there is a source directory d/vendor, then,
    when compiling a source file within the subtree rooted at d,
    import "p" is interpreted as import "d/vendor/p" if that exists.

    When there are multiple possible resolutions,
    the most specific (longest) path wins.

    The short form must always be used: no import path can
    contain “/vendor/” explicitly.

    Import comments are ignored in vendored packages.

下面我们来测试一下这个机制。首先我们临时开启vendor机制,export GO15VENDOREXPERIMENT=1,我们的测试目录规划如下:

testvendor
    vendor/
        tonybai.com/
            foolib/
                foo.go
    main/
        main.go

$GOPATH/src/tonybai.com/foolib/foo.go

//vendor/tonybai.com/foolib/foo.go
package foo

import "fmt"

func Hello() {
    fmt.Println("foo in vendor")
}

//$GOPATH/src/tonybai.com/foolib/foo.go
package foo

import "fmt"

func Hello() {
    fmt.Println("foo in gopath")
}

vendor和gopath下的foo.go稍有不同,主要在输出内容上,以方便后续区分。

现在我们编译执行main.go

//main/main.go
package main

import (
    "tonybai.com/foolib"
)

func main() {
    foo.Hello()
}

$go run main.go
foo in gopath

显然结果与预期不符,我们通过go list -json来看main.go的依赖包路径:

$go list -json
{
… …
    "Imports": [
        "tonybai.com/foolib"
    ],
    "Deps": [
        "errors",
        "fmt",
        "io",
        "math",
        "os",
        "reflect",
        "runtime",
        "strconv",
        "sync",
        "sync/atomic",
        "syscall",
        "time",
        "tonybai.com/foolib",
        "unicode/utf8",
        "unsafe"
    ]
}

可以看出并没有看到vendor路径,main.go import的是$GOPATH下的foo。难道是go 1.5beta1的Bug?于是翻看各种资料,最后在go 1.5beta1发布前最后提交的revison的commit log中得到了帮助:

cmd/go: disable vendoredImportPath for code outside $GOPATH
It was crashing.
This fixes the build for
GO15VENDOREXPERIMENT=1 go test -short runtime

Fixes #11416.

Change-Id: I74a9114cdd8ebafcc9d2a6f40bf500db19c6e825
Reviewed-on: https://go-review.googlesource.com/11964
Reviewed-by: Russ Cox <rsc@golang.org>

从commit log来看,大致意思是$GOPATH之外的代码的vendor机制被disable了(因为某个bug)。也就是说只有$GOPATH路径下的包在 import时才会考虑vendor路径,我们的代码的确没有在$GOPATH下,我们重新设置一下$GOPATH。

$export GOPATH=~/test/go/go15
[tony@TonydeMacBook-Air-2 ~/test/go/go15/src/testvendor/main]$go list -json
{
  
  … …
    "Imports": [
        "testvendor/vendor/tonybai.com/foolib"
    ],
    "Deps": [
        "errors",
        "fmt",
        "io",
        "math",
        "os",
        "reflect",
        "runtime",
        "strconv",
        "sync",
        "sync/atomic",
        "syscall",
        "testvendor/vendor/tonybai.com/foolib",
        "time",
        "unicode/utf8",
        "unsafe"
    ]
}

这回可以看到vendor机制生效了。执行main.go:

$go run main.go
foo in vendor

这回与预期结果就相符了。

前面提到,关闭GOPATH外的vendor机制是因为一个bug,相信go 1.5正式版发布时,这块会被enable的。

三、小结

Go 1.5还增加了很多工具,如trace,但因文档不全,尚不知如何使用。

Go 1.5标准库也有很多小的变化,这个只有到使用时才能具体深入了解。

Go 1.5更多是Go语言骨子里的变化,也就是runtime和编译器重写。语法由于兼容Go 1,所以基本frozen,因此从外在看来,基本没啥变动了。

至于Go 1.5的性能,官方的说法是,有的程序用1.5编译后会变得慢点,有的会快些。官方bench的结果是总体比1.4快上一些。但Go 1.5在性能方面主要是为了减少gc延迟,后续版本才会在性能上做进一步优化,优化空间还较大的,这次runtime、编译器由c变go,很多地方的go 代码并非是最优的,多是自动翻译,相信经过Go team的优化后,更idiomatic的Go code会让Go整体性能更为优异。

Go与C语言的互操作

Go有强烈的C背景,除了语法具有继承性外,其设计者以及其设计目标都与C语言有着千丝万缕的联系。在Go与C语言互操作(Interoperability)方面,Go更是提供了强大的支持。尤其是在Go中使用C,你甚至可以直接在Go源文件中编写C代码,这是其他语言所无法望其项背的。

 
在如下一些场景中,可能会涉及到Go与C的互操作:
 
1、提升局部代码性能时,用C替换一些Go代码。C之于Go,好比汇编之于C。
2、嫌Go内存GC性能不足,自己手动管理应用内存。
3、实现一些库的Go Wrapper。比如Oracle提供的C版本OCI,但Oracle并未提供Go版本的以及连接DB的协议细节,因此只能通过包装C  OCI版本的方式以提供Go开发者使用。
4、Go导出函数供C开发者使用(目前这种需求应该很少见)。
5、Maybe more…
 
一、Go调用C代码的原理
 
下面是一个短小的例子:
package main
 
// #include <stdio.h>
// #include <stdlib.h>
/*
void print(char *str) {
    printf("%s\n", str);
}
*/
import "C"
 
import "unsafe"
 
func main() {
    s := "Hello Cgo"
    cs := C.CString(s)
    C.print(cs)
    C.free(unsafe.Pointer(cs))
}
 
与"正常"Go代码相比,上述代码有几处"特殊"的地方:
1) 在开头的注释中出现了C头文件的include字样
2) 在注释中定义了C函数print
3) import的一个名为C的"包"
4) 在main函数中居然调用了上述的那个C函数-print
 
没错,这就是在Go源码中调用C代码的步骤,可以看出我们可直接在Go源码文件中编写C代码。
 
首先,Go源码文件中的C代码是需要用注释包裹的,就像上面的include 头文件以及print函数定义;
其次,import "C"这个语句是必须的,而且其与上面的C代码之间不能用空行分隔,必须紧密相连。这里的"C"不是包名,而是一种类似名字空间的概念,或可以理解为伪包,C语言所有语法元素均在该伪包下面;
最后,访问C语法元素时都要在其前面加上伪包前缀,比如C.uint和上面代码中的C.print、C.free等。
 
我们如何来编译这个go源文件呢?其实与"正常"Go源文件没啥区别,依旧可以直接通过go build或go run来编译和执行。但实际编译过程中,go调用了名为cgo的工具,cgo会识别和读取Go源文件中的C元素,并将其提取后交给C编译器编译,最后与Go源码编译后的目标文件链接成一个可执行程序。这样我们就不难理解为何Go源文件中的C代码要用注释包裹了,这些特殊的语法都是可以被Cgo识别并使用的。
 
二、在Go中使用C语言的类型
 
1、原生类型
 
* 数值类型
在Go中可以用如下方式访问C原生的数值类型:
 
C.char,
C.schar (signed char),
C.uchar (unsigned char),
C.short,
C.ushort (unsigned short),
C.int, C.uint (unsigned int),
C.long,
C.ulong (unsigned long),
C.longlong (long long),
C.ulonglong (unsigned long long),
C.float,
C.double
 
Go的数值类型与C中的数值类型不是一一对应的。因此在使用对方类型变量时少不了显式转型操作,如Go doc中的这个例子:
 
func Random() int {
    return int(C.random())//C.long -> Go的int
}
 
func Seed(i int) {
    C.srandom(C.uint(i))//Go的uint -> C的uint
}
 
* 指针类型
原生数值类型的指针类型可按Go语法在类型前面加上*,比如var p *C.int。而void*比较特殊,用Go中的unsafe.Pointer表示。任何类型的指针值都可以转换为unsafe.Pointer类型,而unsafe.Pointer类型值也可以转换为任意类型的指针值。unsafe.Pointer还可以与uintptr这个类型做相互转换。由于unsafe.Pointer的指针类型无法做算术操作,转换为uintptr后可进行算术操作。
 
* 字符串类型
C语言中并不存在正规的字符串类型,在C中用带结尾'\0'的字符数组来表示字符串;而在Go中,string类型是原生类型,因此在两种语言互操作是势必要做字符串类型的转换。
 
通过C.CString函数,我们可以将Go的string类型转换为C的"字符串"类型,再传给C函数使用。就如我们在本文开篇例子中使用的那样:
 
s := "Hello Cgo\n"
cs := C.CString(s)
C.print(cs)
 
不过这样转型后所得到的C字符串cs并不能由Go的gc所管理,我们必须手动释放cs所占用的内存,这就是为何例子中最后调用C.free释放掉cs的原因。在C内部分配的内存,Go中的GC是无法感知到的,因此要记着释放。
 
通过C.GoString可将C的字符串(*C.char)转换为Go的string类型,例如:
 
// #include <stdio.h>
// #include <stdlib.h>
// char *foo = "hellofoo";
import "C"
 
import "fmt"
 
func main() {
… …
    fmt.Printf("%s\n", C.GoString(C.foo))
}
 
* 数组类型
C语言中的数组与Go语言中的数组差异较大,后者是值类型,而前者与C中的指针大部分场合都可以随意转换。目前似乎无法直接显式的在两者之间进行转型,官方文档也没有说明。但我们可以通过编写转换函数,将C的数组转换为Go的Slice(由于Go中数组是值类型,其大小是静态的,转换为Slice更为通用一些),下面是一个整型数组转换的例子:
 
// int cArray[] = {1, 2, 3, 4, 5, 6, 7};
 
func CArrayToGoArray(cArray unsafe.Pointer, size int) (goArray []int) {
    p := uintptr(cArray)
    for i :=0; i < size; i++ {
        j := *(*int)(unsafe.Pointer(p))
        goArray = append(goArray, j)
        p += unsafe.Sizeof(j)
    }
 
    return
}
 
func main() {
    … …
    goArray := CArrayToGoArray(unsafe.Pointer(&C.cArray[0]), 7)
    fmt.Println(goArray)
}
 
执行结果输出:[1 2 3 4 5 6 7]
 
这里要注意的是:Go编译器并不能将C的cArray自动转换为数组的地址,所以不能像在C中使用数组那样将数组变量直接传递给函数,而是将数组第一个元素的地址传递给函数。
 
2、自定义类型
 
除了原生类型外,我们还可以访问C中的自定义类型。
 
* 枚举(enum)
 
// enum color {
//    RED,
//    BLUE,
//    YELLOW
// };
 
var e, f, g C.enum_color = C.RED, C.BLUE, C.YELLOW
fmt.Println(e, f, g)
 
输出:0 1 2
 
对于具名的C枚举类型,我们可以通过C.enum_xx来访问该类型。如果是匿名枚举,则似乎只能访问其字段了。
 
* 结构体(struct)
 
// struct employee {
//     char *id;
//     int  age;
// };
 
id := C.CString("1247")
var employee C.struct_employee = C.struct_employee{id, 21}
fmt.Println(C.GoString(employee.id))
fmt.Println(employee.age)
C.free(unsafe.Pointer(id))
 
输出:
1247
21
 
和enum类似,我们可以通过C.struct_xx来访问C中定义的结构体类型。
 
* 联合体(union)
 
这里我试图用与访问struct相同的方法来访问一个C的union:
 
// #include <stdio.h>
// union bar {
//        char   c;
//        int    i;
//        double d;
// };
import "C"
 
func main() {
    var b *C.union_bar = new(C.union_bar)
    b.c = 4
    fmt.Println(b)
}
 
不过编译时,go却报错:b.c undefined (type *[8]byte has no field or method c)。从报错的信息来看,Go对待union与其他类型不同,似乎将union当成[N]byte来对待,其中N为union中最大字段的size(圆整后的),因此我们可以按如下方式处理C.union_bar:
 
func main() {
    var b *C.union_bar = new(C.union_bar)
    b[0] = 13
    b[1] = 17
    fmt.Println(b)
}
 
输出:&[13 17 0 0 0 0 0 0]
 
* typedef
在Go中访问使用用typedef定义的别名类型时,其访问方式与原实际类型访问方式相同。如:
 
// typedef int myint;
 
var a C.myint = 5
fmt.Println(a)
 
// typedef struct employee myemployee;
 
var m C.struct_myemployee
 
从例子中可以看出,对原生类型的别名,直接访问这个新类型名即可。而对于复合类型的别名,需要根据原复合类型的访问方式对新别名进行访问,比如myemployee实际类型为struct,那么使用myemployee时也要加上struct_前缀。
 
三、Go中访问C的变量和函数
 
实际上上面的例子中我们已经演示了在Go中是如何访问C的变量和函数的,一般方法就是加上C前缀即可,对于C标准库中的函数尤其是这样。不过虽然我们可以在Go源码文件中直接定义C变量和C函数,但从代码结构上来讲,大量的在Go源码中编写C代码似乎不是那么“专业”。那如何将C函数和变量定义从Go源码中分离出去单独定义呢?我们很容易想到将C的代码以共享库的形式提供给Go源码。
 
Cgo提供了#cgo指示符可以指定Go源码在编译后与哪些共享库进行链接。我们来看一下例子:
 
package main
 
// #cgo LDFLAGS: -L ./ -lfoo
// #include <stdio.h>
// #include <stdlib.h>
// #include "foo.h"
import "C"
import "fmt“
 
func main() {
    fmt.Println(C.count)
    C.foo()
}
 
我们看到上面例子中通过#cgo指示符告诉go编译器链接当前目录下的libfoo共享库。C.count变量和C.foo函数的定义都在libfoo共享库中。我们来创建这个共享库:
 
// foo.h
 
int count;
void foo();
 
//foo.c
#include "foo.h"
 
int count = 6;
void foo() {
    printf("I am foo!\n");
}
 
$> gcc -c foo.c
$> ar rv libfoo.a foo.o
 
我们首先创建一个静态共享库libfoo.a,不过在编译Go源文件时我们遇到了问题:
 
$> go build foo.go
# command-line-arguments
/tmp/go-build565913544/command-line-arguments.a(foo.cgo2.)(.text): foo: not defined
foo(0): not defined
 
提示foo函数未定义。通过-x选项打印出具体的编译细节,也未找出问题所在。不过在Go的问题列表中我发现了一个issue(http://code.google.com/p/go/issues/detail?id=3755),上面提到了目前Go的版本不支持链接静态共享库。
 
那我们来创建一个动态共享库试试:
 
$> gcc -c foo.c
$> gcc -shared -Wl,-soname,libfoo.so -o libfoo.so  foo.o
 
再编译foo.go,的确能够成功。执行foo。
 
$> go build foo.go && go
6
I am foo!
 
还有一点值得注意,那就是Go支持多返回值,而C中并没不支持。因此当将C函数用在多返回值的调用中时,C的errno将作为err返回值返回,下面是个例子:
 
package main
 
// #include <stdlib.h>
// #include <stdio.h>
// #include <errno.h>
// int foo(int i) {
//    errno = 0;
//    if (i > 5) {
//        errno = 8;
//        return i – 5;
//    } else {
//        return i;
//    }
//}
import "C"
import "fmt"
 
func main() {
    i, err := C.foo(C.int(8))
    if err != nil {
        fmt.Println(err)
    } else {
        fmt.Println(i)
    }
}
 
$> go run foo.go
exec format error
 
errno为8,其含义在errno.h中可以找到:
 
#define ENOEXEC      8  /* Exec format error */
 
的确是“exec format error”。
 
四、C中使用Go函数
 
与在Go中使用C源码相比,在C中使用Go函数的场合较少。在Go中,可以使用"export + 函数名"来导出Go函数为C所使用,看一个简单例子:
 
package main
 
/*
#include <stdio.h>
 
extern void GoExportedFunc();
 
void bar() {
        printf("I am bar!\n");
        GoExportedFunc();
}
*/
import "C"
 
import "fmt"
 
//export GoExportedFunc
func GoExportedFunc() {
        fmt.Println("I am a GoExportedFunc!")
}
 
func main() {
        C.bar()
}
 
不过当我们编译该Go文件时,我们得到了如下错误信息:
 
# command-line-arguments
/tmp/go-build163255970/command-line-arguments/_obj/bar.cgo2.o: In function `bar':
./bar.go:7: multiple definition of `bar'
/tmp/go-build163255970/command-line-arguments/_obj/_cgo_export.o:/home/tonybai/test/go/bar.go:7: first defined here
collect2: ld returned 1 exit status
 
代码似乎没有任何问题,但就是无法通过编译,总是提示“多重定义”。翻看Cgo的文档,找到了些端倪。原来
 
There is a limitation: if your program uses any //export directives, then the C code in the comment may only include declarations (extern int f();), not definitions (int f() { return 1; }).
 
似乎是// extern int f()与//export f不能放在一个Go源文件中。我们把bar.go拆分成bar1.go和bar2.go两个文件:
 
// bar1.go
 
package main
 
/*
#include <stdio.h>
 
extern void GoExportedFunc();
 
void bar() {
        printf("I am bar!\n");
        GoExportedFunc();
}
*/
import "C"
 
func main() {
        C.bar()
}
 
// bar2.go
 
package main
 
import "C"
import "fmt"
 
//export GoExportedFunc
func GoExportedFunc() {
        fmt.Println("I am a GoExportedFunc!")
}
 
编译执行:
 
$> go build -o bar bar1.go bar2.go
$> bar
I am bar!
I am a GoExportedFunc!
 
个人觉得目前Go对于导出函数供C使用的功能还十分有限,两种语言的调用约定不同,类型无法一一对应以及Go中类似Gc这样的高级功能让导出Go函数这一功能难于完美实现,导出的函数依旧无法完全脱离Go的环境,因此实用性似乎有折扣。
 
五、其他
 
虽然Go提供了强大的与C互操作的功能,但目前依旧不完善,比如不支持在Go中直接调用可变个数参数的函数(issue975),如printf(因此,文档中多用fputs)。
 
这里的建议是:尽量缩小Go与C间互操作范围。
 
什么意思呢?如果你在Go中使用C代码时,那么尽量在C代码中调用C函数。Go只使用你封装好的一个C函数最好。不要像下面代码这样:
 
C.fputs(…)
C.atoi(..)
C.malloc(..)
 
而是将这些C函数调用封装到一个C函数中,Go只知道这个C函数即可。
 
C.foo(..)
 
相反,在C中使用Go导出的函数也是一样。

 

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats