30 คะแนน โดย GN⁺ 2025-12-06 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • เปรียบเทียบว่าแต่ละภาษาพยายามแก้ปัญหาอะไร โดยเน้นที่ ความต่างด้านปรัชญาและคุณค่าที่แต่ละภาษายึดถือ
  • Go ถูกอธิบายว่าเป็นภาษาที่ให้ความสำคัญกับความเรียบง่ายและเสถียรภาพ ลดฟีเจอร์ให้เหลือน้อยเพื่อให้การทำงานร่วมกันและการบำรุงรักษาทำได้ง่าย
  • Rust มุ่งแสวงหาทั้งความปลอดภัยและประสิทธิภาพไปพร้อมกัน และรับประกัน ความปลอดภัยของหน่วยความจำ ด้วยระบบชนิดข้อมูลและโครงสร้าง trait ที่ซับซ้อน
  • Zig ถูกพรรณนาว่าเป็นภาษาทดลองที่มอบอำนาจควบคุมทั้งหมดให้แก่นักพัฒนา ผ่านการจัดการหน่วยความจำด้วยตนเองและ การออกแบบที่ยึดข้อมูลเป็นศูนย์กลาง
  • แนวทางที่แตกต่างกันของทั้งสามภาษาเผยให้เห็น ระบบคุณค่าที่ภาษาโปรแกรมพยายามทำให้เป็นจริง และเกณฑ์ในการเลือกก็คือ นักพัฒนาเห็นพ้องกับปรัชญาแบบใด

มุมมองในการเปรียบเทียบภาษา

  • ผู้เขียนต้องการทำความเข้าใจ ระบบคุณค่าของแต่ละภาษา ผ่านการทดลองภาษาใหม่ ไม่ใช่ภาษาที่ใช้ในที่ทำงาน
  • เน้นว่าประเด็นสำคัญไม่ใช่แค่การเทียบรายการฟีเจอร์ แต่คือ ภาษานั้นเลือก trade-off แบบใด
  • Go, Rust และ Zig มีส่วนที่ซ้อนทับกันมากในเชิงความสามารถ แต่ คุณค่าที่ผู้ออกแบบให้ความสำคัญ ต่างกัน
  • เมื่อเข้าใจปรัชญาของแต่ละภาษา ก็จะประเมินได้ว่าเหมาะกับสภาพแวดล้อมและเป้าหมายแบบใด

Go — ภาษาที่เน้นความเรียบง่ายและการทำงานร่วมกัน

  • Go โดดเด่นด้วย มินิมัลลิสม์ และมีลักษณะที่ว่า “สามารถเก็บทั้งภาษาไว้ในหัวได้”
    • generics ถูกเพิ่มเข้ามาหลังผ่านไป 12 ปี และฟีเจอร์อย่าง tagged union หรือ syntax sugar สำหรับการจัดการ error ก็ยังไม่มี
  • มีความระมัดระวังอย่างมากในการเพิ่มฟีเจอร์ ทำให้มี boilerplate code มาก แต่แลกกับ เสถียรภาพและความอ่านง่ายของภาษา ที่สูง
  • slice ของ Go ครอบคลุมบทบาทของ Vec<T> ใน Rust หรือ ArrayList ใน Zig และให้รันไทม์จัดการตำแหน่งหน่วยความจำโดยอัตโนมัติ
  • ออกแบบมาจากความไม่พอใจต่อความซับซ้อนและความล่าช้าในการคอมไพล์ของ C++ โดยมีเป้าหมายคือ ความเรียบง่ายและการคอมไพล์ที่รวดเร็ว
  • ให้ความสำคัญกับ ประสิทธิภาพของการทำงานร่วมกันในสภาพแวดล้อมองค์กร โดยให้ความชัดเจนของโค้ดและความสม่ำเสมอมาก่อนฟีเจอร์ที่ซับซ้อน

Rust — ซับซ้อนแต่ทรงพลังด้านความปลอดภัยและประสิทธิภาพ

  • Rust ชูแนวคิด “zero-cost abstraction” และเป็น ภาษาสาย maximalist ที่รวมแนวคิดหลากหลายเข้าด้วยกัน
  • เหตุผลที่เรียนรู้ยากคือมี ความหนาแน่นของแนวคิด สูง พร้อมระบบชนิดข้อมูลและโครงสร้าง trait ที่ซับซ้อน
  • เป้าหมายหลักของ Rust คือการทำให้ ประสิทธิภาพและความปลอดภัยของหน่วยความจำ อยู่ร่วมกันได้
    • มีการตรวจสอบตั้งแต่ตอนคอมไพล์เพื่อป้องกัน UB (Undefined Behavior)
    • ปิดกั้น พฤติกรรมที่คาดเดาไม่ได้ จากการอ้างอิง pointer ที่ผิดพลาดหรือการ free ซ้ำ
  • เพื่อให้คอมไพเลอร์เข้าใจพฤติกรรมขณะรันของโค้ด นักพัฒนาจึงต้อง กำหนดชนิดข้อมูลและ trait อย่างชัดเจน
  • โครงสร้างลักษณะนี้ทำให้ ความน่าเชื่อถือของโค้ดจากผู้อื่น สูง และช่วยให้ ecosystem ของไลบรารี คงความคึกคักอยู่ได้

Zig — การควบคุมอย่างสมบูรณ์และการออกแบบที่ยึดข้อมูลเป็นศูนย์กลาง

  • Zig เป็น ภาษาที่ใหม่ที่สุด ในบรรดาสามภาษา ปัจจุบันอยู่ในช่วงเวอร์ชัน 0.14 และแทบไม่มีเอกสารของ standard library
  • ใช้ การจัดการหน่วยความจำแบบแมนนวล ทำให้นักพัฒนาต้องเรียก alloc() เองและต้องเลือก allocator ด้วยตนเอง
  • ต่างจาก Rust หรือ Go ตรงที่ สร้างตัวแปร global ได้ง่าย และจะตรวจจับ “illegal behavior” ระหว่างรันไทม์แล้วหยุดโปรแกรม
    • มี โหมดรีลีส 4 แบบ ที่เลือกได้ตอน build เพื่อปรับสมดุลระหว่างประสิทธิภาพกับเสถียรภาพ
  • จงใจตัดฟีเจอร์ของ การเขียนโปรแกรมเชิงวัตถุ (OOP) ออก
    • ไม่มี private field หรือ dynamic dispatch และแม้แต่ std.mem.Allocator ก็ไม่ได้ถูกทำเป็น interface
    • แต่หันไปมุ่งสู่ การออกแบบที่ยึดข้อมูลเป็นศูนย์กลาง (data-oriented design) แทน
  • แม้แต่การจัดการหน่วยความจำก็ยังแนะนำโครงสร้างแบบ จัดสรรและคืนหน่วยความจำเป็นบล็อกใหญ่ตามช่วงเวลา แทนการจัดการรายวัตถุอย่างละเอียดด้วยแนวทาง RAII
  • Zig ถูกพรรณนาว่าเป็นภาษาที่มี นิสัยรักอิสระและขบถต่อกระแสหลัก โดย ตัดวิธีคิดแบบ OOP ออกและขยายการควบคุมโดยนักพัฒนาให้ถึงขีดสุด
  • ปัจจุบันทีมงานกำลังมุ่งเน้นที่ การเขียน dependency ทั้งหมดขึ้นใหม่ และเวอร์ชันเสถียร (1.0) ก็ยังไม่มีกำหนด

บทสรุป — ความต่างของคุณค่าที่ภาษาสะท้อนออกมา

  • Go ยึด การทำงานร่วมกันและความเรียบง่าย, Rust ยึด ความปลอดภัยและประสิทธิภาพ, ส่วน Zig ยึด เสรีภาพและอำนาจในการควบคุม เป็นคุณค่าหลัก
  • ความต่างของทั้งสามภาษาไม่ใช่แค่การเปรียบเทียบฟีเจอร์ แต่สะท้อน การเลือกเชิงปรัชญาต่อการพัฒนาซอฟต์แวร์
  • นักพัฒนาจะเลือกภาษาไปตามว่า ตนเองเห็นพ้องกับคุณค่าแบบใด

1 ความคิดเห็น

 
GN⁺ 2025-12-06
ความคิดเห็นใน Hacker News
  • ใน Rust การสร้าง mutable global variable ไม่ใช่เรื่องยาก
    เพียงแค่ต้องใช้ unsafe หรือสมาร์ตพอยน์เตอร์ที่มีการซิงโครไนซ์
    เพราะ Rust เป็น re-entrant โดยพื้นฐาน และรับประกันความปลอดภัยของเธรดตั้งแต่คอมไพล์ไทม์
    ถ้าไม่ได้กังวลเรื่องความปลอดภัยของเธรดแบบสถิต ก็ทำให้มันง่ายเหมือนใน Zig หรือ C ได้
    ความต่างคือ Rust มี เครื่องมือรับประกัน เกี่ยวกับพฤติกรรมตอนรันไทม์ของโค้ดมากกว่า

    • จากประสบการณ์ที่ใช้ Rust มาหลายปี ผมคิดว่า mutable global variable เป็นตัวอย่างคลาสสิกของคำว่า “ทำได้ ไม่ได้แปลว่าควรทำ”
      เวลากลับไปใช้ภาษาอื่นแล้วเห็นคนใช้สิ่งนี้กันแบบไม่คิดอะไร ก็รู้สึกเหมือนเป็นเรื่องบ้าบอในแง่ของ ความปลอดภัย
    • คำพูดทำนองว่า “มัน trivial แค่ต้องมี ~” เป็นสิ่งที่เคยได้ยินกับ C++, Perl, Haskell เหมือนกัน
      แต่พอ “เรื่องง่ายๆ” แบบนี้สะสมกันเข้า มันก็ไม่ง่ายอีกต่อไป
      Rust ข้ามเส้นนั้นไปแล้ว และตอนนี้มัน ไม่ trivial เลย
    • สงสัยว่า Rust compiler จับ race condition ระหว่างเธรดได้ตั้งแต่คอมไพล์ไทม์หรือไม่
      ถ้าได้ก็ดูน่าสนใจกว่า C
      และก็อยากรู้ด้วยว่ากรณีที่ตัวแปรสองตัวต้องถูกล็อกพร้อมกันเสมอจะจัดการอย่างไร
    • ถ้าผมเป็นคนออกแบบภาษา ผมจะ ห้าม mutable global variable ไปเลย
      เวลาไล่ดีบัก สุดท้ายต้นตอของปัญหาก็มักจะอยู่ตรงนั้นเสมอ
  • สำหรับบทความที่ชี้เรื่องความหนาแน่นของแนวคิดใน Rust ผมคิดว่าในทางปฏิบัติ แค่รู้ 5% ก็ใช้งานแบบ productive ได้แล้ว
    ผมใช้ Rust มานานกว่า 12 ปี แต่ไม่เคยต้องใช้ #[fundamental] เลยสักครั้ง
    Rust ก็ทำ arena allocation ได้ และมีแนวคิดเรื่อง allocator อยู่
    เพียงแต่มี allocator เริ่มต้นให้ และโดยทั่วไปจะใช้การจอง heap แบบชัดเจนอย่าง Box::new
    mutable global ก็ทำได้แบบ static FOO: Mutex<T> = Mutex::new(...) และจำเป็นต้องมี mutex เพื่อ ความปลอดภัยของหน่วยความจำ
    type system ของ Rust ถูกออกแบบมาให้รับประกันไม่ใช่แค่ memory safety แต่รวมถึง semantic safety ของโค้ดด้วย

    • แต่เพราะนักพัฒนาคนอื่นอาจใช้ อีก 5~10% ของแนวคิด ที่ต่างออกไปได้ พอทำงานร่วมกัน สุดท้ายก็ต้องเรียนรู้แนวคิดเพิ่มอยู่ดี
      ใน C ความซับซ้อนแบบนี้มีน้อยกว่า
      ความซับซ้อน จึงเป็นประเด็นสำคัญในท้ายที่สุด
    • คำพูดว่า “Rust ก็ทำ arena allocation ได้” นั้นถูกต้อง แต่โค้ด Rust/Go ส่วนใหญ่มี การจัดสรรจำนวนมากในหน่วยเล็กๆ เป็นเส้นทางปกติ
      ประเด็นจึงไม่ใช่แค่ว่าทำได้หรือไม่ แต่เป็นความต่างของสไตล์การเขียนโปรแกรมพื้นฐาน
    • ถ้า allocator ใน Rust เป็นชนิดข้อมูล ก็สงสัยว่าในโมเดลเธรดแบบ m:n จะให้ arena แยกต่อคำขอ ได้หรือไม่
    • ถามต่อว่า allocator ของ Rust เป็นแบบ global หรือทุกการจัดสรรบน heap ใช้ allocator ตัวเดียวกันทั้งหมดหรือไม่
    • มีการพูดถึง วิดีโอเรื่อง batch allocation ของ Casey Muratori พร้อมชี้ว่านักพัฒนาบางคนเข้าใจสิ่งนี้ผิดแล้วเอาไปวิจารณ์ RAII ของ Rust
      และยังมีกรณีที่ Zig Software Foundation อ้างคำพูดเกี่ยวกับ Rust ของ Asahi Lina ผิดด้วย
      ท่าทีการตลาดของ Zig ที่ชอบกดภาษาอื่นให้ดูแย่ลงนั้นไม่น่าประทับใจนัก
  • เหตุผลที่ชอบ Zig คือมันเป็นภาษาที่จัดการกับภาวะหน่วยความจำหมดได้อย่าง สวยงาม
    ทุกการจัดสรรถูกมองว่าอาจล้มเหลวได้ และต้องจัดการอย่างชัดเจน
    พื้นที่สแตกก็ไม่ได้ถูกปฏิบัติราวกับเป็นเวทมนตร์ แต่คอมไพเลอร์จะวิเคราะห์ call graph เพื่ออนุมานขนาดสูงสุด
    ในสภาพแวดล้อมแบบ embedded การออกแบบที่ยึด ทรัพยากรเป็นศูนย์กลาง แบบนี้เป็นสิ่งจำเป็น

    • แต่บน OS ที่ใช้ overcommit อย่าง Linux ในทางปฏิบัติแล้วการจัดสรรมักจะไม่ล้มเหลวจริง
      สิ่งนี้แก้ไม่ได้ด้วยกลไกระดับภาษา
    • ถ้าถามว่าในเมื่อมี Rust อยู่แล้ว Zig ยังมีเหตุผลอะไรให้ต้องมีอยู่ ก็อยากถามกลับมากกว่าว่า “ในเมื่อมี C แล้ว ทำไมต้อง Zig?”
      เพราะสุดท้ายก็ยังเผชิญปัญหาเดียวกันคือการจัดการหน่วยความจำด้วยมือ
      ถ้าอย่างนั้นผมคิดว่าใช้ ภาษา GC จะดีกว่า
    • สงสัยว่าการอนุมาน ขนาดสแตก ของ Zig ทำงานอย่างไรเมื่อมี recursion หรือการเรียกผ่านฟังก์ชันพอยน์เตอร์
    • Zig ไม่ใช่เจ้าแรก และควรย้อนดู ประวัติศาสตร์ของภาษา systems ตั้งแต่ JOVIAL ในปี 1958
    • ใน Rust ก็รองรับ pre-allocation ได้ดี
      เพียงแต่ standard library ของ Rust ใช้ panic เมื่อเกิด OOM ดังนั้นจึงมี ecosystem แยกสำหรับการพัฒนา embedded ใน สภาพแวดล้อม no-std
  • slice ของ Go แตกต่างจาก Vec<T> ของ Rust
    append() จะคืนค่า slice ใหม่ และอาจแชร์หน่วยความจำเดิมหรือไม่ก็ได้
    ไม่มีวิธีลดขนาดหน่วยความจำ และถ้าเขียนแค่ append(s, ...) ก็จะละเลย slice ใหม่ที่คืนกลับมา
    Go มีท่าทีแบบ “ทำตามที่ฉันบอก” ส่วน Rust คือ “ตรวจสอบว่าคุณทำตามที่ฉันบอกจริงหรือไม่”
    กล่าวคือ Go ยอมให้เกิดความผิดพลาดเพื่อแลกกับความเรียบง่าย ขณะที่ Rust เลือกทาง ลดความผิดพลาด แม้จะซับซ้อนขึ้นก็ตาม

    • จริงๆ แล้วลดหน่วยความจำได้ด้วย slices.Clip
      และถ้าเขียนแค่ append(s, ...) ก็จะเกิด compile error ดังนั้นข้อความต้นฉบับจึงเป็น ข้ออ้างที่ไม่แม่นยำ เล็กน้อย
      Go เป็นภาษาที่ระมัดระวังมากเรื่องการเพิ่มความซับซ้อนเมื่อจะเพิ่มฟีเจอร์ใหม่
    • คิดว่ามือใหม่จะพลาดแบบนั้นไม่ได้อยู่แล้ว เพราะ “append(s, …)” คอมไพล์ไม่ผ่านตั้งแต่แรก
    • น่าสนใจที่แม้ Go จะมี generics แล้ว แต่ชนิดอย่าง List[T] ก็ยังไม่ค่อยถูกใช้อย่างแพร่หลาย
      อาจเป็นเพราะไม่ค่อยมีกรณีที่ต้องส่ง growable list ไปมาโดยตรง
    • ในสเปกและเอกสารของ Go มีการเขียน กับดัก (foot gun) ส่วนใหญ่ไว้อย่างชัดเจน
      หลายครั้งแค่คนไม่ได้อ่านเอกสารแล้วค่อยมาแปลกใจทีหลัง
  • คิดว่าการจับ UB (Undefined Behavior) ของ C/C++ ด้วยการตรวจตอนรันไทม์นั้นทำได้ยากในโลกจริง
    Android ก็เคยใช้ sanitizer กับทุกคอมมิต แต่เพิ่งจะเห็นว่า ช่องทางโจมตีลดลง หลังจากเปลี่ยนมาใช้ Rust

    • มีคนขอ แหล่งอ้างอิง สำหรับคำกล่าวเรื่อง sanitizer ของ Android
  • ชอบที่บทความเปรียบเทียบภาษานี้พูดถึง จุดแข็งและจุดอ่อน ของแต่ละภาษาอย่างตรงไปตรงมา
    แต่ก็เสียดายที่ไม่ได้พูดถึง Raku
    ในมุมมองของผม ถ้า C–Zig–C++–Rust–Go เป็นเส้นต่อเนื่องของภาษาระดับล่าง ฝั่งระดับสูงก็น่าจะเป็น Julia–R–Python–Lua–JS–PHP–Raku–WL

    • มีคนถามว่า WL คืออะไร
    • Raku เป็น ภาษาทั่วไปที่มีพลังในการแสดงออกสูง รองรับ multiple dispatch, roles, gradual typing, lazy evaluation และระบบ regex ที่ทรงพลังในตัว
      มันรองรับการนิยามไวยากรณ์ในระดับภาษา จึงเหมาะกับการทำ DSL หรือ parsing log
      ด้วยความที่รันบน VM ประสิทธิภาพจึงต่ำกว่า แต่เหมาะกับการ ถ่ายทอดโครงสร้างของปัญหาโดยตรง
      ในฐานะผู้สืบทอดของ Perl มันมุ่งไปสู่ภาษาที่ยืดหยุ่นและสอดคล้องกัน
  • เป็นความเข้าใจผิดถ้าคิดว่าใน Rust เมื่อฟังก์ชันคืนค่าพอยน์เตอร์แล้วจะมีการจอง heap เกิดขึ้นอัตโนมัติ
    ตัวแปรภายในฟังก์ชันอยู่บนสแตก และจะหายไปเมื่อคืนค่า ดังนั้นพอยน์เตอร์จึงใช้ต่อไม่ได้
    ในโหมดปลอดภัยของ Rust จะไม่สามารถ dereference พอยน์เตอร์แบบนั้นได้ และในโหมด unsafe นักพัฒนาจะเป็นผู้รับ ความรับผิดชอบในการรับประกันความถูกต้อง เอง
    ดูเหมือนผู้เขียนน่าจะเข้าใจ Box::new ผิดว่าเป็น “การจัดสรรแบบปริยาย”

    • ยากที่จะเข้าใจว่าทำไมถึงสับสนระหว่าง escape analysis ของ Go กับการจัดสรร heap แบบชัดเจนของ Rust
      มันดูเหมือนเป็นการเข้าใจแนวคิดผิด หรือไม่ก็เป็นการ ชี้นำผิด โดยตั้งใจ
  • ข้อดีที่ใหญ่ที่สุดของ Go คือ โมเดล concurrency ที่เรียบง่าย
    ด้วย goroutine จึงเขียนโค้ดขนานได้ง่าย

    • ข้อดีอีกอย่างของ Go คือด้วย ความสม่ำเสมอของโค้ด ทำให้สำรวจ codebase ขนาดใหญ่ได้ง่าย
      แม้การหาจุดที่ implement interface จะทำได้ยาก แต่ความอ่านง่ายช่วยให้การทำงานเป็นทีมสะดวก
    • ปาฐกถา “Concurrency is not Parallelism” ของ Rob Pike อธิบายปรัชญา concurrency ของ Go ได้ดี
      ไม่มี colored function และการสื่อสารผ่าน channel ก็เรียบง่าย ทำให้เขียนโค้ด concurrency ที่ ถูกต้อง ได้รวดเร็ว
    • แต่ก็มีคนมองว่า Structured Concurrency เป็นโมเดลที่ง่ายกว่า
      บทความที่เกี่ยวข้อง: Structured Concurrency or Go Statement Considered Harmful
    • อินเทอร์เฟซ std.Io ใหม่ของ Zig มีลักษณะคล้ายกับโมเดล concurrency ของ Go
      go keyword ตรงกับ std.Io.async, channel ตรงกับ std.Io.Queue, และ select ตรงกับ std.Io.select
    • ถ้าเป็นนักพัฒนา Erlang ก็คงไม่เห็นด้วยว่ารูปแบบ concurrency ของ Go นั้นง่ายที่สุด
  • สิ่งที่ผมอยากได้คือภาษาที่มี ความเรียบง่ายของ Go แต่ผสานการจัดการ result/error/enum ของ Rust และ generics ที่ดีกว่า

    • ผมก็เห็นด้วย มี ความต้องการในตลาด สูงสำหรับภาษาเนทีฟที่มี GC และมี type system ที่ทรงพลังยิ่งขึ้น
      ผมเคยมอง OCaml, D, Swift, Nim, Crystal มาแล้ว แต่ก็ยังไม่มีภาษาไหนครองตลาดได้จริง
    • ได้ยินมาว่า OCaml รุ่นใหม่มี โมเดล concurrency ที่ทรงพลังมาก และประสิทธิภาพก็แข่งขันกับ Go ได้
    • ภาษา Borgo เคยพยายามไปในทางนั้น แต่หยุดพัฒนาแล้ว
      ตอนนี้น่าจะลองดู Gleam แทน
    • error proposal ของ Go น่าสนใจมาก
      หวังว่าจะมีการปรับปรุงที่ช่วยแก้ปัญหาซ้ำๆ แบบนี้ได้
      ส่วน generics ก็น่าจะยังเป็นโจทย์ยากต่อไป
    • ตัวเลือกที่ใกล้ที่สุดคือ C# แต่ก็ยังเป็น ภาษาที่เน้น OOP อยู่ดี
  • ชอบโทนโดยรวมของบทความนี้ เพราะสัมผัสได้ถึง ความกระตือรือร้นและความอยากรู้อยากเห็น ของนักพัฒนาหน้าใหม่
    การที่ Go ไม่มี generics ไม่ได้เป็นเพียงมินิมัลลิสม์แบบเรียบง่าย แต่เป็น ผลลัพธ์ของการชั่งน้ำหนัก trade-off
    lifetime ของ Rust เป็นอุปสรรคใหญ่ที่สุดสำหรับหลายคน และความใหม่ของภาษานี้ก็มาจากการผสมแนวคิดเดิมเข้าด้วยกัน
    การจัดการหน่วยความจำด้วยมือของ Zig ไม่ได้มีรากจากการปฏิเสธ OOP เท่านั้น แต่ยึดตามปรัชญา Data-Oriented Design (DOD)
    ปาฐกถาที่เกี่ยวข้อง: การบรรยาย DOD ของ Andrew

    • ดังที่ Russ Cox เคยพูดไว้ในบทความปี 2009 “The Generic Dilemma”
      แก่นของปัญหาคือ “จะเลือกโปรแกรมเมอร์ช้า คอมไพเลอร์ช้า หรือรันช้า”
      และดูเหมือนว่าทีม Go จะหาทาง ประนีประนอม ที่น่าพอใจได้ในที่สุด