Interface
ในภาษา Go interface เป็นประเภทนามธรรมชนิดหนึ่ง ใช้สำหรับกำหนดชุดลายเซ็นเมธอดโดยไม่มีการนำไปใช้ของเมธอด แนวคิดหลักของ interface คือการอธิบายพฤติกรรม และการนำไปใช้พฤติกรรมที่เฉพาะเจาะจงจัดเตรียมโดยประเภทที่นำไปใช้อินเทอร์เฟซ Interface ในภาษา Go ใช้กันอย่างแพร่หลายเพื่อให้เกิด polymorphism, loose coupling และการใช้โค้ดซ้ำ
แนวคิด
ประวัติการพัฒนาของ Go เกี่ยวกับ interface มีจุดเปลี่ยนหนึ่งจุด ใน Go1.17 และก่อนหน้า ทางการในคู่มืออ้างอิงให้นิยามของ interface ว่า: ชุดของเมธอดชุดหนึ่ง
An interface type specifies a method set called its interface.
นิยามของการนำไปใช้อินเทอร์เฟซคือ
A variable of interface type can store a value of any type with a method set that is any superset of the interface. Such a type is said to implement the interface
แปลแล้วคือ เมื่อเซตเมธอดของประเภทหนึ่งเป็นซูเปอร์เซตของเซตเมธอดของอินเทอร์เฟซหนึ่ง และค่าของประเภทนั้นสามารถเก็บได้โดยตัวแปรของประเภทอินเทอร์เฟซนั้น แล้วกล่าวว่าประเภทนั้นนำไปใช้อินเทอร์เฟซนั้น
แต่ใน Go1.18 นิยามของ interface发生了变化 อินเทอร์เฟซถูกนิยามว่า: เซตของประเภทชุดหนึ่ง
An interface type defines a type set.
นิยามของการนำไปใช้อินเทอร์เฟซคือ
A variable of interface type can store a value of any type that is in the type set of the interface. Such a type is said to implement the interface
แปลแล้วคือ เมื่อประเภทหนึ่งอยู่ใน type set ของอินเทอร์เฟซหนึ่ง และค่าของประเภทนั้นสามารถเก็บได้โดยตัวแปรของประเภทอินเทอร์เฟซนั้น แล้วกล่าวว่าประเภทนั้นนำไปใช้อินเทอร์เฟซนั้น และยังให้นิยามเพิ่มเติมดังนี้
เมื่อเป็นสถานการณ์ต่อไปนี้ สามารถกล่าวว่าประเภท T นำไปใช้อินเทอร์เฟซ I
- T ไม่ใช่อินเทอร์เฟซ และเป็นองค์ประกอบหนึ่งใน type set ของอินเทอร์เฟซ I
- T เป็นอินเทอร์เฟซ และ type set ของ T เป็นสับเซตของ type set ของอินเทอร์เฟซ I
หาก T นำไปใช้อินเทอร์เฟซหนึ่ง แล้วค่าของ T ก็นำไปใช้อินเทอร์เฟซนั้นด้วย
การเปลี่ยนแปลงที่ใหญ่ที่สุดของ Go ใน 1.18 คือการเพิ่ม generics นิยามอินเทอร์เฟซใหม่ให้บริการสำหรับ generics แต่ไม่กระทบการใช้งานอินเทอร์เฟซก่อนหน้าเลย ในขณะเดียวกันอินเทอร์เฟซก็แบ่งออกเป็นสองประเภท
- Basic Interface: อินเทอร์เฟซที่ มีเพียงเซตเมธอดเท่านั้น คือ basic interface
- General Interface: อินเทอร์เฟซที่ มี type set คือ general interface
เซตเมธอดคืออะไร เซตเมธอดคือชุดของเมธอดชุดหนึ่ง ในทำนองเดียวกัน type set คือชุดของประเภทชุดหนึ่ง
TIP
คุณอาจรู้สึกว่าแนวคิดนี้เข้าใจยากและคลุมเครือ แต่จริงๆ แล้วคุณไม่จำเป็นต้องเข้าใจสิ่งทั้งหมดข้างต้นนี้
Basic Interface
ก่อนหน้านี้ได้กล่าวแล้วว่า basic interface คือเซตเมธอด คือชุดของเมธอดชุดหนึ่ง
การประกาศ
ก่อนอื่นมาดูว่า interface มีหน้าตาอย่างไร
type Person interface {
Say(string) string
Walk(int)
}นี่คืออินเทอร์เฟซ Person มีสองเมธอดที่เปิดเผย Walk และ Say ในอินเทอร์เฟซ ชื่อพารามิเตอร์ของฟังก์ชันไม่สำคัญอีกต่อไป แน่นอนว่าหากต้องการเพิ่มชื่อพารามิเตอร์และชื่อค่าส่งกลับก็ได้รับอนุญาต
การเริ่มต้น
มีเพียงอินเทอร์เฟซไม่สามารถเริ่มต้นได้ เพราะมันเป็นเพียงชุดของข้อกำหนด ไม่มีการนำไปใช้ที่เฉพาะเจาะจง แต่สามารถประกาศได้
func main() {
var person Person
fmt.Println(person)
}ผลลัพธ์
<nil>การนำไปใช้
มาดูตัวอย่างกันก่อน บริษัทก่อสร้างแห่งหนึ่งต้องการเครนขนาดพิเศษ จึงให้ข้อกำหนดและแบบแปลนพิเศษของเครน และชี้ว่าเครนควรมีฟังก์ชันยกและแขวนสินค้า บริษัทก่อสร้างไม่รับผิดชอบในการสร้างเครน เพียงให้ข้อกำหนดหนึ่งชุด นี่คือ interface ดังนั้นบริษัท A จึงรับออเดอร์ ตามเทคนิคพิเศษของบริษัทตัวเองสร้างเครนที่ยอดเยี่ยมและส่งมอบให้บริษัทก่อสร้าง บริษัทก่อสร้างไม่สนใจว่านำไปใช้ด้วยเทคนิคอะไร และไม่สนใจว่าเครนที่ยอดเยี่ยมอะไร ตราบใดที่สามารถยกและแขวนสินค้าได้ ก็เพียงใช้เป็นเครนธรรมดาตัวหนึ่ง ให้ฟังก์ชันที่เฉพาะเจาะจงตามข้อกำหนดของ interface นี่คือ implementation ผ่านไปช่วงหนึ่ง เครนที่ยอดเยี่ยมเกิดขัดข้อง บริษัท A ก็หนีไป ดังนั้นบริษัท B จึงสร้างเครนยักษ์ที่ทรงพลังยิ่งขึ้นตามข้อกำหนด เนื่องจากมีฟังก์ชันยกและแขวนสินค้าเหมือนกัน สามารถเชื่อมต่อกับเครนที่ยอดเยี่ยมได้อย่างราบรื่น ไม่กระทบความคืบหน้าการก่อสร้าง การก่อสร้างจึงเสร็จสมบูรณ์อย่างราบรื่น การนำไปใช้ภายในเปลี่ยนแปลงแต่ฟังก์ชันไม่เปลี่ยนแปลง ไม่กระทบการใช้งานก่อนหน้า สามารถแทนที่ได้อย่างอิสระ นี่คือประโยชน์ของการเขียนโปรแกรมแบบ面向接口
ต่อไปจะใช้ Go อธิบายสถานการณ์ข้างต้น
// อินเทอร์เฟซเครน
type Crane interface {
JackUp() string
Hoist() string
}
// เครน A
type CraneA struct {
work int //ฟิลด์ภายในต่างกันแสดงว่ารายละเอียดภายในไม่เหมือนกัน
}
func (c CraneA) Work() {
fmt.Println("ใช้เทคนิค A")
}
func (c CraneA) JackUp() string {
c.Work()
return "jackup"
}
func (c CraneA) Hoist() string {
c.Work()
return "hoist"
}
// เครน B
type CraneB struct {
boot string
}
func (c CraneB) Boot() {
fmt.Println("ใช้เทคนิค B")
}
func (c CraneB) JackUp() string {
c.Boot()
return "jackup"
}
func (c CraneB) Hoist() string {
c.Boot()
return "hoist"
}
type ConstructionCompany struct {
Crane Crane // เก็บเครนตามประเภท Crane เท่านั้น
}
func (c *ConstructionCompany) Build() {
fmt.Println(c.Crane.JackUp())
fmt.Println(c.Crane.Hoist())
fmt.Println("การก่อสร้างเสร็จสมบูรณ์")
}
func main() {
// ใช้เครน A
company := ConstructionCompany{CraneA{}}
company.Build()
fmt.Println()
// เปลี่ยนเป็นเครน B
company.Crane = CraneB{}
company.Build()
}ผลลัพธ์
ใช้เทคนิค A
jackup
ใช้เทคนิค A
hoist
การก่อสร้างเสร็จสมบูรณ์
ใช้เทคนิค B
jackup
ใช้เทคนิค B
hoist
การก่อสร้างเสร็จสมบูรณ์ในตัวอย่างข้างต้น สามารถสังเกตได้ว่าการนำไปใช้อินเทอร์เฟซเป็นแบบโดยนัย สอดคล้องกับนิยามของทางการสำหรับการนำไปใช้ basic interface: เซตเมธอดเป็นซูเปอร์เซตของเซตเมธอดของอินเทอร์เฟซ ดังนั้นใน Go การนำไปใช้อินเทอร์เฟซหนึ่งไม่ต้องการคำสำคัญ implements เพื่อระบุอย่างชัดเจนว่าจะนำไปใช้อินเทอร์เฟซใด ตราบใดที่นำไปใช้เมธอดทั้งหมดของอินเทอร์เฟซหนึ่ง นั่นคือนำไปใช้อินเทอร์เฟซนั้นแล้ว หลังจากมีการนำไปใช้แล้ว ก็สามารถเริ่มต้นอินเทอร์เฟซได้ โครงสร้างบริษัทก่อสร้างประกาศตัวแปรสมาชิกประเภท Crane หนึ่งตัว สามารถเก็บค่าที่นำไปใช้อินเทอร์เฟซ Crane ทั้งหมดได้ เนื่องจากเป็นตัวแปรประเภท Crane ดังนั้นเมธอดที่เข้าถึงได้มีเพียง JackUp และ Hoist เท่านั้น เมธอดอื่นๆ ภายในเช่น Work และ Boot ไม่สามารถเข้าถึงได้
ก่อนหน้านี้ได้กล่าวแล้วว่าประเภทที่กำหนดเองใดๆ สามารถมีเมธอดได้ ดังนั้นตามนิยามของการนำไปใช้ ประเภทที่กำหนดเองใดๆ สามารถนำไปใช้อินเทอร์เฟซได้ ด้านล่างยกตัวอย่างพิเศษบางตัวอย่าง
type Person interface {
Say(string) string
Walk(int)
}
type Man interface {
Exercise()
Person
}เซตเมธอดของอินเทอร์เฟซ Man เป็นซูเปอร์เซตของ Person ดังนั้น Man ก็นำไปใช้อินเทอร์เฟซ Person ด้วย แต่นี่更像เป็นการ "สืบทอด"
type Number int
func (n Number) Say(s string) string {
return "bibibibibi"
}
func (n Number) Walk(i int) {
fmt.Println("can not walk")
}ประเภทพื้นฐานของประเภท Number คือ int แม้สิ่งนี้จะดูน่าตกใจในภาษาอื่น แต่เซตเมธอดของ Number จริงๆ แล้วเป็นซูเปอร์เซตของ Person ดังนั้นก็นับว่านำไปใช้
type Func func()
func (f Func) Say(s string) string {
f()
return "bibibibibi"
}
func (f Func) Walk(i int) {
f()
fmt.Println("can not walk")
}
func main() {
var function Func
function = func() {
fmt.Println("do somthing")
}
function()
}ในทำนองเดียวกัน ประเภทฟังก์ชันก็สามารถนำไปใช้อินเทอร์เฟซได้
Empty Interface
type Any interface{
}อินเทอร์เฟซ Any ไม่มีเซตเมธอด ตามนิยามของการนำไปใช้ ประเภททั้งหมดเป็นการนำไปใช้อินเทอร์เฟซ Any เพราะเซตเมธอดของประเภททั้งหมดเป็นซูเปอร์เซตของเซตว่าง ดังนั้นอินเทอร์เฟซ Any สามารถเก็บค่าของประเภทใดๆ ได้
func main() {
var anything Any
anything = 1
println(anything)
fmt.Println(anything)
anything = "something"
println(anything)
fmt.Println(anything)
anything = complex(1, 2)
println(anything)
fmt.Println(anything)
anything = 1.2
println(anything)
fmt.Println(anything)
anything = []int{}
println(anything)
fmt.Println(anything)
anything = map[string]int{}
println(anything)
fmt.Println(anything)
}ผลลัพธ์
(0xe63580,0xeb8b08)
1
(0xe63d80,0xeb8c48)
something
(0xe62ac0,0xeb8c58)
(1+2i)
(0xe62e00,0xeb8b00)
1.2
(0xe61a00,0xc0000080d8)
[]
(0xe69720,0xc00007a7b0)
map[]ผ่านการส่งออกจะพบว่า ผลลัพธ์การส่งออกสองแบบไม่เหมือนกัน จริงๆ แล้วภายในอินเทอร์เฟซสามารถมองเป็นทูเพิลที่ประกอบด้วย (val,type) ได้ type คือประเภทที่เฉพาะเจาะจง เมื่อเรียกเมธอดจะไปเรียกค่าที่เฉพาะเจาะจงของประเภทที่เฉพาะเจาะจง
interface{}นี่ก็เป็น empty interface เช่นกัน แต่เป็น anonymous empty interface ในการพัฒนามักใช้ anonymous empty interface เพื่อแสดงการรับค่าของประเภทใดๆ ตัวอย่างดังนี้
func main() {
DoSomething(map[int]string{})
}
func DoSomething(anything interface{}) interface{} {
return anything
}ในการอัปเดตต่อมา ทางการได้เสนอวิธีแก้อื่น เพื่อความสะดวก สามารถใช้ any แทน interace{} ได้ ทั้งสองเทียบเท่ากัน เพราะ前者เป็นเพียงชื่อเล่นประเภทหนึ่ง ดังนี้
type any = interface{}เมื่อเปรียบเทียบ empty interface จะเปรียบเทียบประเภทพื้นฐานของมัน หากประเภทไม่ตรงกัน则为 false รองลงมาคือการเปรียบเทียบค่า เช่น
func main() {
var a interface{}
var b interface{}
a = 1
b = "1"
fmt.Println(a == b)
a = 1
b = 1
fmt.Println(a == b)
}ผลลัพธ์คือ
false
trueหากประเภทพื้นฐานไม่สามารถเปรียบเทียบได้ แล้วจะ panic สำหรับ Go แล้ว สถานการณ์ที่ประเภทข้อมูลที่มีอยู่ในตัวสามารถเปรียบเทียบได้มีดังนี้
| ประเภท | สามารถเปรียบเทียบได้ | ตาม |
|---|---|---|
| ประเภทตัวเลข | ใช่ | ค่าเท่ากันหรือไม่ |
| ประเภทสตริง | ใช่ | ค่าเท่ากันหรือไม่ |
| ประเภทอาร์เรย์ | ใช่ | องค์ประกอบทั้งหมดของอาร์เรย์เท่ากันหรือไม่ |
| ประเภทสไลซ์ | ไม่ | ไม่สามารถเปรียบเทียบ |
| สตรักต์ | ใช่ | ค่าฟิลด์เท่ากันทั้งหมดหรือไม่ |
| ประเภท map | ไม่ | ไม่สามารถเปรียบเทียบ |
| แชนเนล | ใช่ | ที่อยู่เท่ากันหรือไม่ |
| พอยน์เตอร์ | ใช่ | ที่อยู่ที่เก็บในพอยน์เตอร์เท่ากันหรือไม่ |
| อินเทอร์เฟซ | ใช่ | ข้อมูลที่เก็บด้านล่างเท่ากันหรือไม่ |
ใน Go มีประเภทอินเทอร์เฟซพิเศษหนึ่งประเภทใช้แทนประเภทที่สามารถเปรียบเทียบได้ทั้งหมด คือ comparable
type comparable interface{ comparable }TIP
หากพยายามเปรียบเทียบประเภทที่ไม่สามารถเปรียบเทียบได้ จะ panic
General Interface
General interface บริการสำหรับ generics ตราบใดที่เข้าใจ generics แล้ว ก็เข้าใจ general interface แล้ว กรุณาย้ายไปที่ Generics
