Go ยังไม่ดีอยู่ดี
(blog.habets.se)- การตัดสินใจด้านการออกแบบหลายอย่างของภาษา Go ถูกทำขึ้นอย่างไม่จำเป็นหรือโดยละเลยประสบการณ์เดิมที่มีอยู่แล้ว
- ปัญหาเรื่อง การจัดการขอบเขตของตัวแปร error ทำให้การอ่านโค้ดและการตามหาบั๊กทำได้ยากขึ้น
- มีการออกแบบที่ ไม่เป็นธรรมชาติและไม่สอดคล้องกับการใช้งานจริง ในหลายส่วน เช่น ความเป็นสองแบบของ nil, การใช้หน่วยความจำ, และความสามารถในการพกพาโค้ด
- ข้อจำกัดของคำสั่ง defer และวิธีจัดการข้อยกเว้นในไลบรารีมาตรฐาน ทำให้การรับประกันความปลอดภัยจากข้อยกเว้นทำได้ยาก
- การจัดการหน่วยความจำและการรองรับ UTF-8 ที่ไม่เพียงพอ รวมถึงปัญหาสะสมอื่น ๆ กำลังส่งผลเสียต่อคุณภาพของโค้ดเบส Go ในระยะยาว
คำวิจารณ์ระยะยาวต่อภาษา Go
- ตามที่เคยเขียนไว้ในโพสต์ก่อนหน้า(Why Go is not my favourite language, Go programs are not portable) ฉันได้ชี้ให้เห็น ปัญหาหลายอย่างของภาษา Go มานานกว่า 10 ปี แล้ว
- โดยเฉพาะอย่างยิ่ง การตัดสินใจออกแบบที่ไม่จำเป็น ซึ่งมองข้ามแนวทางปฏิบัติที่ดีและเป็นที่รู้จักอยู่แล้ว ยิ่งทำให้น่าเสียดายมากขึ้นเรื่อย ๆ
ความไม่เป็นธรรมชาติของขอบเขตตัวแปร error
- ไวยากรณ์ของ Go ทำให้ ขอบเขตของตัวแปร error (
err) กว้างเกินความจำเป็น และเพิ่มโอกาสเกิดความผิดพลาด- ในโค้ดตัวอย่าง ตัวแปร
errยังคงอยู่ตลอดทั้งฟังก์ชันและถูกนำกลับมาใช้ซ้ำ ซึ่งทำให้ ความอ่านง่ายและการบำรุงรักษาโค้ด ลดลง - แม้แต่นักพัฒนาที่มีประสบการณ์ก็อาจเกิดความเข้าใจผิดและเสียเวลาในการตามหาบั๊กเพราะปัญหาของขอบเขตตัวแปรเช่นนี้
- ไวยากรณ์ของภาษาไม่เปิดทางให้จำกัดขอบเขตตัวแปรให้แคบลงอย่างเหมาะสม
- ในโค้ดตัวอย่าง ตัวแปร
nil สองรูปแบบ
- ใน Go มีความสับสนจากการที่ nil ทำงานต่างกันในชนิด
interfaceและชนิด pointer- ตามตัวอย่างด้านล่าง แม้
s(pointer) และi(interface) จะถูกกำหนดเป็น nil แต่s==iกลับถูกประเมินต่างกัน แสดงถึง พฤติกรรมที่ไม่สอดคล้องกัน - นี่เป็นปัญหาประเภทที่โดยทั่วไปแล้วควรหลีกเลี่ยงในการจัดการ null และสะท้อนร่องรอยของการออกแบบที่คิดมาไม่รอบด้านพอ
- ตามตัวอย่างด้านล่าง แม้
ข้อจำกัดด้านความสามารถในการพกพาของโค้ด
- การใช้คอมเมนต์เพื่อทำ conditional compilation ไม่มีประสิทธิภาพอย่างชัดเจนในแง่การบำรุงรักษาและความสามารถในการพกพา
- หากเคยมีประสบการณ์สร้างซอฟต์แวร์ที่พกพาได้จริง จะรู้ว่าวิธีนี้ทั้งยุ่งยากและก่อให้เกิดข้อผิดพลาดได้ง่าย
- ประสบการณ์ที่สั่งสมมาในอดีต ทั้งเรื่องความสามารถในการพกพาของโค้ดและกรณีใช้งานจริง ถูกมองข้ามไป
- รายละเอียดเพิ่มเติมดูได้ที่ Go programs are not portable
ความไม่ชัดเจนของ ownership ใน append
- ความสัมพันธ์ด้าน ownership ระหว่างฟังก์ชัน
appendกับ slice ไม่ชัดเจน ทำให้คาดเดาพฤติกรรมของโค้ดได้ยาก- จากตัวอย่าง เมื่อฟังก์ชัน
fooทำappendกับ slice ก็ยากที่จะรู้ล่วงหน้าว่าจะส่งผลต่อข้อมูลต้นฉบับอย่างไรจริง ๆ - ทำให้ภาษาเต็มไปด้วย “quirk” ที่ต้องจำเพิ่มขึ้น และนำไปสู่ความผิดพลาดได้
- จากตัวอย่าง เมื่อฟังก์ชัน
การออกแบบคำสั่ง defer ที่ยังไม่ดีพอ
- Go ไม่ได้รองรับการปลดปล่อยทรัพยากรอย่างชัดเจนแบบหลักการ RAII(Resource Acquisition Is Initialization)
- เมื่อเทียบกับโครงสร้างจัดการทรัพยากรแบบมีโครงสร้างใน Java และ Python แล้ว ใน Go ไม่ชัดเจนว่าทรัพยากรใดควรถูกปล่อยด้วย
defer - ดังตัวอย่างการทำงานกับไฟล์ ผู้พัฒนาต้องจัดการปัญหา double-close เอง และลำดับกับวิธีการปล่อยทรัพยากรที่ถูกต้องก็ไม่ชัดเจน
- เมื่อเทียบกับโครงสร้างจัดการทรัพยากรแบบมีโครงสร้างใน Java และ Python แล้ว ใน Go ไม่ชัดเจนว่าทรัพยากรใดควรถูกปล่อยด้วย
การจัดการข้อยกเว้นในไลบรารีมาตรฐาน
- แม้ Go จะมีโครงสร้างที่ ไม่รองรับ exception แบบชัดเจน แต่สถานการณ์ผิดปกติอย่าง
panicก็ยังเกิดขึ้นได้- ในบางกรณี
panicไม่ได้ทำให้โปรแกรมหยุดทั้งหมด แต่กลับถูกกลืนหายไป - มีรูปแบบในไลบรารีมาตรฐาน (
fmt.Print, HTTP server ฯลฯ) ที่เพิกเฉยต่อข้อยกเว้น ทำให้ ไม่สามารถรับประกัน exception safety ที่แท้จริงได้ - สุดท้ายแล้วการเขียนโค้ดให้ปลอดภัยจากข้อยกเว้นยังเป็นสิ่งจำเป็น แต่กลับไม่สามารถใช้ข้อยกเว้นได้โดยตรง
- ในบางกรณี
UTF-8 และสตริง
- แม้จะใส่ ข้อมูลไบนารีตามอำเภอใจลงในชนิด
stringGo ก็ยังทำงานต่อไปโดยไม่มีการตรวจสอบพิเศษ- อาจเกิดกรณีที่ชื่อไฟล์ซึ่งสร้างขึ้นก่อนยุคการเข้ารหัส UTF-8 ถูกละทิ้งไปอย่างเงียบ ๆ
- ข้อมูลสำคัญอาจสูญหายได้ เช่น ในงานสำรองข้อมูล และเป็นแนวทางจัดการที่เรียบง่ายเกินไปจนไม่สะท้อนสภาพงานจริง
ข้อจำกัดของการจัดการหน่วยความจำ
- ควบคุมการใช้ RAM ได้โดยตรงยาก และความน่าเชื่อถือของ GC(garbage collection) ก็มีข้อจำกัด
- การใช้หน่วยความจำของ Go เพิ่มขึ้นและเชื่อมโยงไปสู่ปัญหาด้านต้นทุนและประสิทธิภาพในระยะยาว
- ในสภาพแวดล้อมแบบหลายอินสแตนซ์หรือคอนเทนเนอร์ ปัญหาด้านต้นทุนและการขยายระบบเกิดขึ้นจริง
บทสรุป: มีเส้นทางที่ดีกว่านี้
- แม้จะมี แนวทางการออกแบบภาษาที่พิสูจน์ประสิทธิภาพมาแล้ว อยู่ก่อน แต่ Go กลับเมินสิ่งเหล่านั้นในหลายด้าน
- ต่างจากปัญหาในข้อเสนอแรกเริ่มของ Java เพราะในช่วงที่ Go ออกสู่ตลาดนั้น มีแนวทางที่ดีกว่าให้เลือกใช้อยู่แล้ว
เอกสารอ้างอิง
- Uber: Data race patterns in Go
- FasterThanLime: Lies we tell ourselves to keep using Golang
- FasterThanLime: I want off Mr Golang’s wild ride
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
ฉันใช้ Go มาตั้งแต่ยุคก่อน 1.0 ในงานประจำแทบทุกที่ มันเรียบง่ายสำหรับให้เพื่อนร่วมทีมเรียนรู้พื้นฐาน และโดยรวมก็ทำงานได้เสถียร แทบไม่ต้องกังวลเวลาอัปเดตเป็น Go เวอร์ชันใหม่ และฟีเจอร์ที่มีประโยชน์ส่วนใหญ่ก็มีมาให้ในตัวอยู่แล้ว ความเร็วในการคอมไพล์ก็น่าดึงดูดใจ การประมวลผลแบบขนานอาจยุ่งยากนิดหน่อย แต่ถ้าลงเวลาไปกับมันก็จะกลายเป็นวิธีที่ดีในการแสดง data flow ระบบ type ส่วนใหญ่ก็ใช้งานสะดวก แต่บางครั้งก็รู้สึกว่า verbose ไปหน่อย โดยรวมแล้วเป็นเครื่องมือที่ไว้ใจได้ แต่ฉันก็เห็นด้วยกับหลายข้อที่บทความพูดถึงอยู่ดี Go ชัดเจนว่ามีบางส่วนที่นักพัฒนารุ่นเก่ายึดติดกับหลักการมากเกินไปจนพลาดความสะดวกเชิงปฏิบัติไป แน่นอนว่านี่เป็นความรู้สึกส่วนตัว และถ้าแก้ข้อเสียทั้งหมดจริง ๆ มันอาจแย่กว่าตอนนี้ก็ได้ อีกอย่างที่อยากพูดคือในช่วงไม่กี่ปีมานี้รู้สึกว่าบรรยากาศเปิดรับการแก้ไข quirks มากขึ้น สมัยก่อนฉันไม่คิดเลยว่าจะมีการเพิ่ม generics หรือ custom iterators เข้ามา ส่วนคำวิจารณ์เรื่อง RAM และ portability นั้นดูเหมือนเป็นความไม่พอใจส่วนตัวมากกว่า ถ้าปรับปรุงได้ก็ดี แต่ GC แทบไม่ค่อยสร้างปัญหาร้ายแรงกับโปรแกรมส่วนใหญ่ และการดีบักก็ไม่ได้ยากอะไร อีกทั้ง Go ก็รองรับแทบทุกแพลตฟอร์มสำคัญ เพียงแต่ฉันยังรู้สึกไม่สบายใจกับวิธีจัดการ error และ nil อยู่เสมอ มักจะคิดถึงไวยากรณ์แบบ Result[Ok, Err], Optional[T]
ฉันกลับมองว่า Go ไม่ได้ยึดติดกับหลักการหรอก แต่มันหมกมุ่นกับความสะดวกในการแก้ปัญหาตรงหน้าอย่างรวดเร็วมากกว่า ไม่ได้วิเคราะห์ปัญหาให้ถึงรากแล้วแก้ให้ถูกต้อง แต่ให้ความรู้สึกแบบทิ้งแนวคิด ‘Not Invented Here’ แล้วปะติดปะต่อใช้ของเฉพาะหน้า API filesystem ของ Go เป็นตัวอย่างชัดเจน ถ้าต้องการฟังก์ชันเปิดไฟล์ก็ทำแค่
func Open(name string) (*File, error)แล้วจบ แล้วถ้าชื่อไฟล์ไม่ใช่ UTF-8 ล่ะ? พอ 5 ปีแรกยังไม่เจอปัญหาก็ไม่สนใจมันฉันมักรู้สึกว่าหลักการออกแบบของ Go เทน้ำหนักไปที่เป้าหมายว่า “ทำให้คอมไพเลอร์เขียนง่ายและคอมไพล์เร็ว” มากเกินไป โครงสร้างมันโฟกัสที่ตัวคอมไพเลอร์/การคอมไพล์มากกว่าความสะดวกของนักพัฒนา
หลังจาก 20 ปี ฉันได้ใช้ Go อย่างจริงจังครั้งแรกในที่ทำงานใหม่ในฐานะภาษาคอมไพล์แล้ว อาจเป็นเรื่องรสนิยมส่วนตัว แต่พูดตรง ๆ ว่าระหว่างใช้ก็รู้สึกขัดใจอยู่เหมือนกัน ไม่มี default arguments ไม่ชอบวิธีจัดการ error และไม่มี stack trace ที่ดีพอใน production ไวยากรณ์เชิงวัตถุก็ทำให้ต้องแปะตัวอ้างอิงแปลก ๆ ไว้กับทุกฟังก์ชันจนดูไม่สวย pointer ก็เป็นภาระอีก สุดท้ายเลยให้ความรู้สึกเหมือนย้อนกลับไปหาเทคโนโลยีเก่าแบบ C/C++ บรรยากาศเหมือนตอนเรียนมหาวิทยาลัยราวปี 1999 เป๊ะ
ในแง่การประมวลผลแบบขนาน Go เป็นระบบเดียวเท่าที่ฉันเคยเจอที่จัดการ parallelism บน CPU หลายคอร์ได้อย่างเป็นธรรมชาติในระดับภาษาเอง formalism แบบ CSP ของ goroutine/channel ทำให้ตรรกะงานขนานเขียนออกมาได้ตรงไปตรงมา Python ปวดหัวกับ GIL และ async libraries ที่เข้าใจยาก ส่วน C, C++, Java ฯลฯ ต้องพึ่งไลบรารีเสริมภายนอกภาษา ทำให้ reasoning เรื่อง parallelism ในระดับภาษาไม่ง่าย ด้วยเหตุนี้ฉันจึงมองว่า go เหมาะสมอย่างยิ่งกับ HTTP server หรือพวก services และจากประสบการณ์ก็ยังไม่เห็นทางเลือกไหนที่ดีเท่านี้
จากมุมมองนักพัฒนา ในด้าน ergonomics หรือก็คือความเป็นมาตรฐานและความสม่ำเสมอ ฉันรู้สึกว่ามันเกือบสมบูรณ์แบบ ในหลาย microservices codebase ก็ไม่ต้องกังวลว่าสไตล์จะต่างกัน และไม่ต้องเถียงกันเรื่อง format ด้วย เพียงแต่ตอน Go เลือกแนวทางมาตรฐานของตัวเอง มันเหมือนจะยึดติดกับสไตล์เก่าเกินไปหน่อย นักพัฒนายุคนี้คาดหวังเมธอดเชิง functional อย่าง
map/filterมากกว่า แต่ Go ให้มาแค่ลูปที่เสี่ยงพลาดเรื่อง index และระบบ type ก็ไม่ได้ฉลาดระดับ TypeScript การจัดการ error ก็ไม่สะดวก เข้าใจได้ว่าถ้าเพิ่มความสามารถเหล่านี้อาจทำให้มี “วิธีใช้ที่สร้างสรรค์แต่ไม่ดี” มากขึ้น แต่ก็สัมผัสได้จริง ๆ ว่ายากจะโน้มน้าวคนรุ่น JS ให้หันมาใช้ goฉันทุ่มกับโปรเจกต์ Golang ขนาดใหญ่มานานกว่า 5 ปี และพอไปทำคอมโพเนนต์ที่ต้องลดการใช้หน่วยความจำให้มากที่สุด ก็จะเจอด้านที่หลวมของ Go อยู่บ่อย ๆ GC บางทีก็เก็บไม่ทัน หรือปัญหา heap fragmentation ก็รุนแรงมากขึ้น (เพราะ Go ไม่ได้เป็น compacting garbage collector) ด้วยเหตุนี้เลยพยายามหลีกเลี่ยงการ allocation ให้หมด แต่ก็ทำให้เกิดบั๊กได้ง่าย การดีบักก็ยากมากด้วย heap profile แสดงแค่ข้อมูลของ object ที่ยังรอดอยู่ แต่ไม่แสดงขยะที่สะสมจริงหรือรายละเอียด fragmentation เลย ทำให้ต้องเดาส่วนใหญ่ ตัวอย่างเช่น ฟังก์ชัน X อาจแสดงว่า allocate heap แค่ 1KB แต่ถ้าถูกเรียกซ้ำในลูปก็อาจสร้างขยะเป็นหลายสิบ MB ได้ สุดท้ายเลยต้อง preallocate static buffer ไว้แล้ว reuse เอง ซึ่งทำให้ปัญหา ownership ซับซ้อนขึ้นและเกิดช่องโหว่อย่าง
appendตามมา บางครั้งถึงขั้นต้องเขียน standard library ใหม่เองด้วย เราก็รู้ว่ากรณีของเราไม่ใช่เคสทั่วไป แต่ก็อดเสียดายไม่ได้เพราะมันให้ความรู้สึกเหมือนกำลังสู้กับภาษาอยู่จริง ๆในกรณีแบบนี้ เอา memory ออกไปไว้นอก heap อาจเจ็บปวดน้อยกว่า แน่นอนว่าเพราะเป็นภาษา GC มันจึงไม่ง่าย แต่ถ้าต้องฝืนเขียนโค้ดแนว C++/Rust ใน Go ก็อาจดีกว่าถ้าจะเปลี่ยนส่วนนั้นไปใช้ภาษาเหล่านั้นตรง ๆ เลย
ฉันคิดว่าการเลือกภาษา go สำหรับสถานการณ์แบบนี้คือปัญหาตั้งแต่การเลือกภาษาแล้ว C/C++/Rust/Zig น่าจะเหมาะกว่า
มีข่าวว่า garbage collector ตัวใหม่ชื่อ "Green Tea" อาจช่วยได้ มันเป็น parallel mark algorithm ที่จัดการ object ที่อยู่ใกล้กันในหน่วยความจำได้ดีขึ้น แม้จะไม่ได้เน้น memory โดยตรง ข้อมูลที่เกี่ยวข้องดูได้ที่นี่
มีการทดลองเรื่อง arena อยู่ช่วงหนึ่ง แต่ตอนนี้หยุดไปแล้ว ถึงอย่างนั้นก็ยังเป็นเรื่องที่น่าสนใจ
ขอโทษที่พูดแบบไม่ค่อยช่วยอะไร แต่จากสถานการณ์ตอนนี้ ฉันคิดว่าเลือกภาษาผิดเต็ม ๆ บางทีอาจเป็นเพราะนโยบายภาษาทางการของบริษัทเลยต้องฝืนใช้ go ก็ได้ บริษัทใหญ่ ๆ มักอนุมัติ production เฉพาะภาษาที่มีการใช้แพร่หลายเท่านั้น
ฉันยังไม่เข้าใจเลยว่าทำไม
deferของ Go ถึงทำงานเฉพาะใน function scope ไม่ใช่ lexical scope เรื่องนี้มารู้จากประสบการณ์ตรงตอนประมวลผลไฟล์ในลูป แล้วพอรายการไฟล์ใหญ่ขึ้นdeferก็ไม่ปิด handle จนกว่าฟังก์ชันจะจบ ทำให้โปรแกรมล่ม นักพัฒนา Go รอบตัวฉันบอกให้ห่อ body ของลูปด้วย anonymous function ไปเสีย นอกนั้นแม้จะมีจุดเล็ก ๆ น้อย ๆ ที่ขัดใจ Go ก็ยังให้ความรู้สึกใช้งานสบาย ไวยากรณ์มีประสิทธิภาพ และยังช่วยกันวัฒนธรรมการ ‘อวดของ’ ที่ไม่จำเป็นด้วย ฉันเคย rewrite โปรเจกต์ C# ขนาดใหญ่เป็น Go แล้วแม้ฟีเจอร์จะมีแค่หนึ่งในสิบ แต่โค้ดกลับน้อยกว่า มันบังคับให้ใช้ค่าเริ่มต้นที่ทำงานเร็ว แทนที่จะบังคับให้เกิด GC allocation และความสามารถ built-in code generation สำหรับงานอย่าง serialization ก็สะดวกดี ต่างจากไวยากรณ์ C# ที่พยายามแทนทุกอย่างด้วยภาษา เช่น ORM ใน Go บรรยากาศจะเป็นแบบ SQL ก็เขียนเป็น SQL ไปเลย และ gRPC ก็จัดการผ่าน protobuf specบางครั้งเราต้องการ
deferแบบ lexical scope และบางครั้งก็ต้องการแบบ function scope ตัวอย่างเช่น ถ้าในลูปเปิดหลายไฟล์และอยากให้ทุกไฟล์เปิดค้างไว้จนฟังก์ชันจบ ก็ต้องใช้ function scope ตอนนี้มันเป็น function scope อยู่แล้ว แต่ถ้าต้องการ lexical scope ก็ห่อด้วยfuncได้ ถ้ากลับกันคือรองรับแค่ lexical scope แล้วเราต้องการ function scope จะทำยังไงต่อก็ดูไม่ชัดข้อดีคือช่วยลดการเยื้องเข้าไปอีกชั้นโดยไม่ต้องมีฟังก์ชันห่อ การทำงานก็ผูกกับ call stack หรือ stack unwinding โดยธรรมชาติ และถ้ามองจากสไตล์
goto failของ C ก็ถือว่าดูเป็นธรรมชาติดี แน่นอนว่าเวลาใช้deferในลูปก็ต้องห่อด้วยฟังก์ชันต่างหากเลยออกจะไม่สะดวกหน่อยฉันเคยใช้มาทั้งภาษาที่มี block-level และ function-level
deferแล้ว บางทีก็อดคิดไม่ได้ว่าอยากใช้ function-leveldeferในifบ้างเหมือนกันดูไม่น่าจะมีเหตุผลลึกซึ้งอะไรเป็นพิเศษ และก็สงสัยเหมือนกันว่ามันสำคัญมากจริงหรือ
ใน C# ก็ทำงานกับ SQL หรือ protobuf spec ได้เหมือนกัน แค่มีทางเลือกอื่นเพิ่มเข้ามาเท่านั้น
Go มีข้อเสียเยอะก็จริง แต่ถ้ามองในกลุ่มภาษา server-side ฉันยังไม่เห็นภาษาที่สมดุลได้ขนาดนี้ มันเร็วกว่า Node หรือ Python และฉันก็คิดว่าระบบ type ดีกว่าด้วย ขณะเดียวกันก็เข้าถึงง่ายกว่า Rust และมี standard library กับ tooling ที่ยอดเยี่ยม ความเรียบง่ายของไวยากรณ์และการบังคับให้มีวิธีเดียวก็เป็นสิ่งที่ฉันชอบ การจัดการ error มีปัญหาก็จริง แต่ก็ยังดีกว่าแบบ Node ที่
catchรับอะไรมาก็ได้ ฉันสงสัยว่ามีภาษาอื่นไหมที่ตอบโจทย์ทั้งหมดนี้ ฉันไม่ได้คลั่ง Go อะไรนัก ตลอดอาชีพฉันทำ backend ด้วย Node มาเยอะ แต่ช่วงหลังลองใช้ Go แบบทดลองอยู่จริง ๆ แล้วข้อดีทั้งหมดนี้น่าจะพูดกับ Java หรือ C# ได้เหมือนกัน
ฉันขัดใจนิดหน่อยเวลามีคนเรียก 'Node' ว่าเป็นภาษาโปรแกรม Node เป็น JavaScript runtime และทุกวันนี้โปรเจกต์จำนวนมากที่รันบน Node ก็เขียนด้วย TypeScript นั่นหมายความว่าพอพูดว่า Node ก็ยังไม่ชัดว่าหมายถึงภาษาอะไร ถ้าใช้ TypeScript เป็นฐาน ฉันกลับรู้สึกว่ามัน productive กว่าระบบ type ของ Go เสียอีก และก็พูดแบบเดียวกันได้เมื่อเทียบกับ Rust
ภาษาส่วนใหญ่ต่างก็มีจุดที่ใช้งานไม่สะดวกในแบบของตัวเอง Go มีทั้ง performance, portability และ runtime/ecosystem ที่ดีมาก ในทางกลับกันก็มีข้อเสียอย่าง nil pointer, zero value, ไม่มี destructor, ไม่มี macro เป็นต้น (การไม่มี macro ใน Go ทำให้มีการใช้ code generation แบบฝืน ๆ มากขึ้น) ยังมีภาษาที่ดีกว่าอยู่ เช่น Rust แต่ก็ซับซ้อนกว่า Go มากเช่นกัน ซึ่งเป็นผลมาจากการที่ผู้ออกแบบ Go ให้ความสำคัญกับความเรียบง่ายเป็นอันดับแรก
ถ้ามองจากพัฒนาการล่าสุดของระบบ type ใน Python ฉันว่าไปไกลกว่า Go มาก โดยเฉพาะถ้าเทียบเฉพาะ structural typing นี่ Python น่าประทับใจกว่า
ฉันคิดว่าระบบ type ของ Go ยังขาดไปเยอะมาก
ฉันเคยขยาย static site generator ที่เขียนด้วย Go อยู่ครั้งหนึ่ง โค้ดนั้นชัดเจนและอ่านง่ายมาก แต่ขยายต่อได้ยากเพราะช่องโหว่ของภาษา แค่เปลี่ยนอะไรเล็กน้อยก็ต้องไปรื้อหลายจุดอย่างลำบาก การทำ encapsulation และ abstraction หลายระดับเป็นเรื่องยาก และ abstraction ก็ถูกเสียสละไปเพื่อแลกกับ ‘ความเรียบง่าย’ ทั้งที่ abstraction เป็นวิธีสำคัญที่สุดในการทำโค้ดที่ขยายต่อได้ง่าย Go เลือกความเรียบง่ายแทน extensibility โดยรวมแล้วโปรแกรม Go มักให้ความรู้สึกเป็น “ความเรียบง่ายที่ขยายต่อไม่ได้” คนชอบบอกว่า Go ก็เป็นแบบนี้แหละ แต่จากประสบการณ์ฉันก็ยังไม่คล้อยตาม อย่างน้อย ‘ประสบการณ์ในการพัฒนา’ ก็ไม่ได้แย่
บทสนทนาเกี่ยวกับ Go มักให้ความรู้สึกแปลก ๆ เสมอ พอวิจารณ์ก็มักจะได้คำตอบว่า “ภาษานี้มันก็เป็นแบบนั้นอยู่แล้ว” เหมือนให้ยอมรับไปเฉย ๆ เขาบอกว่าความเรียบง่ายเป็นจุดแข็ง แต่ถ้าจะดึงรายการ key ของ map ออกมายังต้องเขียนลูปเอง แบบนี้มันเรียบง่ายกว่าจริงหรือ
อยากถามว่าคุณใช้ Go แค่แป๊บเดียวแล้วกล้าตัดสินแบบนี้ได้อย่างไร ฉันทำงานกับ codebase Go ขนาดใหญ่มากหลายชุดมาตั้งแต่ปี 2015 ระดับหลายล้านบรรทัด และทำงานมาหลายทีมแล้ว ความสามารถในการขยายของ Go ไม่ได้ด้อยไปกว่า C, C#, Java อย่างมีนัยสำคัญ Go แค่มีแนวโน้มเลือกความชัดเจนมากกว่าความ expressive เลยทำให้มี abstraction layers น้อยลง และชินกับการเขียนแบบ concrete และ explicit มากขึ้น แต่ฉันไม่คิดว่านั่นแปลว่าไปต่อไม่ได้ การออกแบบที่ modular และขยายต่อได้เป็นเรื่องที่นักพัฒนาต้องเรียนรู้ ไม่ใช่สิ่งที่ภาษาให้หรือไม่ให้ โค้ดที่คุณเจอแค่ออกแบบมาไม่ดี ไม่ใช่ข้อจำกัดของภาษา Go
ฉันใช้ Go มาหลายปี มันช่วยให้ทำของเล็ก ๆ ได้เร็ว แต่พอระบบใหญ่ขึ้นก็ต้องทรมานกับความไม่สะดวกเล็ก ๆ น้อย ๆ จำนวนมาก โดยเฉพาะการดีบักที่เหมือนฝันร้าย ถ้ามี X ที่ไม่ได้ใช้ (ซึ่งเกิดตลอดเวลาเวลาคอมเมนต์บางส่วนระหว่างดีบัก) ก็จะคอมไพล์ไม่ผ่านเลย เรื่องแบบแผนที่ไม่จำเป็น ชื่อไฟล์พิเศษ และชื่อ field ที่สงวนไว้จำนวนมากก็ยุ่งยาก standard library ยังมี
panicซ่อนอยู่ และการ copy ไป heap แบบไม่คาดคิดก็ทั้งช้าและน่ารำคาญ ส่วนที่ดูเหมือน ‘เวทมนตร์’ ใน Go ส่วนใหญ่เป็นผลข้างเคียงจากการฝืนยืมกลไกเดิมมาใช้ เช่น ชื่อไฟล์พิเศษหรือการใช้ตัวพิมพ์ใหญ่เล็ก ถ้าจะให้เปิดเผยจริง ๆ จะใช้คำว่าpublicตรง ๆ หรือพิมพ์pubก็ยังได้ แต่กลับดื้อดึงอย่างประหลาด ทุกวันนี้ AI ดีขึ้นมาก ถ้าเจอปัญหาเรื่อง type หรือ borrow checker ใน Rust ก็ถาม AI ได้ทันทีแล้วแก้ได้เร็ว ทำให้สนุกกว่ามาก ไม่ต้องเสียเวลาไล่เอกสารหรือ SO เหมือนเมื่อก่อนฉันยังไม่ได้ใช้ Rust จริงจังในช่วงหลัง แต่ตอนลองสั้น ๆ เมื่อเดือนธันวาคมที่ผ่านมา ก็ทึ่งว่า AI รับมือกับ Rust ได้ดีมาก เพราะมันมีทั้ง syntax ที่ละเอียดและข้อมูล type ที่ชัดเจน จนบางที AI แก้ได้ดีกว่าคนเสียอีก
เวลาบ่นเรื่องคอมไพล์เออเรอร์ระหว่างดีบักใน Go ฝั่ง Go ก็มักตำหนิกลับว่า “ก็ใช้เครื่องมือให้ถูกสิ” เหมือนเอาหลักการมาใช้แบบสุดโต่งจนใช้งานลำบาก
ฉันเคยเล่าเรื่องความลำบากในการดีบักนี้ให้หนึ่งในผู้สร้าง Go ฟัง แต่เขาเองก็ยังไม่เข้าใจว่ามันเป็นปัญหาตรงไหน เลยรู้สึกผิดหวังว่าเป็นมุมมองที่ amateur มาก อ้างอิงเพิ่มเติมคือ AI กลับจัดการ Go ได้ไม่ค่อยดี ทั้งที่ภาษาเรียบง่ายกว่า ChatGPT ยังรองรับ Java, C#, Python ได้ดีกว่า
โดยส่วนตัวฉันไม่ชอบ Go และเห็นข้อเสียใหญ่ ๆ ของมันเยอะมาก แต่ก็ชัดเจนว่าทำไมมันยังได้รับความนิยมสูง Go ค่อนข้างเร็ว และด้วยพื้นฐาน goroutine ก็ทำให้เขียนบริการที่มี concurrency สูงได้ง่ายและเชื่อถือได้โดยไม่ต้องจัดการ multithreading ตรง ๆ ตอน Google ปล่อย Go ออกมา แทบไม่มีภาษายอดนิยมแบบ static และ compiled ที่อยู่ในตำแหน่งคล้ายกัน ทุกวันนี้คู่แข่งที่อยู่ใกล้เคียงจริง ๆ ก็มีแค่ Java (ที่ตอนนี้รองรับ virtual threads แล้ว) ภาษาที่รองรับ async/await ก็สัญญาว่าจะได้คล้ายกัน แต่ในทางปฏิบัติมีความซับซ้อนเต็มไปหมด เช่น การหลีกเลี่ยง blocking ใน asynchronous tasks หรือ function coloring ส่วน Erlang ก็คนละหมวดไปเลย สุดท้ายแม้ข้อเสียจะเยอะ แต่ goroutine และชื่อชั้นของ Google project ก็ยังทำให้มันดังอยู่ดี
JVM กำลังค่อย ๆ ลดช่องว่างกับ Go ลง ผ่านโปรเจกต์อย่าง virtual threads, zgc, lilliput, Leyden, Valhalla มันดีขึ้นเรื่อย ๆ การเปลี่ยนแปลงจาก Java 8 ไปถึง 25 ถือว่าใหญ่มาก และจากนี้ไปก็น่าจะสะดวกขึ้นอีก
ความ explicit และความเรียบง่ายของ Go เหมาะมากกับการเขียนโปรแกรมโดยมี LLM ช่วย โค้ด Go 1.x เก่า ๆ ก็ยังรันบนเวอร์ชันใหม่ได้ดีเหมือนเดิม
ที่จริงแล้วภายใน Google เอง Java ที่มี virtual threads ถูกใช้บ่อยกว่า Go มาก
อยากรู้ว่าคุณคิดว่าภาษา “สมัยใหม่” ที่เหมาะที่สุดกับโปรเจกต์ใหม่คือภาษาอะไร
ฉันชอบ Go มาตั้งแต่ก่อนออก 1.0 แต่ไม่เห็นด้วยกับคำวิจารณ์ว่า “จนถึงตอนนี้ก็ยังทำไม่สำเร็จ” แน่นอนว่ามีข้อเสียและเรื่องให้น่าหงุดหงิดอยู่ แต่ฉันก็คิดว่าถ้าผู้ก่อตั้งออกจากโปรเจกต์ไป การรักษาวิสัยทัศน์ส่วนกลางก็ยิ่งยากขึ้น และภาษาก็อาจเสี่ยงแย่ลง การถูกวางตำแหน่งให้เป็นแค่ “ภาษาเซิร์ฟเวอร์” เองก็อาจทำให้คนไหลไปหา Rust หรือ Python มากขึ้น สมัยก่อน Visual Basic ก็เคยโดนดูแคลน แต่สุดท้ายคนที่ต้องใช้ก็ยังใช้กันได้ดี
ถ้าตรวจดูอย่างละเอียด บทความวิจารณ์ข้อเสียของ Go หลายชิ้นจริง ๆ แล้วไม่ได้พูดถึงปัญหาที่ใหญ่ขนาดนั้น ส่วนมากแม้จะถูกต้องในเชิงเทคนิค แต่เป็นเรื่องเล็กน้อย ตรงกันข้าม ปัญหาการออกแบบภาษาที่ร้ายแรงจริง ๆ คือ zero value, การไม่รองรับ constructor, การจัดการ null ที่อ่อน, default mutability, ระบบ type ที่ไม่ได้ออกแบบเผื่อ generics,
intที่ไม่รองรับ arbitrary precision และsliceที่มีปัญหา ownership คลุมเครือ (ประเด็นที่เกี่ยวข้อง 1, ประเด็นที่เกี่ยวข้อง 2) การไม่มี sum type และไม่รองรับ string interpolation ก็เป็นข้อเสียเช่นกันฉันอาจมีอคติเพราะถึงขั้นเขียนหนังสือเกี่ยวกับ Go แต่ในฐานะคนที่ใช้ Go มากว่า 10 ปี ตอนแรกมันให้ความรู้สึกสดใหม่มาก มันมี boilerplate น้อยกว่า Java เรียนรู้ง่าย และ performance ก็ใช้ได้ ไม่มีภาษาที่ดีที่สุด มีแต่ภาษาที่เหมาะที่สุดตามงาน แต่สำหรับงาน backend ทั่วไปแล้ว มันเป็นตัวเลือกที่ไม่น่าเสียใจ