
เริ่มต้น Zig 0.16 ตั้งแต่ติดตั้ง: ทำไม tutorial เก่าที่คุณเจอถึงคอมไพล์ไม่ผ่าน
มีเรื่องหนึ่งที่ต้องบอกก่อนเริ่ม เพราะมันจะประหยัดเวลาคุณไปหลายชั่วโมง
ถ้าคุณ Google หาโค้ดตัวอย่าง Zig วันนี้ แล้วเอามาวางในไฟล์แล้วสั่งคอมไพล์ โอกาสสูงมากที่มันจะไม่ผ่าน ไม่ใช่เพราะคุณพิมพ์ผิด แต่เพราะ Zig 0.16.0 ที่ออกเมื่อ 14 เมษายน 2026 เปลี่ยนของพื้นฐานไปเยอะมาก เยอะขนาดที่ว่า โปรแกรม Hello World ยังเขียนไม่เหมือนเดิม
เว็บสอน Zig ที่คนแนะนำกันมากที่สุดอย่าง zig.guide ตอนที่เขียนบทความนี้ยังปักอยู่ที่เวอร์ชัน 0.15.2 ซึ่งห่างจาก 0.16 หนึ่ง release เต็ม ๆ และเป็น release ที่รื้อ standard library หนักที่สุดในรอบหลายปี
ชุดบทความนี้เลยตั้งใจทำสิ่งเดียว: สอน Zig 0.16 ที่ถูกต้องจริง ตัวอย่างทุกชิ้นในทุกบทถูกคอมไพล์และรันบนเครื่องจริงด้วย Zig 0.16.0 ก่อนจะขึ้นเว็บ ไม่มีโค้ดที่ “น่าจะได้” ปนมา
Zig คืออะไร (แบบไม่ต้องมีพื้นมาก่อน)
Zig เป็น ภาษาโปรแกรมระดับระบบ (systems programming language) — หมวดเดียวกับ C, C++ และ Rust คือภาษาที่ใช้เขียนของที่ต้องคุมทรัพยากรเครื่องอย่างละเอียด เช่น ระบบปฏิบัติการ ฐานข้อมูล เกมเอนจิน เครื่องมือบรรทัดคำสั่ง หรือโปรแกรมบนอุปกรณ์เล็ก ๆ ที่มี RAM ไม่กี่กิโลไบต์
จุดที่ทำให้ Zig ต่างจากภาษาที่คนส่วนใหญ่คุ้น (Java, C#, Go, Python, JavaScript) มีสองข้อใหญ่:
1. ไม่มี garbage collector และไม่มี runtime
ภาษาอย่าง Java, C#, Go มีตัวเก็บขยะ (garbage collector หรือ GC) คอยตามเก็บหน่วยความจำที่ไม่ใช้แล้วให้อัตโนมัติ สะดวกมาก แลกกับว่าโปรแกรมจะหยุดชะงักเป็นช่วง ๆ ตอน GC ทำงาน และต้องแบก runtime ติดไปกับโปรแกรมด้วย
Zig ไม่มีทั้งสองอย่าง คุณขอหน่วยความจำเอง คืนเอง ผลคือได้ binary ไฟล์เดียวขนาดเล็ก ไม่ต้องติดตั้งอะไรเพิ่มบนเครื่องปลายทาง และเวลาที่โปรแกรมใช้ก็คาดเดาได้ ไม่มีจังหวะสะดุดที่คุณไม่ได้สั่ง
2. ไม่มีอะไรเกิดขึ้นลับหลัง
เอกสารทางการของ Zig เขียนไว้ตรง ๆ ว่า “There is no hidden control flow, no hidden memory allocations, no preprocessor, and no macros” แปลว่า ถ้าโค้ดไม่ได้ดูเหมือนกำลังกระโดดไปเรียกฟังก์ชัน แสดงว่ามันไม่ได้เรียก
ในทางปฏิบัติคือ Zig ไม่มี operator overloading (เครื่องหมาย + จะบวกเลขเสมอ ไม่แอบไปเรียกฟังก์ชันที่ใครเขียนไว้) ไม่มี exception ที่โยนข้ามฟังก์ชันไปโผล่อีกที่ ไม่มี destructor ที่ทำงานเองตอนตัวแปรหมดอายุ และไม่มีฟังก์ชันไหนใน standard library แอบขอหน่วยความจำโดยที่คุณไม่ได้ส่ง allocator ให้
ข้อดีคืออ่านโค้ดแล้วรู้ว่าเกิดอะไรขึ้นจริง ข้อเสียคือคุณต้องพิมพ์เองมากกว่า เพราะไม่มีใครทำให้ลับหลัง
ส่วนรายละเอียดว่าทำไมถึงน่าสนใจ เทียบกับ C, C++, Rust, Go, C# และ Java แล้วเป็นยังไง จะมีบทเฉพาะของมันในชุดนี้ ตอนนี้เอาแค่ให้เครื่องพร้อมก่อน
สิ่งที่เปลี่ยนใน 0.16 และทำให้ตัวอย่างเก่าพัง
ก่อนติดตั้ง อยากให้เห็นภาพว่า “เก่าใช้ไม่ได้” ที่พูดถึงคือระดับไหน ตารางนี้คือของที่เจอบ่อยที่สุด
| เขียนแบบเดิม (0.15 หรือเก่ากว่า) | ต้องเขียนแบบนี้ใน 0.16 |
|---|---|
std.fs.File, std.fs.cwd() | std.Io.File, std.Io.Dir.cwd() และทุกคำสั่งที่แตะไฟล์ต้องรับ io |
std.io.getStdOut().writer() | std.Io.File.stdout() แล้วสร้าง writer พร้อม buffer เอง |
std.process.argsAlloc(gpa) | pub fn main(init: std.process.Init) แล้วใช้ init.minimal.args |
std.ArrayList(T).init(gpa) | var list: std.ArrayList(T) = .empty; แล้วส่ง gpa ให้ทุกเมธอด |
std.heap.GeneralPurposeAllocator | std.heap.DebugAllocator (ชื่อเดิมถูกลบทิ้ง ไม่มี alias เหลือไว้) |
std.time.Timer | ถูกลบ ใช้ std.Io.Clock.awake.now(io) แทน |
std.mem.indexOf | เปลี่ยนชื่อเป็น std.mem.find |
usingnamespace, async/await | ถูกถอดออกจากตัวภาษาไปแล้ว |
เห็นแล้วอาจตกใจ แต่ข่าวดีคือของใหม่มีเหตุผลรองรับ และเมื่อเข้าใจหลักคิดเบื้องหลัง (โดยเฉพาะเรื่อง “ทำไม I/O ต้องส่ง io ทุกที่”) ทุกอย่างจะเข้าที่ บทที่ 4 ของชุดนี้จะเล่าเรื่องนั้นเต็ม ๆ
Zig ยังไม่ถึงเวอร์ชัน 1.0 และทีมพัฒนาก็บอกไว้ตรง ๆ ในหมายเหตุการปล่อยเวอร์ชันว่าโปรเจกต์ที่ไม่เล็กอาจต้องเข้าไปร่วมแก้บั๊กกับเขาเอง เลือกใช้ Zig ในงานจริงต้องรับได้กับการแก้โค้ดตามทุก release
ติดตั้ง Zig 0.16.0
Zig ไม่ต้องมี installer ตัวโปรแกรมคือไฟล์เดียวชื่อ zig ที่มีทุกอย่างอยู่ในตัว ทั้งคอมไพเลอร์ ตัวรันเทส ตัว build ระบบ และ C compiler ในตัว วิธีที่ง่ายที่สุดคือใช้ package manager ของแต่ละระบบ
macOS
brew install zig
zig version
ถ้าไม่ใช้ Homebrew ก็โหลดไฟล์บีบอัดจากเว็บทางการแล้วแตกเอง — วิธีนี้ได้เวอร์ชันตรงที่ต้องการแน่นอน
# เครื่อง Apple Silicon (M1 ขึ้นไป) ใช้ aarch64 ส่วน Intel ใช้ x86_64
curl -LO https://ziglang.org/download/0.16.0/zig-aarch64-macos-0.16.0.tar.xz
tar -xf zig-aarch64-macos-0.16.0.tar.xz
sudo mv zig-aarch64-macos-0.16.0 /usr/local/zig
echo 'export PATH="/usr/local/zig:$PATH"' >> ~/.zshrc
Linux
Package ของ distro ส่วนใหญ่ตามเวอร์ชันไม่ทัน (บาง distro ยังอยู่ที่ 0.13) ถ้าจะเรียนตามชุดนี้ให้โหลดจากเว็บทางการตรง ๆ ดีกว่า
curl -LO https://ziglang.org/download/0.16.0/zig-x86_64-linux-0.16.0.tar.xz
tar -xf zig-x86_64-linux-0.16.0.tar.xz
sudo mv zig-x86_64-linux-0.16.0 /opt/zig
echo 'export PATH="/opt/zig:$PATH"' >> ~/.bashrc
source ~/.bashrc
zig version
เครื่อง ARM (เช่น Raspberry Pi รุ่นใหม่ หรือเซิร์ฟเวอร์ Graviton) เปลี่ยนชื่อไฟล์เป็น zig-aarch64-linux-0.16.0.tar.xz
Windows
มีให้เลือกสามตัวจัดการแพ็กเกจ เลือกอันที่คุณมีอยู่แล้ว
# ทางที่ 1 — winget (ติดมากับ Windows 11 อยู่แล้ว)
winget install -e --id zig.zig
# ทางที่ 2 — Scoop
scoop install zig
# ทางที่ 3 — Chocolatey (ต้องเปิด PowerShell แบบ Administrator)
choco install zig
ถ้าอยากคุมเวอร์ชันเองแบบเป๊ะ ๆ ให้โหลด zip แล้วตั้ง PATH เอง
# ดาวน์โหลดและแตกไฟล์ไว้ที่ C:\zig
Invoke-WebRequest -Uri "https://ziglang.org/download/0.16.0/zig-x86_64-windows-0.16.0.zip" -OutFile "$env:TEMP\zig.zip"
Expand-Archive -Path "$env:TEMP\zig.zip" -DestinationPath "C:\"
Rename-Item "C:\zig-x86_64-windows-0.16.0" "C:\zig"
# เพิ่มเข้า PATH ถาวรของผู้ใช้ปัจจุบัน (เปิด terminal ใหม่หลังรันเสร็จ)
[Environment]::SetEnvironmentVariable("Path", $env:Path + ";C:\zig", "User")
อยากสลับหลายเวอร์ชัน
ถ้าต้องดูแลโปรเจกต์เก่าที่ยังติดอยู่ที่ 0.14 หรือ 0.15 พร้อมกับเรียน 0.16 ให้ใช้ตัวจัดการเวอร์ชันอย่าง zvm แทนการติดตั้งทับ
zvm install 0.16.0
zvm use 0.16.0
ตรวจว่าติดตั้งสำเร็จ
zig version
zig env
zig version ต้องขึ้นว่า 0.16.0 เป๊ะ ๆ ถ้าขึ้นเป็น 0.17.0-dev.xxxx แปลว่าคุณได้รุ่นพัฒนามา ซึ่งเปลี่ยนทุกวันและตัวอย่างในชุดนี้อาจใช้ไม่ได้ ให้ย้อนกลับมาที่ 0.16.0 ก่อน
zig env จะบอกที่อยู่ของ standard library ในเครื่องคุณ ซึ่งมีประโยชน์มากกว่าที่คิด — เวลาสงสัยว่าฟังก์ชันไหนมีจริงไหม รับพารามิเตอร์อะไร การเปิดอ่านซอร์สของ std ตรง ๆ คือคำตอบที่แม่นที่สุด แม่นกว่าเอกสารด้วยซ้ำ เพราะเอกสารบางจุดยังตามไม่ทัน 0.16
.{
.zig_exe = "/opt/homebrew/Cellar/zig/0.16.0_1/bin/zig",
.lib_dir = "/opt/homebrew/Cellar/zig/0.16.0_1/lib/zig",
.std_dir = "/opt/homebrew/Cellar/zig/0.16.0_1/lib/zig/std",
.global_cache_dir = "/Users/you/.cache/zig",
.version = "0.16.0",
...
}
โปรแกรมแรก: สองแบบที่ไม่เหมือนกัน
Zig 0.16 มีวิธีพิมพ์ข้อความออกจอสองแบบ และความต่างของมันสำคัญกว่าที่เห็น
แบบที่ 1 — std.debug.print สำหรับดีบัก
สร้างไฟล์ hello.zig
บล็อกนี้เป็นไฟล์เต็มที่รันได้จริง แต่ตัวตรวจอัตโนมัติของเว็บทำได้แค่ตรวจโครงสร้าง (fragment) เพราะ zig test ไม่วิเคราะห์เนื้อใน main ผมรันจริงด้วย zig run บน Zig 0.16.0 แล้วแนบผลจริงไว้ให้ด้านล่าง
const std = @import("std");
pub fn main() void {
std.debug.print("Hello, {s}!\n", .{"World"});
}
zig run hello.zig
ผลที่ได้:
Hello, World!
สังเกตสองอย่าง
อย่างแรก main ตัวนี้ ไม่รับพารามิเตอร์อะไรเลย และไม่คืน error ได้ — เป็นรูปแบบที่สั้นที่สุดที่ Zig ยอมรับ แลกกับว่าอ่าน argument จากบรรทัดคำสั่งไม่ได้ และอ่าน environment variable ไม่ได้
อย่างที่สอง {s} คือช่องสำหรับใส่ค่าที่เป็นข้อความ ส่วน .{"World"} คือรายการค่าที่จะเอาไปเติม (เขียนเป็น tuple เสมอ ต่อให้มีค่าเดียวก็ต้องมีจุดกับปีกกา)
ข้อควรรู้ที่คนพลาดบ่อย: std.debug.print เขียนลง stderr ไม่ใช่ stdout ผมทดสอบแยก stream แล้วยืนยันได้ว่าเป็นแบบนั้นจริง มันเลยเหมาะกับการดีบักเท่านั้น ไม่เหมาะกับ output จริงของโปรแกรมที่คนอาจเอาไป pipe ต่อ
แบบที่ 2 — เขียนลง stdout จริง ๆ
อันนี้คือของจริงที่โปรแกรมใช้งานจริงต้องใช้ และเป็นจุดที่ 0.16 ต่างจากทุกเวอร์ชันก่อนหน้ามากที่สุด
const std = @import("std");
pub fn main(init: std.process.Init) !void {
try greet(init.io, .stdout(), "โลก");
}
fn greet(io: std.Io, file: std.Io.File, name: []const u8) !void {
var buf: [256]u8 = undefined;
var fw: std.Io.File.Writer = .init(file, io, &buf);
const w = &fw.interface;
try w.print("สวัสดี {s}!\n", .{name});
try w.flush(); // ห้ามลืมบรรทัดนี้
}
test "greet เขียนข้อความลงไฟล์จริง" {
const io = std.testing.io;
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const file = try tmp.dir.createFile(io, "out.txt", .{});
try greet(io, file, "โลก");
file.close(io);
const gpa = std.testing.allocator;
const content = try tmp.dir.readFileAlloc(io, "out.txt", gpa, .limited(1024));
defer gpa.free(content);
try std.testing.expectEqualStrings("สวัสดี โลก!\n", content);
}
มีของใหม่อยู่สี่อย่างในนี้ ค่อย ๆ ดูทีละอัน
init: std.process.Init — ตั้งแต่ 0.16 main รับพารามิเตอร์ก้อนนี้ได้ ข้างในมีทุกอย่างที่โปรแกรมต้องใช้: init.io สำหรับงาน I/O, init.gpa เป็น allocator สำหรับขอหน่วยความจำ, init.arena เป็น allocator ที่คืนให้อัตโนมัติตอนโปรแกรมจบ, init.environ_map สำหรับ environment variable และ init.minimal.args สำหรับ argument
io — ทุกอย่างที่ต้องคุยกับระบบปฏิบัติการใน 0.16 ต้องรับตัวนี้ เหตุผลคือ Zig ต้องการให้ “งานที่อาจบล็อกโปรแกรม” เป็นสิ่งที่มองเห็นได้จาก signature ของฟังก์ชัน ไม่ใช่ของ global ที่ใครก็เรียกได้จากทุกที่ ผลพลอยได้คือคุณสลับวิธีทำ I/O ได้โดยไม่ต้องแก้โค้ดที่เรียกใช้ และเวลาเขียนเทสก็ส่งตัวปลอมเข้าไปได้
buf กับ flush() — writer ใน 0.16 ไม่มี buffer ให้เอง คุณเป็นคนส่งเข้าไป ข้อความที่ print จะไปกองใน buf ก่อน แล้วค่อยเขียนลงระบบทีเดียวตอน flush() ถ้าลืม flush() ข้อความจะหายเงียบ ๆ นี่คือบั๊กแรกที่ทุกคนเจอ
เรื่องนี้ดูน่ารำคาญ แต่มันคือความต่างระหว่างเรียกระบบปฏิบัติการหนึ่งครั้งต่อบรรทัด กับเรียกครั้งเดียวต่อ 64 กิโลไบต์ ซึ่งเป็นคนละโลกกันด้านความเร็ว บทเรื่อง optimization ในชุดนี้จะวัดตัวเลขจริงให้ดู
test block — Zig มีระบบทดสอบอยู่ในตัวภาษา ไม่ต้องลง framework อะไรเพิ่ม เขียน test "ชื่อ" { ... } ในไฟล์เดียวกับโค้ดได้เลย แล้วสั่ง zig test hello.zig เทสข้างบนสร้างโฟลเดอร์ชั่วคราวจริง เขียนไฟล์จริง แล้วอ่านกลับมาเทียบ — นี่คือเหตุผลที่ผมกล้าบอกว่าโค้ดในบทความนี้ใช้ได้จริง
zig run hello.zig
zig test hello.zig
ตั้งโปรเจกต์จริงด้วย zig init
ไฟล์เดี่ยว ๆ ใช้ได้ตอนลองของ แต่งานจริงต้องมีโครงโปรเจกต์
mkdir myapp
cd myapp
zig init
จะได้โครงแบบนี้
myapp/
├── build.zig # สคริปต์ build เขียนด้วยภาษา Zig เอง ไม่ใช่ YAML หรือ Makefile
├── build.zig.zon # ข้อมูลโปรเจกต์และ dependency
└── src/
├── main.zig # โปรแกรมหลัก
└── root.zig # โค้ดส่วนที่เป็น library ใช้ซ้ำได้
build.zig.zon ของ 0.16 มีสองจุดที่เข้มกว่าเดิมและเป็นสาเหตุที่โปรเจกต์เก่า build ไม่ผ่าน คือ .name ต้องเป็น enum literal (เขียน .name = .myapp ไม่ใช่ .name = "myapp") และต้องมีฟิลด์ .fingerprint ซึ่ง zig init สร้างให้อัตโนมัติ ห้ามแก้ตัวเลขนี้เองหลังจากสร้างแล้ว เพราะมันคือรหัสประจำตัวโปรเจกต์
คำสั่งที่ใช้บ่อยหลังจากนี้:
zig build # คอมไพล์ ได้ไฟล์ที่ zig-out/bin/
zig build run # คอมไพล์แล้วรันเลย
zig build run -- a b # ส่ง argument a และ b ให้โปรแกรม
zig build test # รันเทสทั้งโปรเจกต์
zig fmt src/ # จัดรูปแบบโค้ด (Zig มีสไตล์เดียว ไม่ต้องเถียงกันในทีม)
zig build เฉย ๆ จะได้โหมด Debug ซึ่งช้าเพราะเปิดการตรวจความปลอดภัยไว้เต็มที่ เวลาจะวัดความเร็วหรือส่งงานจริงต้องระบุโหมดเอง
zig build -Doptimize=ReleaseSafe # เร็ว และยังตรวจความปลอดภัยอยู่
zig build -Doptimize=ReleaseFast # เร็วที่สุด ปิดการตรวจ
zig build -Doptimize=ReleaseSmall # ไฟล์เล็กที่สุด
จำข้อนี้ไว้ให้ดี: ถ้าใครเอาตัวเลข benchmark ของ Zig มาให้ดูโดยไม่บอกว่าใช้โหมดไหน ตัวเลขนั้นไม่มีความหมาย ความต่างระหว่าง Debug กับ ReleaseFast มากพอที่จะทำให้สรุปผิดได้ทั้งเรื่อง
Editor และ zls
zls (Zig Language Server) คือตัวที่ทำให้ editor เติมโค้ดให้ ชี้จุดผิด และกระโดดไปดู definition ได้
สิ่งที่ต้องระวัง: zls ผูกกับเวอร์ชันของ Zig แบบตายตัว ถ้าใช้ Zig 0.16.0 ต้องใช้ zls 0.16.0 เท่านั้น จับคู่ผิดเวอร์ชันมันจะปฏิเสธการทำงานตรง ๆ ไม่ใช่ทำงานผิด ๆ ถูก ๆ ซึ่งถือว่าดีกว่า
# macOS
brew install zls
# ตรวจว่าเวอร์ชันตรงกับ zig
zls --version
# Windows
scoop install zls
ฝั่ง Linux โหลด binary จากหน้า release ของ zls แล้ววางไว้ใน PATH เหมือนตัว zig
ใน VS Code ให้ติดตั้ง extension ชื่อ Zig Language (ziglang.vscode-zig) ตัวเดียวพอ มันจะหา zig กับ zls จาก PATH ให้เอง และตั้งค่าจัดรูปแบบตอนบันทึกไฟล์ให้ด้วย ส่วน Neovim ใช้ nvim-lspconfig ตั้งค่า zls ตามปกติ
สรุปบทนี้
- Zig เป็นภาษาระดับระบบที่ไม่มี GC ไม่มี runtime และตั้งใจไม่ให้มีอะไรเกิดขึ้นลับหลัง
- 0.16 รื้อของพื้นฐานเยอะมาก ตัวอย่างที่หาเจอทั่วไปส่วนใหญ่ใช้ไม่ได้ ต้องดูว่าเขียนสำหรับเวอร์ชันไหนเสมอ
- ติดตั้งได้ทั้ง Windows, macOS, Linux และตัว
zigไฟล์เดียวมีครบทั้งคอมไพเลอร์ ตัวเทส และระบบ build std.debug.printลง stderr ใช้ดีบัก ส่วน output จริงต้องผ่านIo.File.Writerที่คุณส่ง buffer ให้เอง และต้องflush()main(init: std.process.Init)คือประตูสู่io, allocator, argument และ environment variable- เวลาสงสัย API ให้เปิดอ่านซอร์ส std ที่
zig envบอกที่อยู่ไว้ — แม่นกว่าเอกสาร
บทหน้าเราจะลงลึกที่ตัวภาษาจริง ๆ ทั้งชนิดข้อมูล ตัวแปร การควบคุมการทำงาน และของสองอย่างที่คนมาจากภาษาอื่นสะดุดเสมอ คือ optional (?T) กับ slice ซึ่งเป็นหัวใจของการเขียน Zig ให้ไม่พัง
Navigation
- ถัดไป: บทที่ 2: ชนิดข้อมูลและการควบคุมการทำงานใน Zig
- ดูทั้งชุด: บทความแท็ก zig