DEV Community

Cover image for OOP ใน Go ฉบับเข้าใจง่าย (มั้ง)
Perajit
Perajit

Posted on

OOP ใน Go ฉบับเข้าใจง่าย (มั้ง)

ออกตัวก่อนว่าไม่ได้เชี่ยวชาญ Go อะไรมากมาย แค่นึกไม่ออกว่าเขียนเรื่องอะไรดี แล้วเรื่องนี้ก็ผุดขึ้นมา

ใน Go สามารถทำ OOP ได้ แต่จะค่อนข้างต่างจากภาษาอื่นที่เคยเจอมา คือไม่มีสิ่งที่เรียกว่า class แต่จะใช้ struct ซึ่งเป็นการ define โครงสร้างข้อมูล เช่น

type Animal struct {
  Name string
  Weight int
}
Enter fullscreen mode Exit fullscreen mode

เป็นการประกาศโครงสร้างของข้อมูลที่ประกอบด้วยฟิลด์ชื่อ Name และ Weight และเนื่องจากใน Go จะใส่ zero value ให้ค่าต่างๆ เสมอ ดังนั้นถ้าประกาศตัวแปรด้วย type ของ struct แต่ละ field ก็จะมีค่าเริ่มต้นเป็น zero value ของแต่ละชนิดข้อมูลโดยอัตโนมัติ ซึ่งก็คือทำหน้าที่เก็บข้อมูลได้เหมือน data class ในภาษาอื่นนั่นเอง

จากตัวอย่าง Cat ถ้าประกาศตัวแปรแบบนี้

cat := Animal{}
Enter fullscreen mode Exit fullscreen mode

เราก็จะได้สิ่งที่เหมือนกับ instance ของ data class ที่มีค่า cat.Name = "" และ cat.Weight = 0

หรือจะกำหนดค่าเริ่มต้นลงไปตั้งแต่ตอนประกาศค่าก็ได้ เช่น

cat := Animal{Name: "Kitty", Weight: 2}
Enter fullscreen mode Exit fullscreen mode

ก็จะได้ค่าตามที่กำหนดคือ cat.Name = "Kitty" และ cat.Weight = 2

Class with methods

ตอนนี้เราสามารถสร้างสิ่งที่เหมือนกับ class ที่มี property แล้ว แต่ว่าในโลกของ OOP เนี่ย class ไม่ได้มีแค่ property แต่ยังมี method ด้วยใช่มั้ย?

ใน Go เราสามารถสร้าง method ให้กับ struct ได้โดยไม่ได้เขียนลงใน struct เลย แต่ใช้การการผูกฟังก์ชั่นเข้าไปโต้งๆ ทีหลัง ซึ่งคำว่า "ผูกโต้งๆ" อาศัยสิ่งที่เรียกว่า receiver

ตัวอย่างเช่น เราจะเติม method Eat() ให้กับ Animal ได้แบบนี้

func(a *Animal) Eat() {
  a.Weight++
}
Enter fullscreen mode Exit fullscreen mode

สังเกตว่ารูปแบบการเขียนฟังก์ชั่นจะมีบางอย่างเพิ่มเข้ามา คือ (a *Animal) สิ่งนี้แหละที่เรียกว่า receiver โดย a คือ receiver variable ส่วน *Animal เป็น receiver type สังเกตว่าจะไม่มีการใช้คีย์เวิร์ด this เหมือนภาษาอื่น แต่จะใช้ receiver อ้างอิงไปเลย

ลองเขียนโค้ดทดสอบดู

cat := Animal{Name: "Kitty", Weight: 2}
cat.Eat()
fmt.Printf("Weight: %v", a.Weight)
Enter fullscreen mode Exit fullscreen mode

จะได้ค่า a.Weight = 3

แล้วทีนี้อยากให้มี method อีกสักกี่อันก็แค่เขียนฟังก์ชั่นโดยผูก receiver เข้าไป

ข้อดีของการเขียนลักษณะนี้คือทำให้สามารถเติม method เข้าไปได้โดย struct ไม่บวม (อยู่คนละไฟล์ได้) และการใช้ชื่อตัวแปร receiver แทน this ทำให้ refactor โค้ดจาก function ธรรมดามาเป็น method ได้ง่ายๆ โดยไม่ต้องแก้ชื่อตัวแปรเป็น this

เสริมอีกนิดว่า receiver มี 2 แบบคือ value receiver กับ pointer receiver สำหรับในตัวอย่างเราใช้ pointer receiver ซึ่งคือแบบนี้ (a *Animal) เพราะต้องการแก้ไขข้อมูล เวลาใช้ value receiver ซึ่งคือแบบนี้ (a Animal) เราจะไม่สามารถแก้ไขข้อมูลได้

เช่น ถ้าแก้โค้ดของ method Eat() เป็นแบบนี้

// การใช้ value receiver จะไม่สามารถแก้ไขข้อมูลได้
func(a Animal) Eat {
  a.Weight++
}
Enter fullscreen mode Exit fullscreen mode

ผลที่ได้คือคือค่าของ a.Weight จะไม่เปลี่ยน

Inheritance (หรือจริงๆ คือ Composition)

การ "สืบทอด" ใน Go ก็แตกต่างจากภาษาอื่นที่ใช้คีย์เวิร์ด extends แต่ Go ออกแบบมาให้ "Composition over Inheritance" จึงใช้การ "ฝัง" (embedding) struct ซ้อนเข้าไปข้างใน เช่น

type Cat struct {
  Animal
  SlaveName string
}
func(c *Cat) Meow() {
  fmt.Println("Meow")
}
Enter fullscreen mode Exit fullscreen mode

เราจะได้ Cat ที่มี property และ method ของ Animal ติดมาด้วยทันที
ทดสอบด้วยการเขียนโค้ดตามด้านล่างนี้

cat := Cat{}
cat.Eat()
fmt.Printf("Name: %v, Weight: %v, SlaveName: %v", cat.Name, cat.Weight, cat.SlaveName)
Enter fullscreen mode Exit fullscreen mode

โค้ดจะไม่พัง และจะได้ผลลัพธ์เป็น Name:, Weight:1, SlaveName:

  • Name เป็นสตริงว่าง จาก zero value
  • Weight เป็น 1 เนื่องจากมีการสั่ง cat.Eat() ทำการเพิ่มค่าจาก zero value ครั้งนึง
  • SlaveName เป็นสตริงว่างจาก zero value

แล้วถ้าเกิดเราต้องการใส่ค่าเริ่มต้นให้กับ cat ล่ะ? ในเมื่อเราสามารถเรียก property ที่มาจาก Animal ได้ตรงๆ งั้นเราลองใส่ Weight เป็น 2 ให้กับ cat ดูหน่อย

cat := Cat{Weight: 2, SlaveName: "Jack"}
Enter fullscreen mode Exit fullscreen mode

ปรากฏว่าเขียนแบบนี้จะเจอ syntax error…

นี่เป็นเรื่องที่เราเจอตอนลองเขียนครั้งแรก แล้วก็งงว่าจะใส่ค่าเริ่มต้นให้มันได้ยังไง แล้วก็ค้นพบว่า วิธีที่ถูกต้องคือ ต้องระบุชื่อ struct ที่ embed มาด้วย!

เนื่องจากตอนประกาศ struct เราใส่ Animal เข้าไป ตอนเซ็ตค่าเริ่มต้นเราก็ต้องระบุ Animal เหมือนกัน แบบนี้

cat := Cat{Animal: Animal{Weight: 2}, SlaveName: "Jack"}
Enter fullscreen mode Exit fullscreen mode

ทีนี้ถ้าลองสั่ง

cat.Eat()
fmt.Printf("Name: %v, Weight: %v, SlaveName: %v", cat.Name, cat.Weight, cat.SlaveName)
Enter fullscreen mode Exit fullscreen mode

เราจะได้ผลลัพธ์ Name:, Weight:3, SlaveName:Jack

  • Name ยังเป็นสตริงว่างเพราะไม่ได้กำหนดค่า
  • Weight เป็น 3 เนื่องจากค่าเริ่มต้นถูกเซ็ตเป็น 2
  • SlaveName ได้ค่า Jack ที่เซ็ตเข้าไป

ตอนที่ดึงค่าออกมาจาก field หรือเรียก method สามารถเรียกได้เลยตรงๆ แต่ตอนประกาศค่าต้องระบุชื่อ struct ที่ embed มาเสมอ

สำหรับ OOP ใน Go คร่าวๆ ก็ประมาณนี้ค่ะ

Top comments (0)