4 Go ภาษาวิพากษ์วิจารณ์

เผยแพร่แล้ว: 2022-03-11

Go (aka Golang) เป็นหนึ่งในภาษาที่ผู้คนให้ความสนใจมากที่สุด ณ เดือนเมษายน 2018 นั้น อยู่ในอันดับที่ 19 ในดัชนี TIOBE ผู้คนจำนวนมากขึ้นเรื่อยๆ เปลี่ยนจาก PHP, Node.js และภาษาอื่นๆ เป็น Go และใช้งานจริงในการผลิต ซอฟต์แวร์เจ๋งๆ มากมาย (เช่น Kubernetes, Docker และ Heroku CLI) เขียนโดยใช้ Go

แล้วกุญแจสู่ความสำเร็จของ Go คืออะไร? มีหลายสิ่งหลายอย่างในภาษาที่ทำให้มันเจ๋งจริงๆ แต่สิ่งสำคัญอย่างหนึ่งที่ทำให้ Go เป็นที่นิยมคือความเรียบง่าย ตามที่ Rob Pike ผู้สร้างคนหนึ่งชี้ให้เห็น

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

Golang มีไว้เพื่อให้การพัฒนาเร็วขึ้น แต่ในหลาย ๆ สถานการณ์ คุณกำลังเขียนโค้ดมากกว่าที่คุณจะเขียนโดยใช้ภาษาโปรแกรมอื่นๆ ฉันจะอธิบายกรณีดังกล่าวบางส่วนในการวิพากษ์วิจารณ์ภาษา Go ด้านล่าง

การวิพากษ์วิจารณ์ภาษา 4 Go

1. ขาดฟังก์ชันโอเวอร์โหลดและค่าเริ่มต้นสำหรับอาร์กิวเมนต์

ฉันจะโพสต์ตัวอย่างโค้ดจริงที่นี่ เมื่อฉันกำลังทำงานกับ Selenium ของ Golang ฉันต้องเขียนฟังก์ชันที่มีสามพารามิเตอร์ สองคนเป็นทางเลือก นี่คือสิ่งที่ดูเหมือนหลังจากการนำไปใช้:

 func (wd *remoteWD) WaitWithTimeoutAndInterval(condition Condition, timeout, interval time.Duration) error { // the actual implementation was here } func (wd *remoteWD) WaitWithTimeout(condition Condition, timeout time.Duration) error { return wd.WaitWithTimeoutAndInterval(condition, timeout, DefaultWaitInterval) } func (wd *remoteWD) Wait(condition Condition) error { return wd.WaitWithTimeoutAndInterval(condition, DefaultWaitTimeout, DefaultWaitInterval) }

ฉันต้องใช้ฟังก์ชันที่แตกต่างกันสามฟังก์ชัน เนื่องจากฉันไม่สามารถโอเวอร์โหลดฟังก์ชันหรือส่งผ่านค่าเริ่มต้นได้ เพราะ Go ไม่ได้จัดเตรียมไว้โดยการออกแบบ ลองนึกภาพว่าจะเกิดอะไรขึ้นถ้าฉันโทรผิดโดยไม่ตั้งใจ นี่คือตัวอย่าง:

ฉันได้รับ `undefined` . มากมาย

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

จะปรับปรุงได้อย่างไร?

นี่คือตัวอย่างเดียวกัน (เกือบจะเหมือนกัน) ใน JavaScript:

 function Wait (condition, timeout = DefaultWaitTimeout, interval = DefaultWaitInterval) { // actual implementation here }

อย่างที่คุณเห็นมันดูชัดเจนขึ้นมาก

ฉันยังชอบวิธีการ Elixir ในเรื่องนั้น นี่คือสิ่งที่จะเกิดขึ้นใน Elixir (ฉันรู้ว่าฉันสามารถใช้ค่าเริ่มต้นได้ เช่นในตัวอย่างด้านบน—ฉันแค่แสดงมันเป็นวิธีที่สามารถทำได้):

 defmodule Waiter do @default_interval 1 @default_timeout 10 def wait(condition, timeout, interval) do // implementation here end def wait(condition, timeout), do: wait(condition, timeout, @default_interval) def wait(condition), do: wait(condition, @default_timeout, @default_interval) end Waiter.wait("condition", 2, 20) Waiter.wait("condition", 2) Waiter.wait("condition")

2. ขาด Generics

นี่เป็นคุณสมบัติที่ผู้ใช้ Go ต้องการมากที่สุด

ลองนึกภาพว่าคุณต้องการเขียนฟังก์ชันแผนที่ ซึ่งคุณกำลังส่งอาร์เรย์ของจำนวนเต็มและฟังก์ชัน ซึ่งจะนำไปใช้กับองค์ประกอบทั้งหมดของมัน ฟังดูง่ายใช่มั้ย?

ลองทำเป็นจำนวนเต็ม:

 package main import "fmt" func mapArray(arr []int, callback func (int) (int)) []int { newArray := make([]int, len(arr)) for index, value := range arr { newArray[index] = callback(value) } return newArray; } func main() { square := func(x int) int { return x * x } fmt.Println(mapArray([]int{1,2,3,4,5}, square)) // prints [1 4 9 16 25] }

ดูดีใช่มั้ย?

ลองนึกภาพว่าคุณต้องทำเพื่อสตริงด้วย คุณจะต้องเขียนการใช้งานอื่น ซึ่งเหมือนกันทุกประการ ยกเว้นลายเซ็น ฟังก์ชันนี้ต้องใช้ชื่ออื่น เนื่องจาก Golang ไม่รองรับฟังก์ชันโอเวอร์โหลด ด้วยเหตุนี้ คุณจะมีฟังก์ชันที่คล้ายกันจำนวนมากซึ่งมีชื่อต่างกัน และจะมีลักษณะดังนี้:

 func mapArrayOfInts(arr []int, callback func (int) (int)) []int { // implementation } func mapArrayOfFloats(arr []float64, callback func (float64) (float64)) []float64 { // implementation } func mapArrayOfStrings(arr []string, callback func (string) (string)) []string { // implementation }

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

การขาดยาชื่อสามัญหมายถึงฟังก์ชันที่หลากหลายกว่าร้อยแบบ

อีกวิธีหนึ่งคือการใช้การนำไปใช้งานเดี่ยวกับ interface{} เป็นพารามิเตอร์ แต่อาจส่งผลให้เกิดข้อผิดพลาดรันไทม์เนื่องจากการตรวจสอบประเภทรันไทม์มักมีข้อผิดพลาดมากกว่า และจะช้ากว่าด้วย ดังนั้นจึงไม่มีวิธีง่ายๆ ในการใช้ฟังก์ชันเหล่านี้เป็นฟังก์ชันเดียว

จะปรับปรุงได้อย่างไร?

มีภาษาดีๆ มากมายที่รวมถึงการสนับสนุนชื่อสามัญ ตัวอย่างเช่น นี่คือรหัสเดียวกันใน Rust (ฉันใช้ vec แทน array เพื่อให้ง่ายขึ้น):

 fn map<T>(vec:Vec<T>, callback:fn(T) -> T) -> Vec<T> { let mut new_vec = vec![]; for value in vec { new_vec.push(callback(value)); } return new_vec; } fn square (val:i32) -> i32 { return val * val; } fn underscorify(val:String) -> String { return format!("_{}_", val); } fn main() { let int_vec = vec![1, 2, 3, 4, 5]; println!("{:?}", map::<i32>(int_vec, square)); // prints [1, 4, 9, 16, 25] let string_vec = vec![ "hello".to_string(), "this".to_string(), "is".to_string(), "a".to_string(), "vec".to_string() ]; println!("{:?}", map::<String>(string_vec, underscorify)); // prints ["_hello_", "_this_", "_is_", "_a_", "_vec_"] }

โปรดทราบว่ามีการใช้งานฟังก์ชัน map เพียงครั้งเดียว และสามารถใช้กับประเภทใดก็ได้ที่คุณต้องการ แม้กระทั่งประเภทที่กำหนดเอง

3. การจัดการการพึ่งพา

ใครก็ตามที่มีประสบการณ์ใน Go สามารถพูดได้ว่าการจัดการการพึ่งพานั้นยากจริงๆ Go tools ให้ผู้ใช้ติดตั้งไลบรารี่ต่างๆ ได้ด้วยการรัน go get <library repo> ปัญหาคือการจัดการเวอร์ชัน หากผู้ดูแลไลบรารีทำการเปลี่ยนแปลงที่เข้ากันไม่ได้แบบย้อนหลังและอัปโหลดไปยัง GitHub ใครก็ตามที่พยายามใช้โปรแกรมของคุณหลังจากนั้นจะได้รับข้อผิดพลาด เพราะ go get ไม่ได้ทำอะไรเลยนอกจาก git clone ที่เก็บของคุณลงในโฟลเดอร์ไลบรารี นอกจากนี้ หากไม่ได้ติดตั้งไลบรารี่ โปรแกรมจะไม่คอมไพล์ด้วยเหตุนั้น

คุณสามารถทำได้ดีขึ้นเล็กน้อยโดยใช้ Dep สำหรับจัดการการพึ่งพา (https://github.com/golang/dep) แต่ปัญหาที่นี่คือคุณกำลังจัดเก็บการพึ่งพาทั้งหมดของคุณบนที่เก็บของคุณ (ซึ่งไม่ดีเพราะที่เก็บของคุณจะ ไม่เพียงแต่มีรหัสของคุณเท่านั้นแต่ยังมีรหัสการขึ้นต่อกันนับพันบรรทัด) หรือเพียงแค่เก็บรายการแพ็คเกจ (แต่อีกครั้ง หากผู้ดูแลการขึ้นต่อกันทำการเปลี่ยนแปลงที่เข้ากันไม่ได้แบบย้อนกลับ การเปลี่ยนแปลงทั้งหมดจะล้มเหลว)

จะปรับปรุงได้อย่างไร?

ฉันคิดว่าตัวอย่างที่สมบูรณ์แบบที่นี่คือ Node.js (และโดยทั่วไปฉันคิดว่า JavaScript) และ NPM NPM เป็นที่เก็บแพ็คเกจ มันเก็บเวอร์ชั่นต่าง ๆ ของแพ็คเกจ ดังนั้นหากคุณต้องการเวอร์ชั่นเฉพาะของแพ็คเกจ ไม่มีปัญหา—คุณสามารถรับได้จากที่นั่น นอกจากนี้ สิ่งหนึ่งที่ในแอปพลิเคชัน Node.js/JavaScript คือไฟล์ package.json ในที่นี้ มีการแสดงรายการการพึ่งพาและเวอร์ชันทั้งหมด ดังนั้นคุณสามารถติดตั้งได้ทั้งหมด (และรับเวอร์ชันที่ใช้งานได้กับโค้ดของคุณอย่างแน่นอน) ด้วย npm install

ตัวอย่างที่ดีของการจัดการแพ็คเกจ ได้แก่ RubyGems/Bundler (สำหรับแพ็คเกจ Ruby) และ Crates.io/Cargo (สำหรับไลบรารี Rust)

4. การจัดการข้อผิดพลาด

การจัดการข้อผิดพลาดใน Go นั้นง่ายมาก ใน Go โดยทั่วไปคุณสามารถคืนค่าหลายค่าจากฟังก์ชัน และฟังก์ชันสามารถคืนค่าข้อผิดพลาดได้ บางอย่างเช่นนี้:

 err, value := someFunction(); if err != nil { // handle it somehow }

ลองนึกภาพว่าคุณต้องเขียนฟังก์ชันที่ทำสามการกระทำที่ส่งกลับข้อผิดพลาด มันจะมีลักษณะดังนี้:

 func doSomething() (err, int) { err, value1 := someFunction(); if err != nil { return err, nil } err, value2 := someFunction2(value1); if err != nil { return err, nil } err, value3 := someFunction3(value2); if err != nil { return err, nil } return value3; }

มีโค้ดที่ทำซ้ำได้มากมายที่นี่ ซึ่งไม่ดี และด้วยฟังก์ชันขนาดใหญ่ อาจแย่ยิ่งกว่านี้อีก! คุณอาจต้องใช้คีย์บนแป้นพิมพ์สำหรับสิ่งนี้:

ภาพตลกขบขันของรหัสจัดการข้อผิดพลาดบนแป้นพิมพ์

จะปรับปรุงได้อย่างไร?

ฉันชอบแนวทางของ JavaScript ในเรื่องนั้น ฟังก์ชั่นสามารถโยนข้อผิดพลาดและคุณสามารถจับมันได้ พิจารณาตัวอย่าง:

 function doStuff() { const value1 = someFunction(); const value2 = someFunction2(value1); const value3 = someFunction3(value2); return value3; } try { const value = doStuff(); // do something with it } catch (err) { // handle the error }

มีความชัดเจนมากขึ้นและไม่มีรหัสที่ทำซ้ำได้สำหรับการจัดการข้อผิดพลาด

สิ่งดีๆในโก

แม้ว่า Go จะมีข้อบกพร่องมากมายจากการออกแบบ แต่ก็มีคุณสมบัติที่ยอดเยี่ยมเช่นกัน

1. โรคกระดูกพรุน

การเขียนโปรแกรม Async ทำได้ง่ายมากใน Go ในขณะที่การเขียนโปรแกรมแบบมัลติเธรดมักจะทำได้ยากในภาษาอื่นๆ การวางไข่ของเธรดใหม่และเรียกใช้ฟังก์ชันในนั้นเพื่อไม่ให้บล็อกเธรดปัจจุบันนั้นง่ายมาก:

 func doSomeCalculations() { // do some CPU intensive/long running tasks } func main() { go doSomeCalculations(); // This will run in another thread; }

2. เครื่องมือที่มาพร้อมกับ Go

ในขณะที่ในภาษาการเขียนโปรแกรมอื่นๆ คุณต้องติดตั้งไลบรารี/เครื่องมือต่างๆ สำหรับงานต่างๆ (เช่น การทดสอบ การจัดรูปแบบโค้ดคงที่ เป็นต้น) มีเครื่องมือเจ๋งๆ มากมายที่รวมอยู่ใน Go ตามค่าเริ่มต้นแล้ว เช่น:

  • gofmt - เครื่องมือสำหรับการวิเคราะห์รหัสคงที่ เมื่อเปรียบเทียบกับ JavaScript ที่คุณต้องติดตั้งการพึ่งพาเพิ่มเติม เช่น eslint หรือ jshint จะรวมสิ่งนี้ไว้โดยค่าเริ่มต้น และโปรแกรมจะไม่คอมไพล์ถ้าคุณไม่เขียนโค้ด Go-style (ไม่ใช้ตัวแปรที่ประกาศ นำเข้าแพ็คเกจที่ไม่ได้ใช้ ฯลฯ)
  • go test - กรอบการทดสอบ อีกครั้ง เมื่อเปรียบเทียบกับ JavaScript คุณต้องติดตั้งการพึ่งพาเพิ่มเติมสำหรับการทดสอบ (Jest, Mocha, AVA เป็นต้น) ที่นี่จะรวมอยู่โดยค่าเริ่มต้น และช่วยให้คุณทำสิ่งดีๆ ได้มากมายตามค่าเริ่มต้น เช่น การเปรียบเทียบ การแปลงโค้ดในเอกสารประกอบเป็นการทดสอบ เป็นต้น
  • godoc - เครื่องมือเอกสาร ยินดีที่ได้รวมไว้ในเครื่องมือเริ่มต้น
  • คอมไพเลอร์นั้นเอง รวดเร็วอย่างไม่น่าเชื่อ เมื่อเทียบกับภาษาอื่น ๆ ที่คอมไพล์แล้ว!

3. เลื่อนเวลา

ฉันคิดว่านี่เป็นหนึ่งในคุณสมบัติที่ดีที่สุดในภาษา ลองนึกภาพว่าคุณต้องเขียนฟังก์ชันที่เปิดไฟล์สามไฟล์ และหากมีบางอย่างล้มเหลว คุณจะต้องปิดไฟล์ที่เปิดอยู่ ถ้ามีสิ่งปลูกสร้างแบบนี้เยอะก็จะดูเลอะเทอะ พิจารณาตัวอย่างรหัสเทียมนี้:

 function openManyFiles() { let file1, file2, file3; try { file1 = open('path-to-file1'); } catch (err) { return; } try { file2 = open('path-to-file2'); } catch (err) { // we need to close first file, remember? close(file1); return; } try { file3 = open('path-to-file3'); } catch (err) { // and now we need to close both first and second file close(file1); close(file2); return; } // do some stuff with files // closing files after successfully processing them close(file1); close(file2); close(file3); return; }

ดูซับซ้อน นั่นคือสิ่งที่การ defer ของ Go เกิดขึ้น:

 package main import ( "fmt" ) func openFiles() { // Pretending we're opening files fmt.Printf("Opening file 1\n"); defer fmt.Printf("Closing file 1\n"); fmt.Printf("Opening file 2\n"); defer fmt.Printf("Closing file 2\n"); fmt.Printf("Opening file 3\n"); // Pretend we've got an error on file opening // In real products, an error will be returned here. return; } func main() { openFiles() /* Prints: Opening file 1 Opening file 2 Opening file 3 Closing file 2 Closing file 1 */ }

อย่างที่คุณเห็น หากเราพบข้อผิดพลาดในการเปิดไฟล์หมายเลขสาม ไฟล์อื่นๆ จะถูกปิดโดยอัตโนมัติ เนื่องจากคำสั่ง defer จะถูกดำเนินการก่อนที่จะส่งคืนในลำดับที่กลับกัน นอกจากนี้ ยังดีที่มีการเปิดและปิดไฟล์ไว้ที่เดียวกัน แทนที่จะเป็นส่วนต่างๆ ของฟังก์ชัน

บทสรุป

ฉันไม่ได้พูดถึงสิ่งที่ดีและไม่ดีทั้งหมดใน Go เพียงแค่สิ่งที่ฉันคิดว่าดีที่สุดและแย่ที่สุด

Go เป็นหนึ่งในภาษาโปรแกรมที่น่าสนใจที่ใช้อยู่ในปัจจุบัน และมีศักยภาพจริงๆ มันให้เครื่องมือและคุณสมบัติที่ยอดเยี่ยมแก่เรา อย่างไรก็ตาม มีหลายสิ่งที่สามารถปรับปรุงได้ที่นั่น

หากเราในฐานะนักพัฒนา Go จะใช้การเปลี่ยนแปลงเหล่านี้ มันจะเป็นประโยชน์ต่อชุมชนของเราอย่างมาก เพราะจะทำให้การเขียนโปรแกรมกับ Go น่าพึงพอใจยิ่งขึ้น

ในระหว่างนี้ หากคุณกำลังพยายามปรับปรุงการทดสอบด้วย Go ให้ลองใช้ แอป Test Your Go: เริ่มต้นอย่างถูกต้อง โดย Gabriel Aszalos เพื่อน Toptaler

ที่เกี่ยวข้อง: ตรรกะที่มีโครงสร้างดี: บทช่วยสอน Golang OOP