Go 程序員喜歡 Ziglang?

在編程語言領域,Zig 相對來說是新成員。Zig 由 Andrew Kelly 創立,於 2016 年正式面世,Zig 社區將其定位爲 “用於開發健壯、優化和可複用軟件的通用編程語言”。作爲一名 Go 開發者,我對 Zig 及其工具鏈的能力尤爲感興趣。在研究 Zig 時,發現這兩種語言(Zig 和 Go)在某些方面有着共同的特性。

譯|zouyee

爲了幫助讀者深入瞭解 Kubernetes 在各種應用場景下所面臨的挑戰和解決方案,以及如何進行性能優化。我們推出了 <<Kubernetes 經典案例 30 篇 >>

在編程語言領域,Zig 相對來說是新成員。Zig 由 Andrew Kelly 創立,於 2016 年正式面世,Zig 社區將其定位爲 “用於開發健壯、優化和可複用軟件的通用編程語言”。

在這簡單的描述中,可以發現其蘊含着一些宏大的目標。例如,Zig 被視爲一個能夠與 C 語言競爭的編程語言(Rust 在 Linux 內核方向進展更快一些)。此外,Zig 提供一整套編譯工具鏈,可以替代現有 C 編譯器(Rust 可以使用 cargo-zigbuild 跨平臺編譯)。

作爲一名 Go 開發者,我對 Zig 及其工具鏈的提議尤爲感興趣。在研究 Zig 時,發現這兩種語言(Zig 和 Go)在某些方面有着共同的特性。在這篇博文中,將重點介紹作爲 Go 程序員,對 Zig 感興趣的一些特性。

Ziglang 與 Go

簡潔性

兩種語言都秉承簡潔的設計哲學,以減少語言的干擾,使開發者更快上手並能夠高效地完成開發任務。Zig 中沒有宏、預處理器或操作符重載等功能,避免了執行流程中的 “魔法”。

Go 通過運行時處理內存分配和釋放。而 Zig 則堅持其 “無隱藏控制流” 的準則,沒有自動內存管理,而是通過標準庫提供內存管理 API,讓開發者管理內存。

強類型

作爲一門系統編程語言,Zig 圍繞類型系統設計了許多功能,注重安全性和 C ABI(應用二進制接口)兼容性。這裏簡要介紹一些有趣的特性:

a. 有符號 / 無符號整數(預設大小從 8 位到 128 位)

b. 任意大小的有符號 / 無符號整數(例如 i7 表示 7 位整數)

c. 浮點數(精度從 16 位到 128 位)

d. 切片和數組(例如 []u8{‘h’, ‘i’, ‘!’}[4]i32{1, 2, 3, 4}

e. 以 UTF-8 編碼的字符串字面量,存儲爲以空字符結尾的字節數組

f. 具有豐富功能的結構體類型,可與 C ABI 兼容

g. 具有隱式 / 顯式序數值的枚舉,並支持方法

h. 可用於存儲多種類型值的聯合

i. 支持使用向量並行操作

j. 傳統指針及多元素指針與切片表達式

錯誤處理

Zig 中的錯誤處理機制非常有特色,融合了 try-catch 異常語義和 Go 的錯誤值模式。

首先,所有 Zig 錯誤都是必須分配和處理的值(否則會導致編譯錯誤)。錯誤聲明使用 error 關鍵字,如下所示:

const DigitError = error{ TooLarge, };

有趣的是,Zig 錯誤值可以與普通類型的值結合,使用 ! 運算符形成一個聯合類型。下面的函數可以返回錯誤或 u32 類型的值:

fn addSingleDigits(a: u32, b: u32) !u32 {
    if (a > 9) return error.TooLarge;
    if (b > 9) return error.TooLarge;
    return a + b;
}

此外,Zig 提供了類似 Java 等語言的 catch 關鍵字,用於錯誤處理:

pub fn main() void {
    const result = addSingleDigits(4, 5) catch |err| {
        std.debug.print("Error: {}\n", .{err});
        return;
    };
    std.debug.print("Result: {}\n", .{result});
}

Zig 還支持通過 try 關鍵字將錯誤向上級調用傳播。此外,還可以通過 if-else-switch 語句更精確地篩選和處理錯誤。

Zig 測試

在 Zig 中,測試是語言的關鍵性能力,使用 test 關鍵字聲明:

test "test that 1 + 1 equals 2" {
    const result = 1 + 1;
    assert(result == 2);
}

使用 zig test 命令運行源代碼中的測試,在標準庫中,測試大多跟源代碼處於同一文件。

Zig 運行

類似於 go run,Zig 提供了 zig run 命令,將編譯和運行源代碼的步驟結合:

zig run my_program.zig

Defer

Zig 與 Go 一樣,通過 defer 概念來管理退出堆棧,當作用域塊結束時執行清理操作等。

const print = @import("std").debug.print;
fn addSingleDigits(a: u32, b: u32) !u32 {
    defer print("this is deferred!");
    if (a > 9) return error.TooLarge;
    if (b > 9) return error.TooLarge;
    return a + b;
}

Comptime

comptime 是 Zig 的一項有趣特性。Zig 沒有單獨的宏系統,而是通過 comptime 將其代碼編寫的靈活性擴展到編譯階段。

comptime 允許在編譯時進行如下操作:

a. 在編譯時解析變量和表達式

b. 根據編譯時值行爲的函數

c. 編譯期間有選擇性地執行 comptime 代碼塊

d. 編譯時執行的元編程

泛型

在 Zig 中,comptime 提供了對類型值的訪問,可以像普通數據值一樣存儲和傳遞這些類型值。

這使得可以創建帶有類型參數的函數,如下所示:

fn max(comptime T: type, a: T, b: T) T {
    return if (a > b) a else b;
}
test "max with different types" {
    const condition = false;
    const result = max(if (condition) f32 else u64, 1234, 5678);
    _ = result;
}

由於 comptime 類型值被視爲普通類型,Zig 允許使用它們來構建泛型數據結構。例如,MakeList 使用 **comptime **類型信息來返回一個編譯時結構體:

const std = @import("std");
fn MakeList(comptime T: type, comptime size: usize) type {
    return struct {
        items: [size]T,
    };
}
pub fn main() void {
    var list = MakeList(i32, 3){
        .items = [3]i32{1, 2, 3},
    };
    std.debug.print("List {}\n", .{list});
}

在這個示例中,MakeList 函數使用 comptime 類型和大小參數,返回一個包含固定數量元素的結構體。

Zig 編譯

1. Zig 作爲 C (交叉) 編譯器

Zig 工具鏈包含完整的 C 編譯器,因此可以使用 Zig 來替換當前的 C 編譯器工具鏈。以下是 hello.c 的源代碼文件:

#include <stdio.h>
int main(int argc, char **argv) {
    printf("Hello world\n");
    return 0;
}

使用以下命令,Zig 可以將該源代碼編譯成可執行的二進制文件:

zig build-exe hello.c --library c

2. Zig 和 C 交叉編譯

Zig 讓交叉編譯(無論是 C 代碼還是 Zig 代碼亦或 Rust)變得簡單。無需繁瑣的 “自行準備交叉編譯工具鏈”。Zig 提供所有必要的工具和庫,確保您可以面向其支持的任何架構進行編譯。

例如,Zig 可以將上述 C 源代碼交叉編譯成一個面向 Linux 的靜態二進制文件(使用 musl 庫):

zig build-exe hello.c --library c -target x86_64-linux-musl

3. Zig 和 CGo 交叉編譯

Zig 對 C 的交叉編譯對在交叉編譯啓用了 CGo 的 Go 源代碼時特別有用。例如, add.c 中的 C 函數 add

#include <stdint.h>
int32_t add(int32_t a, int32_t b) {
    return a + b;
}

我們可以在 Go 中 調用它:

package main
/*
#include "add.c"
*/
import "C"
import (
    "fmt"
)
func main() {
    a, b := int32(3), int32(4)
    result := C.add(a, b)
    fmt.Printf("%d + %d = %d\n", a, b, result)
}

假設我們在 MacOS 上構建代碼,可以使用命令 zig cc 來使用 Zig 的 C 編譯器,將 C 代碼交叉編譯成目標文件並與 Go 的目標文件鏈接,以構建適用於 x86 架構 Linux 的靜態二進制文件:

CGO_ENABLED=1 GOOS=linux CC="zig cc -target x86_64-linux-musl" go build .

要讓這一步成功,您只需安裝 Zig 工具鏈,無其他依賴項!

Zig 的突出特點

Zig 致力於成爲一個更好的 C 語言替代品,其不僅適用於低級系統編程,還適用於開發通用軟件系統,具有以下突出特點:

****設計簡單 ****

現代化語言的設計目標是提供一套設計良好的語法,而不像彙編語言那樣原子化。如果語言的抽象過於接近彙編語言,開發人員可能需要編寫冗長的代碼。另一方面,當語言被抽象成接近人類可讀時,它可能與硬件相距甚遠,可能不適合系統編程的需求。

Zig 提供了輕量級的、類 Rust 的語法,其大多數 C 提供的能力都已具備,但是它不提供 Rust 和 C++ 那些複雜的功能集和語法,而是提供了一個像 Go 那樣簡單性爲先的開發路徑。

****性能和安全性 ****

性能和安全性是選擇的關鍵因素。語言的性能通常取決於其標準庫、核心運行時功能的性能,以及編譯器生成的二進制文件的質量。同時,安全設計實現邊界檢查、溢出處理和內存範圍,並幫助開發人員減少關鍵安全漏洞。

Zig 構建系統提供了四種構建模式,開發人員可以根據其性能和安全性要求使用。Zig 還可以在編譯時理解變量溢出。

此外,它可以生成帶有運行時安全檢查的優化二進制文件,就像 Rust 一樣,也可以生成不帶運行時安全檢查的超輕量級二進制文件,就像 C 一樣。Zig 官方文檔聲稱,由於其基於 LLVM 的優化和改進的未定義行爲,Zig 在理論上比 C 更快!

完整的系統編程解決方案

大多數編程語言都有一個或多個標準編譯器和標準庫實現。例如,您可以使用以下編譯 C:

但是這兩個組件對於現代系統編程需求來說還不夠。程序員通常需要建立工具、包管理器和交叉編譯工具等。

因此,在 C 生態系統中,像 CMake、Ninja、Meson 這樣的構建工具以及類似 Conan 這樣的包管理器逐漸流行,而像 Go 和 Rust 這樣的現代語言官方內置了包管理器、構建工具及 API、交叉編譯支持和測試集成等。

與 Go 及 Rust 等現代語言一樣,Zig 內置了包管理器、構建系統及 API、支持交叉編譯和測試集成,這提高了 Zig 成爲更好的 C 的機會,因爲它解決了 C(和 C++)開發人員面臨的關鍵系統編程問題。從語言設計的角度來看,Zig 提供了 C 開發人員期望的現代語言的所有功能,因此 C 程序員可以逐步將他們的系統遷移到現代 Zig,而無需重新編寫他們遺留的代碼庫。

總結

本文,爲您簡單介紹了 Zig 的功能。Zig 融合了簡潔性、強大性、安全性和對 C 的兼容性,爲開發者提供了一個令人興奮的選擇。無論您是爲新項目尋找語言,還是像我一樣想擴展編程技能,Zig 都是一個值得探索的創新選擇。

Zig 仍然是一種新語言,ZSF 仍在定期實現和測試更多功能。學習 Zig 是一個很好的決定,因爲它作爲一種更好的 C 語言,有着光明的前景。

由於筆者時間、視野、認知有限,本文難免出現錯誤、疏漏等問題,期待各位讀者朋友、業界專家指正交流。

參考文獻

1.https://ziglang.org/documentation/master/.

2.https://medium.com/@vladimirvivien/things-i-like-about-zig-as-a-go-programmer-75eb02aab00f

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/pdDSGEBp7XcT5QyeYYMHFw