Skip to content

เมธอดใน Go

ความแตกต่างระหว่างเมธอดและฟังก์ชันคือ เมธอดมีตัวรับ แต่ฟังก์ชันไม่มี และมีเพียงประเภทที่กำหนดเองเท่านั้นที่สามารถมีเมธอดได้ ดูตัวอย่างก่อน

go
type IntSlice []int

func (i IntSlice) Get(index int) int {
  return i[index]
}
func (i IntSlice) Set(index, val int) {
  i[index] = val
}

func (i IntSlice) Len() int {
  return len(i)
}

ก่อนอื่นประกาศประเภทหนึ่ง IntSlice ประเภทพื้นฐานของมันคือ []int จากนั้นประกาศสามเมธอด Get, Set และ Len รูปลักษณ์ของเมธอดไม่แตกต่างจากฟังก์ชันมากนัก เพียงมี (i IntSlice) เพิ่มมาเล็กน้อย i คือตัวรับ IntSlice คือประเภทของตัวรับ ตัวรับคล้ายกับ this หรือ self ในภาษาอื่น แต่ใน Go ต้องระบุอย่างชัดเจน

go
func main() {
   var intSlice IntSlice
   intSlice = []int{1, 2, 3, 4, 5}
   fmt.Println(intSlice.Get(0))
   intSlice.Set(0, 2)
   fmt.Println(intSlice)
   fmt.Println(intSlice.Len())
}

การใช้เมธอดคล้ายกับการเรียกเมธอดสมาชิกของคลาสหนึ่ง ก่อนประกาศ แล้วเริ่มต้น แล้วเรียกใช้

ตัวรับค่า

ตัวรับแบ่งเป็นสองประเภท คือ ตัวรับค่าและตัวรับพอยน์เตอร์ ดูตัวอย่างก่อน

go
type MyInt int

func (i MyInt) Set(val int) {
   i = MyInt(val) // แก้ไขแล้ว แต่ไม่ส่งผลกระทบใดๆ
}

func main() {
   myInt := MyInt(1)
   myInt.Set(2)
   fmt.Println(myInt)
}

หลังจากโค้ดข้างต้นรันแล้ว จะพบว่าค่าของ myInt ยังคงเป็น 1 ไม่ได้ถูกแก้ไขเป็น 2 เมื่อเมธอดถูกเรียกใช้ จะส่งค่าของตัวรับเข้าไปในเมธอด ตัวรับในตัวอย่างข้างต้นเป็นตัวรับค่า สามารถมองเป็นพารามิเตอร์รูปแบบหนึ่งได้ง่ายๆ และการแก้ไขค่าของพารามิเตอร์รูปแบบหนึ่งจะไม่ส่งผลกระทบใดๆ ต่อค่านอกเมธอด แล้วหากเรียกผ่านพอยน์เตอร์จะเป็นอย่างไร

go
func main() {
  myInt := MyInt(1)
  (&myInt).Set(2)
  fmt.Println(myInt)
}

น่าเสียดายที่โค้ดแบบนี้ยังคงไม่สามารถแก้ไขค่าภายในได้ เพื่อให้ตรงกับประเภทของตัวรับ Go จะทำการ dereference อธิบายเป็น (*(&myInt)).Set(2)

ตัวรับพอยน์เตอร์

เพียงแก้ไขเล็กน้อย ก็สามารถแก้ไขค่าของ myInt ได้ตามปกติ

go
type MyInt int

func (i *MyInt) Set(val int) {
   *i = MyInt(val)
}

func main() {
   myInt := MyInt(1)
   myInt.Set(2)
   fmt.Println(myInt)
}

ตอนนี้ตัวรับเป็นตัวรับพอยน์เตอร์ แม้ว่า myInt เป็นประเภทค่า เมื่อเรียกเมธอดตัวรับพอยน์เตอร์ผ่านประเภทค่า Go จะอธิบายเป็น (&myint).Set(2) ดังนั้นเมื่อตัวรับของเมธอดเป็นพอยน์เตอร์ ไม่ว่าผู้เรียกจะเป็นพอยน์เตอร์หรือไม่ ก็สามารถแก้ไขค่าภายในได้

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

TIP

ต้องเข้าใจ อินเทอร์เฟซ ก่อน

go
type Animal interface {
   Run()
}

type Dog struct {
}

func (d *Dog) Run() {
   fmt.Println("Run")
}

func main() {
   var an Animal
   an = Dog{}
   // an = &Dog{} วิธีที่ถูกต้อง
   an.Run()
}

โค้ดส่วนนี้จะไม่สามารถผ่านการคอมไพล์ได้ คอมไพเลอร์จะแสดงข้อผิดพลาดดังนี้

cannot use Dog{} (value of type Dog) as type Animal in assignment:
  Dog does not implement Animal (Run method has pointer receiver)

แปลว่า ไม่สามารถใช้ Dog{} เริ่มต้นตัวแปรประเภท Animal ได้ เพราะ Dog ไม่ได้ดำเนินการ Animal มีวิธีแก้ไขสองวิธี หนึ่งคือเปลี่ยนตัวรับพอยน์เตอร์เป็นตัวรับค่า สองคือเปลี่ยน Dog{} เป็น &Dog{} ต่อไปจะอธิบายทีละวิธี

go
type Dog struct {
}

func (d Dog) Run() { // เปลี่ยนเป็นตัวรับค่า
   fmt.Println("Run")
}

func main() { // สามารถรันได้ตามปกติ
   var an Animal
   an = Dog{}
   // an = &Dog{} ก็ได้เช่นกัน
   an.Run()
}

ในโค้ดเดิม ตัวรับของเมธอด Run คือ *Dog ดังนั้นผู้ที่ดำเนินการอินเทอร์เฟซ Animal โดยธรรมชาติคือพอยน์เตอร์ Dog ไม่ใช่สตรักเจอร์ Dog นี่คือสองประเภทที่ต่างกัน ดังนั้นคอมไพเลอร์จึงคิดว่า Dog{} ไม่ใช่การดำเนินการของ Animal จึงไม่สามารถกำหนดค่าให้กับตัวแปร an ได้ ดังนั้นวิธีแก้ไขวิธีที่สองคือกำหนดค่าพอยน์เตอร์ Dog ให้กับตัวแปร an แต่เมื่อใช้ตัวรับค่า พอยน์เตอร์ Dog ยังคงสามารถกำหนดค่าให้กับ animal ได้ตามปกติ นี่เป็นเพราะ Go จะทำการ dereference พอยน์เตอร์ในสถานการณ์ที่เหมาะสม เพราะผ่านพอยน์เตอร์สามารถหาสตรักเจอร์ Dog ได้ แต่ในทางกลับกัน ไม่สามารถหาพอยน์เตอร์ Dog ผ่านสตรักเจอร์ Dog ได้ หากเพียงใช้ตัวรับค่าและตัวรับพอยน์เตอร์ปนกันในสตรักเจอร์อย่างเดียวก็ไม่เป็นไร แต่หลังจากใช้อินเทอร์เฟซร่วมกันแล้ว จะเกิดข้อผิดพลาด ไม่เช่นนั้นก็ใช้ตัวรับค่าทั้งหมด หรือใช้ตัวรับพอยน์เตอร์ทั้งหมด สร้างมาตรฐานที่ดี ก็สามารถลดภาระการบำรุงรักษาในภายหลังได้

ยังมีอีกสถานการณ์หนึ่ง คือเมื่อตัวรับค่าสามารถหาที่อยู่ได้ Go จะแทรกตัวดำเนินการพอยน์เตอร์โดยอัตโนมัติเพื่อเรียกใช้ เช่น สไลซ์สามารถหาที่อยู่ได้ ยังคงสามารถแก้ไขค่าภายในผ่านตัวรับค่าได้ เช่นโค้ดด้านล่าง

go
type Slice []int

func (s Slice) Set(i int, v int) {
  s[i] = v
}

func main() {
  s := make(Slice, 1)
  s.Set(0, 1)
  fmt.Println(s)
}

เอาต์พุต

[1]

แต่สิ่งนี้จะนำไปสู่อีกปัญหาหนึ่ง คือหากเพิ่มองค์ประกอบให้มัน สถานการณ์จะต่างกัน ดูตัวอย่างด้านล่าง

type Slice []int

func (s Slice) Set(i int, v int) {
  s[i] = v
}

func (s Slice) Append(a int) {
  s = append(s, a)
}

func main() {
  s := make(Slice, 1, 2)
  s.Set(0, 1)
  s.Append(2)
  fmt.Println(s)
}
[1]

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

go
type Slice []int

func (s *Slice) Set(i int, v int) {
  (*s)[i] = v
}

func (s *Slice) Append(a int) {
  *s = append(*s, a)
}

func main() {
  s := make(Slice, 1, 2)
  s.Set(0, 1)
  s.Append(2)
  fmt.Println(s)
}

เอาต์พุต

[1 2]

Golang by www.golangdev.cn edit