อย่าเพิ่งใช้ fiber ถ้ายังไม่ได้อ่าน doc
Published: 2023-11-01 03:39:35

เนื่องจากก่อนหน้าที่ผมจะเคยได้ใช้ fiber ผมก็ลองใช้มาหลาย framework เช่น echo หรือ gin ซึ่งที่จริงก็ลองมาหลายตัว แต่จะใช้จริงจังอยู่ 2 ตัวนี้เป็นหลัก แต่หลังๆมาช่วง 2ปีมานี้ ผมเริ่มได้ยินว่ามีคนเริ่มใช้ fiber กันเยอะ ซึ่งแรกๆผมก็เข้าใจว่า ก็คงไม่ได้ต่างอะไรกับ framework อื่นๆมากนัก จนกระทั่งได้ลองไปอ่าน [docs](https://docs.gofiber.io/) มันดู ก็ได้สะดุดกับหัวข้อนึงที่เขาอธิบายเรื่อง `Zero Allocation` แล้วก็มีประโยคนี้

> Because fiber is optimized for high-performance, values returned from fiber.Ctx are not immutable by default and will be re-used across requests

ด้วยควาสงสัยก็เลยลองไปค้นข้อมูลดูก็พบว่าสิ่งนี้เคยมีคนไปเปิด issue ไว้เมื่อปี 2020 https://github.com/gofiber/fiber/issues/426

ความจริงมันไม่น่าจะเป็นปัญหา ถ้าเราทุกคนอ่าน document กันก่อน🤭

ผมยอมรับตรงๆว่า ผมก็ไม่ได้เอะใจว่าจะต้องมาอ่าน doc ก่อน เพราะคิดเอาเองว่ามันก็เหมือนๆกันนั่นแหล่ะ ทีนี้ก็เลยต้องมาอธิบายเรื่องที่เขาบอกว่าไอ้เจ้า fiber.Ctx มันไม่ `immutable by default` ว่ามันหมายความว่าอะไร

ซึ่งก็มีคนอธิบายเรื่องนี้ิอยู่บ้างเช่น https://www.meetgor.com/golang-mutable-immutable/

ทีนี้ถ้าจะให้สรุปสั้นๆง่ายๆก็จะประมาณว่า ถ้า data type ใดที่เราสามารถเปลี่ยนแปลงค่ามันได้ โดยไม่กระกบต่อการ allocate memory เลย มันคือ mutable ในทางกลับกัน ถ้าเราจะเปลี่ยนแปลงค่าใน data type แล้วมันต้องไปทำการ allocate memory ใหม่ มันก็จะเป็น immutable

ยกตัวอย่าง string ใน Go ก็เป็น immutable เช่นกัน ทุกครั้งที่เราแก้ไขค่าให้ตัวแปร string มันจะไป allocate mem ที่ใหม่เสมอ ลองดู code ชุดนี้

```go
s := "Hello"
fmt.Printf("%x %x\n", &s, unsafe.StringData(s))
s += " World"
fmt.Printf("%x %x\n", &s, unsafe.StringData(s))
```

เราจะทดลองประกาศตัวแปร s และกำหนดค่าเป็น "Hello" ไว้ แล้วเรามาลอง print address มันดู โดยเราจะพิมพ์ออกมาจาก &s ตัวหนึ่ง และใช้ `unsafe.StringData` มาช่วยอีกตัวหนึ่ง จากนั้นเราเพิ่มคำให้กับ `s` แล้วลองพิมพ์ address ออกมาอีกรอบ

```
14000028250 100b140fc
14000028250 1400000e190
```

ผลลัพธ์ก์จะประมาณนี้ โดยที่ `&s` จะได้ address เดิมออกมาทั้งสองครั้ง แต่พอใช้ `unsafe.StringData` มันจะได้ address ที่ไม่เหมือนกัน
เพราะว่า ที่จริงแล้ว string ใน Go มันคือ pointer ที่อ้างถึง array ของ byte โดย Russ Cox เคยเขียนอธิบายไว้ที่นี่ https://research.swtch.com/godata

นั่นทำให้เมื่อเราพิมพ์ `&s` ออกมา เราก็จะได้ address ของ s ซึ่งไม่ใช่ address ของข้อความจริงๆที่มันอ้างถึง และถ้าเราอยากจะรู้ address จริงๆของข้อความ เราก็เลยต้องใช้ `unsafe.StringData` มาช่วย โดย `StringData` จะคืน *byte ที่เป็น underlaying ของ s ออกมา

หรือถ้าต้องการทดสอบว่ามันมีการ allocate จริงๆ ก็ลองเขียน Benchmark เล่นๆดูแบบนี้

immutable.go
```go
func Immutable() {
	var s string
	for i := 0; i < 1000; i++ {
		s += "a"
	}
}

func Mutable() {
	var b [1000]byte
	for i := 0; i < 1000; i++ {
		b[i] = 'a'
	}
}
```

immutable_test.go
```go
func BenchmarkImmutable(b *testing.B) {
	for i := 0; i < b.N; i++ {
		Immutable()
	}
	b.ReportAllocs()
}
func BenchmarkMutable(b *testing.B) {
	for i := 0; i < b.N; i++ {
		Mutable()
	}
	b.ReportAllocs()
}
```

เวลารัน benchmark ให้ใช้คำสั่ง

> go test -bench=.

ทีนี้มันก็จะ report ออกมาประมาณนี้ (ข้อความยาว อาจจะต้อง scroll ไปทางขวา เพื่อดู)

```
BenchmarkImmutable-12              18628             57846 ns/op          530277 B/op        999 allocs/op
BenchmarkMutable-12              4029364               299.0 ns/op             0 B/op          0 allocs/op
```

เราจะเห็นที่ column `B/op` ว่าตอนที่เราใช้ string แล้วเพิ่มข้อความไปเรื่อยๆ มันจะเกิดการ allocate พื้นที่ใหม่ไป 999 ครั้งในแต่ละรอบ ตามจำนวนที่เราเปลี่ยนข้อความใน Immutable 1000 รอบ โดยในรอบแรกจะไม่ allocate

และพอเรามาเขียนด้วยการใช้ array ของ byte ใน `mutable` จะเห็นว่ามันไม่จำเป็นต้อง allocate เลย เนื่องจาก array เป็น mutable data type แล้วพอไปดูที่ความเร็วที่มันทำได้ เราก็จะเห็นว่า เมื่อเราใช้ mutable data type ความเร็วมันจะเร็วกว่ามาก(ดูที่ค่า ns/op) นั่นคือเหตุผลว่าทำไม fiber ถึงทำความเร็วได้ดีกว่า framework อื่น

แต่มันก็ต้องมีสิ่งที่ต้องแลกถ้าเราอยากได้ความเร็ว คือ ถ้าเราเผลอเอาค่าจาก fiber.Ctx ไปใช้นอก handler โดยเฉพาะใน Goroutine ทดสอบง่ายๆด้วยการเขียนตาม issue ที่เขาเปิดไว้(สามารถทดลองได้จากที่นี่ https://github.com/pallat/fibertest) ซึ่ง fiber เองก็เขียนวิธีแก้เอาไว้แล้ว (อ่านใน docs)

หวังว่าข้อความนี้จะเป็นประโยชน์ในการเลือกใช้ framework นะครับ