สร้าง API ให้ใช้งานง่ายด้วยหลักการของ Functional Options

Nuttavut Thongjor

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

สมมติเราต้องการออกแบบ API สำหรับข้อมูลบ้านในโฉนดที่ดินภายใต้ชื่อของ NewHouse โดยกำหนดให้บ้านทุกหลังจำเป็นต้องมีเลขที่บ้าน/ที่อยู่ ฟังก์ชัน NewHouse จึงมีหน้าตาแบบนี้

Go
1type House struct {
2
3}
4
5func NewHouse(address string) *House

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

Go
1func NewHouse(address string, doors uint, floors uint) *House

แม้เด็กอนุบาลก็ยังทราบ เมื่อกำหนดให้ฟังก์ชันสามารถรับอาร์กิวเมนต์ได้หลายค่าย่อมเป็นภาระกับผู้เรียกใช้ จะเรียก NewHouse ซักครั้งต้องมานั่งกังวลกับการใส่ลำดับของ address, doors หรือ floors ให้ถูกต้อง แถมกลับมาอ่านอีกครั้งก็ยังเข้าใจยากอีกว่าแต่ละตำแหน่งที่ส่งไปในฟังก์ชันคือค่าของสิ่งใด

Go
1// 3 คือค่าของ doors หรือ floors? แล้ว 4 ละ?
2NewHouse("111/11", 3, 4)

การออกแบบ API ที่ดีควรสร้างให้ฟังก์ชัน NewHouse เป็นอย่างไรกันแน่ ถึงจะง่ายต่อการอ่านและใช้งาน? เราจะแก้ไขปัญหานี้กับหลักการของ Functional Options

ปัญหาของการสร้าง Configuration Struct

แน่นอนว่า NewHouse ของเราอาจไม่ได้มีแค่ address, doors หรือ floors ที่สามารถระบุค่าได้ หากแต่ยังต้องมี ชื่อบ้าน (name), ขนาดที่ดิน (area), เจ้าของ (owner) และค่าอื่น ๆ อีกมาก ถ้าจะให้ระบุค่าเหล่านี้เรียงตับตอนส่งผ่านฟังก์ชัน NewHouse มันต้องลำบากเป็นแน่

เอาใหม่ ๆ เราทราบอยู่แล้วว่า address ทุกบ้านนั้นต้องมีก็ให้ระบุเป็นพารามิเตอร์ตัวแรกเช่นเดิม เพิ่มเติมคือค่าอื่น ๆ ให้โยนลงไปใน struct ชื่อ Config แทน จากนั้นจึงทำการสร้างตัวแปรจาก struct นี้พร้อมกำหนดค่าแล้วจึงส่งเป็นพารามิเตอร์ตัวที่สองของ NewHouse

Go
1type Config struct {
2 Doors uint
3 Floors uint
4 Name string
5 Area float32
6 Owner string
7}
8
9func NewHouse(address string, config *Config) *House

ตัวอย่างการเรียกใช้ NewHouse เช่น

Go
1house := NewHouse("111/11", &Config{
2 Doors: 3,
3 Floors: 4,
4 Name: "Somchai's House",
5 Area: 59.99,
6 Owner: "Somchai",
7})

วิธีการนี้ก็ดูเหมือนจะดีกว่าการไล่ส่งค่าทีละตัวลงฟังก์ชัน แต่จุดบอดของวิธีนี้ยังพบได้อยู่ 3 อย่าง

สิ่งแรกคือทุกครั้งที่ต้องการเพิ่มค่าสำหรับการสร้างบ้านก็ไม่พ้นที่จะต้องแก้ไข Config นั่นทำให้ struct นี้นับวันมีแต่จะใหญ่ขึ้นเรื่อย ๆ

ถัดมาคือหากเราต้องการกำหนดให้ house แทนบ้านของหมู่บ้านหนึ่งซึ่งแน่นอนว่าส่วนใหญ่จะมีจำนวนที่ดิน ประตูและชั้นเท่า ๆ กัน เราจึงต้องการให้การสร้าง house ผ่าน NewHouse ไม่ต้องทำการระบุค่า Config เมื่อไม่ระบุค่านี้ขอให้ใช้ค่า default ของแบบบ้านในหมู่บ้านนี้ อย่างไรก็ตามเราไม่สามารถละการส่งค่า Config ไปได้ เนื่องจากฟังก์ชัน NewHouse ต้องระบุค่า Config ลงไปเป็นอาร์กิวเมนต์ตัวที่สองเสมอ สิ่งที่เราทำได้จึงเป็นการส่ง nil ไปแทน เพื่อบอก NewHouse ให้ใช้ค่า default

Go
1house := NewHouse("111/11", nil)

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

Go
1house := NewHouse("111/11")

ประการสุดท้ายเราจะสังเกตได้ว่าในหลาย ๆ กรณีชื่อของบ้าน (name) มักใช้ชื่อเป็นชื่อของเจ้าของบ้าน (owner) แทน เมื่อเป็นเช่นนี้เราจึงต้องการให้ Config อนุญาตให้เราละเว้นการส่ง name ได้ เมื่อไหร่ที่ไม่มีค่า name ส่งมาให้ถือเอาชื่อเจ้าของบ้านเป็นชื่อบ้านแทน

Go
1// คาดหวังว่า name จะเป็น Somchai's House
2house := NewHouse("111/11", &Config{
3 Doors: 3,
4 Floors: 4,
5 Area: 59.99,
6 Owner: "Somchai"
7})

โดยทั่วไปโค้ดของ NewHouse อาจเป็นเพียงการกำหนดค่าจาก Config ใส่ลงไปเพื่อสร้าง house ก็ได้

Go
1func NewHouse(address string, config *Config) *House {
2 house := House{
3 Address: address,
4 Doors: config.Doors,
5 // ค่าอื่น ๆ จาก config
6 }
7
8 if house.Name == "" {
9 house.Name = config.Owner + " 's House"
10 }
11
12 return &house
13}

ตามหลักการ Zero Values ในภาษา Go เมื่อเราไม่ระบุค่า name ผ่าน Config name จึงมีค่าเป็น "" ไม่ใช่ค่า nil ครั้นเราจะตรวจสอบว่า name มีค่าเป็น "" หรือไม่ หากมีค่าเป็น "" ให้กำหนดค่า name เป็นชื่อเจ้าของ เช่นนี้ก็ทำไม่ได้ นั่นเพราะกรณีที่บ้านยังไม่ถูกขายส่วนของ Config ต้องระบุค่า name เป็นค่า "" เข้ามาตรง ๆ เพื่อเป็นการบอกให้บ้านนี้ไม่มีชื่อ (ไม่ใช่ให้ตั้งค่า default เป็นชื่อของเจ้าของบ้าน) แต่เงื่อนไขบรรทัดที่ 8 ของเราดันเช็คค่า Name จาก "" ชื่อของบ้านจึงกลายเป็น 's House' แทน

แก้ปัญหาการส่ง nil ด้วย Variadic Functions

ปัญหาการส่ง nil เพื่อบอกให้ฟังก์ชันใช้ค่า default แทนนั้นสามารถแก้ได้ด้วย Variadic Functions

Go
1house := NewHouse("111/11", nil)

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

Go
1func NewHouse(address string, config ...*Config) *House
2
3// จะส่งแบบนี้
4house := NewHouse("111/11")
5
6// หรือส่งแบบนี้ก็ย่อมได้
7house := NewHouse("111/11", &Config{
8 Doors: 3,
9 Floors: 4,
10 Name: "Somchai's House",
11 Area: 59.99,
12 Owner: "Somchai",
13})

Functional Options คืออะไร

ย้อนกลับไปดูปัญหาแรกที่บอกว่าการมี Config เดียวทำให้ทุกครั้งที่ต้องการเพิ่มออฟชั่นให้กับบ้านต้องกระทำผ่าน Config เสมอ ทำให้ struct นี้นับวันมีแต่จะใหญ่ขึ้น หากเราจะแก้ปัญหานี้ด้วยการแยกเป็น struct ย่อย ๆ คือ struct สำหรับการตั้งค่า Doors ในชื่อของ DoorsConfig หรือ struct ชื่อ FloorsConfig สำหรับการตั้งค่า Floors ก็ย่อยทำได้ เพียงแต่ปัญหาสุดท้ายที่เราทิ้งไว้คือการกำหนดค่า default (เช่น ไม่ระบุค่า name ให้ใช้ชื่อของ owner แทน) ก็ยังไม่หมดไปอยู่ดี

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

Go
1type House struct {
2 Address string
3 Doors uint
4 Floors uint
5 Name string
6 Area float32
7 Owner string
8}
9
10type HouseOption func(*House)
11
12func NewHouse(address string, options ...HouseOption) *House
13
14func WithFloors(floors uint) HouseOption {
15 return func(h *House) {
16 h.Floors = floors
17 }
18}
19
20// ฟังก์ชันสำหรับการตั้งค่าอื่น ๆ กระทำเหมือนกัน
21
22// เรียกใช้งาน
23func main() {
24 house := NewHouse("111/11", WithFloors(3))
25}

จากบรรทัดที่ 24 พบว่าเราไม่มี struct ชื่อ Config อีกต่อไป แต่เราทำการสร้างฟังก์ชันชื่อ WithFloors สำหรับการตั้งค่าจำนวนชั้นขึ้นมาแทน ฟังก์ชันนี้จะทำการคืนฟังก์ชันที่รับค่า *House เข้ามาเพื่อใช้ในการเปลี่ยนแปลงค่า Floors ของบ้าน (บรรทัดที่ 16) อีกทีนึง กรณีของค่าอื่นที่ไม่ได้ระบุ ให้ฟังก์ชัน NewHouse เป็นผู้จัดการกำหนดค่า default ให้เอง

ด้วยความที่ฟังก์ชัน WithFloors เป็นตัวกำหนดค่าของออฟชั่น (ในที่นี้คือจำนวนชั้น) เราจึงเรียกเทคนิคนี้ว่า Functional Options

เพื่อให้ NewHouse สามารถกำหนดค่า default และนำค่าจาก HouseOption ต่าง ๆ เช่น Floors มาใช้งานได้ เราจึงทำการเขียนโปรแกรมดังต่อไปนี้

Go
1func NewHouse(address string, options ...HouseOption) *House {
2 // กำหนดค่าเริ่มต้น
3 house := House{
4 Address: address,
5 Doors: 3,
6 Floors: 3,
7 }
8
9 for _, option := range options {
10 // กำหนดออฟชั่นด้วยการส่ง house ไปให้
11 option(&house)
12 }
13
14 return &house
15}

บรรทัดที่ 9-12 เราทำการวนลูปเพื่อส่งค่าของ house เข้าไปใช้ในการกำหนดออฟชั่น เช่นส่งเข้าไปในฟังก์ชันที่คืนจาก WithFloors เพื่อกำหนดค่าจำนวนชั้น เป็นต้น

ต่อไปเราจะสร้างฟังก์ชัน WithOwner สำหรับกำหนดค่าของ name ให้กับบ้าน หากไม่มีการระบุค่าของ name เข้ามาให้ถือเอาชื่อของเจ้าของบ้านมาเป็นชื่อบ้านแทน

Go
1func WithOwner(owner string) HouseOption {
2 return func(h *House) {
3 h.Owner = owner
4
5 // หากบ้านนี้ไม่มีชื่อ ให้กำหนดค่าเริ่มต้นเป็นชื่อของเจ้าของบ้าน
6 if h.Name == "" {
7 h.Name = h.Owner + "'s House"
8 }
9 }
10}

การสร้างออฟชั่นผ่านฟังก์ชันนั้นยืดหยุ่นกว่า เราจึงสามารถแยกการทำงานที่ซับซ้อนออกไปในแต่ละฟังก์ชันได้แทนที่จะไปเขียนรวมกันใน NewHouse

กรณีของ Name ขึ้นตรงอยู่กับชื่อของเจ้าของบ้าน เราจึงย้ายส่วนการกำหนดชื่อนี้ไปไว้ที่ WithOwner ปัญหาของการกำหนดชื่อที่เคยพูดถึงมาก็จะหมดไป นั้นเพราะเราจะเรียกใช้ WithOwner กรณีที่มีเจ้าของบ้านแล้วเท่านั้น หากยังไม่มีเจ้าของบ้านก็จะไม่เรียก เป็นผลทำให้ชื่อของบ้านซึ่งถูกกำหนดใน WithOwner ไม่ถูกตั้งค่าตามไปด้วย

Go
1func main() {
2 // ยังไม่มีเจ้าของบ้าน ชื่อจะยังไม่ถูกตั้งด้วยเช่นกัน
3 NewHouse("111/11")
4
5 // มีเจ้าของบ้านแล้ว ชื่อจะถูกตั้งตามเจ้าของบ้าน
6 NewHouse("111/12", WithOwner("Somchai"))
7}

การจัดการ Options ที่ซับซ้อนด้วย Functional Options

แม้ว่าการกำหนดค่าออฟชั่นผ่าน struct อย่าง Config จะมีรูปแบบการประกาศและใช้งานที่ง่ายกว่าในกรณีที่มีออฟชั่นน้อย แต่ Functional Options นั้นยืดหยุ่นกว่าในแง่ของการทำงานกับออฟชั่นที่ซับซ้อน

สมมติให้ Owner ของบ้านไม่เก็บค่าเป็น string อีกต่อไป หากแต่เก็บเป็นค่าจาก struct คือ Owner เราสามารถทำการสร้างและกำหนดค่าจาก struct ดังกล่าว พร้อมกำหนดค่าของ owner และค่าเริ่มต้นของชื่อบ้านผ่าน WithOwner ได้เช่นเดิม ดังนี้

Go
1type Owner struct {
2 Name string
3 Age uint
4}
5
6func WithOwner(name string, age uint) HouseOption {
7 return func(h *House) {
8 h.Owner = &HouseOwner{Name: name, Age: age}
9
10 if h.Name == "" {
11 h.Name = h.Owner.Name + "'s House"
12 }
13 }
14}
15
16// เรียกใช้งาน
17func main() {
18 house := NewHouse(
19 "111/11",
20 WithOwner(&Owner{ name: "Somchai", age: 24 }),
21 )
22}

หากเราไม่ได้ใช้ Functional Options แต่ใช้ struct คือ Config แทน การกำหนดค่าเริ่มต้นที่ซับซ้อนของ name จะต้องผลักภาระไปให้ NewHouse แทน นั่นแปลว่า NewHouse จะกลายเป็นฟังก์ชันที่มีอีกหนึ่งหน้าที่คือต้องจัดการออฟชั่นด้วยนั่นเอง

Go
1type Config struct {
2 Doors uint
3 Floors uint
4 Name string
5 Area float32
6 Owner *Owner
7}
8
9func NewHouse(address string, config *Config) *House {
10 // กำหนดค่าเริ่มต้น
11 house := House{
12 Address: address,
13 // กำหนดค่าอื่น ๆ จาก config
14 }
15
16 // เขียนโค้ดเพื่อเพิ่ม config.Owner.Name ให้เป็นค่าของ house.Name
17 // ในกรณีที่ house.Name ไม่มีค่า
18
19 return &house
20}
21
22// เรียกใช้งาน
23func main() {
24 house := NewHouse(
25 "111/11",
26 &Config{Owner: &Owner{ name: "Somchai", age: 24 }},
27 )
28}

รูปแบบของการกำหนด Presets

ฟังก์ชัน NewHouse ข้างต้นพบว่ามีการกำหนดค่าเริ่มต้นของบ้านไว้ภายใต้ตัวมันเอง

Go
1func NewHouse(address string, options ...HouseOption) *House {
2 // กำหนดค่าเริ่มต้น
3 house := House{
4 Address: address,
5 Doors: 3,
6 Floors: 3,
7 }
8
9 for _, option := range options {
10 option(&house)
11 }
12
13 return &house
14}

เพื่อให้เกิดความยืดหยุ่นมากขึ้นเราจึงแยกโค้ดดังกล่าวออกมาเป็นค่าเริ่มต้นภายใต้ชื่อ DefaultPreset

Go
1var DefaultPreset = []HouseOption{WithFloors(3), WithDoors(3)}
2
3func NewHouse(address string, options ...HouseOption) *House {
4 house := House{
5 Address: address,
6 }
7
8 // กำหนดค่าเริ่มต้นจาก DefaultPreset
9 options = append([]HouseOption{DefaultPreset}, options...)
10
11 for _, option := range options {
12 option(&house)
13 }
14
15 return &house
16}

[]HouseOption จากบรรทัดที่ 1 และ 9 ทำให้โค้ดของเราดูอ่านยาก เราจึงควรสร้างฟังก์ชัน Options เพื่อดำเนินการกับออฟชั่นเหล่านี้แทนที่จะมาประกาศเป็น slice

Go
1func Options(options ...HouseOption) HouseOption {
2 return func(h *House) {
3 for _, option := range options {
4 option(h)
5 }
6 }
7}
8
9var DefaultPreset = Options(WithFloors(3), WithDoors(3))
10
11func NewHouse(address string, options ...HouseOption) *House {
12 house := House{
13 Address: address,
14 }
15
16 // กำหนดค่าเริ่มต้นจาก DefaultPreset
17 DefaultPreset(&house)
18
19 for _, option := range options {
20 option(&house)
21 }
22
23 return &house
24}

ตัวแปร DefaultPreset ที่เราสร้างไว้ล่วงหน้าเพื่อการเรียกใช้งานนี้เราเรียกว่า Presets เราสามารถใช้ Presets เพื่อกำหนดกลุ่มของออฟชั่นไว้ก่อนได้ เช่น เราทราบอยู่แล้วว่าคอนโดทั่วไปมักมี 1 ชั้น 3 ประตู เราจึงสร้าง CondoPreset ขึ้นมาดังนี้

Go
1var CondoPreset = Options(
2 WithFloors(1),
3 WithDoors(3),
4)
5
6// เรียกใช้งาน
7func main() {
8 house := NewHouse(
9 "111/11",
10 CondoPreset,
11 WithOwner("Somchai", 24),
12 )
13}

บรรทัดที่ 10 เราทำการส่ง CondoPreset ไปยังฟังก์ชัน เป็นผลให้เกิดการตั้งค่าชั้นเป็น 1 และจำนวนประตูเป็น 3 ผู้อ่านจะสังเกตเห็นได้ว่าเรายังสามารถกำหนดค่าอื่นเป็นออฟชั่นเพิ่มได้อีก เช่น บรรทัดที่ 11 ที่เป็นการเพิ่ม Owner ให้กับบ้านดังกล่าว

รวมกลุ่ม API ด้วย Package

เพื่อให้การสร้างบ้านของเรามีความเป็น API มากขึ้น เราจึงรวมกลุ่มของสิ่งที่สร้างภายใต้แพคเกจคือ house

Go
1package house
2
3type HouseOwner struct {
4 Name string
5 Age uint
6}
7
8type House struct {
9 Address string
10 Doors uint
11 Floors uint
12 Name string
13 Area float32
14 Owner *HouseOwner
15}
16
17type HouseOption func(*House)
18
19func Options(options ...HouseOption) HouseOption {
20 return func(h *House) {
21 for _, option := range options {
22 option(h)
23 }
24 }
25}
26
27func Floors(floors uint) HouseOption {
28 return func(h *House) {
29 h.Floors = floors
30 }
31}
32
33func Doors(floors uint) HouseOption {
34 return func(h *House) {
35 h.Floors = floors
36 }
37}
38
39func Owner(name string, age uint) HouseOption {
40 return func(h *House) {
41 h.Owner = &HouseOwner{Name: name, Age: age}
42
43 if h.Name == "" {
44 h.Name = h.Owner.Name + "'s House"
45 }
46 }
47}
48
49var (
50 DefaultPreset = Options(Floors(3), Doors(3))
51 CondoPreset = Options(Floors(1), Doors(3))
52)
53
54func New(address string, options ...HouseOption) *House {
55 house := House{
56 Address: address,
57 }
58
59 DefaultPreset(&house)
60
61 for _, option := range options {
62 option(&house)
63 }
64
65 return &house
66}

ส่วนของการเรียกใช้งานใหม่เป็นดังนี้

Go
1package main
2
3import "house"
4
5func main() {
6 house.New("111/11", house.CondoPreset, house.Owner("Somchai", 24))
7}

Functional Options กับการสร้างเอกสาร

นอกจากการใช้ Functional Options จะช่วยให้ API ของเรายืดหยุ่นต่อการเรียกใช้งานแล้ว เวลาเราใช้ godoc ในการสร้างเอกสาร ภาษา go จะทำการรวมกลุ่มออฟชั่นเข้าด้วยกันทำให้การอ่านเอกสารประกอบการใช้งานนั้นง่ายขึ้น

Godoc

เมื่อไหร่ควรใช้ Functional Options

โดยทั่วไปการระบุค่าออฟชั่นผ่าน struct เช่น Config ก็เพียงพอแล้ว แต่เมื่อใดที่เราต้องการละเว้นการระบุออฟชั่นบางค่า แต่ไม่ต้องการให้ API เข้าใจว่าออฟชั่นดังกล่าวมีค่าเป็น zero values ของมัน Functional Options จะช่วยแก้ไขปัญหานี้

กำหนดให้แพคเกจ server มี Config ที่สามารถระบุค่า Timeout และ Port เป็นออฟชั่นตอนสร้างเซิฟเวอร์

Go
1package server
2
3type Config struct {
4 Timeout time.Duration
5 Port int
6}
7
8func new(addr string, config *Config) *Server

เมื่อเราทำการเรียกใช้งานโดยระบุเพียง Timeout ลงไปใน Config จะทำให้ Port มีค่าเป็น 0 ตามหลักการ zero values แม้ความเป็นจริงค่า 0 ของ Port ควรหมายถึงให้ทำการสุ่มพอร์ตก็ตาม แต่ API จะแยกแยะไม่ได้ว่า 0 ที่ส่งผ่าน Config นั้นเป็นค่าที่ผู้เรียกใช้งานระบุเข้ามา หรือเป็นเพียง zero values ของตัว Port เอง

Go
1server.New("localhost", &server.Config{Timeout: 100 * time.Second})

ปัญหาดังกล่าวแก้ได้ด้วย Functional Options ตามที่กล่าวมาแล้ว นอกจากนี้ Functional Options ยังเหมาะกับการแยกความซับซ้อนของแต่ละออฟชั่นออกจากกันผ่านการแบ่งแยกฟังก์ชันอีกด้วย

เอกสารอ้างอิง

Dave Cheney (2014). Functional options for friendly APIs. Retrieved July, 14, 2020, from https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis

Soham Kamani (2019). Functional Options in Go: Implementing the Options Pattern in Golang. Retrieved July, 14, 2020, from https://www.sohamkamani.com/golang/options-pattern/

Márk Sági-Kazár (2020). Functional options on steroids. Retrieved July, 14, 2020, from https://sagikazarmark.hu/blog/functional-options-on-steroids/

Rob Pike (2014). Self-referential functions and the design of options. Retrieved July, 14, 2020, from https://commandcenter.blogspot.com/2014/01/self-referential-functions-and-design.html

สารบัญ

สารบัญ

  • ปัญหาของการสร้าง Configuration Struct
  • แก้ปัญหาการส่ง nil ด้วย Variadic Functions
  • Functional Options คืออะไร
  • การจัดการ Options ที่ซับซ้อนด้วย Functional Options
  • รูปแบบของการกำหนด Presets
  • รวมกลุ่ม API ด้วย Package
  • Functional Options กับการสร้างเอกสาร
  • เมื่อไหร่ควรใช้ Functional Options
  • เอกสารอ้างอิง