เรียนรู้การใช้ภาษา Go ใน 15 นาที
Go เป็นอีกหนึ่งภาษาโปรแกรมที่ป็อบปูล่าสุดๆ เนื้อหอมน่าตามล่ายิ่งกว่าเจ๊ปูน้ำในหูไม่เท่ากันซะอีก
ผลสำรวจจากสวนสัตว์ดุสิตโพลล์พบว่า ส่วนใหญ่คนเขียน Go มักมาจากประชากรโปรแกรมเมอร์ที่เขียนโปรแกรมภาษาใดภาษาหนึ่งเป็นอยู่แล้ว
เพราะภาษาส่วนใหญ่มักมีหลายสิ่งเหมือนกัน จะให้โปรแกรมเมอร์ไปเริ่มภาษาใหม่ด้วยการนั่งอ่านวิธีประกาศตัวแปร ความหมายของ character หรือรู้จักว่า if statement และ for loop ต่างกันยังไง โลกนี้คงตายด้านน่าดู คิดแล้วขอบินไปเปิดหูเปิดตาที่ดูไบบ้างจะดีกว่า
เราเข้าใจคุณ! ชุดบทความนี้ผมจะแนะนำการโปรแกรมด้วยภาษา Go ในมุมมองของโปรแกรมเมอร์ที่มั่วได้ซักภาษา เราจะไม่มา Intro to Programming 101 แต่เราจะท่องดูไบไปดูซิว่า Go มีอะไรเด่นและเด็ดบ้างจนคุณต้องเผ่นออกนอกประเทศ~
สำหรับบทความนี้เราจะทัศนากันว่า Go มีอะไรเด่น รวมถึงการใช้งานเบื้องต้น เพื่อให้มองเห็นภาพรวมกว้างๆของการเขียนโปรแกรมด้วยภาษา Go
โอเค เตรียมถุงกาวในมือให้พร้อม แล้วไปดึงดาวกันกับบทความนี้
อะไร อะไรก็ Go
อะไรคือการที่ Go คลอดตอนปี 2007?
Go เป็นภาษาที่อุบัติในยุคที่อะไรๆก็ไฮโซ CPU ก็เร็ว และเป็นยุคที่ใครยังใช้ CPU คอร์เดียวถือว่าอยู่หลังเขาหิมาลัยละ
ความที่ Go เกิดทีหลัง จึงซึมซับสิ่งดีๆจากภาษาอื่น และอัปเปหิข้อผิดพลาดทั้งหลายของการโปรแกรมแบบเก่าๆ นั่นรวมถึงไวยากรณ์ยุ่งยากคร่ำครึสมัยพ่อขุนราม การเขมือบหน่วยความจำชนิดอดอยากมาสิบปี (Java ไง, เพื่อนเราเอง) หรือการทำงานแบบ Concurrency ที่ยากราวๆข้อสอบ A-NET
ความเร็วในการพัฒนา
จะเลือกอะไรดี ความเร็วในการพัฒนาหรือความเร็วในการประมวลผล?
แน่นอนว่าสองสิ่งนี้ยากที่จะมาด้วยกัน ภาษา C มีประสิทธิภาพดีกว่าภาษา Ruby แต่กลับแย่กว่าในเชิงของการพัฒนาโปรแกรม มุมกลับคือ Ruby เขียนและบำรุงรักษาโปรแกรมได้ง่ายกว่า C แต่ประสิทธิภาพคือลาก่อย
จะดีกว่าไหม ถ้าจะมีซักภาษาที่ทำให้เราพัฒนาโปรแกรมได้ง่าย ความเร็วก็ดี แม้จะไม่ได้เขียนง่ายเท่า Ruby หรือเร็วเท่า Assembly แต่ก็ดีจนเป็นคนที่ใช่ และพร้อมฝากใจให้เป็นแม่ของลูกเรา
Go คือภาษานั้นครับ ด้วยความที่ Go มีไวยากรณ์ไม่ซับซ้อน library ก็ครบครัน แถมใช้เวลาในการ build ได้สั้นเท่าจู๋มด (ใครเคย build โปรเจคใหญ่ๆของภาษา C จะเข้าใจ) Go จึงช่วยให้ชีวิตการพัฒนาโปรแกรมง่ายขึ้น นอกจากนี้ประสิทธิภาพของ Go ก็ดีเยี่ยม ทำงานได้เร็ว แต่ไม่รู้ว่าเร็วเท่า Shinkansen หรือรถไฟความเลวสูงแถวๆนี้ อันนี้ต้องดูอีกที
Google คือยักษ์
ภาษาที่ดีไม่เพียงเกิดจากไวยากรณ์ที่ดี แต่ต้องมีคอมมูนิตี้ที่ดีด้วย
Go เป็นภาษาที่เกิดจากยักษ์ Google และมีการใช้อย่างแพร่หลายโดย Google Adobe IBM Intel และอื่นๆอีกมาก โปรเจคดังๆหลายตัว เช่น Docker และ Kubernate ก็ใช้ Go แล้วตอนนี้หละ คุณพร้อมจะเปิดใจเลือกใช้ Go หรือยัง?
รู้จักภาษา Go ฉบับคนแปลกหน้า
Go เป็น static language นั่นแปลว่าตัวแปรใดต้องมีการประการชนิดข้อมูล และตัวแปรภาษาจะสามารถตรวจสอบชนิดข้อมูลได้ว่าเรากำหนดถูกต้องหรือไม่ตั้งแต่ช่วงของการคอมไพล์
Go ยังเป็นภาษาประเภท compiled language นั่นคือเราสามารถสั่ง compiler ให้อ่านซอร์สโค้ดแล้วสร้างผลลัพธ์ในรูปของโปรแกรม (Executable File) ที่ทำงานบนแพลตฟอร์มนั้นๆได้เลยโดยไม่ทำงานอยู่บน VM เฉกเช่น JVM ในภาษา Java
ภาษา Go นั้นเหมาะกับงานจำพวก System Programs ไม่ว่าจะสร้าง API Server ก็ได้ ทำ Network Applications ก็ดี หรือจะคูลๆชิคๆไปกับการสร้าง Game Engines ก็ไม่ว่ากัน
สวัสดี Golang
เพื่อให้ Go รู้ว่าจะเริ่มทำงานจากจุดไหน จึงจำเป็นที่เราต้องสร้าง package ชื่อ main พร้อมทั้งประกาศฟังก์ชันชื่อ main เช่นกัน
1// ประกาศ package ชื่อ main2package main34// ฟังก์ชัน main จะเป็นจุดเริ่มต้นการทำงานของเรา5func main() {67}
การโปรแกรมด้วย Go สามารถแยกส่วนโค้ดด้วย package ได้ import
จึงเป็นคำสำคัญเพื่อใช้ในการนำเข้า package อื่นมาใช้ในโค้ดเรา
1package main23// นำเข้า package fmt มาใช้งาน4import "fmt"56func main() {7 // ภายใต้ fmt มีฟังก์ชันชื่อ Println ให้เราสามารถพิมพ์ข้อความออกจอได้8 fmt.Println("Hello, Go")9}
การ import นั้นไม่จำกัดอยู่เฉพาะไลบรารี่ของ Go เอง เรายังสามารถ import ซอร์สโค้ดจากที่อื่น เช่น Github มาใช้ได้ด้วย เช่น
1import "github.com/jinzhu/gorm"
คอมไพเลอร์ของ Go นั้นเป็นคนแก่ขี้บ่น เห็นอะไรไม่ต้องตาเป็นต้องเอะอะโวยวาย หากเรา import บางสิ่งเข้ามาแต่ไม่ได้เรียกใช้ เจ๊ Go ก็จะบ่นดังๆตอนเราคอมไพล์ เช่น
1package main23import "fmt"45func main {67}
จากโค้ดข้างต้นพบว่าเรา import แพคเกจ fmt เข้ามา แต่ไม่ได้เรียกใช้งาน เมื่อทำการคอมไพล์ Go ก็จะกริ้วนิดหน่อย ด้วยการบ่นๆดังนี้
1main.go:3:1: imported and not used: "fmt"
Build Run และ Format
สมมติโครงสร้างโปรเจคภายใต้ชื่อ godubai
ของเราเป็นดังนี้
1-- src2 -- godubai << โปรเจคเรา3 -- main.go
เมื่อเราต้องการสร้าง executable file เราสามารถออกคำสั่ง go build
ได้ ซึ่งจะได้ผลลัพธ์ตามชื่อโปรเจคคือ godubai นั่นหมายความว่าเราสามารถสั่งโปรแกรมนี้ของเราให้ทำงานได้ด้วยการเรียก ./godubai
นั่นเอง
ระหว่างช่วงการพัฒนาโปรแกรม เราคงต้องการสั่งโปรแกรมให้ทำงานเพียงเพื่อต้องการเห็นผลลัพธ์ แต่ไม่ต้องการ executable file ออกมา เราสามารถสั่ง go run main.go
แทนได้ ด้วยคำสั่งนี้ซอร์สโค้ดของเราจะถูกอ่าน build และ run โดยไม่มีการสร้างไฟล์ผลลัพธ์ให้เป็นขยะในโฟลเดอร์ของเรา
การทำงานร่วมกันเป็นทีม ต่างคนย่อมต่างสไตล์ หน้าตาของโค้ดจึงถูกฟอร์แมตต่างกันได้ แน่นอนหละครับเมื่อทำงานเป็นทีมเราก็คงคาดหวังให้โค้ดของเราออกมาสไตล์เดียว เป็นสไตล์สากลในจักรวาลของ Go
1// โค้ดเล๊ะเทะ2package main3import "fmt"4func main() {5fmt.Println("Hello, Go")6}
Go ได้เตรียมเครื่องมือ gofmt
เพื่อจัดการฟอร์แมตซอร์สโค้ดของคุณให้เป็นตามมาตรฐาน GO ภายหลังออกคำสั่ง gofmt -w main.go
หน้าตาโค้ดเราก็จะเข้าสู่สภาวะเป็นผู้เป็นคน
1package main23import "fmt"45func main() {6 fmt.Println("Hello, Go")7}
สำหรับแฟลค -w ที่ใส่เข้าไปใน gofmt เป็นการบอกว่าให้เครื่องมือดังกล่าวช่วยเขียนผลลัพธ์ใหม่ลงไฟล์เดิม แทนที่จะเป็นการส่งผลลัพธ์หลังฟอร์แมตออกหน้าจอนั่นเอง
การใช้ gofmt แม้จะดูง่าย แต่ชีวิตเราจะง่ายกว่านี้หากไม่ต้องมาคอยรัน gofmt ดังนั้นเราจึงควรตั้งค่าให้ gofmt ทำงานทุกครั้งหลังจากเรากดเซฟไฟล์ และทำงานอีกครั้งตอนเราส่งโค้ดขึ้น version control
ไหนๆ gofmt ก็ทำให้ชีวิตเป็นเรื่องง่ายแล้ว จะให้ง่ายกว่านี้อีกหน่อยได้ไหม ด้วยการช่วย import package ให้เราอัตโนมัติซะเลย จากในตัวอย่างพบว่าเรามีการใช้งาน fmt แต่ยังไม่ import package ดังกล่าว
1package main23func main() {4 fmt.Println("Hello, Go")5}
Go ได้เตรียมเครื่องมือชื่อ goimports ให้กับเรา หลังออกคำสั่ง goimports -w main.go
แพคเกจไหนที่ยังไม่ได้นำเข้า Go ก็จะ import เข้ามาให้ โปรดสังเกตเราใส่ -w เพื่อบอกให้เครื่องมือดังกล่าวเขียนผลลัพธ์ทับไฟล์เดิม
1package main23import "fmt"45func main() {6 fmt.Println("Hello, Go")7}
goimports นั้น นอกจากจะทำการ import package ต่างๆที่เราใช้งานในซอร์สโค้ดเข้ามาให้ มันยังทำการฟอร์แมตโค้ดให้กับเราอัตโนมัติโดยไม่ต้องเรียก gofmt เลยด้วยครับ
ประโยคควบคุมในภาษา Go
ประโยคควบคุมในภาษา Go มีไม่เยอะมาก โดยแต่ละตัวก็จะมีลักษณะงานที่เหมาะสมจำเพาะกับมันไปเลย ไม่เหมือนภาษาอื่นๆที่มีทั้ง while และ for-loop ที่ชวนปวดหัวว่าจะใช้ตัวไหนดี
if statement
1if i % 2 == 0 {2 // even3} else {4 // odd5}
โปรดสังเกต นอกจาก Go จะไม่ต้องปิดท้าย statement ด้วย semicolon แล้ว ประโยคควบคุมยังไม่ต้องครอบด้วยวงเล็บอีกด้วย
for-loop
1for i := 1; i <= 10; i++ {2 fmt.Println(i)3}
for-loop ไม่แตกต่างจากภาษาอื่นมากนัก นั่นคือสามารถแบ่งย่อยออกได้เป็นสามส่วนโดยใช้ semicolon เป็นตัวแบ่ง ส่วนแรกคือการตั้งค่าเริ่มต้น ส่วนถัดมาเป็นเงื่อนไข และส่วนสุดท้ายคือการทำงานหลังจบรอบ
switch
1switch i {2 case 0: fmt.Println("Zero")3 case 1: fmt.Println("One")4 case 2: fmt.Println("Two")5 case 3: fmt.Println("Three")6 case 4: fmt.Println("Four")7 case 5: fmt.Println("Five")8 default: fmt.Println("Unknown Number")9}
ตัวแปรและชนิดข้อมูลในภาษา Go
โดยหลักแล้วเราสามารถประกาศตัวแปรในภาษา Go ได้สองวิธีด้วยกัน
Manual Type Declaration
วิธีการนี้คือการประกาศตัวแปรพร้อมระบุชนิดข้อมูล
1var <ชื่อตัวแปร> <ชนิดข้อมูล>
หลังจากการประกาศตัวแปรแล้ว เราสามารถกำหนดค่าตัวแปรด้วยชนิดข้อมูลที่ระบุได้
1var message string2message = "Hello, Go"
คำถามครับ สมมติเราประกาศตัวแปรขึ้นมา แต่ยังไม่ได้กำหนดค่าให้มันหละ เช่นนี้ค่าของตัวแปรดังกล่าวจะเป็นอะไร?
Zero Values เป็นคำเรียกเก๋ๆสำหรับค่า "default value" ในกรณีที่เราประกาศตัวแปรขึ้นมาแต่ไม่ได้กำหนดค่าให้กับมัน ตัวอย่างเช่น
Type | Zero Value |
---|---|
bool | false |
int | 0 |
float | 0.0 |
string | "" |
function | nil |
เพราะฉะนั้น หากเราประกาศ var message string
ขึ้นมาลอยๆ ตอนนี้คงตอบได้แล้วซิครับว่า message นั้นมีค่าเป็น "" นั่นเอง
Type Inference
กรณีที่เราต้องการประกาศตัวแปรพร้อมทั้งกำหนดค่าพร้อมกันในคราเดียว เราสามารถใช้ไวยากรณ์แบบนี้ได้ครับ
1<ชื่อตัวแปร> := <ค่า>23// เช่น4message := "Hello, Go"
การประกาศตัวแปรพร้อมระบุค่าด้วยการใช้ :=
Go จะดูว่าฝั่งขวานั้นมีชนิดข้อมูลเป็นอะไร เพื่อนำไปอนุมานว่าตัวแปรดังกล่าวควรเป็นชนิดข้อมูลอะไร เราจึงเรียกวิธีกำหนดค่าแบบนี้ว่า Type Inference
Arrays และ Slices ในภาษา Go
อาร์เรย์ในภาษา Go ก็มีวิธีประกาศและใช้ไม่ต่างอะไรจากภาษาอื่นมากนัก
1// อาร์เรย์ของข้อความจำนวนสามช่อง2var names [3]string34names[0] = "Somchai"5names[1] = "Somsree"6names[2] = "Somset
หรือถ้าคุณเป็นสายย่อ โค้ดต่อไปนี้จะดูกรุบกริบ
1names := [3]string{"Somchai", "Somsree", "Somset"}
อาร์เรย์อาจไม่ยืดหยุ่นเพียงพอสำหรับเรา การประกาศอาร์เรย์เราต้องระบุขนาดของมัน แต่การใช้งานจริงของเราเราอาจต้องการยืดและหดขนาดได้อย่างอิสระ Slices จึงเข้ามาตอบโจทย์ตรงนี้แทน Arrays
1// การประกาศ slice เราไม่ต้องระบุจำนวนช่อง2// เพราะเราสามารถเพิ่ม element เข้าไปได้อย่างอิสระภายหลัง3var names []string45// เพิ่ม element เข้าไป6names = append(names, "Somchai")7names = append(names, "Somsree")8names = append(names, "Somset")
สำหรับสายย่อ slices ก็มีวิธีประกาศเช่นเดียวกับ arrays เพียงแต่ไม่ต้องระบุจำนวน
1names := []string{"Somchai", "Somsree", "Somset"}
arrays และ slices ยังสามารถใช้ for เพื่อวนลูปได้ผ่าน range
1names := [3]string{"Somchai", "Somsree", "Somset"}23for index, name := range names {4 fmt.Println(index, name)5}67// ผลลัพธ์8// 0 Somchai9// 1 Somsree10// 2 Somset
ฟังก์ชันในภาษา Go
ฟังก์ชันในภาษา Go ก็เป็นปกติทั่วไปตามแบบฉบับภาษาตระกูลมี Type ต่างกันนิดหน่อยตรงที่ชื่อตัวแปรจะมาก่อนชนิดข้อมูล เช่น
1package main23import (4 "fmt"5)67func main() {8 printFullName("Babel", "Coder")9}1011// โปรดสังเกต ชื่อตัวแปรจะมาก่อนชนิดข้อมูล12func printFullName(firstName string, lastName string) {13 fmt.Println(firstName + " " + lastName)14}
ในกรณีที่มีการคืนค่ากลับจากฟังก์ชัน เราต้องระบุชนิดข้อมูลของค่าที่จะคืนออกไปด้วย
1// คืนค่ากลับเป็น string2func getMessage() string {34}
ฟังก์ชันในภาษา Go สามารถคืนค่าได้มากกว่าหนึ่งค่า และเป็นธรรมเนียมปฏิบัติที่เรามักจะคืนค่า error เพื่อบ่งบอกว่าการทำงานของฟังก์ชันมีปัญหาหรือไม่ออกมาด้วย
1package main23import (4 "errors"5 "fmt"6)78func main() {9 // เพราะว่าฟังก์ชันคืนค่าสองค่า เราจึงประกาศตัวแปรมารองรับได้พร้อมกันสองตัว10 result, err := divide(5, 3)1112 // ตรวจสอบก่อนว่ามี error ไหม ถ้ามีก็จบโปรแกรมไปแบบไม่ค่อยสวยด้วย Exit(1)13 if err != nil {14 os.Exit(1)15 }1617 fmt.Println(result)18}1920// คืนค่า float และ error ออกไปพร้อมกันจากฟังก์ชัน21func divide(dividend float32, divisor float32) (float32, error) {22 if divisor == 0.0 {23 err := errors.New("Division by zero!")24 return 0.0, err25 }2627 return dividend / divisor, nil28}
นิยามโครงสร้างข้อมูลด้วย Structs
แม้ภาษา Go จะไม่มีคลาส แต่เรามี structs ที่สามารถนิยามโครงสร้างของข้อมูลขึ้นมาเองได้
1type human struct {2 name string3 age int4}
หลังจากที่เรานิยามโครงสร้างข้อมูลภายใต้ชื่อ Human เราก็สามารถสร้างข้อมูลเหล่านี้พร้อมตั้งค่าให้กับมันได้ ดังนี้
1somchai := human{name: "Somchai", age: 23}2somsree := human{name: "Somsree", age: 32}
เมื่อ structs เป็นตัวแทนของโครงสร้างข้อมูลที่เรานิยามขึ้นมา เราจึงสามารถนิยามพฤติกรรมที่สัมพันธ์กับ structs นี้ได้
1type human struct {2 name string3 age int4}56// printInfo จะผูกติดกับ human7// สังเกตมีการระบุ human เข้าไปก่อนหน้าชื่อเมธอด8func (h human) printInfo() {9 fmt.Println(h.name, h.age)10}1112func main() {13 somchai := human{name: "Somchai", age: 23}14 somchai.printInfo() // Somchai 2315}
พอยเตอร์ในภาษา Go
สมมติเราต้องการสร้างฟังก์ชันใหม่ชื่อ setIsAdult ทำหน้าที่ในการตรวจสอบอายุของคนนั้นๆ หากอายุเกิน 18 ปีจะถือว่าเป็นผู้ใหญ่แล้ว
1package main23import "fmt"45type human struct {6 name string7 age int8 isAdult bool // Zero Value คือ false9}1011// setAdult รับ human เข้ามา12// หากอายุเกิน 18 isAdult จะมีค่าเป็น true13// นอกนั้นมีค่าเป็น false14func setAdult(h human) {15 h.isAdult = h.age >= 1816}1718func main() {19 somchai := human{name: "Somchai", age: 23}20 setAdult(somchai)21 fmt.Println(somchai) // {Somchai 23 false}22}
เมื่อผ่าน somchai เข้าไปยังฟังก์ชัน setAdult ปรากฎว่าค่า isAdult ของ somchai ไม่ถูกเซ็ตเป็น true ทำไมจึงเป็นเช่นนั้น?
ภาษา Go ส่งค่าเข้าฟังก์ชันแบบ Pass by Value ความหมายคือมันจะทำการก็อบปี้ข้อมูลต้นทางมาไว้กับฟังก์ชันอีกชุด ดังนั้น somchai และตัวแปร h สำหรับฟังก์ชันจึงเป็นคนละตัว การแก้ไขค่า h.isAdult
จึงไม่ใช่การแก้ไข somchai.isAdult
นั่นเอง
วิธีแก้คือเราต้องส่ง Reference ของ somchai ไปให้กับฟังก์ชัน โดยที่ฟังก์ชันเมื่อได้รับ reference มาแล้วต้องทำการ dereference หรือถอดเอาค่าที่แท้จริงออกมา
1package main23import "fmt"45type human struct {6 name string7 age int8 isAdult bool9}1011// ใช้ * แทนการ dereference หรือการถอดเอาค่าที่แท้จริงออกมา12func setAdult(h *human) {13 h.isAdult = h.age >= 1814}1516func main() {17 somchai := human{name: "Somchai", age: 23}18 // ใช้ & แทนการอ้างถึง reference19 setAdult(&somchai)20 fmt.Println(somchai) // {Somchai 23 true}21}
ภาษา Go ไม่มีการสืบทอดนะจ๊ะ
การสืบทอด (inheritance) เป็นหนึ่งในหลักการที่โปรแกรมเมอร์มักใช้กันผิดๆ ลองดูการสืบทอดที่เราทำกันผิดๆในภาษาอื่นๆกัน
สมมติว่าในสำนักงานเรามีอุปกรณ์มากมาย ส่วนใหญ่เข้าถึงได้จาก IP เราจึงสร้างคลาส Devise แทนอุปกรณ์ของเรา
1class Devise {2 ip: string3 location: string // ที่จัดเก็บ4}
เนื่องจากออฟฟิตเรามีเครื่องพิมพ์ จึงเพิ่ม printer ให้สืบทอดจาก Devise เพราะเป็นอุปกรณ์เหมือนกัน
1class Device {2 ip: string3 location: string4}56class Printer extends Device {7 print() {8 // เป็นปริ้นเตอร์ ก็ต้องปริ้นงานได้ซิ9 }10}
ฝ่ายขายเห็นฮาร์ดดิสก์ลดราคา เลยจัดมาซักสองสามตัว
1class Device {2 ip: string3 location: string4}56class Harddisk extends Device {7 store() {8 // จัดเก็บข้อมูล9 }10}
ทว่า Harddisk ไม่ได้ติดต่อผ่าน IP แต่เราดันเก็บ IP ไว้กับ Device เป็นผลให้ Harddisk มีฟิลด์ IP ลอยหน้าลอยตาเล่นๆ โดยไม่ได้ใช้งาน
Go นั้นชอบให้เราประกอบร่างจากชิ้นส่วนเล็กๆ ให้เป็นชิ้นส่วนใหญ่ ตามหลักการ Composition over Inheritance มากกว่า เราจึงไม่เห็นไวยากรณ์ของการสืบทอดในภาษา Go
สำหรับเพื่อนๆที่สนใจสามารถอ่านเพิ่มเติมได้จาก รู้จัก Composition over Inheritance หลักการออกแบบคลาสให้ยืดหยุ่น
Interfaces และ Duck Typing
Go ก็มี Interface เหมือนภาษา Java และ C# เพียงแต่ Go ไม่ต้อง implements Interface แบบเดียวกับภาษาเหล่านั้น
1package main23import "fmt"45type human struct {6 name string7 age int8}910type parrot struct {11 name string12 age int13}1415type talker interface {16 talk()17}1819func (h human) talk() {20 fmt.Println("Human - I'm talking.")21}2223func (p parrot) talk() {24 fmt.Println("Parrot - I'm talking.")25}2627func main() {28 talkers := [2]talker{29 human{name: "Somchai", age: 23},30 parrot{name: "Dum", age: 2},31 }3233 for _, talker := range talkers {34 talker.talk()35 }3637 // ผลลัพธ์38 // Human - I'm talking.39 // Parrot - I'm talking.40}
จากตัวอย่างพบว่าทั้งนกแก้วและคนต่างเป็นนักพูด เราจึงประกาศ interface ชื่อ talker ขึ้นมา การที่จะทำให้ Go รู้ว่าคนและนกแก้วเป็นนักพูดนั้น เราไม่ต้อง implements interface แบบภาษาอื่น Go ถือหลักการของ Duck Typing นั่นคือ ถ้าคุณร้องก๊าบๆและเดินเหมือนเป็ด คุณก็คือเป็ด หาก struct คุณมีเมธอด talk คุณก็คือ taker นั่นเอง!
Concurrency และ Parallelism
หนึ่งในจุดเด่นของภาษา Go คือการโปรแกรมเพื่อทำ Concurrency ที่ง่ายกว่าและมีประสิทธิภาพมากกว่าด้วย Goroutines แต่ก่อนที่เราจะไปทำความรู้จักกับฟีเจอร์นี้ เราต้องแยกแยะสองคำที่แตกต่างคือ Concurrency และ Parallelism ออกจากกันเสียก่อน
เราต้องการสร้างระบบค้นหารูปภาพด้วยชื่อจากโฟลเดอร์ต่างๆในคอมพิวเตอร์ สมมติว่ามีสามโฟลเดอร์คือ Document Image และ Library
โดยทั่วไปการค้นหารูปภาพด้วยโค้ดปกติของเรา เราจะทำการค้นหาไปทีละโฟลเดอร์ เช่น ไล่ตั้งแต่ Document เมื่อค้นหาเสร็จ จึงไปค้นหาที่โฟลเดอร์ Image ต่อ เมื่อเสร็จสิ้นจึงค้นหาที่โฟลเดอร์ Library เป็นรายการสุดท้าย
1func search(keyword string) {2 folders := [3]string{"Document", "Image", "Library"}34 // ค้นหาจากทีละโฟลเดอร์ตามลำดับ5 for _, folder := range folders {6 // ทำการค้นหา7 }8}910func main() {11 search("dog")12}
จากโค้ดข้างต้นเราพบว่า โปรแกรมของเราจะไม่สามารถค้นหารูปภาพจากโฟลเดอร์อื่นได้เลย หากการค้นหาในโฟลเดอร์ก่อนหน้ายังไม่เสร็จสิ้น
เราทราบว่าการค้นหารูปภาพจากโฟลเดอร์ต่างๆเป็นอิสระต่อกัน การค้นหารูปภาพใน Image ไม่เกี่ยวอะไรกับการค้นหารูปภาพใน Document หรือ Library เลย ด้วยเหตุนี้เราจึงแบ่งการทำงานของโปรแกรมเราออกเป็นสามส่วน คือ ค้นหา Document ค้นหา Image และ ค้นหา Library อย่างเป็นอิสระต่อกัน เราเรียกการโปรแกรมเพื่อแยกงานให้สามารถทำงานได้อย่างเป็นอิสระจากกันนี้ว่า Concurrency
เมื่อทุกส่วนของโปรแกรมเป็นอิสระต่อกัน เราจะเริ่มทำส่วนไหนก่อนก็ไม่ใช่ปัญหา จะเริ่มค้นหาที่ Document ก่อน หรือจะเริ่มค้นหาที่ Image ก่อน แบบไหนก็ผลลัพธ์ไม่แตกต่าง
นี่คือยุค 2017 ที่ใครๆก็ใช้ CPU หลายคอร์ละนะ เมื่องานแต่ละชิ้นเป็นอิสระต่อกัน ก็แจกจ่ายงานแต่ละส่วนให้ CPU แต่ละคอร์ทำงานไปแบบอิสระต่อกันซะ การทำงานแบบขนานเช่นนี้เราเรียกว่า Parallelism
จากรูปข้างบนจะสังเกตเห็นว่า CPU คอร์แรกสามารถค้นหารูปภาพจาก Document และขนานการทำงานไปกับ CPU คอร์ที่สองที่ค้นหารูปภาพจาก Image และ CPU คอร์สุดท้ายที่ค้นหารูปภาพจาก Library โดยการค้นหาจาก Image ใช้เวลานานสุดที่ 3 วินาที จึงถือเป็นเวลารวมของการทำงานทั้งหมด
Goroutines
เราสามารถสร้างการทำงานแบบ Concurrency ได้ด้วยการใช้ Goroutines เพียงแค่เติม go
เข้าไปข้างหน้าฟังก์ชัน ทุกอย่างก็จะสดชื่น
1func searchFromFolder(keyword string, folder string) {2 // ทำการค้นหา3}45func search(keyword string) {6 folders := [3]string{"Document", "Image", "Library"}78 for _, folder := range folders {9 // ตรงนี้10 go searchFromFolder(keyword, folder)11 }12}1314func main() {15 search("dog")16}
แน่นอนว่าการทำงานแบบ Concurrency ด้วย Goroutines นี้หากเราทำงานบน CPU หลายคอร์ก็จะเปลี่ยนเป็นการทำงานแบบขนานได้
หากใครได้ลองรันโปรแกรมดังกล่าวจะพบว่าโปรแกรมหยุดการทำงานทันที สำหรับภาษา Go เมื่อ main สิ้นสุด การทำงานก็จะสิ้นสุดตาม เราจึงต้องเพิ่มอะไรบางอย่างเพื่อบอกให้ Go คอยการทำงานของ Goroutines ให้เสร็จสิ้นเสียก่อน สิ่งนั้นก็คือ WaitGroup ภายใต้แพคเกจ sync
1import "sync"23func search(keyword string) {4 var wg sync.WaitGroup5 // ...6}78func main() {9 search("dog")10}
เพื่อให้ Go ทราบว่าจะต้องรอการทำงานของ Goroutines กี่ตัว เราจึงต้องระบุจำนวนลงไป
1import "sync"23func search(keyword string) {4 folders := [3]string{"Document", "Image", "Library"}5 var wg sync.WaitGroup6 // จำนวน goroutines เท่ากับ 3 อันเป็นความยาวของอาร์เรย์ folders7 wg.Add(len(folders))8 // ...9}1011func main() {12 search("dog")13}
และเพื่อป้องกันไม่ให้ Go หยุดการทำงานไปในทันที เราจึงต้อง Wait จนกว่า Goroutines จะทำงานเสร็จหมด
1import "sync"23func search(keyword string) {4 folders := [3]string{"Document", "Image", "Library"}5 var wg sync.WaitGroup6 wg.Add(len(folders))7 // ...8 // รอ ฉันรอเธออยู่~9 wg.Wait()10}1112func main() {13 search("dog")14}
คำถามถัดมา Go จะรู้ได้อย่างไรว่า Goroutine ตัวไหนทำงานเสร็จแล้วบ้าง เราจึงต้องสั่ง Done ในแต่ละ routine เพื่อบอกว่าการทำงานของมันเสร็จสิ้นแล้ว
1import "sync"23// โปรดสังเกต เราต้องรับพอยเตอร์ของ sync.WaitGroup เข้ามาด้วย4func searchFromFolder(keyword string, folder string, wg *sync.WaitGroup) {5 // ทำการค้นหา6 // เมื่อค้นหาเสร็จ ต้องแจ้งให้ WaitGroup ทราบว่าเราทำงานเสร็จแล้ว7 // WaitGroup จะได้นับถูกว่าเหลือ Goroutines ที่ต้องรออีกกี่ตัว8 wg.Done()9}1011func search(keyword string) {12 folders := [3]string{"Document", "Image", "Library"}13 var wg sync.WaitGroup14 wg.Add(len(folders))15 for _, folder := range folders {16 // เราต้องส่ง reference ของ wg ไปด้วย เพื่อที่จะสั่ง Done17 go searchFromFolder(keyword, folder, &wg)18 }19 wg.Wait()20}2122func main() {23 search("dog")24}
เป็นอันจบพิธี~
รู้จัก Package ในภาษา Go
เมื่อโปรแกรมมีขนาดใหญ่ขึ้น เราอาจต้องมีการแยกโค้ดของเราเป็นหลายแพคเกจ
1-- src2 -- godubai3 -- main.go4 -- search5 -- main.go
เราอาจสร้างแพคเกจ search ขึ้นมาเพื่อแยกการค้นหาออกจากแพคเกจหลักคือ main
1func searchFromFolder(keyword string, folder string, wg *sync.WaitGroup) {2 // ทำการค้นหา3 wg.Done()4}56func Run(keyword string) {7 folders := [3]string{"Document", "Image", "Library"}8 var wg sync.WaitGroup9 wg.Add(len(folders))10 for _, folder := range folders {11 go searchFromFolder(keyword, folder, &wg)12 }13 wg.Wait()14}
ส่วนของ main ต้องมีการ import แพคเกจดังกล่าวเข้ามาใช้งาน
1import "godubai/search"23func main() {4 search.Run("dog")5}
หลังจากผ่านโค้ดกันมาเยอะแล้ว เพื่อนๆคงอาจสงสัย เอ๊ะทำไมฟังก์ชันบางตัวก็ขึ้นต้นด้วยตัวเล็ก บางตัวก็ขึ้นต้นด้วยตัวใหญ่?
สำหรับภาษา Go การขึ้นต้นด้วยตัวเล็กใหญ่มีความสำคัญครับ ถ้าตัวแปร ฟังก์ชัน structs หรืออื่นใด ที่ขึ้นต้นด้วยตัวเล็ก สิ่งนั้นจะอ้างอิงถึงได้เฉพาะในแพคเกจตัวเอง หากต้องการให้เข้าถึงได้จากแพคเกจอื่น เราต้องขึ้นต้นสิ่งเหล่านั้นด้วยอักษรตัวใหญ่แทน นี่จึงเป็นเหตุผลที่ว่าทำไม Run
ในแพคเกจ search ถึงขึ้นต้นด้วยตัวใหญ่ เพราะมีการเรียกใช้จากแพคเกจอื่นคือ main นั่นเอง
สรุป
บทความนี้เป็นแค่การชิมลางให้มองเห็นภาพรวมกว้างๆของการใช้งาน Go ครับ สำหรับ Go ของเล่นอื่นๆยังมีอีกเยอะ ใครสนใจอ่านเพิ่มเติมก็โปรดติดตามชุดบทความนี้แบบติดๆเลยนะฮะ
สารบัญ
- อะไร อะไรก็ Go
- รู้จักภาษา Go ฉบับคนแปลกหน้า
- สวัสดี Golang
- Build Run และ Format
- ประโยคควบคุมในภาษา Go
- ตัวแปรและชนิดข้อมูลในภาษา Go
- Arrays และ Slices ในภาษา Go
- ฟังก์ชันในภาษา Go
- นิยามโครงสร้างข้อมูลด้วย Structs
- พอยเตอร์ในภาษา Go
- ภาษา Go ไม่มีการสืบทอดนะจ๊ะ
- Interfaces และ Duck Typing
- Concurrency และ Parallelism
- Goroutines
- รู้จัก Package ในภาษา Go
- สรุป