เรียนรู้การใช้ภาษา Go ใน 15 นาที

Nuttavut Thongjor

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

Executable File

ภาษา Go นั้นเหมาะกับงานจำพวก System Programs ไม่ว่าจะสร้าง API Server ก็ได้ ทำ Network Applications ก็ดี หรือจะคูลๆชิคๆไปกับการสร้าง Game Engines ก็ไม่ว่ากัน

สวัสดี Golang

เพื่อให้ Go รู้ว่าจะเริ่มทำงานจากจุดไหน จึงจำเป็นที่เราต้องสร้าง package ชื่อ main พร้อมทั้งประกาศฟังก์ชันชื่อ main เช่นกัน

Go
1// ประกาศ package ชื่อ main
2package main
3
4// ฟังก์ชัน main จะเป็นจุดเริ่มต้นการทำงานของเรา
5func main() {
6
7}

การโปรแกรมด้วย Go สามารถแยกส่วนโค้ดด้วย package ได้ import จึงเป็นคำสำคัญเพื่อใช้ในการนำเข้า package อื่นมาใช้ในโค้ดเรา

Go
1package main
2
3// นำเข้า package fmt มาใช้งาน
4import "fmt"
5
6func main() {
7 // ภายใต้ fmt มีฟังก์ชันชื่อ Println ให้เราสามารถพิมพ์ข้อความออกจอได้
8 fmt.Println("Hello, Go")
9}

การ import นั้นไม่จำกัดอยู่เฉพาะไลบรารี่ของ Go เอง เรายังสามารถ import ซอร์สโค้ดจากที่อื่น เช่น Github มาใช้ได้ด้วย เช่น

Go
1import "github.com/jinzhu/gorm"

คอมไพเลอร์ของ Go นั้นเป็นคนแก่ขี้บ่น เห็นอะไรไม่ต้องตาเป็นต้องเอะอะโวยวาย หากเรา import บางสิ่งเข้ามาแต่ไม่ได้เรียกใช้ เจ๊ Go ก็จะบ่นดังๆตอนเราคอมไพล์ เช่น

Go
1package main
2
3import "fmt"
4
5func main {
6
7}

จากโค้ดข้างต้นพบว่าเรา import แพคเกจ fmt เข้ามา แต่ไม่ได้เรียกใช้งาน เมื่อทำการคอมไพล์ Go ก็จะกริ้วนิดหน่อย ด้วยการบ่นๆดังนี้

Code
1main.go:3:1: imported and not used: "fmt"

Build Run และ Format

สมมติโครงสร้างโปรเจคภายใต้ชื่อ godubai ของเราเป็นดังนี้

Code
1-- src
2 -- godubai << โปรเจคเรา
3 -- main.go

เมื่อเราต้องการสร้าง executable file เราสามารถออกคำสั่ง go build ได้ ซึ่งจะได้ผลลัพธ์ตามชื่อโปรเจคคือ godubai นั่นหมายความว่าเราสามารถสั่งโปรแกรมนี้ของเราให้ทำงานได้ด้วยการเรียก ./godubai นั่นเอง

ระหว่างช่วงการพัฒนาโปรแกรม เราคงต้องการสั่งโปรแกรมให้ทำงานเพียงเพื่อต้องการเห็นผลลัพธ์ แต่ไม่ต้องการ executable file ออกมา เราสามารถสั่ง go run main.go แทนได้ ด้วยคำสั่งนี้ซอร์สโค้ดของเราจะถูกอ่าน build และ run โดยไม่มีการสร้างไฟล์ผลลัพธ์ให้เป็นขยะในโฟลเดอร์ของเรา

การทำงานร่วมกันเป็นทีม ต่างคนย่อมต่างสไตล์ หน้าตาของโค้ดจึงถูกฟอร์แมตต่างกันได้ แน่นอนหละครับเมื่อทำงานเป็นทีมเราก็คงคาดหวังให้โค้ดของเราออกมาสไตล์เดียว เป็นสไตล์สากลในจักรวาลของ Go

Go
1// โค้ดเล๊ะเทะ
2package main
3import "fmt"
4func main() {
5fmt.Println("Hello, Go")
6}

Go ได้เตรียมเครื่องมือ gofmt เพื่อจัดการฟอร์แมตซอร์สโค้ดของคุณให้เป็นตามมาตรฐาน GO ภายหลังออกคำสั่ง gofmt -w main.go หน้าตาโค้ดเราก็จะเข้าสู่สภาวะเป็นผู้เป็นคน

Go
1package main
2
3import "fmt"
4
5func main() {
6 fmt.Println("Hello, Go")
7}

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

การใช้ gofmt แม้จะดูง่าย แต่ชีวิตเราจะง่ายกว่านี้หากไม่ต้องมาคอยรัน gofmt ดังนั้นเราจึงควรตั้งค่าให้ gofmt ทำงานทุกครั้งหลังจากเรากดเซฟไฟล์ และทำงานอีกครั้งตอนเราส่งโค้ดขึ้น version control

ไหนๆ gofmt ก็ทำให้ชีวิตเป็นเรื่องง่ายแล้ว จะให้ง่ายกว่านี้อีกหน่อยได้ไหม ด้วยการช่วย import package ให้เราอัตโนมัติซะเลย จากในตัวอย่างพบว่าเรามีการใช้งาน fmt แต่ยังไม่ import package ดังกล่าว

Go
1package main
2
3func main() {
4 fmt.Println("Hello, Go")
5}

Go ได้เตรียมเครื่องมือชื่อ goimports ให้กับเรา หลังออกคำสั่ง goimports -w main.go แพคเกจไหนที่ยังไม่ได้นำเข้า Go ก็จะ import เข้ามาให้ โปรดสังเกตเราใส่ -w เพื่อบอกให้เครื่องมือดังกล่าวเขียนผลลัพธ์ทับไฟล์เดิม

Go
1package main
2
3import "fmt"
4
5func main() {
6 fmt.Println("Hello, Go")
7}

goimports นั้น นอกจากจะทำการ import package ต่างๆที่เราใช้งานในซอร์สโค้ดเข้ามาให้ มันยังทำการฟอร์แมตโค้ดให้กับเราอัตโนมัติโดยไม่ต้องเรียก gofmt เลยด้วยครับ

ประโยคควบคุมในภาษา Go

ประโยคควบคุมในภาษา Go มีไม่เยอะมาก โดยแต่ละตัวก็จะมีลักษณะงานที่เหมาะสมจำเพาะกับมันไปเลย ไม่เหมือนภาษาอื่นๆที่มีทั้ง while และ for-loop ที่ชวนปวดหัวว่าจะใช้ตัวไหนดี

if statement

Go
1if i % 2 == 0 {
2 // even
3} else {
4 // odd
5}

โปรดสังเกต นอกจาก Go จะไม่ต้องปิดท้าย statement ด้วย semicolon แล้ว ประโยคควบคุมยังไม่ต้องครอบด้วยวงเล็บอีกด้วย

for-loop

Go
1for i := 1; i <= 10; i++ {
2 fmt.Println(i)
3}

for-loop ไม่แตกต่างจากภาษาอื่นมากนัก นั่นคือสามารถแบ่งย่อยออกได้เป็นสามส่วนโดยใช้ semicolon เป็นตัวแบ่ง ส่วนแรกคือการตั้งค่าเริ่มต้น ส่วนถัดมาเป็นเงื่อนไข และส่วนสุดท้ายคือการทำงานหลังจบรอบ

switch

Go
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

วิธีการนี้คือการประกาศตัวแปรพร้อมระบุชนิดข้อมูล

Go
1var <ชื่อตัวแปร> <ชนิดข้อมูล>

หลังจากการประกาศตัวแปรแล้ว เราสามารถกำหนดค่าตัวแปรด้วยชนิดข้อมูลที่ระบุได้

Go
1var message string
2message = "Hello, Go"

คำถามครับ สมมติเราประกาศตัวแปรขึ้นมา แต่ยังไม่ได้กำหนดค่าให้มันหละ เช่นนี้ค่าของตัวแปรดังกล่าวจะเป็นอะไร?

Zero Values เป็นคำเรียกเก๋ๆสำหรับค่า "default value" ในกรณีที่เราประกาศตัวแปรขึ้นมาแต่ไม่ได้กำหนดค่าให้กับมัน ตัวอย่างเช่น

TypeZero Value
boolfalse
int0
float0.0
string""
functionnil

เพราะฉะนั้น หากเราประกาศ var message string ขึ้นมาลอยๆ ตอนนี้คงตอบได้แล้วซิครับว่า message นั้นมีค่าเป็น "" นั่นเอง

Type Inference

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

Go
1<ชื่อตัวแปร> := <ค่า>
2
3// เช่น
4message := "Hello, Go"

การประกาศตัวแปรพร้อมระบุค่าด้วยการใช้ := Go จะดูว่าฝั่งขวานั้นมีชนิดข้อมูลเป็นอะไร เพื่อนำไปอนุมานว่าตัวแปรดังกล่าวควรเป็นชนิดข้อมูลอะไร เราจึงเรียกวิธีกำหนดค่าแบบนี้ว่า Type Inference

Arrays และ Slices ในภาษา Go

อาร์เรย์ในภาษา Go ก็มีวิธีประกาศและใช้ไม่ต่างอะไรจากภาษาอื่นมากนัก

Go
1// อาร์เรย์ของข้อความจำนวนสามช่อง
2var names [3]string
3
4names[0] = "Somchai"
5names[1] = "Somsree"
6names[2] = "Somset

หรือถ้าคุณเป็นสายย่อ โค้ดต่อไปนี้จะดูกรุบกริบ

Go
1names := [3]string{"Somchai", "Somsree", "Somset"}

อาร์เรย์อาจไม่ยืดหยุ่นเพียงพอสำหรับเรา การประกาศอาร์เรย์เราต้องระบุขนาดของมัน แต่การใช้งานจริงของเราเราอาจต้องการยืดและหดขนาดได้อย่างอิสระ Slices จึงเข้ามาตอบโจทย์ตรงนี้แทน Arrays

Go
1// การประกาศ slice เราไม่ต้องระบุจำนวนช่อง
2// เพราะเราสามารถเพิ่ม element เข้าไปได้อย่างอิสระภายหลัง
3var names []string
4
5// เพิ่ม element เข้าไป
6names = append(names, "Somchai")
7names = append(names, "Somsree")
8names = append(names, "Somset")

สำหรับสายย่อ slices ก็มีวิธีประกาศเช่นเดียวกับ arrays เพียงแต่ไม่ต้องระบุจำนวน

Go
1names := []string{"Somchai", "Somsree", "Somset"}

arrays และ slices ยังสามารถใช้ for เพื่อวนลูปได้ผ่าน range

Go
1names := [3]string{"Somchai", "Somsree", "Somset"}
2
3for index, name := range names {
4 fmt.Println(index, name)
5}
6
7// ผลลัพธ์
8// 0 Somchai
9// 1 Somsree
10// 2 Somset

ฟังก์ชันในภาษา Go

ฟังก์ชันในภาษา Go ก็เป็นปกติทั่วไปตามแบบฉบับภาษาตระกูลมี Type ต่างกันนิดหน่อยตรงที่ชื่อตัวแปรจะมาก่อนชนิดข้อมูล เช่น

Go
1package main
2
3import (
4 "fmt"
5)
6
7func main() {
8 printFullName("Babel", "Coder")
9}
10
11// โปรดสังเกต ชื่อตัวแปรจะมาก่อนชนิดข้อมูล
12func printFullName(firstName string, lastName string) {
13 fmt.Println(firstName + " " + lastName)
14}

ในกรณีที่มีการคืนค่ากลับจากฟังก์ชัน เราต้องระบุชนิดข้อมูลของค่าที่จะคืนออกไปด้วย

Go
1// คืนค่ากลับเป็น string
2func getMessage() string {
3
4}

ฟังก์ชันในภาษา Go สามารถคืนค่าได้มากกว่าหนึ่งค่า และเป็นธรรมเนียมปฏิบัติที่เรามักจะคืนค่า error เพื่อบ่งบอกว่าการทำงานของฟังก์ชันมีปัญหาหรือไม่ออกมาด้วย

Go
1package main
2
3import (
4 "errors"
5 "fmt"
6)
7
8func main() {
9 // เพราะว่าฟังก์ชันคืนค่าสองค่า เราจึงประกาศตัวแปรมารองรับได้พร้อมกันสองตัว
10 result, err := divide(5, 3)
11
12 // ตรวจสอบก่อนว่ามี error ไหม ถ้ามีก็จบโปรแกรมไปแบบไม่ค่อยสวยด้วย Exit(1)
13 if err != nil {
14 os.Exit(1)
15 }
16
17 fmt.Println(result)
18}
19
20// คืนค่า 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, err
25 }
26
27 return dividend / divisor, nil
28}

นิยามโครงสร้างข้อมูลด้วย Structs

แม้ภาษา Go จะไม่มีคลาส แต่เรามี structs ที่สามารถนิยามโครงสร้างของข้อมูลขึ้นมาเองได้

Go
1type human struct {
2 name string
3 age int
4}

หลังจากที่เรานิยามโครงสร้างข้อมูลภายใต้ชื่อ Human เราก็สามารถสร้างข้อมูลเหล่านี้พร้อมตั้งค่าให้กับมันได้ ดังนี้

Go
1somchai := human{name: "Somchai", age: 23}
2somsree := human{name: "Somsree", age: 32}

เมื่อ structs เป็นตัวแทนของโครงสร้างข้อมูลที่เรานิยามขึ้นมา เราจึงสามารถนิยามพฤติกรรมที่สัมพันธ์กับ structs นี้ได้

Go
1type human struct {
2 name string
3 age int
4}
5
6// printInfo จะผูกติดกับ human
7// สังเกตมีการระบุ human เข้าไปก่อนหน้าชื่อเมธอด
8func (h human) printInfo() {
9 fmt.Println(h.name, h.age)
10}
11
12func main() {
13 somchai := human{name: "Somchai", age: 23}
14 somchai.printInfo() // Somchai 23
15}

พอยเตอร์ในภาษา Go

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

Go
1package main
2
3import "fmt"
4
5type human struct {
6 name string
7 age int
8 isAdult bool // Zero Value คือ false
9}
10
11// setAdult รับ human เข้ามา
12// หากอายุเกิน 18 isAdult จะมีค่าเป็น true
13// นอกนั้นมีค่าเป็น false
14func setAdult(h human) {
15 h.isAdult = h.age >= 18
16}
17
18func 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 หรือถอดเอาค่าที่แท้จริงออกมา

Go
1package main
2
3import "fmt"
4
5type human struct {
6 name string
7 age int
8 isAdult bool
9}
10
11// ใช้ * แทนการ dereference หรือการถอดเอาค่าที่แท้จริงออกมา
12func setAdult(h *human) {
13 h.isAdult = h.age >= 18
14}
15
16func main() {
17 somchai := human{name: "Somchai", age: 23}
18 // ใช้ & แทนการอ้างถึง reference
19 setAdult(&somchai)
20 fmt.Println(somchai) // {Somchai 23 true}
21}

ภาษา Go ไม่มีการสืบทอดนะจ๊ะ

การสืบทอด (inheritance) เป็นหนึ่งในหลักการที่โปรแกรมเมอร์มักใช้กันผิดๆ ลองดูการสืบทอดที่เราทำกันผิดๆในภาษาอื่นๆกัน

สมมติว่าในสำนักงานเรามีอุปกรณ์มากมาย ส่วนใหญ่เข้าถึงได้จาก IP เราจึงสร้างคลาส Devise แทนอุปกรณ์ของเรา

TypeScript
1class Devise {
2 ip: string
3 location: string // ที่จัดเก็บ
4}

เนื่องจากออฟฟิตเรามีเครื่องพิมพ์ จึงเพิ่ม printer ให้สืบทอดจาก Devise เพราะเป็นอุปกรณ์เหมือนกัน

TypeScript
1class Device {
2 ip: string
3 location: string
4}
5
6class Printer extends Device {
7 print() {
8 // เป็นปริ้นเตอร์ ก็ต้องปริ้นงานได้ซิ
9 }
10}

ฝ่ายขายเห็นฮาร์ดดิสก์ลดราคา เลยจัดมาซักสองสามตัว

TypeScript
1class Device {
2 ip: string
3 location: string
4}
5
6class 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 แบบเดียวกับภาษาเหล่านั้น

Go
1package main
2
3import "fmt"
4
5type human struct {
6 name string
7 age int
8}
9
10type parrot struct {
11 name string
12 age int
13}
14
15type talker interface {
16 talk()
17}
18
19func (h human) talk() {
20 fmt.Println("Human - I'm talking.")
21}
22
23func (p parrot) talk() {
24 fmt.Println("Parrot - I'm talking.")
25}
26
27func main() {
28 talkers := [2]talker{
29 human{name: "Somchai", age: 23},
30 parrot{name: "Dum", age: 2},
31 }
32
33 for _, talker := range talkers {
34 talker.talk()
35 }
36
37 // ผลลัพธ์
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 เป็นรายการสุดท้าย

Go
1func search(keyword string) {
2 folders := [3]string{"Document", "Image", "Library"}
3
4 // ค้นหาจากทีละโฟลเดอร์ตามลำดับ
5 for _, folder := range folders {
6 // ทำการค้นหา
7 }
8}
9
10func main() {
11 search("dog")
12}

จากโค้ดข้างต้นเราพบว่า โปรแกรมของเราจะไม่สามารถค้นหารูปภาพจากโฟลเดอร์อื่นได้เลย หากการค้นหาในโฟลเดอร์ก่อนหน้ายังไม่เสร็จสิ้น

Sequential Search

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

เมื่อทุกส่วนของโปรแกรมเป็นอิสระต่อกัน เราจะเริ่มทำส่วนไหนก่อนก็ไม่ใช่ปัญหา จะเริ่มค้นหาที่ Document ก่อน หรือจะเริ่มค้นหาที่ Image ก่อน แบบไหนก็ผลลัพธ์ไม่แตกต่าง

Concurrency

นี่คือยุค 2017 ที่ใครๆก็ใช้ CPU หลายคอร์ละนะ เมื่องานแต่ละชิ้นเป็นอิสระต่อกัน ก็แจกจ่ายงานแต่ละส่วนให้ CPU แต่ละคอร์ทำงานไปแบบอิสระต่อกันซะ การทำงานแบบขนานเช่นนี้เราเรียกว่า Parallelism

Parallelism

จากรูปข้างบนจะสังเกตเห็นว่า CPU คอร์แรกสามารถค้นหารูปภาพจาก Document และขนานการทำงานไปกับ CPU คอร์ที่สองที่ค้นหารูปภาพจาก Image และ CPU คอร์สุดท้ายที่ค้นหารูปภาพจาก Library โดยการค้นหาจาก Image ใช้เวลานานสุดที่ 3 วินาที จึงถือเป็นเวลารวมของการทำงานทั้งหมด

Goroutines

เราสามารถสร้างการทำงานแบบ Concurrency ได้ด้วยการใช้ Goroutines เพียงแค่เติม go เข้าไปข้างหน้าฟังก์ชัน ทุกอย่างก็จะสดชื่น

Go
1func searchFromFolder(keyword string, folder string) {
2 // ทำการค้นหา
3}
4
5func search(keyword string) {
6 folders := [3]string{"Document", "Image", "Library"}
7
8 for _, folder := range folders {
9 // ตรงนี้
10 go searchFromFolder(keyword, folder)
11 }
12}
13
14func main() {
15 search("dog")
16}

แน่นอนว่าการทำงานแบบ Concurrency ด้วย Goroutines นี้หากเราทำงานบน CPU หลายคอร์ก็จะเปลี่ยนเป็นการทำงานแบบขนานได้

Parallelism

หากใครได้ลองรันโปรแกรมดังกล่าวจะพบว่าโปรแกรมหยุดการทำงานทันที สำหรับภาษา Go เมื่อ main สิ้นสุด การทำงานก็จะสิ้นสุดตาม เราจึงต้องเพิ่มอะไรบางอย่างเพื่อบอกให้ Go คอยการทำงานของ Goroutines ให้เสร็จสิ้นเสียก่อน สิ่งนั้นก็คือ WaitGroup ภายใต้แพคเกจ sync

Go
1import "sync"
2
3func search(keyword string) {
4 var wg sync.WaitGroup
5 // ...
6}
7
8func main() {
9 search("dog")
10}

เพื่อให้ Go ทราบว่าจะต้องรอการทำงานของ Goroutines กี่ตัว เราจึงต้องระบุจำนวนลงไป

Go
1import "sync"
2
3func search(keyword string) {
4 folders := [3]string{"Document", "Image", "Library"}
5 var wg sync.WaitGroup
6 // จำนวน goroutines เท่ากับ 3 อันเป็นความยาวของอาร์เรย์ folders
7 wg.Add(len(folders))
8 // ...
9}
10
11func main() {
12 search("dog")
13}

และเพื่อป้องกันไม่ให้ Go หยุดการทำงานไปในทันที เราจึงต้อง Wait จนกว่า Goroutines จะทำงานเสร็จหมด

Go
1import "sync"
2
3func search(keyword string) {
4 folders := [3]string{"Document", "Image", "Library"}
5 var wg sync.WaitGroup
6 wg.Add(len(folders))
7 // ...
8 // รอ ฉันรอเธออยู่~
9 wg.Wait()
10}
11
12func main() {
13 search("dog")
14}

คำถามถัดมา Go จะรู้ได้อย่างไรว่า Goroutine ตัวไหนทำงานเสร็จแล้วบ้าง เราจึงต้องสั่ง Done ในแต่ละ routine เพื่อบอกว่าการทำงานของมันเสร็จสิ้นแล้ว

Go
1import "sync"
2
3// โปรดสังเกต เราต้องรับพอยเตอร์ของ sync.WaitGroup เข้ามาด้วย
4func searchFromFolder(keyword string, folder string, wg *sync.WaitGroup) {
5 // ทำการค้นหา
6 // เมื่อค้นหาเสร็จ ต้องแจ้งให้ WaitGroup ทราบว่าเราทำงานเสร็จแล้ว
7 // WaitGroup จะได้นับถูกว่าเหลือ Goroutines ที่ต้องรออีกกี่ตัว
8 wg.Done()
9}
10
11func search(keyword string) {
12 folders := [3]string{"Document", "Image", "Library"}
13 var wg sync.WaitGroup
14 wg.Add(len(folders))
15 for _, folder := range folders {
16 // เราต้องส่ง reference ของ wg ไปด้วย เพื่อที่จะสั่ง Done
17 go searchFromFolder(keyword, folder, &wg)
18 }
19 wg.Wait()
20}
21
22func main() {
23 search("dog")
24}

เป็นอันจบพิธี~

รู้จัก Package ในภาษา Go

เมื่อโปรแกรมมีขนาดใหญ่ขึ้น เราอาจต้องมีการแยกโค้ดของเราเป็นหลายแพคเกจ

Code
1-- src
2 -- godubai
3 -- main.go
4 -- search
5 -- main.go

เราอาจสร้างแพคเกจ search ขึ้นมาเพื่อแยกการค้นหาออกจากแพคเกจหลักคือ main

Go
1func searchFromFolder(keyword string, folder string, wg *sync.WaitGroup) {
2 // ทำการค้นหา
3 wg.Done()
4}
5
6func Run(keyword string) {
7 folders := [3]string{"Document", "Image", "Library"}
8 var wg sync.WaitGroup
9 wg.Add(len(folders))
10 for _, folder := range folders {
11 go searchFromFolder(keyword, folder, &wg)
12 }
13 wg.Wait()
14}

ส่วนของ main ต้องมีการ import แพคเกจดังกล่าวเข้ามาใช้งาน

Go
1import "godubai/search"
2
3func 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
  • สรุป