Golang นั้นมี concept อย่าง Goroutines และ Channels ที่ช่วยให้สามารถจัดการงานแบบคู่ขนานได้ง่าย ๆ อยู่
เริ่มจากวิธีสร้าง channel
// var := make(chan <channel_type>, <buffer_size>?)
unbufferedChannel := make(chan int)
bufferedChannel := make(chan int, 10)
buffer ในแชนแนล คือการระบุว่าแชนแนลนี้สามารถรับค่าเข้ามาให้ในแชนแนลได้สูงสุดกี่ตัว
การส่งค่ารับค่าในแชนแนลนั้นสามารถทำได้โดย
bufferedChannel <- 1
newVariable := <- bufferedChannel
ความหมายก็ให้นำค่า 1 ไปเก็บไว้ใน bufferedChannel และบรรทัดต่อมาให้ดึงค่าจากแชนแนลออกมาให้ตัวแปร newVariable ซึ่งในตอนนี้มีค่า 1; ดังนั้น newVariable จะมีค่าเท่ากับ 1 หลังจากจบโปรแกรมนี้
Channels นั้นทำงานเสมือนกับ Queue (FIFO) ดังนั้นในโปรแกรมข้างล่างนี้ a,b,c จะมีค่าเป็น 1,2,3 ตามลำดับ
bufferedChannel <- 1
bufferedChannel <- 2
bufferedChannel <- 3
a := <- bufferedChannel
b := <- bufferedChannel
c := <- bufferedChannel
ทีนี้ unbuffered channels ล่ะต่างกันยัง
unbufferedChannel <- 1
d := <- unbufferedChannel
ทำแบบนี้โปรแกรมจะไม่สามารถทำงานต่อได้ เนื่องจากว่า unbuffered channels นั้นไม่สามารถใช้เก็บค่าใด ๆ ได้; งั้นแล้วมีให้ใช้มาทำไม? เราสามารถทำให้ค่า 1 ข้างบนถูกเข้า unbufferedChannel ได้ แต่มีเงื่อนไขว่าจะต้องมีตัวแปรมารับจาก unbufferedChannel เหมือนกัน; ในส่วนนี้ Goroutines จะเข้ามามีบทบาทสำหรับงานนี้
go func() {
unbufferedChannel <- 1
}()
d := <- unbufferedChannel
Goroutines นั้นจะทำงานอยู่ใน background ของตัวโปรแกรมหลัก ดังนั้นในขณะที่ d จะไม่รอรับค่าจาก unbufferedChannel จนติด deadlock เพราะมีฟังก์ชันที่ทำงานอยู่และมีการ assign ค่า 1 ให้กับ unbufferedChannel; อาจจะเข้ามา unbuffered channels นั้นไม่สามารถเก็บค่าได้ แต่ถ้ามีตัวแปรที่รอรับค่าจาก channel นั้นอยู่จะไม่มีปัญหาอะไร
สำหรับ buffered channels นั้นก็ไม่ใช่ว่าจะติด deadlock ไม่ได้
anotherChannel := make(chan int, 2)
anotherChannel <- 1
anotherChannel <- 2
anotherChannel <- 3
anotherChannel สามารถเก็บค่า int ได้อยู่สองตัว แต่ว่าในโค้ดด้านบนนั้นมีการ assign ค่าถึงสามครั้ง ดังนั้นในกรณีนี้จะติด deadlock เอาได้ หากว่าอยากจะให้รับค่า 3 อาจจะต้องเอาค่าบางส่วนออกจากแชนแนลก่อน
เราสามารถใช้ฟังก์ชัน close เพื่อทำการปิดแชนแนลที่ไม่มีการทำงานต่อแล้วได้
sampleChannel := make(chan int, 2)
sampleChannel <- 1
sampleChannel <- 2
close(sampleChannel)
แต่ก็ยังสามารถอ่านค่าที่ข้างในแชนแนลอยู่ได้
s1, s2 := <- sampleChannel, <- sampleChannel
สำหรับ channels ที่การันตีว่าจะโดนใช้ close แน่ ๆ จะสามารถใช้ for range อ่านค่าได้
for v := range sampleChannel {
fmt.Println(v)
}
สามารถใช้ได้ทั้งสองแบบ ให้เลือกใช้แบบนึง
สามารถเช็คว่าแชนแนลนั้นถูกปิดหรือยังได้โดยเพิ่ม variable ตัวที่สองเข้ามาในตอนที่อ่านค่าจากแชนแนล
v, ok := <- sampleChannel
if ok {
// not close
} else {
// closed
}
กรณีที่ปิดไปแล้ว v จะมีค่าเท่ากับ default value ของ type แชนแนลนั้น
select เป็นฟังก์ชันที่ใช้เวลาทำงานร่วมกันหลาย ๆ แชนแนลนั้น
sumChannel := make(chan int)
printChannel := make(chan struct{})
go func() {
sum := 0
for {
select {
case v := <- sumChannel:
sum += v
case <- printChannel:
fmt.Println(sum)
}
}
}()
sumChannel <- 4
sumChannel <- 7
sumChannel <- 11
printChannel <- struct{}{}
time.Sleep(time.Second)
select มีการใช้การที่คล้ายกับ switch; select จะ block การทำงานจนกว่าจะมี case ไหนในแชนแนลทำงานได้ (มีค่าจากแชนแนลส่งมา); แต่ select ก็รองรับ default case เหมือนกัน
nonstopChannel := make(chan int)
select {
case <- nonstopChannel:
fmt.Println("nonstopChannel is working")
default:
fmt.Println("default case is working")
}
ในกรณีนี้ select จะไม่รอรับค่าจาก nonstopChannel หากเวลาที่ select ทำงานแล้วไม่มีค่าใด ๆ ส่งมา default case จะถูกเรียกใช้ กรณีนี้ก็จะแสดงข้อความว่า "default case is working"