返回文章列表文章索引阅读全文
STARBLOG / 阅读手记
Go

When CGO Meets Zig: A More Elegant Way to Tinker, Compared with the GCC Backend

“GCC won the benchmarks, but Zig won at life.” (GCC wins the benchmark, but Zig wins the DX/Developer Experience.) Switching the cgo compiler from gcc to zig on Windows? GCC is fast but environment setup can be a hassle; Zig is a lightweight single file but compiles slowly and first-run latency is ridiculous. Is the performance gap large? Maybe, but the gain in developer efficiency is worth the tinkering.

曦远 (DealiAxy)约 20 分钟
目录12 个章节

Introduction

Recently, while building a Go project involving a database (SQLite) and audio processing in a Windows environment, I ran into an expected error:

cgo: C compiler "gcc" not found: exec: "gcc": executable file not found in %PATH%

That's because Go's cgo mechanism needs to call an external C compiler to process C code. Windows does not include a GCC environment by default, so we need to manually configure a C toolchain.

Quickly Installing GCC with Scoop

For developers who are used to command-line tools, I don't recommend downloading an installer and configuring environment variables by hand. With Scoop, you can solve this very quickly:

scoop install gcc

After installation, run gcc --version in the terminal. If you see version output, that means Scoop has already configured the environment variables automatically.

At this point, if you run go build, the project can usually compile normally.

Using Zig Instead of GCC

Although GCC can solve the problem, after further research I found that using Zig as the C compiler is a more efficient choice. Although Zig is a programming language, its compiler (zig cc) can be used as a complete C/C++ compiler frontend.

Why Choose Zig?

  • Lighter installation: A single scoop install zig gives you a single-file compiler, without having to manage a complex folder structure like MinGW.
  • Built-in standard library: Zig ships libc source for multiple platforms, so compilation does not depend on the host's system libraries.
  • Static linking advantages: Binaries compiled with Zig are more stable when handling static linking, and can reduce the generated .exe file's dependence on external .dlls.
  • Native cross-compilation support: This is Zig's strongest feature. You can compile cgo programs targeting Linux or macOS directly on Windows, just by specifying the -target parameter, without configuring a complex cross-compilation toolchain.

Installing Zig

Install it with Scoop the same way

scoop install zig

Configuring Go to Invoke the Zig Compiler

If you have already installed Zig, you can make Go use it by default with the following steps:

Enable CGO:

go env -w CGO_ENABLED=1

Specify the C/C++ compiler:

Point the CC and CXX Go environment variables at Zig:

go env -w CC="zig cc"
go env -w CXX="zig c++"

Compiling for a specific platform (example):

If you need to compile for the Linux platform:

$env:GOOS="linux"
$env:GOARCH="amd64"
$env:CC="zig cc -target x86_64-linux-gnu"
go build

Let's Test It

To test cgo compilation, I had Grandpa LLM help write a snippet that reverses a string. This example covers CGO's core flow: C code defining the logic, Go code calling it via CGO, and data being passed between Go and C.

Create the file main.go

package main

/*
// 使用 Zig 的编译器驱动
#cgo LDFLAGS: -s -w

#include <stdlib.h>
#include <string.h>

// 这是一个简单的 C 函数,用于反转字符串
void reverse_string(char* str) {
    int len = strlen(str);
    for (int i = 0; i < len / 2; i++) {
        char temp = str[i];
        str[i] = str[len - 1 - i];
        str[len - 1 - i] = temp;
    }
}
*/
import "C"
import (
	"fmt"
	"unsafe"
)

func main() {
	input := "Hello from Zig + CGO!"
	fmt.Printf("Original: %s\n", input)

	// 将 Go 字符串转换为 C 字符串(会在堆上分配内存)
	cStr := C.CString(input)
	// 必须手动释放 C 内存
	defer C.free(unsafe.Pointer(cStr))

	// 调用 C 函数
	C.reverse_string(cStr)

	// 将修改后的 C 字符串转回 Go 字符串
	output := C.GoString(cStr)
	fmt.Printf("Reversed: %s\n", output)
}

Local Compilation

First test with gcc

$env:CGO_ENABLED="1"
go build main.go

Then test with zig

$env:CGO_ENABLED="1"
$env:CC="zig cc"
go build main.go

Note

There's a small pitfall here

After version 1.9.4, Go introduced a security check mechanism that by default intercepts some CGO flags it considers “unsafe”

-s and -w are commonly used size-reduction flags, but they are intercepted by default

So you need to add an environment variable to allow them

$env:CGO_LDFLAGS_ALLOW="-s|-w"

You can also extract the C code into separate files

Create hello.h

void reverse_string(char* str);

Create hello.c

#include <string.h>
void reverse_string(char* str) {
    int len = strlen(str);
    for (int i = 0; i < len / 2; i++) {
        char temp = str[i];
        str[i] = str[len - 1 - i];
        str[len - 1 - i] = temp;
    }
}

Modify main.go

package main

/*
#include "hello.h"
*/
import "C"
import (
    "fmt"
    "unsafe"
)

func main() {
    s := C.CString("Zig is awesome")
    defer C.free(unsafe.Pointer(s))
    C.reverse_string(s)
    fmt.Println(C.GoString(s))
}

After extracting the code into a .c file, CGO will automatically look for C files in the same directory and hand them off to CC (i.e. zig cc). You usually don't need to write LDFLAGS in main.go by hand. This not only keeps the code cleaner, but also neatly sidesteps Go's flag-checking mechanism.

Performance Comparison

Although Zig is wonderful, I had Grandpa LLM help write another script to test compile speed and the resulting binary's execution speed across different backends.

The results showed...

DimensionGCC (15.2)Zig (0.15.2)Gap / Conclusion
Compile time (Avg)6.67 s16.40 sZig is 1.4× slower. The LLVM architecture is heavier, and parsing the cross-platform compatibility layer takes time.
Runtime (Avg)18.54 ms30.83 msZig is about 66% slower. Affected by cold-start latency and CGO call overhead.
Max first-run latency25.30 ms483.68 msRidiculous! The Zig binary appears to trigger a Windows Defender scan.
Runtime stability (StdDev)1.34 ms45.54 msGCC is extremely stable; Zig fluctuates wildly (dragged down by cold-start outliers).
Environment dependenciesComplex (requires installing MinGW/MSYS2)Minimal (single file, no deps)This is where Zig's core advantage lies.

Note: Zig's first-run latency (483ms) usually only happens the first time the system executes that binary. If you add app_zig.exe to the Windows Defender whitelist, its median runtime will stabilize around 26ms. Although still slightly behind GCC, it is already within an acceptable range for engineering purposes.

Conclusion

Don't blindly buy into Zig's performance optimizations.

Although Zig is hailed as a challenger to modern C, in the CGO scenario on Windows, GCC is still king for local performance and compile speed.

So why bother tinkering with Zig?

Because setting up a GCC environment on Windows is an absolute nightmare for beginners. Zig's real value isn't those few milliseconds of execution speed, but that it lets you escape the complex MinGW environment and achieve the ultimate developer experience of 'one command, compile for every platform, zero-dependency deployment'. This is a classic tradeoff of a little performance cost for developer efficiency.

Wrap-up

For handling cgo compilation on Windows, Scoop + GCC is the fastest basic solution. But if you want a clean build environment, or have a need for cross-platform compilation, Scoop + Zig is a more advanced choice in terms of technical design.

This way, we can avoid heavyweight Visual Studio or a complex MinGW environment, and keep the development environment clean and efficient.

_EOF_

When CGO Meets Zig: A More Elegant Way to Tinker, Compared with the GCC Backend

本文作者:
程序设计实验室
版权声明:
本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

相关文章

讨论

共 0 条

良言一句三冬暖,恶语伤人六月寒。

加载评论中…