Go (a.k.a. Golang) เป็นหนึ่งในภาษาที่ผู้คนให้ความสนใจมากที่สุด ณ เดือนเมษายน 2018 อยู่ในอันดับที่ 19 ในดัชนี TIOBE . ผู้คนจำนวนมากขึ้นเรื่อย ๆ เปลี่ยนจาก PHP, Node.js และภาษาอื่น ๆ มาใช้ Go และใช้มันในการผลิต ซอฟต์แวร์เจ๋ง ๆ มากมาย (เช่น Kubernetes, Docker และ Heroku CLI) เขียนโดยใช้ Go
แล้วอะไรคือกุญแจสู่ความสำเร็จของ Go มีหลายสิ่งหลายอย่างในภาษาที่ทำให้มันเจ๋งมาก แต่สิ่งสำคัญอย่างหนึ่งที่ทำให้ Go เป็นที่นิยมคือความเรียบง่าย ตามที่ Rob Pike ผู้สร้างคนหนึ่งชี้ให้เห็น .
ความเรียบง่ายนั้นยอดเยี่ยม: คุณไม่จำเป็นต้องเรียนรู้คำหลักมากมาย ทำให้การเรียนรู้ภาษาทำได้ง่ายและรวดเร็ว อย่างไรก็ตามในทางกลับกันบางครั้งนักพัฒนาขาดคุณสมบัติบางอย่างที่พวกเขามีในภาษาอื่นดังนั้นพวกเขาจำเป็นต้องเขียนโค้ดเพื่อแก้ไขปัญหาหรือเขียนโค้ดเพิ่มเติมในระยะยาว น่าเสียดายที่ Go ไม่มีคุณสมบัติมากมายตามการออกแบบและบางครั้งก็น่ารำคาญจริงๆ
Golang มีจุดประสงค์เพื่อให้การพัฒนาเร็วขึ้น แต่ในหลาย ๆ สถานการณ์คุณกำลังเขียนโค้ดมากกว่าที่คุณจะเขียนโดยใช้ภาษาโปรแกรมอื่น ๆ ฉันจะอธิบายบางกรณีดังกล่าวในการวิพากษ์วิจารณ์ภาษา Go ของฉันด้านล่าง
ฉันจะโพสต์ตัวอย่างโค้ดจริงที่นี่ เมื่อฉันทำงานเกี่ยวกับการผูกซีลีเนียมของ 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 ไม่ได้จัดเตรียมฟังก์ชันนี้ตามการออกแบบ ลองนึกดูว่าจะเกิดอะไรขึ้นถ้าฉันโทรผิดโดยไม่ได้ตั้งใจ? นี่คือตัวอย่าง:
ฉันต้องยอมรับว่าบางครั้งการทำงานมากเกินไปอาจทำให้โค้ดยุ่งได้ ในทางกลับกันด้วยเหตุนี้โปรแกรมเมอร์จึงต้องเขียนโค้ดมากขึ้น
นี่คือตัวอย่างเดียวกัน (เช่นเดียวกัน) ใน 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 (Don’t Repeat Yourself) อย่างแน่นอนซึ่งระบุว่าคุณต้องเขียนโค้ดคัดลอก / วางให้น้อยที่สุดเท่าที่จะเป็นไปได้และย้ายไปที่ฟังก์ชันแล้วนำมาใช้ใหม่แทน
อีกวิธีหนึ่งคือการใช้การใช้งานเดี่ยวกับ 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 tools อนุญาตให้ผู้ใช้ติดตั้งไลบรารีต่างๆโดยเรียกใช้ 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 ในเรื่องนี้ ฟังก์ชันนี้อาจทำให้เกิดข้อผิดพลาดและคุณสามารถจับได้ ลองพิจารณาตัวอย่าง:
การขุดข้อมูล twitter ด้วย python
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 }
เป็นวิธีที่ชัดเจนมากขึ้นและไม่มีรหัสที่ทำซ้ำได้สำหรับการจัดการข้อผิดพลาด
การเพิ่มประสิทธิภาพการค้นหาใน sql server 2012
แม้ว่า Go จะมีข้อบกพร่องมากมายจากการออกแบบ แต่ก็มีคุณสมบัติที่ยอดเยี่ยมเช่นกัน
การเขียนโปรแกรม Async ทำได้ง่ายมากใน 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; }
ดูซับซ้อน. นั่นคือที่มาของ Go 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 ของคุณ: เริ่มต้นอย่างถูกวิธี โดยเพื่อน ApeeScapeer Gabriel Aszalos
ที่เกี่ยวข้อง: ลอจิกที่มีโครงสร้างดี: บทช่วยสอน Golang OOPมีเส้นบาง ๆ ระหว่างคำจำกัดความของสคริปต์และโปรแกรม แต่ฉันจะบอกว่ามันไม่ใช่ภาษาสคริปต์เนื่องจากโปรแกรม Go ไม่ได้ทำงานในรันไทม์ - พวกมันถูกคอมไพล์และรันเป็นไฟล์ปฏิบัติการ
ไม่ Go เป็นสิ่งที่ยอดเยี่ยมและปรับปรุงประสบการณ์ของนักพัฒนาซอฟต์แวร์ แต่ก็ไม่สมบูรณ์แบบตามที่ฉันอธิบายไว้ในบทความนี้ มันอาจจะไม่สมบูรณ์แบบ แต่ฉันเชื่อว่าเราสามารถนำมันมาใกล้ได้