Go (a.k.a. Golang) เป็นหนึ่งในภาษาที่ผู้คนให้ความสนใจมากที่สุด ณ เดือนเมษายน 2018 อยู่ในอันดับที่ 19 ในดัชนี TIOBE . ผู้คนจำนวนมากขึ้นเรื่อย ๆ เปลี่ยนจาก PHP, Node.js และภาษาอื่น ๆ มาใช้ Go และใช้มันในการผลิต ซอฟต์แวร์เจ๋ง ๆ มากมาย (เช่น Kubernetes, Docker และ Heroku CLI) เขียนโดยใช้ Go
แล้วอะไรคือกุญแจสู่ความสำเร็จของ Go? มีหลายสิ่งหลายอย่างภายในภาษาที่ทำให้มันเจ๋งมาก แต่สิ่งสำคัญอย่างหนึ่งที่ทำให้มันเป็นที่นิยมอย่าง“ ฉันไปสู่ความเรียบง่าย” ตามที่ผู้สร้างคนหนึ่งกล่าวไว้ ร็อบไพค์ .
ความเรียบง่ายนั้นยอดเยี่ยม - คุณไม่จำเป็นต้องเรียนรู้คำหลักมากมาย ทำให้การเรียนรู้ภาษาทำได้ง่ายและรวดเร็ว อย่างไรก็ตามในทางกลับกันบางครั้งนักพัฒนาก็ขาดคุณสมบัติบางอย่างที่มีในภาษาอื่นดังนั้นจึงจำเป็นต้องเขียนโค้ดเพื่อแก้ปัญหาหรือเขียนโค้ดเพิ่มเติมในระยะยาว น่าเสียดายที่ Go ไม่มีคุณสมบัติมากมายตามการออกแบบและบางครั้งมันก็น่ารำคาญจริงๆ
Golang มีจุดประสงค์เพื่อให้การพัฒนาเร็วขึ้น แต่ในหลาย ๆ สถานการณ์คุณกำลังเขียนโค้ดมากกว่าที่คุณจะใช้ภาษาโปรแกรมอื่น ๆ ฉันจะอธิบายบางกรณีเหล่านี้ในบทวิจารณ์ Go ของฉันด้านล่าง
ฉันจะโพสต์ตัวอย่างโค้ดจริงที่นี่ เมื่อฉันทำงานกับการเชื่อม Golang Selenium ฉันจำเป็นต้องเขียนฟังก์ชันที่มีพารามิเตอร์สามตัว สองคนเป็นทางเลือก นี่คือสิ่งที่ดูเหมือนว่าหลังจากการปรับใช้:
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 ไม่ได้จัดเตรียมไว้ตามการออกแบบ ลองนึกดูว่าจะเกิดอะไรขึ้นถ้าฉันโทรผิดโดยไม่ได้ตั้งใจ นี่คือตัวอย่าง:
ฉันต้องยอมรับว่าบางครั้งการทำงานเกินพิกัดอาจส่งผลให้โค้ดยุ่งได้ ในทางกลับกันด้วยเหตุนี้โปรแกรมเมอร์จึงต้องเขียนโค้ดมากขึ้น
นี่คือตัวอย่างเดียวกัน (ดีเกือบเหมือนกัน) ใน 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')
นี่เป็นคุณสมบัติที่ผู้ใช้ 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 }
นั่นขัดกับหลักการ DRY ซึ่งระบุว่าคุณควรเขียนโค้ดคัดลอก / วางให้น้อยที่สุดเท่าที่จะเป็นไปได้และย้ายไปอยู่ในฟังก์ชันและใช้อีกครั้งแทน
วิธีจัดการกับข้อยกเว้นในบริการเว็บที่สงบ
อีกวิธีหนึ่งคือการใช้การใช้งานเดี่ยวกับ interface {}
เป็นพารามิเตอร์ แต่อาจส่งผลให้เกิดข้อผิดพลาดรันไทม์เนื่องจากการตรวจสอบประเภทรันไทม์มีแนวโน้มที่จะเกิดข้อผิดพลาดมากกว่า และจะช้าลงด้วยดังนั้นจึงไม่มีวิธีง่ายๆในการใช้ฟังก์ชันเหล่านี้เป็นหนึ่งเดียว
มีภาษาดีๆมากมายที่รวมถึงการสนับสนุนทั่วไป ตัวอย่างเช่นนี่คือรหัสเดียวกันใน Rust (ฉันใช้ vec
แทน array
เพื่อให้ง่ายขึ้น):
fn map(vec:Vec, callback:fn(T) -> T) -> Vec { 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::(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_vec, underscorify)); // prints ['_hello_', '_this_', '_is_', '_a_', '_vec_'] }
โปรดทราบว่ามีการใช้งานฟังก์ชัน map
เพียงครั้งเดียวและสามารถใช้กับประเภทใดก็ได้ที่คุณต้องการแม้แต่แบบกำหนดเอง
ใครก็ตามที่มีประสบการณ์ Go สามารถบอกได้ว่าการจัดการการพึ่งพานั้นยากจริงๆ เครื่องมือ Go ช่วยให้ผู้ใช้สามารถติดตั้งไลบรารีต่างๆได้โดยเรียกใช้ go get
ปัญหาคือการจัดการเวอร์ชัน หากผู้ดูแลไลบรารีทำการเปลี่ยนแปลงที่เข้ากันไม่ได้ย้อนหลังและอัปโหลดไปยัง 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)
การจัดการข้อผิดพลาดใน 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 จะมีข้อบกพร่องมากมายจากการออกแบบ แต่ก็มีคุณสมบัติที่ยอดเยี่ยมจริงๆ
การเขียนโปรแกรมแบบอะซิงโครนัสทำได้ง่ายมากใน Go ในขณะที่การเขียนโปรแกรมแบบมัลติเธรดมักทำได้ยากในภาษาอื่น ๆ การวางไข่เธรดใหม่และเรียกใช้ฟังก์ชันเพื่อไม่บล็อกเธรดปัจจุบันนั้นง่ายมาก:
func doSomeCalculations() { // do some CPU intensive/long running tasks } func main() { go doSomeCalculations(); // This will run in another thread; }
ในขณะที่ภาษาโปรแกรมอื่น ๆ คุณต้องติดตั้งไลบรารี / เครื่องมือที่แตกต่างกันสำหรับงานที่แตกต่างกัน (เช่นการทดสอบการจัดรูปแบบโค้ดแบบคงที่ ฯลฯ ) มีเครื่องมือที่ยอดเยี่ยมมากมายที่รวมอยู่ใน Go ตามค่าเริ่มต้นแล้วเช่น
gofmt
- เครื่องมือสำหรับการวิเคราะห์รหัสคงที่ เมื่อเปรียบเทียบกับ JavaScript ซึ่งคุณต้องติดตั้งการอ้างอิงเพิ่มเติมเช่น eslint
หรือ jshint
ที่นี่จะรวมไว้โดยค่าเริ่มต้น และโปรแกรมจะไม่คอมไพล์ด้วยซ้ำหากคุณไม่เขียนโค้ด Go-style (โดยไม่ใช้ตัวแปรที่ประกาศการนำเข้าแพ็คเกจที่ไม่ได้ใช้งาน ฯลฯ )go test
- กรอบการทดสอบ อีกครั้งเมื่อเปรียบเทียบกับ JavaScript คุณต้องติดตั้งการอ้างอิงเพิ่มเติมสำหรับการทดสอบ (Jest, Mocha, AVA และอื่น ๆ ) ที่นี่จะรวมไว้โดยปริยาย และยังช่วยให้คุณสามารถทำสิ่งดีๆมากมายตามค่าเริ่มต้นเช่นการเปรียบเทียบการแปลงรหัสในเอกสารประกอบเป็นการทดสอบเป็นต้นgodoc
- เครื่องมือจัดทำเอกสาร ยินดีที่ได้รวมไว้ในเครื่องมือเริ่มต้นฉันคิดว่านี่เป็นหนึ่งในคุณสมบัติที่ดีที่สุดในภาษา สมมติว่าคุณต้องเขียนฟังก์ชันที่เปิดไฟล์สามไฟล์ และหากมีบางอย่างล้มเหลวคุณจะต้องปิดไฟล์ที่เปิดอยู่ ถ้ามีอาคารแบบนี้เยอะ ๆ จะเหมือนหายนะ พิจารณาตัวอย่างของรหัสหลอกนี้:
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
เข้ามาแทน:
package main import ( 'fmt' ) func openFiles() { // Pretending we’re opening files fmt.Printf('Opening file 1
'); defer fmt.Printf('Closing file 1
'); fmt.Printf('Opening file 2
'); defer fmt.Printf('Closing file 2
'); fmt.Printf('Opening file 3
'); // 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 ของคุณ - เริ่มต้นใช้งานได้ทันที โดยเพื่อนร่วมงาน ApeeScape Gabriel Aszalos