- บทความนี้อธิบายข้อจำกัดและปัญหาของภาษา Go ที่ผู้เขียนรู้สึกหลังจากใช้งานมาหลายปีแล้วเปลี่ยนไปใช้ Java
- นำเสนอมุมมองว่าคุณลักษณะที่ Go เป็นภาษาที่เรียบง่ายและน่าเบื่อ (boring) อาจไม่ใช่ข้อดีเสมอไป แต่อาจกลายเป็นข้อเสียได้
- ปรัชญาของ Go: ทีมออกแบบ Go ของ Google เน้นความเรียบง่ายและข้อจำกัด แต่สิ่งนี้ทำให้เกิดงานซ้ำ ๆ ที่ผู้ใช้ต้องแก้เอง
1. การที่ Go "ไม่น่าสนุก" อาจเป็นข้อเสียได้
- ข้ออ้างของ Russ Cox:
- ย้ำว่าการที่ Go "น่าเบื่อ (boring)" เป็นข้อดี
- มีลูปเพียง
for แบบเดียว และไม่มีฟังก์ชันอย่าง filter, map, reduce ให้มาในตัว
- การไม่มีฟีเจอร์ขั้นสูงหลากหลายแบบที่ภาษาส่วนใหญ่อื่น ๆ มี ถือเป็นส่วนหนึ่งของความเรียบง่าย
- ความเห็นของผู้ใช้ Reddit:
- เส้นแบ่งระหว่าง "น่าเบื่อ" กับ "ทรงพลัง" นั้นคลุมเครือ
- มองว่าการขาดฟังก์ชันพื้นฐานของ Go มีโอกาสสูงที่จะถูกเพิ่มเข้ามาในภาษาในอนาคต
- การพึ่งพาแพ็กเกจจากภายนอก:
- แพ็กเกจ
samber/lo ที่มักใช้เพื่อชดเชยฟังก์ชันที่ขาดไป:
- มีความสามารถสำคัญอย่าง filter, map, search
- ได้ 18.1k ดาวบน GitHub และถูกใช้ในมากกว่า 12.6k โปรเจกต์
- บางฟังก์ชันถูกเพิ่มเข้ามาในแพ็กเกจ
slices แล้ว แต่ก็ยังขาดความสามารถอยู่
- ความไม่พอใจของผู้เขียน:
- ถูกบังคับให้เขียนลูปซ้ำ ๆ
- จัดการงานอย่าง filter และ map ให้กระชับได้ยาก
- แม้จะแยกออกเป็น receiver method ได้ แต่ก็ทำให้โค้ดดูไม่สะอาดนัก
- ความเรียบง่ายของ Go เป็นข้อดีในหลายกรณี แต่การขาดฟังก์ชันอำนวยความสะดวกพื้นฐานก็อาจเป็นข้อเสียที่ลดทั้งประสิทธิภาพการทำงานและความอ่านง่ายของโค้ดได้
2. ขัดขวางหลักการ Clean Code
- ปัญหาการจัดการ error:
- ฟังก์ชันส่วนใหญ่มักรวม
error ไว้ในค่าที่คืนกลับมาด้วย:
- ต้องใช้แพตเทิร์น
if err != nil ซ้ำไปซ้ำมา
- พอพยายามจัดระเบียบโค้ดกลับยิ่งทำให้โค้ดซับซ้อนขึ้น
- โค้ด HTTP handler ในโปรเจกต์เล็ก ๆ ก็อาจยาวเกิน 20 บรรทัด
- ทั้งที่เป้าหมายเดิมคืออยากให้คงไว้ราว 4 บรรทัด
- ถึงขั้นหงุดหงิดกับแนวทางจัดการ error จนคิดจะใช้
panic() และ recovery middleware
- การแนะนำให้ใช้ชื่อสั้น:
- มีการแนะนำให้ใช้ชื่อตัวแปร เมธอด และฟังก์ชันแบบสั้น ๆ:
- ชื่ออย่าง
c, a ไม่ชัดว่าแปลว่าอะไร
- เช่น
c หมายถึง Command, Controller, Argument หรือ Amendment กันแน่?
- แม้การใช้ชื่อยาวจะช่วยให้ชัดขึ้น แต่ปรัชญาของ Go กลับโน้มเอียงไปทางชื่อสั้น
- สิ่งนี้นำไปสู่การถกเถียงไม่รู้จบใน code review ของทีม เช่น เรื่องชื่อเมธอดทดสอบ
- แม้ปรัชญาของ Go จะเน้นความกระชับและเรียบง่ายของโค้ด แต่ผลลัพธ์อาจกลับกลายเป็นความซับซ้อนและความไม่มีประสิทธิภาพที่ขัดกับหลักการ clean code
3. ปรัชญาภาษาเล็กโดยเจตนาและวัฒนธรรม DIY
- ขาดความสามารถพื้นฐาน:
- การทำ HTTP handler แบบง่ายนั้นไม่ยาก แต่ถ้าต้องการ middleware พื้นฐาน (เช่น exponential backoff, การตั้งค่า cross-site ฯลฯ) ก็ต้องไปไล่หาแพ็กเกจหลายตัว
- และยากจะมั่นใจว่าแพ็กเกจเหล่านั้น (1) ยังมีการดูแลอยู่หรือไม่ และ (2) ทำงานได้ตามที่คาดหวังจริงหรือไม่
- งานซ้ำเพิ่มขึ้น:
- ปรัชญาการออกแบบของ Go ที่พยายามรักษาความเรียบง่าย กลับกลายเป็นการผลักให้นักพัฒนาต้อง "ประดิษฐ์ล้อขึ้นมาใหม่"
- ตัวอย่างเช่น แม้แต่ฟังก์ชัน filter ง่าย ๆ ก็ยังต้องลงมือทำเอง
- ระบบนิเวศของแพ็กเกจที่ยังไม่สุกงอม:
- โปรเจกต์บน GitHub จำนวนมากถูกปล่อยทิ้งไว้ หรือออกมาเพียงไม่กี่เวอร์ชัน
- แม้จะอาจไม่ยุติธรรมที่จะเทียบกับ .NET/Java เพราะ Go ยังเป็นภาษาที่อายุน้อยกว่า แต่ในทางปฏิบัติก็ยังขาดทั้งเสถียรภาพและความสุกงอมของแพ็กเกจ
- ข้อจำกัดของ ORM:
- แพ็กเกจ ORM หลักของ Go อย่าง Gorm ยังตามหลัง Hibernate หรือ Entity Framework ในด้านความสามารถ
- มีทั้งพฤติกรรมแปลก ๆ และปัญหาด้านเอกสารประกอบที่ไม่เพียงพอ
- ปฏิกิริยาจากชุมชน Go: "Go ไม่ต้องมี ORM ก็ได้, Do It Yourself!"
- ความเรียบง่ายของ Go อาจเป็นข้อดีได้ ขึ้นอยู่กับโปรเจกต์และทีม แต่การขาดความสามารถที่ควรมีมาให้ตั้งแต่ต้นอาจส่งผลลบต่อทั้งประสิทธิภาพการทำงานและประสบการณ์ของนักพัฒนา
4. ใน Go ไม่ได้มีเพียงวิธีเดียวเสมอไป
- ความเข้าใจผิดเรื่องความสม่ำเสมอและความเป็นหนึ่งเดียว:
- Table Test
- ใช้ test suite อย่าง
stretchr/testify (ถูกใช้ใน 557k โปรเจกต์)
- เขียน custom subtest ภายใน table test
- สิ่งนี้ทำให้เห็นช่องว่างระหว่างปรัชญาแบบ "มีวิธีที่เป็นหนึ่งเดียว" ของ Go กับความเป็นจริง
- ก่อให้เกิดความขัดแย้งภายในทีม:
- การถกเถียงเรื่องสไตล์การทดสอบและวิธี implementation ระหว่างทีมกลับเพิ่มมากขึ้น
- แม้แต่ปรัชญาและทีมออกแบบของ Go เองก็ยังขาดความสม่ำเสมอ:
- เช่น ความไม่สอดคล้องกันเรื่องการตั้งชื่อ getter method
- การปฏิเสธฟีเจอร์และการพึ่งพาแพ็กเกจ:
- ทีม Go ปฏิเสธการเพิ่มฟังก์ชัน assertion และถูกวิจารณ์ว่ามีท่าทีโทษผู้เขียนโปรแกรมแทนที่จะแก้ปัญหา
- สุดท้ายถ้าต้องการใช้ฟีเจอร์ที่จำเป็น ก็ต้องติดตั้งแพ็กเกจเพิ่มอีกอยู่ดี (
go get)
- แม้ Go จะมุ่งสู่ความเรียบง่ายและความเป็นหนึ่งเดียว แต่ในความเป็นจริงกลับมีวิธีทำหลากหลายแบบและข้อถกเถียงตามมา ขณะที่ความกำกวมของปรัชญาการออกแบบภาษายิ่งทำให้ปัญหาแย่ลง
5. การดีบักใน Go ไม่น่าสนุก
- ไม่สามารถประเมิน expression ระหว่างดีบักได้:
- ระหว่างเซสชันดีบัก ไม่สามารถประเมิน expression หรือดูการแสดงผลข้อความแบบกำหนดเองของอ็อบเจ็กต์ได้
- ทำให้เข้าใจสถานะของอ็อบเจ็กต์ขณะรันได้ยาก
- stack trace และ log ที่ไม่เป็นมิตร:
- เมื่อการทดสอบขนาดใหญ่ล้มเหลว (เช่น รันการทดสอบหลายพันรายการใน CI) จะได้ stack trace และ log ที่ชวนสับสน
- ส่งผลให้การดีบักยากขึ้นและประสิทธิภาพการทำงานลดลง
- ประสบการณ์ดีบักสไตล์ C:
- toolchain สำหรับดีบักของ Go ทำงานบนฐานแบบ C:
- ให้ประสบการณ์ดีบักที่ดิบและคล้าย C
- ไม่เป็นมิตรกับนักพัฒนา
- เปรียบเทียบกับ Rust:
- Rust ปรับปรุงข้อจำกัดของ Go ได้ดีกว่า:
- ให้ข้อมูล error ที่ชัดเจนและมีประโยชน์
- มีคำแนะนำการแก้ไขที่แม่นยำอยู่ในข้อความ error
- ประสบการณ์การดีบักของ Go ตั้งอยู่บนปรัชญาการออกแบบที่เน้นการสร้างไบนารีที่เหมาะสมต่อการใช้งานจริง แต่ต้องแลกมาด้วยประสบการณ์ของนักพัฒนา ในสภาพแวดล้อมที่ให้ความสำคัญกับประสิทธิภาพการดีบัก การพิจารณาภาษาอื่นอาจเหมาะสมกว่า
สรุป: ความเหมาะสมและข้อจำกัดของ Go
- ข้อดีของเครื่องมือในตัวของ Go:
- มี toolchain พื้นฐานสำหรับ package management, การทดสอบ และ performance monitoring
- ใช้งานได้โดยไม่ต้องตั้งค่าเพิ่ม ช่วยให้การตั้งค่าสภาพแวดล้อมเริ่มต้นง่ายขึ้น
- ข้อจำกัด:
- "โค้ดน่าเบื่อ" และงานซ้ำ ๆ:
- toolchain ของ Go ใช้งานได้จริง แต่บังคับให้เขียนงานซ้ำ ๆ (Plumbing Code) เวลาลงมือเขียนโค้ด
- เช่น ไวยากรณ์ที่จำเจและความสามารถที่จำกัดทำให้ความน่าสนใจของงานลดลง
- "import cycle not allowed":
- ไม่อนุญาตให้มี circular dependency (import cycle) ในการทดสอบ
- เมื่อทำงานแบบ domain-driven design (DDD) จึงเพิ่มความซับซ้อนจากข้อจำกัดเชิงโครงสร้าง
- การพึ่งพาเทคนิค
struct embedding:
- กลไก
struct embedding ที่แปลกและมีข้อจำกัด ทำให้ใช้งานได้อย่างลำบาก
- ขอบเขตที่เหมาะสม:
- เหมาะกับงานพัฒนา infrastructure:
- เครื่องมือระดับระบบอย่าง Docker, Drone, Hugo ถูกเขียนด้วย Go
- มีประโยชน์สำหรับการพัฒนาเซิร์ฟเวอร์ขนาดเบาและแอปพลิเคชัน CLI
- ขอบเขตที่ไม่เหมาะสม:
- การพัฒนาแอปพลิเคชันองค์กรที่ซับซ้อน (เช่น ระบบ ERP):
- ปรัชญาภาษาที่จำกัดและเครื่องมือที่มีอยู่ทำให้การจัดการ business logic ขนาดใหญ่ไม่มีประสิทธิภาพ
- Go มีประสิทธิภาพยอดเยี่ยมสำหรับงานบางประเภท โดยเฉพาะงานด้าน infrastructure แต่ไม่ใช่เครื่องมือที่เหมาะกับแอปพลิเคชันที่มี business domain ซับซ้อน แม้ CTO จะเอนเอียงไปทางเทคสแต็กของ Google ก็ควรระมัดระวังในการเลือกเทคโนโลยี
5 ความคิดเห็น
ถ้ามีแค่
?ของ Rust ก็คงดีกว่าตอนนี้มากแล้วไม่ใช่หรือ...พอได้ใช้ Go แล้วก็ทำให้คิดขึ้นมาว่า ที่ผ่านมาผมจัดการข้อผิดพลาดแบบปริยายมาเยอะแค่ไหน
แน่นอนว่าการจัดการข้อผิดพลาดไว้ที่จุดเดียวอาจดูสะอาดตาในเชิงโครงสร้าง แต่ผมรู้สึกว่าการทำให้เห็นอย่างชัดเจนว่านี่คือการทำงานที่อาจตอบกลับด้วยข้อผิดพลาดได้ กลับช่วยให้เขียนโค้ดได้อย่างปลอดภัยมากขึ้น
if err != nil {}นี่พูดตรง ๆ ว่าก็น่ารำคาญอยู่เหมือนกันครับ และก็เห็นด้วยกับข้อเสียที่ถูกชี้ไว้ด้วย ถึงอย่างนั้น ถ้าเข้าใจให้ชัดเจนว่าภาษานี้มุ่งไปทางไหน และคิดว่าจะดึงจุดเด่นอะไรมาใช้เพิ่มเติมได้บ้าง ก็น่าจะใช้งานมันได้ดีขึ้นแม้จะมีข้อเสียตามที่ถูกพูดถึงก็ตามคล้ายกับ C แต่มี GC รองรับ แถมยังรองรับ generics แบบมีข้อจำกัด และยัง cross-compile ได้อีก! มองแบบนี้แล้วมันก็เป็นภาษาที่คุ้มค่ามากภาษาหนึ่งไม่ใช่หรือครับ?
ตอนย้ายจาก Java มาใช้ Go ช่วงแรกผมก็น่าจะรู้สึกว่ามันคล้ายกันอยู่บ้างครับ
ตอนนี้ผมสนุกกับ Go มากจนถึงขั้นรู้สึกเสียดายเวลาที่เคยใช้กับ Java ไปเลย การที่บอกว่าไม่เหมาะกับแอปพลิเคชันธุรกิจที่ซับซ้อน กลับทำให้ผมคิดว่าแอปพลิเคชันนั้นคงไม่ได้คิดมากพอที่จะทำให้ระบบเรียบง่ายลง
ความคิดเห็นจาก Hacker News
จะเกิดปัญหาเมื่อโปรแกรมเมอร์ Java พยายามบังคับสไตล์แบบ Java ให้กับ Go
นักพัฒนาจำนวนมากพยายามทำ abstraction เร็วเกินไป
standard library ของ Go มีขนาดใหญ่ แต่ก็ไม่ได้ใหญ่พอที่จะทำได้ทุกอย่าง
มีความท้าทายที่ใหญ่กว่าการเลือกภาษาโปรแกรมมิง
ยากที่จะเข้าใจว่าทำไมคนถึงชอบ Go
การที่ทีมหลักของ Go ย้อนกลับการตัดสินใจที่ผิดพลาดทำให้รู้สึกหงุดหงิด
Go มีปัญหาแบบเดียวกับ UNIX