Skip to content

CGO

Da Go eine Garbage Collection (GC) benötigt, ist es für einige Szenarien mit höheren Leistungsanforderungen möglicherweise nicht geeignet. C als traditionelle Systemprogrammiersprache bietet hervorragende Leistung, und CGO kann beide Sprachen miteinander verbinden, sodass Go C aufrufen kann, um leistungsensitive Aufgaben an C zu delegieren, während Go die übergeordnete Logik verarbeitet. CGO unterstützt auch das Aufrufen von Go durch C, obwohl dieses Szenario weniger verbreitet und nicht unbedingt empfehlenswert ist.

TIP

Der im Artikel verwendete Code wurde in einer Windows 10-Umgebung mit git bash als Befehlszeile getestet. Windows-Benutzern wird empfohlen, MinGW im Voraus zu installieren.

Eine einfache Einführung zu CGO finden Sie unter: C? Go? Cgo! - The Go Programming Language. Für detailliertere Informationen können Sie die Standardbibliothek cmd/cgo/doc.go oder die Dokumentation cgo command - cmd/cgo - Go Packages konsultieren. Beide Inhalte sind identisch.

Code-Aufruf

Betrachten Sie das folgende Beispiel:

go
package main

//#include <stdio.h>
import "C"

func main() {
  C.puts(C.CString("hello, cgo!"))
}

Um die CGO-Funktionalität zu verwenden, muss die Import-Anweisung import "C" verwendet werden. Beachten Sie, dass C großgeschrieben werden muss und der Importname nicht geändert werden kann. Außerdem muss sichergestellt werden, dass die Umgebungsvariable CGO_ENABLED auf 1 gesetzt ist. Standardmäßig ist diese Umgebungsvariable aktiviert.

bash
$ go env | grep CGO
$ go env -w CGO_ENABLED=1

Darüber hinaus muss lokal eine C/C++-Build-Toolchain, also gcc, vorhanden sein. Unter Windows ist dies MinGW, um eine erfolgreiche Kompilierung des Programms zu gewährleisten. Führen Sie den folgenden Befehl zur Kompilierung aus. Mit aktiviertem CGO ist die Kompilierzeit länger als bei reinem Go.

bash
$ go build -o ./ main.go
$ ./main.exe
hello, cgo!

Ein weiterer wichtiger Punkt ist, dass mit aktiviertem CGO keine Cross-Compilation mehr möglich ist.

Go mit eingebettetem C-Code

CGO ermöglicht es, C-Code direkt in Go-Quelldateien zu schreiben und aufzurufen. Im folgenden Beispiel wird eine Funktion namens printSUM definiert und in der Go-main-Funktion aufgerufen.

go
package main

/*
#include <stdio.h>
void printSum(int a, int b) {
  printf("c:%d+%d=%d",a,b,a+b);
}
*/
import "C"

func main() {
  C.printSum(C.int(1), C.int(2))
}

Ausgabe:

c:1+2=3

Dies eignet sich für einfache Szenarien. Bei umfangreichem C-Code, der mit Go-Code vermischt wird, ist die Lesbarkeit jedoch stark beeinträchtigt, was nicht empfehlenswert ist.

Fehlerbehandlung

In Go wird die Fehlerbehandlung über Rückgabewerte realisiert, aber C erlaubt keine Mehrfachrückgabewerte. Hier kann errno aus C verwendet werden, um Fehler während des Funktionsaufrufs anzuzeigen. CGO bietet hierfür eine Kompatibilität, sodass bei CGO-Aufrufen ähnlich wie in Go mit Rückgabewerten Fehler behandelt werden können. Um errno zu verwenden, muss zunächst errno.h eingebunden werden. Siehe das folgende Beispiel:

go
package main

/*
#include <stdio.h>
#include <stdint.h>
#include <errno.h>

int32_t sum_positive(int32_t a, int32_t b) {
  if (a <= 0 || b <= 0) {
    errno = EINVAL;
    return 0;
  }
  return a + b;
}

*/
import "C"
import (
  "fmt"
  "reflect"
)

func main() {
  sum, err := C.sum_positive(C.int32_t(0), C.int32_t(1))
  if err != nil {
    fmt.Println(reflect.TypeOf(err))
    fmt.Println(err)
    return
  }
  fmt.Println(sum)
}

Ausgabe:

syscall.Errno
The device does not recognize the command.

Der Fehlertyp ist syscall.Errno. In errno.h sind viele weitere Fehlercodes definiert, die bei Bedarf nachgelesen werden können.

Go mit C-Dateien

Durch das Einbinden von C-Dateien kann das oben genannte Problem gelöst werden. Erstellen Sie zunächst eine Header-Datei sum.h mit folgendem Inhalt:

c
int sum(int a, int b);

Erstellen Sie dann sum.c mit der konkreten Implementierung:

c
#include "sum.h"

int sum(int a, int b) {
    return a + b;
}

Importieren Sie die Header-Datei in main.go:

go
package main

//#include "sum.h"
import "C"
import "fmt"

func main() {
  res := C.sum(C.int(1), C.int(2))
  fmt.Printf("cgo sum: %d\n", res)
}

Bei der Kompilierung muss der aktuelle Ordner angegeben werden, da sonst die C-Datei nicht gefunden wird:

$ go build -o sum.exe . && ./sum.exe
cgo sum: 3

Die Variable res ist eine Go-Variable, C.sum ist eine C-Funktion. Der Rückgabetyp ist C-int und nicht Go-int. Der erfolgreiche Aufruf ist möglich, weil CGO die Typkonvertierung übernimmt.

C ruft Go auf

Wenn von C-Aufrufen in Go die Rede ist, bezieht sich dies auf CGO-Aufrufe von C nach Go, nicht auf native C-Programme, die Go aufrufen. Die Aufrufkette lautet go-cgo-c->cgo->go. Go ruft C auf, um dessen Ökosystem und Leistung zu nutzen. Es gibt kaum Anforderungen, bei denen native C-Programme Go aufrufen müssen. Falls doch, wird empfohlen, stattdessen Netzwerkkommunikation zu verwenden.

CGO ermöglicht das Exportieren von Go-Funktionen für C-Aufrufe. Um eine Go-Funktion zu exportieren, muss über der Funktionssignatur der Kommentar //export func_name hinzugefügt werden. Parameter und Rückgabewerte müssen von CGO unterstützte Typen sein. Beispiel:

go
//export sum
func sum(a, b C.int32_t) C.int32_t {
  return a + b
}

Ändern Sie die sum.c-Datei wie folgt:

c
#include <stdint.h>
#include <stdio.h>
#include "sum.h"
#include "_cgo_export.h"

extern int32_t sum(int32_t a, int32_t b);

void do_sum() {
  int32_t a = 10;
  int32_t b = 10;
  int32_t c = sum(a, b);

  printf("%d", c);
}

Ändern Sie gleichzeitig die Header-Datei sum.h:

void do_sum();

Exportieren Sie die Funktion in Go:

go
package main

/*
#include <stdio.h>
#include <stdint.h>
#include "sum.h"
*/
import "C"

func main() {
  C.do_sum()
}

//export sum
func sum(a, b C.int32_t) C.int32_t {
  return a + b
}

Die in C verwendete sum-Funktion wird nun von Go bereitgestellt. Die Ausgabe lautet:

20

Der Schlüsselpunkt ist der Import von _cgo_export.h in der sum.c-Datei. Diese Datei enthält alle von Go exportierten Typen. Ohne diesen Import können Go-Funktionen nicht verwendet werden. Ein weiterer wichtiger Punkt ist, dass _cgo_export.h nicht in Go-Dateien importiert werden darf, da die Generierung dieser Header-Datei voraussetzt, dass alle Go-Quelldateien erfolgreich kompiliert werden können. Daher ist die folgende Schreibweise falsch:

go
package main

/*
#include <stdint.h>
#include <stdio.h>
#include "_cgo_export.h"

void do_sum() {
  int32_t a = 10;
  int32_t b = 10;
  int32_t c = sum(a, b);

  printf("%d", c);
}
*/
import "C"

func main() {
  C.do_sum()
}

//export sum
func sum(a, b C.int32_t) C.int32_t {
  return a + b
}

Der Compiler meldet, dass die Header-Datei nicht existiert:

fatal error: _cgo_export.h: No such file or directory
 #include "_cgo_export.h"
          ^~~~~~~~~~~~~~~
compilation terminated.

Falls eine Go-Funktion mehrere Rückgabewerte hat, gibt C bei Aufruf eine Struktur zurück.

Zusätzlich können Go-Zeiger über C-Funktionsparameter an C übergeben werden. Während des C-Funktionsaufrufs gewährleistet CGO die Speichersicherheit. Allerdings dürfen exportierte Go-Funktionsrückgaben keine Zeiger enthalten, da CGO in diesem Fall nicht feststellen kann, ob sie referenziert werden, und auch den Speicher nicht fixieren kann. Wenn der zurückgegebene Speicher referenziert wird und dann in Go durch GC freigegeben oder verschoben wird, führt dies zu einem Zeiger-Überlauf. Beispiel:

go
//export newCharPtr
func newCharPtr() *C.char {
  return new(C.char)
}

Diese Schreibweise darf standardmäßig nicht kompiliert werden. Um diese Prüfung zu deaktivieren, kann Folgendes eingestellt werden:

GODEBUG=cgocheck=0

Es gibt zwei Prüfstufen: 1 und 2. Je höher die Stufe, desto größer der Laufzeit-Overhead. Weitere Details unter cgo command - passing_pointer.

Typkonvertierung

CGO bietet eine Typzuordnung zwischen C und Go, um die Laufzeitaufrufe zu erleichtern. Für C-Typen kann in Go nach dem Import von import "C" in den meisten Fällen direkt über

C.typename

zugegriffen werden, zum Beispiel:

C.int(1)
C.char('a')

C-Typen können jedoch aus mehreren Schlüsselwörtern bestehen, wie zum Beispiel:

unsigned char

In diesem Fall ist ein direkter Zugriff nicht möglich. Das C-Schlüsselwort typedef kann verwendet werden, um einem Typ einen Alias-Namen zu geben, was der Go-Typalias-Funktion entspricht. Beispiel:

c
typedef unsigned char byte;

Dadurch kann über C.byte auf den Typ unsigned char zugegriffen werden. Beispiel:

go
package main

/*
#include <stdio.h>

typedef unsigned char byte;

void printByte(byte b) {
  printf("%c\n",b);
}
*/
import "C"

func main() {
  C.printByte(C.byte('a'))
  C.printByte(C.byte('b'))
  C.printByte(C.byte('c'))
}

Ausgabe:

a
b
c

In den meisten Fällen hat CGO bereits Alias-Namen für gängige Typen (wie Grundtypen) definiert. Eigene Definitionen nach der oben genannten Methode führen zu keinen Konflikten.

char

char in C entspricht int8 in Go, unsigned char entspricht uint8 bzw. byte in Go.

go
package main

/*
#include <stdio.h>
#include<complex.h>

char ch;

char get() {
  return ch;
}

void set(char c) {
  ch = c;
}
*/
import "C"
import (
  "fmt"
  "reflect"
)

func main() {
  C.set(C.char('c'))
  res := C.get()
  fmt.Printf("type: %s, val: %v", reflect.TypeOf(res), res)
}

Ausgabe:

type: main._Ctype_char, val: 99

Wird der Parameter von set zu C.char(math.MaxInt8 + 1) geändert, schlägt die Kompilierung fehl mit folgendem Fehler:

cannot convert math.MaxInt8 + 1 (untyped int constant 128) to type _Ctype_char

Strings

CGO bietet einige Pseudo-Funktionen zur Übergabe von Strings und Byte-Slices zwischen C und Go. Diese Funktionen existieren tatsächlich nicht und ihre Definitionen können nicht gefunden werden. Ähnlich wie import "C" existiert das Paket C nicht wirklich, sondern dient nur der Entwicklerfreundlichkeit. Nach der Kompilierung werden sie in andere Operationen umgewandelt.

go
// 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

Ein Go-String ist im Wesentlichen eine Struktur, die eine Referenz auf ein zugrunde liegendes Array enthält. Bei der Übergabe an C-Funktionen muss C.CString() verwendet werden, um mit malloc in C einen "String" zu erstellen und Speicherplatz zuzuweisen. Anschließend wird ein C-Zeiger zurückgegeben. Da C keinen String-Typ hat, wird üblicherweise char* verwendet, also ein Zeiger auf ein Zeichenarray. Nach der Verwendung muss der Speicher mit free freigegeben werden.

go
package main

/*
#include <stdio.h>
#include <stdlib.h>

void printfGoString(char* s) {
  puts(s);
}
*/
import "C"
import "unsafe"

func main() {
  cstring := C.CString("this is a go string")
  C.printfGoString(cstring)
  C.free(unsafe.Pointer(cstring))
}

Alternativ kann ein char-Array verwendet werden. Beide sind im Wesentlichen Zeiger auf das erste Element.

c
void printfGoString(char s[]) {
  puts(s);
}

Byte-Slices können ebenfalls übergeben werden. Da C.CBytes() einen unsafe.Pointer zurückgibt, muss dieser vor der Übergabe an die C-Funktion in *C.char konvertiert werden.

go
package main

/*
#include <stdio.h>
#include <stdlib.h>

void printfGoString(char* s) {
  puts(s);
}
*/
import "C"
import "unsafe"

func main() {
  cbytes := C.CBytes([]byte("this is a go string"))
  C.printfGoString((*C.char)(cbytes))
  C.free(unsafe.Pointer(cbytes))
}

Die Ausgabe aller obigen Beispiele ist identisch:

this is a go string

Diese String-Übergabemethoden beinhalten eine Speicherkopie. Danach existieren separate Kopien im C- und Go-Speicher, was sicherer ist. Dennoch können Zeiger direkt an C-Funktionen übergeben und Strings in Go direkt von C modifiziert werden. Siehe das folgende Beispiel:

go
package main

/*
#include <stdio.h>
#include <stdlib.h>

void printfGoString(char* s) {
  puts(s);
}
*/
import "C"
import "unsafe"

func main() {
  ptr := unsafe.Pointer(unsafe.SliceData([]byte("this is a go string")))
  C.printfGoString((*C.char)(ptr))
}

Ausgabe:

this is a go string

Das Beispiel verwendet unsafe.SliceData, um direkt den Zeiger auf das zugrunde liegende Array des Strings zu erhalten, ihn in einen C-Zeiger zu konvertieren und an die C-Funktion zu übergeben. Der Speicher dieses Strings wird von Go verwaltet, sodass kein free erforderlich ist. Der Vorteil ist, dass während der Übergabe keine Kopie erforderlich ist, jedoch besteht ein gewisses Risiko. Das folgende Beispiel zeigt die Modifikation eines Go-Strings in C:

go
package main

/*
#include <stdio.h>
#include <stdlib.h>

void printfGoString(char* s, int len) {
  puts(s);
  s[8] = 'c';
  puts(s);
}
*/
import "C"
import (
  "fmt"
  "unsafe"
)

func main() {
  var buf []byte
  buf = []byte("this is a go string")
  ptr := unsafe.Pointer(unsafe.SliceData(buf))
  C.printfGoString((*C.char)(ptr), C.int(len(buf)))
  fmt.Println(string(buf))
}

Ausgabe:

this is a go string
this is c go string
this is c go string

Ganzzahlen

Die Zuordnung von Ganzzahltypen zwischen Go und C ist in der folgenden Tabelle dargestellt. Weitere Informationen zur Typzuordnung finden Sie in der Standardbibliothek cmd/cgo/gcc.go.

GoCCGO
int8signed charC.schar
uint8unsigned charC.uchar
int16shortC.short
uint16unsigned shortC.ushort
int32intC.int
uint32unsigned intC.uint
int32longC.long
uint32unsigned longC.ulong
int64long long intC.longlong
uint64unsigned long long intC.ulonglong

Beispielcode:

go
package main

/*
#include <stdio.h>

void printGoInt8(signed char n) {
  printf("%d\n",n);
}
void printGoUInt8(unsigned char n) {
  printf("%d\n",n);
}
void printGoInt16(signed short n) {
  printf("%d\n",n);
}
void printGoUInt16(unsigned short n) {
  printf("%d\n",n);
}
void printGoInt32(signed int n) {
  printf("%d\n",n);
}
void printGoUInt32(unsigned int n) {
  printf("%d\n",n);
}
void printGoInt64(signed long long int n) {
  printf("%ld\n",n);
}
void printGoUInt64(unsigned long long int n) {
  printf("%ld\n",n);
}
*/
import "C"

func main() {
  C.printGoInt8(C.schar(1))
  C.printGoInt8(C.schar(1))
  C.printGoInt16(C.short(1))
  C.printGoUInt16(C.ushort(1))
  C.printGoInt32(C.int(1))
  C.printGoUInt32(C.uint(1))
  C.printGoInt64(C.longlong(1))
  C.printGoUInt64(C.ulonglong(1))
}

CGO unterstützt auch Ganzzahltypen aus <stdint.h>. Diese Typen haben klarere Speichergrößen und ihr Namensstil ähnelt dem von Go.

GoCCGO
int8int8_tC.int8_t
uint8uint8_tC.uint8_t
int16int16_tC.int16_t
uint16uint16_tC.uint16_t
int32int32_tC.int32_t
uint32uint32_tC.uint32_t
int64int64_tC.int64_t
uint64uint64_tC.uint64_t

Bei der Verwendung von CGO wird empfohlen, Ganzzahltypen aus <stdint.h> zu verwenden.

Gleitkommazahlen

Die Zuordnung von Gleitkommatypen zwischen Go und C ist wie folgt:

GoCCGO
float32floatC.float
float64doubleC.double

Beispielcode:

go
package main

/*
#include <stdio.h>

void printGoFloat32(float n) {
  printf("%f\n",n);
}
void printGoFloat64(double n) {
  printf("%lf\n",n);
}
*/
import "C"

func main() {
  C.printGoFloat32(C.float(1.11))
  C.printGoFloat64(C.double(3.14))
}

Slices

Slices funktionieren ähnlich wie oben bei Strings beschrieben. Der Unterschied besteht darin, dass CGO keine Pseudo-Funktionen zum Kopieren von Slices bereitstellt. Um C den Zugriff auf Go-Slices zu ermöglichen, muss der Zeiger des Slices übergeben werden. Siehe das folgende Beispiel:

go
package main

/*
#include <stdio.h>
#include <stdint.h>

void printInt32Arr(int32_t* s, int32_t len) {
  for (int32_t i = 0; i < len; i++) {
    printf("%d ", s[i]);
  }
}
*/
import "C"
import (
  "unsafe"
)

func main() {
  var arr []int32
  for i := 0; i < 10; i++ {
    arr = append(arr, int32(i))
  }
  ptr := unsafe.Pointer(unsafe.SliceData(arr))
  C.printInt32Arr((*C.int32_t)(ptr), C.int(len(arr)))
}

Ausgabe:

0 1 2 3 4 5 6 7 8 9

Hier wird der Zeiger auf das zugrunde liegende Array des Slices an die C-Funktion übergeben. Da der Speicher dieses Arrays von Go verwaltet wird, wird empfohlen, dass C keine langfristigen Zeigerreferenzen darauf hält. Umgekehrt kann ein C-Array als zugrunde liegendes Array eines Go-Slices verwendet werden:

go
package main

/*
#include <stdio.h>
#include <stdint.h>

int32_t s[] = {1, 2, 3, 4, 5, 6, 7};

*/
import "C"
import (
  "fmt"
  "unsafe"
)

func main() {
  l := unsafe.Sizeof(C.s) / unsafe.Sizeof(C.s[0])
  fmt.Println(l)
  goslice := unsafe.Slice(&C.s[0], l)
  for i, e := range goslice {
    fmt.Println(i, e)
  }
}

Ausgabe:

7
0 1
1 2
2 3
3 4
4 5
5 6
6 7

Die Funktion unsafe.Slice kann verwendet werden, um einen Array-Zeiger in ein Slice zu konvertieren. Intuitiv könnte man denken, dass ein C-Array ein Zeiger auf das erste Element ist und daher wie folgt verwendet werden sollte:

go
goslice := unsafe.Slice(&C.s, l)

Wie die Ausgabe zeigt, führt dies jedoch dazu, dass außer dem ersten Element alle anderen Speicherbereiche ungültig sind.

0 [1 2 3 4 5 6 7]
1 [0 -1 0 0 0 3432824 0]
2 [0 0 -1 -1 0 0 -1]
3 [0 0 0 255 0 0 0]
4 [2 0 0 0 3432544 0 0]
5 [0 3432576 0 3432592 0 3432608 0]
6 [0 0 3432624 0 0 0 1422773729]

Obwohl ein C-Array nur ein Kopfzeiger ist, wird es durch CGO zu einem Go-Array mit eigener Adresse. Daher sollte die Adresse des ersten Array-Elements verwendet werden:

goslice := unsafe.Slice(&C.s[0], l)

Strukturen

Auf C-Strukturen kann mit dem Präfix C.struct_ gefolgt vom Strukturnamen zugegriffen werden. C-Strukturen können nicht als anonyme Strukturen in Go-Strukturen eingebettet werden. Hier ein einfaches Beispiel für eine C-Struktur:

go
package main

/*
#include <stdio.h>
#include <stdint.h>

struct person {
  int32_t age;
  char*  name;
};

*/
import "C"
import (
  "fmt"
  "reflect"
)

func main() {
  var p C.struct_person
  p.age = C.int32_t(18)
  p.name = C.CString("john")
  fmt.Println(reflect.TypeOf(p))
  fmt.Printf("%+v", p)
}

Ausgabe:

main._Ctype_struct_person
{age:18 name:0x1dd043b6e30}

Wenn bestimmte Mitglieder einer C-Struktur bit-field enthalten, ignoriert CGO diese Strukturmitglieder. Wird person wie folgt geändert:

c
struct person {
  int32_t age: 1;
  char*  name;
};

Führt dies zu einem Fehler:

p.age undefined (type _Ctype_struct_person has no field or method age)

Die Speicher-Ausrichtungsregeln für Strukturfelder in C und Go sind unterschiedlich. Bei aktiviertem CGO dominiert in den meisten Fällen C.

Unions

Auf C-Unions kann mit C.union_ gefolgt vom Unionsnamen zugegriffen werden. Da Go Unions nicht unterstützt, existieren sie in Go als Byte-Arrays. Hier ein einfaches Beispiel:

go
package main

/*
#include <stdio.h>
#include <stdint.h>

union data {
  int32_t age;
  char  ch;
};

*/
import "C"
import (
  "fmt"
  "reflect"
)

func main() {
  var u C.union_data
  fmt.Println(reflect.TypeOf(u), u)
}

Ausgabe:

[4]uint8 [0 0 0 0]

Über unsafe.Pointer kann darauf zugegriffen und es kann modifiziert werden:

go
func main() {
  var u C.union_data
  ptr := (*C.int32_t)(unsafe.Pointer(&u))
  fmt.Println(*ptr)
  *ptr = C.int32_t(1024)
  fmt.Println(*ptr)
  fmt.Println(u)
}

Ausgabe:

0
1024
[0 4 0 0]

Enumerationen

Auf C-Enumerationen kann mit dem Präfix C.enum_ gefolgt vom Enumerationstypnamen zugegriffen werden. Hier ein einfaches Beispiel:

go
package main

/*
#include <stdio.h>
#include <stdint.h>

enum player_state {
  alive,
  dead,
};

*/
import "C"
import "fmt"

type State C.enum_player_state

func (s State) String() string {
  switch s {
  case C.alive:
    return "alive"
  case C.dead:
    return "dead"
  default:
    return "unknown"
  }
}

func main() {
  fmt.Println(C.alive, State(C.alive))
  fmt.Println(C.dead, State(C.dead))
}

Ausgabe:

0 alive
1 dead

Zeiger

Bei Zeigern darf der Speicher nicht außer Acht gelassen werden. Das größte Problem bei gegenseitigen CGO-Aufrufen ist, dass die Speichermodelle der beiden Sprachen unterschiedlich sind. Der Speicher in C wird vollständig manuell vom Entwickler verwaltet: Mit malloc() wird Speicher allokiert und mit free() freigegeben. Ohne manuelle Freigabe wird der Speicher nicht automatisch freigegeben, was die Speicherverwaltung in C sehr stabil macht. In Go hingegen gibt es eine GC, und der Stack-Speicher von Goroutines kann dynamisch angepasst werden. Bei unzureichendem Stack-Speicher wird dieser erweitert, wodurch sich Speicheradressen ändern können. Wie in der obigen Abbildung gezeigt (die Abbildung ist nicht streng korrekt), kann ein Zeiger in C zu einem häufigen dangling pointer werden. Obwohl CGO in den meisten Fällen das Verschieben von Speicher verhindern kann (durch runtime.Pinner zum Fixieren des Speichers), empfiehlt Go offiziell nicht, dass C langfristig Go-Speicher referenziert. Umgekehrt ist es jedoch sicherer, wenn Go-Zeiger auf C-Speicher verweisen, es sei denn, C.free() wird manuell aufgerufen, da dieser Speicher nicht automatisch freigegeben wird.

Bei der Zeigerübergabe zwischen C und Go muss dieser zunächst in unsafe.Pointer und dann in den entsprechenden Zeigertyp konvertiert werden, ähnlich wie void* in C. Hier zwei Beispiele: Das erste zeigt einen C-Zeiger, der auf eine Go-Variable verweist und diese modifiziert.

go
package main

/*
#include <stdio.h>
#include <stdint.h>

void printNum(int32_t* s) {
  printf("%d\n", *s);
  *s = 3;
  printf("%d\n", *s);
}
*/
import "C"
import (
  "fmt"
  "unsafe"
)

func main() {
  var num int32 = 1
  ptr := unsafe.Pointer(&num)
  C.printNum((*C.int32_t)(ptr))
  fmt.Println(num)
}

Ausgabe:

1
3
3

Das zweite Beispiel zeigt einen Go-Zeiger, der auf eine C-Variable verweist und diese modifiziert.

go
package main

/*
#include <stdio.h>
#include <stdint.h>

int32_t num = 10;
*/
import "C"
import (
  "fmt"
  "unsafe"
)

func main() {
  fmt.Println(C.num)
  ptr := unsafe.Pointer(&C.num)
  iptr := (*int32)(ptr)
  *iptr++
  fmt.Println(C.num)
}

Ausgabe:

10
11

CGO unterstützt keine Funktionszeiger in C.

Bibliotheken verlinken

C verfügt nicht über eine Abhängigkeitsverwaltung wie Go. Um fertige Bibliotheken zu verwenden, gibt es neben dem direkten Bezug des Quellcodes auch die Möglichkeit, statische und dynamische Link-Bibliotheken zu verwenden. CGO unterstützt diese ebenfalls, wodurch in Go-Programmen fertige Bibliotheken ohne Quellcode importiert werden können.

Dynamische Link-Bibliotheken können nicht eigenständig ausgeführt werden. Zur Laufzeit werden sie zusammen mit der ausführbaren Datei in den Speicher geladen. Im Folgenden wird die Erstellung einer einfachen dynamischen Link-Bibliothek und deren Aufruf mit CGO demonstriert. Erstellen Sie zunächst eine Datei lib/sum.c mit folgendem Inhalt:

c
#include <stdint.h>

int32_t sum(int32_t a, int32_t b) {
    return a + b;
}

Erstellen Sie die Header-Datei lib/sum.h:

c
#include <stdint.h>

int sum(int32_t a, int32_t b);

Verwenden Sie anschließend gcc zur Erstellung der dynamischen Link-Bibliothek. Kompilieren Sie zunächst die Objektdatei:

bash
$ cd lib

$ gcc -c sum.c -o sum.o

Erstellen Sie dann die dynamische Link-Bibliothek:

bash
$ gcc -shared -o libsum.dll sum.o

Nach der Erstellung binden Sie die Header-Datei sum.h in den Go-Code ein und verwenden Makros, um CGO mitzuteilen, wo die Bibliotheksdatei zu finden ist:

go
package main

/*
#cgo CFLAGS: -I ./lib
#cgo LDFLAGS: -L${SRCDIR}/lib -llibsum
#include "sum.h"

*/
import "C"
import "fmt"

func main() {
  res := C.sum(C.int32_t(1), C.int32_t(2))
  fmt.Println(res)
}
  • CFLAGS: -I gibt den relativen Pfad für die Suche nach Header-Dateien an.
  • -L gibt den Suchpfad für Bibliotheken an. ${SRCDIR} steht für den absoluten Pfad des aktuellen Verzeichnisses, da der Parameter ein absoluter Pfad sein muss.
  • -l gibt den Namen der Bibliotheksdatei an. sum entspricht sum.dll.

CFLAGS und LDFLAGS sind GCC-Kompilierungsoptionen. Aus Sicherheitsgründen hat CGO einige Parameter deaktiviert. Details unter cgo command.

Legen Sie die dynamische Bibliothek im gleichen Verzeichnis wie die exe-Datei ab:

bash
$ ls
go.mod  go.sum  lib/  libsum.dll*  main.exe*  main.go

Kompilieren und führen Sie das Go-Programm aus:

bash
$ go build main.go && ./main.exe
3

Damit war der Aufruf der dynamischen Link-Bibliothek erfolgreich.

Im Gegensatz zu dynamischen Link-Bibliotheken wird beim Import einer statischen Link-Bibliothek mit CGO diese mit der Go-Zieldatei zu einer ausführbaren Datei verknüpft. Am Beispiel von sum.c wird zunächst die Quelldatei in eine Objektdatei kompiliert:

bash
$ gcc -o sum.o -c sum.c

Anschließend wird die Objektdatei in eine statische Link-Bibliothek gepackt (muss mit lib beginnen, sonst wird sie nicht gefunden):

bash
$ ar rcs libsum.a sum.o

Inhalt der Go-Datei:

go
package main

/*
#cgo CFLAGS: -I ./lib
#cgo LDFLAGS: -L${SRCDIR}/lib -llibsum
#include "sum.h"

*/
import "C"
import "fmt"

func main() {
  res := C.sum(C.int32_t(1), C.int32_t(2))
  fmt.Println(res)
}

Kompilieren:

bash
$ go build && ./main.exe
3

Damit war der Aufruf der statischen Link-Bibliothek erfolgreich.

Fazit

Obwohl der Einsatz von CGO ursprünglich der Leistung dient, verursacht der Wechsel zwischen C und Go ebenfalls Leistungseinbußen. Bei sehr einfachen Aufgaben ist die Effizienz von CGO nicht so hoch wie bei reinem Go. Betrachten Sie das folgende Beispiel:

go
package main

/*
#include <stdint.h>

int32_t cgo_sum(int32_t a, int32_t b) {
  return a + b;
}

*/
import "C"
import (
  "fmt"
  "time"
)

func go_sum(a, b int32) int32 {
  return a + b
}

func testSum(N int, do func()) int64 {
  var sum int64
  for i := 0; i < N; i++ {
    start := time.Now()
    do()
    sum += time.Now().Sub(start).Nanoseconds()
  }
  return sum / int64(N)
}

func main() {
  N := 1000_000
  nsop1 := testSum(N, func() {
    C.cgo_sum(C.int32_t(1), C.int32_t(2))
  })
  fmt.Printf("cgo_sum: %d ns/op\n", nsop1)
  nsop2 := testSum(N, func() {
    go_sum(1, 2)
  })
  fmt.Printf("pure_go_sum: %d ns/op\n", nsop2)
}

Dies ist ein sehr einfacher Test, bei dem sowohl in C als auch in Go eine Funktion zur Addition zweier Zahlen geschrieben und jeweils 1 Million Mal ausgeführt wird, um die durchschnittliche Zeit zu ermitteln. Die Testergebnisse lauten:

cgo_sum: 49 ns/op
pure_go_sum: 2 ns/op

Wie die Ergebnisse zeigen, ist die durchschnittliche Zeit von CGO mehr als zwanzigmal so hoch wie die von reinem Go. Bei komplexeren Aufgaben, die mehr Zeit in Anspruch nehmen, wäre der Vorteil von CGO größer. Darüber hinaus hat die Verwendung von CGO folgende Nachteile:

  1. Viele Go-Tools wie go test und pprof können nicht verwendet werden. Das obige Testbeispiel kann nicht mit go test ausgeführt werden, sondern muss manuell geschrieben werden.
  2. Die Kompilierungsgeschwindigkeit verringert sich, und die integrierte Cross-Compilation kann nicht mehr verwendet werden.
  3. Speichersicherheitsprobleme
  4. Abhängigkeitsprobleme: Wenn andere Ihre Bibliothek verwenden, muss ebenfalls CGO aktiviert werden.

Bevor Sie CGO in einem Projekt einführen, sollten Sie dies sorgfältig abwägen. Für einige sehr komplexe Aufgaben kann CGO durchaus Vorteile bieten, aber bei einfachen Aufgaben ist es besser, bei reinem Go zu bleiben.

Golang by www.golangdev.cn edit