Go program start from main function and stop when main function exit
Tags: go
Published: 2021-07-15 01:07:30

โปรแกรมที่เราเขียน ไม่ว่าจะเขียนด้วยภาษาอะไร มันจะมีวงจรชีวิต (life cycle) ของมันว่าจะเริ่มทำงานที่จุดไหน และ สิ้นสุดที่ตรงไหน เสมอ

ถ้าเราเข้าใจ life cycle ของโปรแกรม ของภาษาที่เราใช้ หรือ เข้าใจ life cycle ของ runtime ที่เราใช้ เราก็จะไล่การทำงานของโปรแกรมได้ง่ายขึ้นเยอะเลย

อย่างโปรแกรมที่เขียนด้วย Go นั้นจะเริ่มทำงานที่ฟังก์ชัน main ใน package main และจะหยุดทำงานเมื่อ main ฟังก์ชันจบการทำงาน ไม่ว่าจะด้วยการ return ปกติ, การเรียก os.Exit ฟังก์ชัน หรือโดน signal จาก OS ให้หยุดทำงาน

ตัวอย่างเช่นโค้ดแบบนี้

```go
package main

import (
	"fmt"
)

func main() {
	fmt.Println("Hello, playground")
}
```

เมื่อเรารันก็ได้ข้อความ `Hello, playground` ในออกไปที่ standard output แต่ถ้าเราเปลี่ยนตรง `fmt.Println` ให้ไปไปรันในอีก goroutine ล่ะ แบบนี้

```go
package main

import (
	"fmt"
)

func main() {
	go func() {
		fmt.Println("Hello, playground")
	}()
}
```

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

ถ้าอยากให้ main รอให้ goroutine ที่แยกไปทำงานเสร็จก่อนก็ทำได้แบบนี้

```go
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 ง่ายๆแบบนี้

```go
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 แบบนี้อยู่

```go
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 นั่นเอง