เมธอดใน 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 ต้องระบุอย่างชัดเจน
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())
}การใช้เมธอดคล้ายกับการเรียกเมธอดสมาชิกของคลาสหนึ่ง ก่อนประกาศ แล้วเริ่มต้น แล้วเรียกใช้
ตัวรับค่า
ตัวรับแบ่งเป็นสองประเภท คือ ตัวรับค่าและตัวรับพอยน์เตอร์ ดูตัวอย่างก่อน
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 เมื่อเมธอดถูกเรียกใช้ จะส่งค่าของตัวรับเข้าไปในเมธอด ตัวรับในตัวอย่างข้างต้นเป็นตัวรับค่า สามารถมองเป็นพารามิเตอร์รูปแบบหนึ่งได้ง่ายๆ และการแก้ไขค่าของพารามิเตอร์รูปแบบหนึ่งจะไม่ส่งผลกระทบใดๆ ต่อค่านอกเมธอด แล้วหากเรียกผ่านพอยน์เตอร์จะเป็นอย่างไร
func main() {
myInt := MyInt(1)
(&myInt).Set(2)
fmt.Println(myInt)
}น่าเสียดายที่โค้ดแบบนี้ยังคงไม่สามารถแก้ไขค่าภายในได้ เพื่อให้ตรงกับประเภทของตัวรับ Go จะทำการ dereference อธิบายเป็น (*(&myInt)).Set(2)
ตัวรับพอยน์เตอร์
เพียงแก้ไขเล็กน้อย ก็สามารถแก้ไขค่าของ myInt ได้ตามปกติ
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
ต้องเข้าใจ อินเทอร์เฟซ ก่อน
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{} ต่อไปจะอธิบายทีละวิธี
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 จะแทรกตัวดำเนินการพอยน์เตอร์โดยอัตโนมัติเพื่อเรียกใช้ เช่น สไลซ์สามารถหาที่อยู่ได้ ยังคงสามารถแก้ไขค่าภายในผ่านตัวรับค่าได้ เช่นโค้ดด้านล่าง
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 มีค่าส่งคืน หลังจากเพิ่มองค์ประกอบลงในสไลซ์แล้วต้องทับสไลซ์เดิม โดยเฉพาะหลังจากขยายขนาด การแก้ไขตัวรับค่าในเมธอดจะไม่ส่งผลกระทบใดๆ ซึ่งนำไปสู่ผลลัพธ์ในตัวอย่าง เปลี่ยนเป็นตัวรับพอยน์เตอร์ก็ปกติแล้ว
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]