โปรแกรมที่เราเขียน ไม่ว่าจะเขียนด้วยภาษาอะไร มันจะมีวงจรชีวิต (life cycle) ของมันว่าจะเริ่มทำงานที่จุดไหน และ สิ้นสุดที่ตรงไหน เสมอ
ถ้าเราเข้าใจ life cycle ของโปรแกรม ของภาษาที่เราใช้ หรือ เข้าใจ life cycle ของ runtime ที่เราใช้ เราก็จะไล่การทำงานของโปรแกรมได้ง่ายขึ้นเยอะเลย
อย่างโปรแกรมที่เขียนด้วย Go นั้นจะเริ่มทำงานที่ฟังก์ชัน main ใน package main และจะหยุดทำงานเมื่อ main ฟังก์ชันจบการทำงาน ไม่ว่าจะด้วยการ return ปกติ, การเรียก os.Exit ฟังก์ชัน หรือโดน signal จาก OS ให้หยุดทำงาน
ตัวอย่างเช่นโค้ดแบบนี้
package main
import (
"fmt"
)
func main() {
fmt.Println("Hello, playground")
}
เมื่อเรารันก็ได้ข้อความ Hello, playground
ในออกไปที่ standard output แต่ถ้าเราเปลี่ยนตรง fmt.Println
ให้ไปไปรันในอีก goroutine ล่ะ แบบนี้
package main
import (
"fmt"
)
func main() {
go func() {
fmt.Println("Hello, playground")
}()
}
เวลารันกลับไปเห็นเกิดอะไรขึ้นเลย นั่นเพราะว่า พอ main ฟังก์ชันเริ่มทำงาน มันสร้าง goroutine ใหม่ เสร็จแล้วมันก็จบการทำงานของตัวเองโดยไม่รอให้ฟังก์ชันของ goroutine อีกอันที่สร้างใหม่ได้ทำงานเลย เมื่อ main จบการทำงาน ถือว่าจบโปรแกรม goroutine ที่เหลือก็จบการทำงานด้วยไปโดยอัตโนมัติ
ถ้าอยากให้ main รอให้ goroutine ที่แยกไปทำงานเสร็จก่อนก็ทำได้แบบนี้
package main
import (
"fmt"
)
func main() {
done := make(chan struct{})
go func() {
fmt.Println("Hello, playground")
close(done)
}()
<-done
}
ใช้ channel ช่วย โดยให้ main รอรับค่า ส่วนใน goroutine เมื่อทำงานเสร็จก็ close channel ซะทำให้ตรงส่วนที่รอรับค่าไม่ block เลยทำให้ main จบการทำงานต่อไปได้
ทีนี้ตัวอย่างถัดไป โค้ด HTTP server ง่ายๆแบบนี้
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello, World")
})
http.ListenAndServe(":8000", nil)
}
จะเห็นว่า โปรแกรมเรามันรันแล้วค้างไว้ รอรับ request มาที่ port 8000 แล้วตอบกลับไปด้วยข้อความ Hello, World
โดยที่จบการทำงานเมื่อเราสั่ง ctrl+c เพื่อให้ OS ส่ง signal ไปจบ process
แสดงว่าโค้ดที่เราเรียก http.ListenAndServe มันเป็นฟังก์ชันที่รันนานๆเลยไม่จบจนกว่าจะมี signal มาหยุดมันนั่นเอง มันเลย block main ฟังก์ชันเอาไว้ให้ยังรันอยู่ไม่ไปไหน
ถ้าเราไล่โค้ดใน http.ListenAndServe
เราจะไต่ไปถึงจุดที่เรียก method (srv *Server) Serve(l net.Listener) error
ที่มีโค้ด for loop แบบนี้อยู่
for {
rw, err := l.Accept()
if err != nil {
select {
case <-srv.getDoneChan():
return ErrServerClosed
default:
}
if ne, ok := err.(net.Error); ok && ne.Temporary() {
if tempDelay == 0 {
tempDelay = 5 * time.Millisecond
} else {
tempDelay *= 2
}
if max := 1 * time.Second; tempDelay > max {
tempDelay = max
}
srv.logf("http: Accept error: %v; retrying in %v", err, tempDelay)
time.Sleep(tempDelay)
continue
}
return err
}
connCtx := ctx
if cc := srv.ConnContext; cc != nil {
connCtx = cc(connCtx, rw)
if connCtx == nil {
panic("ConnContext returned nil")
}
}
tempDelay = 0
c := srv.newConn(rw)
c.setState(c.rwc, StateNew, runHooks) // before Serve can return
go c.serve(connCtx)
}
มันคือ infinity loop ที่ทำงานรอรับ request แล้วส่งไปโปรเซสต่อเพื่อทำงานตาม handler ที่เรา register เอาไว้ใน main ฟังก์ชันนั่นเอง
การที่เรารู้ว่าทุกอย่างเริ่มที่ main ฟังก์ชัน แต่ก็ไม่จำเป็นว่าเราต้องไล่โค้ดเริ่มที่ main ฟังก์ชันเสมอไป เพราะโค้ดใน main ฟังก์ชันจะเป็นการ initialize ค่าเริ่มต้นของสิ่งต่างๆมากกว่าเป็น business logic จริงๆแล้วเราเป็นคนออกแบบ life cycle ของโปรแกรมเราอีกทีว่า จะทำงานอะไร เมื่อมีเงื่อนไขแบบไหน เช่นถ้าเราทำ Web API เราสนใจแค่การทำงานของ endpoint หนึ่ง เราก็ไม่จำเป็นต้องเริ่มที่ main ฟังก์ชันก็ได้ แต่ขอให้เรารู้ว่าสิ่งต่างๆถูก setup เริ่มต้นมาจาก main ก่อน ก่อนที่จะเริ่ม life cycle ในการรับ request แล้ว process response นั่นเอง
Top comments (1)
Good beginner article. Someone looking for an intro to Go without the code, see devopedia.org/go-language