ภาพวาดโทนมิ้นต์สว่าง อิกัวนาตัวเล็กยืนบนโต๊ะยาวข้างเครื่องมือชิ้นเดียวที่ต่อหัวได้หลายแบบ ตรงข้ามมีเครื่องมือเฉพาะทางสี่ชิ้นวางเรียงกัน อิกัวนากำลังหมุนเปลี่ยนหัวสีอำพันเข้ากับด้ามเดียวนั้น

Zig เด่นตรงไหน เทียบกับ C, C++, Rust, Go, C# และ Java แบบไม่เชียร์

พี่โชว์Engineering

เก้าบทที่ผ่านมาเราเรียนวิธีเขียน Zig บทนี้ตอบคำถามที่ควรถามตั้งแต่ต้น — แล้วทำไมต้อง Zig

ผมจะพยายามไม่เชียร์ ทุกข้อดีจะมาพร้อมราคาที่ต้องจ่าย และช่วงท้ายมีหัวข้อ “เมื่อไหร่ไม่ควรเลือก Zig” ที่ตั้งใจเขียนให้ยาวพอ ๆ กับข้อดี เพราะการเลือกภาษาผิดแพงกว่าการเขียนโค้ดผิดมาก

ตัวเลขทุกตัวในบทนี้ผมวัดเองบนเครื่อง Apple M5 Pro (aarch64-macOS) ด้วย Zig 0.16.0 ตัวไหนที่ลอกมาจากที่อื่นจะบอกไว้ชัด


1. ไม่มีอะไรเกิดขึ้นลับหลัง

นี่คือแกนกลางของภาษา เว็บทางการเขียนไว้ตรง ๆ ว่า

ไม่มีการไหลของการทำงานที่ซ่อนอยู่ ไม่มีการขอหน่วยความจำที่ซ่อนอยู่ ไม่มี preprocessor และไม่มี macro ถ้าโค้ด Zig ไม่ได้ดูเหมือนกำลังกระโดดไปเรียกฟังก์ชัน แสดงว่ามันไม่ได้เรียก

ที่น่าสนใจคือเว็บทางการยกตัวอย่างภาษาอื่นเองด้วย โดยบอกว่า D มี @property ที่ทำให้การอ่านฟิลด์กลายเป็นการเรียกฟังก์ชัน, “C++, D และ Rust มี operator overloading เครื่องหมาย + จึงอาจไปเรียกฟังก์ชัน” และ “C++, D และ Go มี exception ดังนั้น foo() อาจโยน exception แล้วทำให้ bar() ไม่ถูกเรียกเลย”

Zig จึงไม่มีทั้งสามอย่างนี้ บวกกับไม่มี destructor ที่ทำงานเองตอนตัวแปรหมดอายุ และไม่มีฟังก์ชันไหนใน standard library ที่ขอหน่วยความจำโดยไม่ได้รับ allocator จากคุณ

ราคาที่จ่าย: คุณต้องพิมพ์มากกว่า ต้อง defer เอง ต้องส่ง gpa และ io ต่อกันไปทุกชั้น โค้ด Zig ยาวกว่าโค้ด Go หรือ Python ที่ทำงานเดียวกัน ประมาณ 20-40% จากประสบการณ์เขียนชุดบทความนี้

ได้อะไรกลับมา: อ่านโค้ดแล้วรู้ว่าเกิดอะไรขึ้นจริง ตอนตี 2 ที่ระบบล่มและคุณไม่ใช่คนเขียนโค้ดนั้น ความสามารถนี้มีค่ามาก


2. comptime: กลไกเดียวแทนสี่อย่าง

ภาษาอื่นแก้ปัญหา “เขียนโค้ดที่สร้างโค้ด” ด้วยเครื่องมือหลายชิ้นที่ต้องเรียนแยกกัน

ภาษาเครื่องมือที่ต้องเรียน
Cpreprocessor macro, _Generic
C++template, constexpr/consteval, macro, concept
Rustgenerics, trait, declarative macro, procedural macro
Java / C#generics, annotation/attribute, reflection ตอนรัน
Zigcomptime อย่างเดียว

comptime ไม่ใช่ภาษาใหม่ที่ซ้อนอยู่ข้างใน — มันคือ Zig ตัวเดิมที่รันตอนคอมไพล์ ลูป for ตัวเดิม if ตัวเดิม ฟังก์ชันตัวเดิม

ผลที่ตามมาที่คนไม่ค่อยพูดถึงคือ reflection ของ Zig ไม่มีค่าใช้จ่ายตอนรัน ตอนที่บทที่ 5 เราอ่านโครงสร้าง struct แล้วสร้างโค้ดพิมพ์ตาราง โค้ดที่ได้ใน binary เหมือนคุณเขียนเองทุกบรรทัด ไม่มีการ inspect อะไรตอนรันเลย ต่างจาก Java/C# ที่ reflection จ่ายทุกครั้งที่เรียก

ราคาที่จ่าย: ยิ่งใช้ comptime มาก ยิ่งคอมไพล์นานขึ้น และ error ที่เกิดข้างในโค้ด generic อ่านยากกว่าภาษาที่ประกาศเงื่อนไขไว้ล่วงหน้าอย่าง Rust


3. Toolchain: ตัว zig ไฟล์เดียวแทนทั้งชุด

ข้อนี้เป็นข้อที่คนประทับใจที่สุดเวลาลองครั้งแรก และมันคือเรื่องจริง

ผมทดสอบบน MacBook เครื่องเดียว ไม่ได้ติดตั้ง cross toolchain อะไรเพิ่มเลย แล้วคอมไพล์โปรแกรม Zig ตัวเดียวกันไปยังทุกเป้าหมายนี้ — ผ่านหมดทุกตัว

zig build-exe hello.zig -target x86_64-linux-musl  -O ReleaseSafe
zig build-exe hello.zig -target aarch64-linux-musl -O ReleaseSafe
zig build-exe hello.zig -target x86_64-windows-gnu -O ReleaseSafe
zig build-exe hello.zig -target aarch64-macos-none -O ReleaseSafe
zig build-exe hello.zig -target x86_64-macos-none  -O ReleaseSafe

ขนาดไฟล์ที่ได้จริง (โปรแกรม Hello World ที่เขียนด้วย Io.File.Writer แบบบทที่ 4, ใส่ -fstrip -fsingle-threaded)

เป้าหมาย                 ReleaseSmall    ReleaseSafe
aarch64-macos-none          145,232        188,560
x86_64-linux-musl           128,192        225,440
aarch64-linux-musl          122,640        211,288
x86_64-windows-gnu          383,488        487,936

ทุกไฟล์เป็น binary เดี่ยว ๆ ไม่ต้องติดตั้ง runtime อะไรบนเครื่องปลายทาง ฝั่ง Linux ได้ static binary ที่รันได้ทั้งบน Alpine และ Ubuntu

หมายเหตุที่ต้องพูดให้ตรง: เว็บทางการโชว์ตัวเลข Hello World ที่ 9,944 ไบต์ ซึ่งเล็กกว่าที่ผมวัดได้มาก ผมเลยลองไล่ดูว่าขนาดมาจากไหน

เขียนแบบไหน (x86_64-linux-musl, ReleaseSmall, -fstrip)     ขนาด
เรียก syscall ตรง ๆ ไม่แตะ std                              1,760 bytes
ใช้ std.debug.print                                       112,712 bytes
ใช้ Io.File.Writer แบบบทที่ 4                             128,192 bytes

สรุปคือ ขนาด binary ขึ้นกับว่าคุณดึง standard library เข้ามาแค่ไหน ไม่ใช่ตัวเลขเดียวที่ใช้อ้างได้ทุกกรณี ถ้าใครโชว์เลขหลักพันไบต์ ให้ถามว่าโปรแกรมนั้นเขียนยังไง

zig cc: คอมไพเลอร์ C ที่ติดมาด้วย

ตัว zig เป็นคอมไพเลอร์ C/C++ ในตัว และ ข้ามแพลตฟอร์มได้ด้วย เพราะมัน ship ซอร์สของ libc มาให้แล้วคอมไพล์ให้ตอนใช้งาน ผมทดสอบแล้วบนเครื่อง

zig cc hello.c -o hello                              # คอมไพล์ปกติ
zig cc hello.c -target x86_64-linux-musl -o hello_linux   # ข้ามไป Linux จาก macOS

ไฟล์ที่ได้จากคำสั่งที่สองคือ ELF สำหรับ x86-64 แบบ static link จริง ๆ (ตรวจด้วย file แล้ว) — ทำได้จาก MacBook ที่ไม่มี cross toolchain ติดตั้ง

ประโยชน์ในโลกจริงคือหลายทีมใช้ zig cc เป็นตัว build โปรเจกต์ C ของตัวเอง โดยไม่ได้เขียน Zig สักบรรทัด เพราะมันทำให้ build ข้ามแพลตฟอร์มง่ายกว่า toolchain แบบเดิมมาก

ข้อควรระวัง: โพสต์ต้นฉบับของ Andrew Kelley เรื่อง zig cc เขียนไว้ตั้งแต่ปี 2020 และตอนนั้นเขาเองระบุว่ายัง “ทดลอง” อยู่ ตัวเลขและคำเตือนในโพสต์นั้นเก่ามากแล้ว ใช้เป็นแนวคิดได้ แต่อย่าอ้างตัวเลข

0.16 ยังเปลี่ยนที่เก็บ dependency

เดิม dependency ไปกองที่ cache กลางของเครื่อง 0.16 ย้ายมาไว้ใน zig-pkg ข้าง ๆ build.zig ของโปรเจกต์ และ zig build จะ ล้มเหลว ถ้า dependency ไหนไม่มีฟิลด์ fingerprint หรือยังใช้ name เป็นข้อความอยู่


4. ความปลอดภัยแบบเลือกได้

Zig ไม่ได้ขายตัวเองว่าเป็นภาษาที่ปลอดภัยที่สุด แต่ขายว่าคุณ เลือกจุดสมดุลได้เอง ทั้งระดับโปรเจกต์และระดับ block

จากที่ผมวัดในบทที่ 7: u8 ที่ล้นทำให้โปรแกรมหยุดทันทีในโหมด Debug และ ReleaseSafe แต่ได้ผลลัพธ์ผิดเงียบ ๆ ในโหมด ReleaseFast

Zig เรียกพฤติกรรมกลุ่มนี้ว่า Illegal Behavior (ไม่ใช่ undefined behavior) และแยกเป็นสองชั้น: แบบที่ตรวจได้จะมีการตรวจให้ในโหมดปลอดภัย ส่วนแบบที่ตรวจไม่ได้ เอกสารเขียนตรง ๆ ว่า “อะไรก็เกิดขึ้นได้ ตัวปรับโค้ดมีสิทธิ์ทำให้มันเรียกฟังก์ชันสุ่มหรือเขียนทับข้อมูลสุ่มได้”

รายการที่ตรวจให้ในโหมดปลอดภัย: เข้าถึงนอกช่วง, ตัวเลขล้น (ทั้ง signed และ unsigned ต่างจาก C ที่ตรวจแค่ signed), หารด้วยศูนย์, แกะ optional ที่ว่าง, แกะ error ที่ไม่ได้เช็ค, แปลง enum ผิดค่า, อ่าน union ผิดฟิลด์, pointer ที่ alignment ไม่ตรง

คำแนะนำที่ผมยึด: ส่ง production ด้วย ReleaseSafe เป็นค่าเริ่มต้น แล้วค่อยเปิด @setRuntimeSafety(false) เฉพาะ block ที่วัดแล้วว่าคุ้ม ไม่ใช่เปิด ReleaseFast ทั้งโปรแกรมเพราะ “อยากได้เร็ว”

ข้อจำกัดที่ต้องพูดให้ชัด: ไม่มี borrow checker

เอกสารทางการเขียนไว้เองว่า

เป็นความรับผิดชอบของโปรแกรมเมอร์ Zig ที่จะต้องไม่เข้าถึง pointer เมื่อหน่วยความจำที่มันชี้อยู่ไม่มีอีกแล้ว ให้สังเกตว่า slice ก็เป็น pointer รูปแบบหนึ่ง

แปลว่า use-after-free และ dangling slice เกิดได้จริงใน Zig และคอมไพเลอร์ไม่ได้กันให้ นี่คือความต่างที่ใหญ่ที่สุดกับ Rust และไม่ควรพูดอ้อม ๆ

สิ่งที่ Zig ให้แทนคือเครื่องมือตอนรัน — DebugAllocator ที่ไม่นำที่อยู่เดิมกลับมาใช้ซ้ำและจับ leak ให้, std.testing.allocator ที่ทำให้เทสแดงเมื่อมีของค้าง, defer/errdefer ที่ทำให้จุดคืนของอยู่ติดกับจุดสร้าง ทั้งหมดนี้ช่วยได้เยอะ แต่ อ่อนกว่าการรับประกันตอนคอมไพล์อย่างชัดเจน


5. ตารางเทียบ

แกนZig 0.16CC++RustGoC# / Java
จัดการหน่วยความจำด้วยมือ ผ่าน allocator ที่เป็นพารามิเตอร์ด้วยมือ ผ่าน malloc ตัวกลางRAII + smart pointerownership ตรวจตอนคอมไพล์GCGC
งานที่เกิดลับหลังไม่มีเกือบไม่มีoverload, ctor/dtor, exceptionoverload, Drop, panicGC, panicexception, GC, boxing
ระบบ errorerror union !T เมินไม่ได้คืนค่า/errnoexceptionResult + ?ค่าที่สองexception
metaprogrammingcomptime อย่างเดียวmacrotemplate + constexpr + macrogenerics + macro 2 แบบgenericsgenerics + reflection
คอมไพล์ข้ามแพลตฟอร์มในตัว ทำได้ทันทีต้องมี toolchain ต่อเป้าหมายเหมือน C แต่ยากกว่าดี แต่ dependency C ยังต้องพึ่ง toolchainดีมาก (ถ้าไม่ใช้ cgo)ต้องพึ่ง runtime หรือ AOT
ความปลอดภัยตรวจตอนรันในโหมดปลอดภัย ไม่มี borrow checkerน้อยมากน้อย + เครื่องมือเสริมแข็งแรงที่สุดปลอดภัยด้วย GCปลอดภัยด้วย GC
ผลลัพธ์ที่ deploybinary เดี่ยว ไม่มี runtimebinary + libcbinary + libstdc++binary เดี่ยวbinary เดี่ยว (ใหญ่กว่า)ต้องมี runtime หรือ AOT
ความนิ่งของภาษายังไม่ถึง 1.0 แตกทุก releaseมาตรฐาน ISOมาตรฐาน ISOนิ่งตั้งแต่ 2015นิ่งตั้งแต่ 2012นิ่งมาก
ระบบนิเวศเล็กใหญ่มากใหญ่มากใหญ่ใหญ่ใหญ่มาก

Zig คล้ายอะไรมากที่สุด

คล้าย C ที่สุด ในแง่โมเดลความคิด — คุณรู้ตลอดว่าข้อมูลอยู่ตรงไหน ใครเป็นเจ้าของ และเมื่อไหร่มันหายไป ต่างกันที่ Zig เพิ่ม slice ที่รู้ความยาว, optional ที่แทน null ได้จริง, error ที่เมินไม่ได้, defer, generics ผ่าน comptime, ระบบ build และ package manager ในตัว

คล้าย Rust ในแง่ระบบ error (!T กับ Result) และปรัชญาว่าต้นทุนต้องมองเห็นได้ ต่างกันที่ Zig ไม่มี borrow checker ไม่มี lifetime และภาษาเล็กกว่ามาก — เรียนสองสัปดาห์เขียนงานจริงได้ ขณะที่ Rust หลายคนใช้เวลาเป็นเดือนกว่าจะเลิกสู้กับคอมไพเลอร์

คล้าย Go ในแง่ปรัชญา “ภาษาเล็ก อ่านโค้ดได้โดยไม่ต้องเดา” ต่างกันที่ Go มี GC และ Zig ไม่มี


6. เมื่อไหร่ ไม่ควร เลือก Zig

หัวข้อนี้สำคัญที่สุดในบทความ

1. เมื่อต้องการความนิ่งของ API หมายเหตุการปล่อยเวอร์ชัน 0.16 เขียนเองว่า “Zig มีบั๊ก มีการคอมไพล์ผิด และมีการถอยหลัง แม้จะเป็น 0.16.x การทำโปรเจกต์ที่ไม่เล็กด้วย Zig อาจต้องเข้าร่วมกระบวนการพัฒนาด้วย” ชุดบทความนี้เกิดขึ้นเพราะ 0.16 รื้อของจนตัวอย่างเก่าทั้งอินเทอร์เน็ตใช้ไม่ได้ — ถ้าคุณรับกับการแก้โค้ดตามทุก 8 เดือนไม่ได้ อย่าเพิ่ง

2. เมื่อความปลอดภัยหน่วยความจำเป็นข้อบังคับ ถ้าโดเมนของคุณต้องการการรับประกันเรื่อง memory safety แบบพิสูจน์ได้ Rust คือคำตอบที่ถูกต้อง ไม่ใช่ Zig

3. เมื่อทีมไม่มีพื้นฐานภาษาระดับระบบ Zig ไม่ได้ยากเรื่องไวยากรณ์ แต่มันไม่ยกโทษให้เรื่องอายุของข้อมูล คนที่มาจาก Java/Python/JavaScript ต้องเรียนแนวคิดใหม่จริง ๆ ไม่ใช่แค่ไวยากรณ์ใหม่

4. เมื่อต้องการไลบรารีเยอะ ๆ ไม่มี web framework ที่ครบเครื่อง ไม่มี ORM ที่โตเต็มที่ ไม่มีไลบรารี ML ระบบนิเวศเล็กมากเมื่อเทียบกับทุกภาษาในตาราง

5. เมื่อเครื่องมือรอบข้างสำคัญมาก zls ผูกกับเวอร์ชันแบบตายตัว และตัว linker ELF ใหม่ใน 0.16 ยังสร้างข้อมูล debug สำหรับโค้ด Zig ไม่ได้ ส่วนหนึ่งของประสบการณ์ดีบักจึงยังไม่เท่าภาษาที่โตเต็มที่

6. บนเครื่อง Apple Silicon อย่าเพิ่งหวังพึ่ง backend ตัวใหม่ ข่าวที่ว่า “self-hosted backend คอมไพล์เร็วกว่า LLVM หลายเท่า” เป็นเรื่องของ x86_64 ผมลอง -fno-llvm บน aarch64-macOS แล้ว มันค้างเกินสองนาทีจนต้องหยุดเอง ใช้งานจริงไม่ได้ ณ ตอนนี้

7. เมื่อต้องใช้ async/networking ที่นิ่ง โมเดล I/O ใหม่ของ 0.16 มีเพียง Io.Threaded ที่พร้อมใช้จริง ส่วนตัวที่เป็น event loop ยัง ไม่รองรับ networking เลย และผู้สร้างภาษาเองก็เขียนไว้ว่า “API เหล่านี้ยังไม่ตายตัว น่าจะต้องปรับอีกหลายรอบกว่าจะลงตัว”


7. เรื่องที่ไม่ควรเชื่อ

ระหว่างทำชุดบทความนี้ผมเจอข้ออ้างที่ถูกส่งต่อกันเยอะแต่ตรวจแล้วไม่ตรง

  • “Zig เร็วกว่า C” — ประโยคนี้อยู่บนเว็บทางการจริง แต่ไม่มี benchmark แนบมาด้วย มันคือคำโฆษณา ไม่ใช่ผลการวัด
  • “error return trace ใช้ได้ใน ReleaseSafe” — ผมรันแล้ว ไม่ได้ ต้องเติม -ferror-tracing เอง (บทที่ 6 มีผลจริง)
  • “เรียงฟิลด์ struct จากใหญ่ไปเล็กเพื่อประหยัดที่” — ใน Zig คอมไพเลอร์เรียงให้เองอยู่แล้ว ผมวัดได้ 16 ไบต์เท่ากันทั้งเรียงดีและเรียงแย่ ต่างกันเฉพาะ extern struct (16 vs 24)

เรื่องนี้พิสูจน์ได้ในสิบวินาที บล็อกนี้รันผ่านจริงบน Zig 0.16.0

const std = @import("std");

const Messy = struct { a: u8, b: u64, c: u8, d: u32 }; // เรียงสลับเล็กใหญ่
const Tidy = struct { b: u64, d: u32, a: u8, c: u8 }; // เรียงใหญ่ไปเล็ก
const MessyExtern = extern struct { a: u8, b: u64, c: u8, d: u32 };
const TidyExtern = extern struct { b: u64, d: u32, a: u8, c: u8 };

test "ความเชื่อเรื่องเรียงฟิลด์ และขนาดของ optional" {
    // struct ธรรมดา: เรียงยังไงก็เท่ากัน เพราะคอมไพเลอร์จัดให้
    try std.testing.expectEqual(@sizeOf(Messy), @sizeOf(Tidy));
    try std.testing.expectEqual(@as(usize, 16), @sizeOf(Tidy));

    // extern struct: ลำดับมีผลจริง เพราะต้องตรงกับ ABI ของ C
    try std.testing.expectEqual(@as(usize, 24), @sizeOf(MessyExtern));
    try std.testing.expectEqual(@as(usize, 16), @sizeOf(TidyExtern));

    // optional ที่เป็น pointer ไม่กินที่เพิ่ม — ภาษารับประกันไว้
    try std.testing.expectEqual(@sizeOf(*i32), @sizeOf(?*i32));

    // แต่ optional ของค่าธรรมดากินเพิ่ม
    try std.testing.expect(@sizeOf(?u32) > @sizeOf(u32));
}
  • “Zig ไม่มีปัญหา function color” — ยังเป็นข้อถกเถียงที่ไม่จบ การย้ายจากคำว่า async มาเป็นพารามิเตอร์ io คือการเปลี่ยนรูปแบบของปัญหา ไม่ใช่การทำให้มันหายไป
  • ตัวเลข benchmark ของ allocator จากหมายเหตุเวอร์ชัน 0.14 — วัดบนเครื่องอื่น เทียบกับ glibc ซึ่งบน macOS ตัวเทียบคือ Apple libmalloc คนละเรื่องกัน

สรุป

Zig เหมาะกับคุณถ้า:

  • คุณเขียน C หรือ C++ อยู่แล้ว และอยากได้เครื่องมือปี 2026 โดยไม่ต้องเรียนภาษาที่ใหญ่กว่าเดิม
  • คุณต้องส่ง binary ไปหลายแพลตฟอร์มและเบื่อการดูแล cross toolchain
  • คุณต้องการเวลาทำงานที่คาดเดาได้ ไม่มีจังหวะ GC
  • คุณสนุกกับการที่ทุกอย่างมองเห็นได้และยอมพิมพ์มากขึ้นเพื่อแลกกับมัน
  • ทีมของคุณรับได้กับการแก้โค้ดตามทุก release จนกว่าจะถึง 1.0

ไม่เหมาะถ้า: ต้องการความนิ่ง ต้องการการรับประกันเรื่อง memory safety ต้องการไลบรารีครบ หรือทีมไม่มีเวลาไล่ตามภาษาที่ยังเปลี่ยนอยู่

ส่วนตัวผมคิดว่าจุดที่ Zig ชนะขาดที่สุดตอนนี้ไม่ใช่ตัวภาษา แต่คือ toolchain — ความสามารถในการคอมไพล์ทั้ง Zig และ C ไปยังทุกแพลตฟอร์มจากเครื่องเดียวโดยไม่ต้องติดตั้งอะไรเพิ่ม เป็นเรื่องที่ผมทดสอบเองแล้วยังรู้สึกว่ามันดีเกินคาด

บทหน้าเริ่ม workshop — เราจะสร้างเครื่องมือบรรทัดคำสั่งที่ใช้งานได้จริงชื่อ zlog สำหรับวิเคราะห์ log ของเว็บเซิร์ฟเวอร์ ตั้งแต่โครงเปล่าจนถึง binary ที่ส่งขึ้นทุกแพลตฟอร์ม