Go Generics คืออะไร? เรียนรู้การใช้งาน Generics ผ่าน Type Parameters ในภาษา Go (Golang)

Nuttavut Thongjor

Generics ฟีเจอร์ใหม่ของภาษา Go ที่เพิ่มเข้ามาในเวอร์ชั่น 1.18 ช่วยทำให้ชีวิตมีสีสันมากขึ้น ด้วยการอนุญาตให้ฟังก์ชันใด ๆ สามารถรับพารามิเตอร์เป็นชนิดข้อมูลที่แตกต่างกันได้ ไม่เฉพาะฟังก์ชันเท่านั้นนะเยาวรุ่น Generics ยังสามารถใช้ควบคู่กับการนิยามชนิดข้อมูลใหม่ที่เข้ากันได้กับชนิดข้อมูลใด ๆ ตามที่กำหนด ทั้งหมดนี้คือสิ่งที่เราจะพูดถึงกันในหัวข้อของ Go Generics ในบทความนี้นั่นเอง

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

กาลครั้งหนึ่งครั้น Generics ยังไม่อุบัติ

สมมติเรามี struct ชื่อ Article ดังนี้

Go
1type Article struct {
2 Title string
3 Content string
4}

เมื่อกำหนดให้ Article มีเมธอดชื่อ Slug ที่ทำการคืนค่า Title แบบไร้ช่องว่างทั้งด้านหน้าและหลังของคำ รวมถึงทำการแทนที่ช่องว่างระหว่างคำด้วยเครื่องหมาย -

Go
1func (a Article) Slug() string {
2 r := regexp.MustCompile(" +")
3 slug := strings.ToLower(a.Title)
4 slug = strings.TrimSpace(slug)
5
6 return r.ReplaceAllString(slug, "-")
7}

ท้ายที่สุดเราอยากได้ฟังก์ชันชื่อ SlugifyAll ที่สามารถส่ง slice ของ Article เพื่อทำการแปลง slice ดังกล่าวกลับคืนมาด้วยรูปแบบของ []string เมื่อแต่ละช่องของ slice ผลลัพธ์เกิดจากการเรียกเมธอด Slugify ของแต่ละ Article ใน slice

Go
1func SlugifyAll(s []Article) []string {
2 r := make([]string, len(s))
3
4 for i, v := range s {
5 r[i] = v.Slug()
6 }
7
8 return r
9}

และนี่คือวิธีการเรียกใช้ฟังก์ชัน SlugifyAll จาก main

Go
1func main() {
2 r := SlugifyAll([]Article{
3 {Title: " Lorem Ip sum "},
4 {Title: "Babel Coder"},
5 })
6
7 fmt.Println(r)
8}

เพอร์เฟค! ทุกอย่างดูสมบูรณ์แบบจนกระทั่งการมาของชนิดข้อมูลใหม่... Person

Go
1type Person struct {
2 FirstName string
3 LastName string
4}
5
6func (p Person) Slug() string {
7 r := regexp.MustCompile(" +")
8 slug := strings.ToLower(p.FirstName + " " + p.LastName)
9 slug = strings.TrimSpace(slug)
10
11 return r.ReplaceAllString(slug, "-")
12}

หากอยู่ดี ๆ เราก็กระเหี้ยนกระหือรืออยากใช้งานฟังก์ชัน SlugifyAll แต่เปลี่ยนจากที่ส่ง []Article ไปเป็น []Person แบบนี้จะทำยังไงดี?

แน่นอนว่าอยู่ดี ๆ เราคงส่ง []Person ไปยัง SlugifyAll ไม่ได้ เพราะฟังก์ชัน SlugifyAll นั้นรับ []Article ไม่ใช่ []Person

Go
1func SlugifyAll(s []Article) []string

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

Go
1func SlugifyAllArticle(s []Article) []string {
2 r := make([]string, len(s))
3
4 for i, v := range s {
5 r[i] = v.Slug()
6 }
7
8 return r
9}
10
11func SlugifyAllPerson(s []Person) []string {
12 r := make([]string, len(s))
13
14 for i, v := range s {
15 r[i] = v.Slug()
16 }
17
18 return r
19}

ชีวิตดูบัดซบเกินให้อภัยปัญหาง่าย ๆ แค่นี้กลับต้องเบิ้ลบรรทัดเป็นสองเท่าเลย

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

Go
1func SlugifyAll(s []Slugger) []string {
2 r := make([]string, len(s))
3
4 for i, v := range s {
5 r[i] = v.Slug()
6 }
7
8 return r
9}

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

Go
1func main() {
2 // cannot use ([]Article literal) (value of type []Article)
3 // as []Slugger value in argument to SlugifyAll
4 r := SlugifyAll([]Article{
5 {Title: " Lorem Ip sum "},
6 {Title: "Babel Coder"},
7 })
8
9 fmt.Println(r)
10}

เพื่อให้สามารถกลับมาเรียกใช้ฟังก์ชัน Sluggify ได้อีกครั้ง เราต้องทำการแก้ไขส่วนของ main ด้วยการแปลง []Article เป็น []Slugger ดังนี้

Go
1func main() {
2 r := SlugifyAll([]Slugger{
3 Article{Title: " Lorem Ip sum "},
4 Article{Title: "Babel Coder"},
5 })
6
7 fmt.Println(r)
8}

คำถามคือเราควรประกาศฟังก์ชันในส่วนของพารามิเตอร์อย่างไรดี เพื่อให้สามารถส่งได้ทั้งสามรูปแบบ ไม่ว่าจะเป็น []Article, []Person หรือ []Slugger ก็ตาม

Go
1// ทั้งสามรูปแบบต้องส่งไปให้ฟังก์ชัน SluggifyAll ได้ทั้งหมด
2SlugifyAll([]Slugger{
3 Article{Title: " Lorem Ip sum "},
4 Person{FirstName: "Somchai", LastName: "Haha"},
5})
6
7SlugifyAll([]Article{
8 {Title: " Lorem Ip sum "},
9 {Title: "Babel Coder"},
10})
11
12SlugifyAll([]Person{
13 {FirstName: "Somchai", LastName: "Haha"},
14 {FirstName: "Somset", LastName: "Haha"},
15})

ชนิดข้อมูล any

ก่อนหน้านี้เรามีอีกวิธีหนึ่งในการแก้ปัญหาด้วยการใช้ interface{} เมื่อต้องการให้ SlugifyAll รองรับชนิดข้อมูลใด ๆ เราจึงสามารถเปลี่ยนรูปแบบฟังก์ชันได้ดังนี้

Go
1func SlugifyAll(s interface{}) []string {
2 // ...
3}

Go เวอร์ชัน 1.18 เราสามารถใช้ any แทน interface{} ได้

Go
1func SlugifyAll(s any) []string {
2 // ...
3}

สิ่งที่เราสังเกตได้จากการแปลงฟังก์ชันชุดนี้ก็คือเราต้องใช้ interface{} หรือ any เพื่อแทนความเป็นไปได้ทั้งหมดของค่าที่ส่งมาเป็นพารามิเตอร์ ด้วยรูปแบบเช่นที่ว่า การเรียกใช้งานสามรูปแบบข้างต้นสามารถส่งผ่านฟังก์ชัน SluggifyAll ได้หมด

Go
1SlugifyAll([]Slugger{
2 Article{Title: " Lorem Ip sum "},
3 Person{FirstName: "Somchai", LastName: "Haha"},
4})
5
6SlugifyAll([]Article{
7 {Title: " Lorem Ip sum "},
8 {Title: "Babel Coder"},
9})
10
11SlugifyAll([]Person{
12 {FirstName: "Somchai", LastName: "Haha"},
13 {FirstName: "Somset", LastName: "Haha"},
14})

ปัญหาเก่าได้รับการแก้ไขแล้วแต่ปัญหาใหม่ดันเกิดขึ้นแทน จริงอยู่ว่าเราใช้ interface{} หรือ any ในส่วนของพารามิเตอร์ได้ แต่การใช้ Syntax เช่นที่ว่าไม่ได้สื่อความเลยซักนิดว่าฟังก์ชันนี้รับเฉพาะชนิดข้อมูลแบบ slice ครั้นจะประกาศชนิดข้อมูลของพารามิเตอร์เป็น []interface{} หรือ []any ก็ไม่สามารถทำได้ ด้วยข้อจำกัดของโครงสร้างการจัดเก็บข้อมูลที่แตกต่างกัน

Go
1func SlugifyAll(s []interface{}) []string {
2 //...
3}
4
5func main() {
6 a := Article{Title: " Lorem Ip sum "}
7 // cannot use ([]Article literal) (value of type []Article)
8 // as []interface{} value in argument to SlugifyAll
9 fmt.Println(SlugifyAll([]Article{a}))
10}

Generics และ Type Parameters ในภาษา Go 1.18

Go 1.18 มาพร้อมกับฟีเจอร์ Generics ที่มีการใช้งานอยู่แล้วในหลากหลายภาษา Generics เป็นความสามารถในการกำหนดสิ่งที่เรียกว่า Type Parameters เมื่อค่านี้ถูกกำหนด คอมไพเลอร์จะตรัสรู้ว่า Type Parameters แท้จริงแล้วควรเป็นชนิดข้อมูลใดก็ต่อเมื่อมีการกำหนด Type Arguments

เมื่อ Generics ถูกนำมาใช้ควบคู่กับฟังก์ชัน เราจะเรียกฟังก์ชันนั้นว่า Generic Functions ตามนิยามข้างต้นเมื่อฟังก์ชันเป็น Generics เราจึงสามารถกำหนด Type Parameters ในความหมายที่ว่าพารามิเตอร์ของฟังก์ชันอนุญาตให้เป็นชนิดข้อมูลใดได้บ้าง

ต่อไปนี้คือรูปแบบ Generic Function ของฟังก์ชัน SluggifyAll ที่อนุญาตให้ส่งพารามิเตอร์ s เป็น slice ของชนิดข้อมูลใดก็ได้

Go
1func SluggifyAll[T any](s []T) []string {
2 // ...
3}

ส่วนของค่า T ที่ปรากฎเรียกว่า Type Parameters เป็นส่วนนิยามคุณสมบัติของชนิดข้อมูลที่เป็นไปได้ทั้งหมด (constraints) Type Paramers จะถูกกำหนดในส่วนของ Type Paramerter List ที่อยู่ในขอบเขตของ [ และ ] หลังชื่อฟังก์ชัน จากตัวอย่างของฟังก์ชัน SluggifyAll เรากล่าวได้ว่า ส่วนของ Type Paramerter List คือ [T any] โดยมีค่า T เป็น Type Parameter ที่กำหนด constraint เป็น any ในความหมายที่ว่า T เข้าคู่กับชนิดข้อมูลใดก็ได้ เมื่อ T ถูกกำหนดความสัมพันธ์เป็น any ส่วนของพารามิเตอร์ s ที่อ้างอิงถึง T แบบ []T จึงหมายถึงพารามิเตอร์นี้รับชนิดข้อมูลใดก็ได้ขอเพียงแค่เป็น slice โดยไม่สนใจว่าจะเป็น slice ของชนิดข้อมูลใด

การใช้ Generic Functions เพื่อนิยาม SluggifyAll ของเราในครั้งนี้แก้ปัญหาก่อนหน้านี้ได้ ตอนนี้เราสามารถส่งค่า []Article, []Person หรือ []Slugger ผ่านฟังก์ชันได้ทั้งหมด อีกทั้งเมื่อ่านรูปแบบของฟังก์ชันก็ทราบได้ทันทีว่า s ต้องเป็น slice

ทุกอย่างเหมือนจะดีแต่ฉากจบนั้นไม่สวย แม้ s จะเป็น slice แต่เพราะมันเป็น slice ของชนิดข้อมูลใดก็ได้ นั่นจึงไม่อาจทำให้ Go เชื่อใจได้ว่าชนิดข้อมูล T ที่ส่งให้กับมันจะเป็นชนิดข้อมูล T ที่มีเมธอดชื่อ Slug นั่นเอง โค้ดชุดใหม่ของเราจึงเกิดข้อผิดพลาดได้

Go
1func SlugifyAll[T any](s []T) []string {
2 r := make([]string, len(s))
3
4 for i, v := range s {
5 // v.Slug undefined (type T has no field or method Slug)
6 r[i] = v.Slug()
7 }
8
9 return r
10}

Generic Constraints

เราทราบอยู่แล้วว่าชนิดข้อมูลใดก็ตามที่มีเมธอด Slug สิ่งนั้นต้อง implement อินเตอร์เฟสชื่อ Slugger เป็นแน่แท้ นั่นแปลว่าภาษา Go มีวิธีการกำหนด constraints อยู่แล้วด้วยการใช้ interface

สำหรับสถานการณ์ของเราเราสามารถใช้ interface ชื่อ Slugger เพื่อเป็น constraints ด้วยความหมายที่ว่า T อนุญาตให้เป็นชนิดข้อมูลใดก็ได้ตราบเท่าที่ T มีเมธอด Slug อยู่นั่นเอง

Go
1func SlugifyAll[T Slugger](s []T) []string {
2 r := make([]string, len(s))
3
4 for i, v := range s {
5 r[i] = v.Slug()
6 }
7
8 return r
9}

ตอนนี้คอมไพเลอร์ของภาษา Go จะตายตาหลับแล้ว เมื่อ T ต้องมี Slug การเรียก v.Slug() จึงเกิดขึ้นได้แน่นอน

การอนุมานชนิดข้อมูลด้วย Type Inference

รูปแบบของฟังก์ชัน SluggifyAll นั้นค่า T คือ Type Parameters ที่ถูกกำหนด constraint ให้เป็นชนิดข้อมูล Slugger คอมไพเลอร์ของภาษา Go จะยังไม่รู้ชนิดข้อมูล T ที่แน่ชัดจนกว่าฟังก์ชันจะถูกเรียกพร้อมส่ง Type Arguments ด้วยเหตุนี้เราจึงต้องส่งชนิดของ T มาด้วยเมื่อทำการเรียกฟังก์ชัน

Go
1SlugifyAll[Slugger]([]Slugger{
2 Article{Title: " Lorem Ip sum "},
3 Person{FirstName: "Somchai", LastName: "Haha"},
4})
5
6SlugifyAll[Article]([]Article{
7 Article{Title: " Lorem Ip sum "},
8})
9
10SlugifyAll[Person]([]Person{
11 Person{FirstName: "Somchai", LastName: "Haha"},
12})

ค่าของ [Slugger], [Article] หรือ [Person] ที่อยู่หลังชื่อฟังก์ชันคือ Type Arguments เมื่อเข้าคู่กับค่า T จะทำให้คอมไพเลอร์ภาษา Go ทราบถึงชนิดข้อมูลที่แท้จริงของ T นั่นเอง

อย่างไรก็ตามส่วนใหญ่แล้วคอมไพเลอร์ภาษา Go สามารถทำการอนุมานชนิดข้อมูล (Type Inference) ได้ กรณีของ SlugifyAll แม้เราจะไม่ระบุ Type Arguments คอมไพเลอร์ยังคงสามารถอนุมานชนิดข้อมูล T ได้จากค่าอาร์กิวเมนต์ที่ส่งให้ฟังก์ชัน

Go
1// T คือ Slugger
2SlugifyAll([]Slugger{
3 Article{Title: " Lorem Ip sum "},
4 Person{FirstName: "Somchai", LastName: "Haha"},
5})
6
7// T คือ Article
8SlugifyAll([]Article{
9 Article{Title: " Lorem Ip sum "},
10})
11
12// T คือ Person
13SlugifyAll([]Person{
14 Person{FirstName: "Somchai", LastName: "Haha"},
15})

การใช้งาน type parameters หลายค่า

Type Parameters นั้นสามารถมีได้หลายค่าและสามารถใช้ได้กับทั้งตำแหน่งของพารามิเตอร์และ Return Type

Go
1func PrefixSlugifyAll[S Slugger, P any](s []S, p []P) []string {
2 r := make([]string, len(s))
3
4 for i, v := range s {
5 r[i] = fmt.Sprintf("%v: %v", p[i], v.Slug())
6 }
7
8 return r
9}

ฟังก์ชัน PrefixSlugifyAll กำหนด Type Paramerters S และ P โดย S กำกับให้เป็น Slugger ในขณะที่ P เป็น any ส่วนของการเรียกใช้สามารถกำกับ Type Arguments ได้ดังนี้

Go
1func main() {
2 r := PrefixSlugifyAll[Slugger, string]([]Slugger{
3 Article{Title: " Lorem Ip sum "},
4 Person{FirstName: "Somchai", LastName: "Haha"},
5 }, []string{"Article", "Person"})
6
7 fmt.Println(r)
8}

กรณีที่ต้องการให้คอมไพเลอร์ทำการอนุมานชนิดข้อมูล เราสามารถละส่วนของ Type Arguments ได้ดังนี้

Go
1PrefixSlugifyAll([]Slugger{
2 Article{Title: " Lorem Ip sum "},
3 Person{FirstName: "Somchai", LastName: "Haha"},
4}, []string{"Article", "Person"})

Type Parameters สามารถใช้กับ Return Type ได้ด้วยเช่นกัน เช่น

Go
1func Head[T any](v []T) T {
2 return v[0]
3}

การดำเนินการสัญลักษณ์ด้วย Union Types และ Generic Operators

พิจารณาฟังก์ชัน Add ต่อไปนี้

Go
1func Add[T any](a, b T) float64 {
2 return float64(a) + float64(b)
3}

a และ b สามารถเป็นชนิดข้อมูลใด ๆ ก็ได้ แต่เราทราบอยู่แล้วว่าไม่ใช่ทุกชนิดข้อมูลที่จะนำมาบวกผ่านเครื่องหมาย + ได้ นี่จึงไม่แปลกใจที่คอมไพเลอร์ภาษา Go จะไม่อนุญาตให้โค้ดนี้ถูกทำงานได้ อย่างไรก็ตามเราทราบอยู่แล้วว่ากลุ่มของตัวเลข เช่น int หรือ float32 ต่างสามารถใช้เครื่องหมาย + ได้ เราจึงควรกำหนด constraints ให้กับ Type Parameter T ให้เป็นกลุ่มของตัวเลข ในที่นี้จะกำหนดให้ T เป็นได้เฉพาะ int, int32, int64, float32 หรือ float64 เท่านั้นผ่านสิ่งที่เรียกว่า Union Types

Go
1func Add[T int | int32 | int64 | float32 | float64](a, b T) float64 {
2 return float64(a) + float64(b)
3}

ตอนนี้ a และ b ของเราสามารถระบุชนิดข้อมูลเป็น int, int32, int64, float32 หรือ float64 ก็ได้ แต่เนื่องจาก a และ b ต่างแชร์ T ร่วมกัน นั่นแปลว่าหากส่ง a ด้วยชนิดข้อมูล int เราย่อมต้องส่ง b ด้วยชนิดข้อมูล T เดียวกันคือ int นั่นเอง หากเราส่งชนิดข้อมูลแตกต่างกันย่อมเกิดข้อผิดพลาด

Go
1// default type float64 of 1.2 does not match inferred type int for T
2Add(1, 1.2)

ในกรณีที่เราต้องการให้ส่ง a และ b ด้วยชนิดข้อมูลที่แตกต่างกันได้ ต้องทำการแยก Type Parameters ของทั้งสองออกจากกัน ดังนี้

Go
1func Add[T int | int32 | int64 | float32 | float64, V int | int32 | int64 | float32 | float64](a T, b V) float64 {
2 return float64(a) + float64(b)
3}

วิธีนี้จะพบว่า T และ V ต่างใช้ constraints ร่วมกัน มีวิธีที่ดีกว่านี้ไหมในการป้องกันการนิยาม constraints ซ้ำเช่นนี้

การใช้ Constraint elements ใน interface

โดยทั่วไปแล้ว interface อนุญาตให้ประกอบด้วยสองสิ่งคือ รูปแบบการประกาศเมธอด (method signatures) และ การฝัง interface อื่น (embedded interface types) ตามที่เราทราบจากหัวข้อก่อนหน้า interface นั้นยังถูกใช้ในฐานะของการเป็น constraints ได้ด้วย Go 1.18 จึงเพิ่มความสามารถให้กับ interface ด้วยการนิยาม constraints ได้ผ่านการระบุ Constraint elements

Arbitrary type constraint element คืออีลีเมนต์ที่เป็นชนิดข้อมูลใด ๆ อันถูกฝังอยู่ใน interface เพื่อใช้กำหนด constraints ว่า Type Parameters นั้น ๆ ต้องเป็นชนิดข้อมูลตามอีลีเมนต์ที่กำหนด เช่น เมื่อกำหนด type Integer interface { int } แล้วนำ Integer ไปใช้เป็น constraints จึงหมายความว่าชนิดข้อมูลของ Type Parameter นั้นต้องเป็นชนิดข้อมูล int เท่านั้น กรณีที่ต้องการให้ชนิดข้อมูลปลายทางอยู่ในกลุ่มของชนิดข้อมูลตัวใดตัวหนึ่งในลิสต์ ให้ระบุด้วยการใช้ Union

ย้อนกลับไปที่ฟังก์ชัน Add ของเรา เพื่อแก้ปัญหาดังกล่าวเราจึงทำการสร้าง interface ชื่อ Number ที่ระบุ Arbitrary type constraint element ดังนี้

Go
1type Number interface {
2 int | int32 | int64 | float32 | float64
3}

จากนั้นจึงนำชนิดข้อมูลดังกล่าวมาใช้นิยาม constraints ให้กับ Type Paramerters คือ T และ V

Go
1func Add[T, V Number](a T, b V) float64 {
2 return float64(a) + float64(b)
3}

การประกาศ constraints ด้วย Approximation constraint element

สมมติเรามีฟังก์ชัน Next ที่ทำการคืนค่าเลขถัดไป พร้อมกำหนด Type Parameter T เป็น Interger ดังนี้

Go
1type Integer interface {
2 int | int8 | int16 | int32 | int64 |
3 uint | uint8 | uint16 | uint32 | uint64
4}
5
6func Next[T Integer](n T) T {
7 return n + 1
8}

จากนั้นจึงทำการส่งค่า 1 ที่มีชนิดข้อมูลเป็น int ผ่านฟังก์ชัน Next(1) สิ่งนี้ย่อมทำได้เพราะ T อนุญาตให้ส่ง int ได้

อย่างไรก็ตามหากเรามีชนิดข้อมูลใหม่คือ Step ที่มีชนิดข้อมูลฐานราก (Underlying Types) เป็น int

Go
1type Step int

แม้ว่า Step แท้จริงแล้วจะมีข้อมูลเบื้องหลังเป็น int แต่มันก็ไม่ใช่ Integer จึงไม่สามารถใช้กับ Next ได้

Go
1// Step does not implement Integer
2Next(Step(1))

Go 1.18 มีอีกวิธีหนึ่งในการประกาศ element ใน interface ให้เป็น constraints ที่สนใจชนิดข้อมูลฐานราก (Underlying Types) หาก Type Parameters ที่กำหนดมี Underlying Types ตามที่ระบุใน interface นี้ สิ่งนั้นจะได้รับการอนุญาตจากคอมไพเลอร์ให้สามารถใช้งานได้ อีลีเมนต์ดังกล่าวเรียกว่า Approximation constraint element

กรณีของเราไม่ได้สนใจว่า Integer ต้องเป็นชนิดข้อมูลของเลขจำนวนเต็มเท่านั้น แต่หาก Underlying Types เป็นจำนวนเต็ม ย่อมถือเป็น Integer ด้วยเช่นกัน โจทย์นี้จึงแก้ได้ด้วยการใช้ Approximation constraint element ผ่านการระบุเครื่องหมาย ~ นำหน้า Underlying Types ดังนี้

Go
1type Integer interface {
2 ~int | ~int8 | ~int16 | ~int32 | ~int64 |
3 ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64
4}

ตอนนี้เราสามารถเรียกใช้ Next(Step(1)) ได้เป็นที่เรียบร้อยแล้ว

ข้อกำหนดอย่างนึงของ Approximation constraint element คือ เมื่อ ~T เป็นชนิดข้อมูลใด ๆ ที่มี T เป็น Underlying Type ชนิดข้อมูลฐานรากของ T ต้องเป็นตัวเอง ดังนั้นค่าที่ T เป็นได้จึงเป็น ชนิดข้อมูลที่นิยามไว้อยู่แล้ว เช่น int หรือเป็น Type Literals เช่น []int เป็นต้น

การใช้งาน Comparable types

เราอยากสร้างฟังก์ชัน CountOfOccurrences ที่รับพารามิเตอร์เป็น slice กับคำค้น ผลลัพธ์ที่คืนกลับจากฟังก์ชัน จะเป็นการนับว่าใน slice ที่ส่งเข้ามาพบคำค้นเป็นจำนวนเท่าใด โดย slice ที่ส่งให้กับฟังก์ชันสามารถเป็นข้อมูลชนิดใดก็ได้ตราบเท่าที่สามารถใช้เครื่องหมายเท่ากับ == ในการเทียบค่ากับคำค้นได้

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

Go
1CountOfOccurrences([]int{1, 2, 3, 2, 2, 2, 1}, 2) // 4
2CountOfOccurrences([]string{"high", "low", "low"}, "low") // 2

หน้าตาที่ควรเป็นของฟังก์ชัน CountOfOccurrences ควรเป็นดังนี้ เพียงแต่ค่า T ที่เหมาะสมควรกำหนด constraints เช่นใดดี?

Go
1// เมื่อ ??? คือ constraints ที่เรากำลังจะระบุ
2func CountOfOccurrences[T ???](s []T, h T) int {
3 var count int
4
5 for _, v := range s {
6 if v == h {
7 count++
8 }
9 }
10
11 return count
12}

Go 1.18 มาพร้อมกับ constraint ชื่อว่า comparable ที่นิยามความเข้ากันได้กับ Type Paramerters ไว้ว่า T ใด ๆ จะเข้าคู่กับ comparable ก็ต่อเมื่อชนิดข้อมูลนั้นสามารถเปรียบเทียบด้วยการใช้เครื่องหมาย == และ != ได้

เอาละกลับมาที่ CountOfOccurrences ของเรา ตอนนี้เราสามารถกำหนด comparable ให้เป็น constraint ของ T ได้แล้ว

Go
1func CountOfOccurrences[T comparable](s []T, h T) int {
2 var count int
3
4 for _, v := range s {
5 if v == h {
6 count++
7 }
8 }
9
10 return count
11}

การประกาศ Generic Types

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

Go
1type Integer interface {
2 ~int | ~int8 | ~int16 | ~int32 | ~int64 |
3 ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64
4}
5
6type Point2D[T Integer] struct {
7 x T
8 y T
9}

การเรียกใช้งาน Generic Types ใด ๆ ต้องทำการระบุ Type Arguments ก่อนเสมอ เรียกว่า instantiation ไม่เช่นนั้นจะเกิดข้อผิดพลาดได้

Go
1func main() {
2 // cannot use generic type Point2D[T Integer] without instantiation
3 a := Point2D{1, 2}
4 // cannot use generic type Point2D[T Integer] without instantiation
5 b := Point2D{3, 4}
6
7 fmt.Println(a, b)
8}

โค้ดข้างต้นเมื่อทำการระบุ Type Arguments จะได้ผลลัพธ์ที่ถูกต้องดังนี้

Go
1func main() {
2 a := Point2D[int]{1, 2}
3 b := Point2D[int{3, 4}
4
5 fmt.Println(a, b)
6}

Generic Types สามารถมีเมธอดได้ โดยเมธอดต้องมีการระบุ Type Parameters ให้ครบตามจำนวนที่กำหนดจากที่ประกาศชนิดข้อมูล

Go
1func (p Point2D[T]) Slope(o Point2D[T]) float64 {
2 return float64((o.y - p.y) / (o.x - p.x))
3}

สรุป

Generics เป็นคุณสมบัติใหม่ที่เพิ่มเข้ามาใน Go 1.18 ความสามารถนี้ทำให้เรานิยามทั้งฟังก์ชันหรือชนิดข้อมูลใหม่เพื่อสนับสนุนการทำงานกับชนิดข้อมูลอื่น ๆ ได้ดีขึ้น แทนที่จะใช้ interface{} เหมือนเช่นแต่ก่อนนั่นเอง

สารบัญ

สารบัญ

  • กาลครั้งหนึ่งครั้น Generics ยังไม่อุบัติ
  • ชนิดข้อมูล any
  • Generics และ Type Parameters ในภาษา Go 1.18
  • Generic Constraints
  • การอนุมานชนิดข้อมูลด้วย Type Inference
  • การใช้งาน type parameters หลายค่า
  • การดำเนินการสัญลักษณ์ด้วย Union Types และ Generic Operators
  • การใช้ Constraint elements ใน interface
  • การประกาศ constraints ด้วย Approximation constraint element
  • การใช้งาน Comparable types
  • การประกาศ Generic Types
  • สรุป