跳至主要内容

[Golang] interfaces

此篇為各筆記之整理,非原創內容,資料來源可見文末參考資料:

TL;DR

  • interface 的概念有點像是的藍圖,先定義某個方法的名稱(function name)、會接收的參數及型別(list of argument type)、會回傳的值與型別(list of return types)。定義好藍圖之後,並不去管實作的細節,實作的細節會由每個型別自行定義實作(implement)
// 任何型別,只要符合定義規則的話,就可以被納入 bot interface 中
type bot interface {
// getGreeting 這個函式需要接收兩個參數(string, int),並回傳 (string, error) 才符合入會資格
getGreeting(string, int) (string, error)

// getBotVersion 這個函式需要回傳 float 才符合入會資格
getBotVersion() float64
}

Interface 是什麼?

透過 interface 可以定義一系列的 method signatures 來讓 Type 透過 methods 加以實作,也就是說 interface 可以用來定義 type 有哪些行為(behavior)。

interface 就像藍圖一樣,在裡面會定義函式的名稱、接收的參數型別以及最終回傳的資料型別,而 Type 只需要根據這樣的藍圖加以實作(implement)出這些方法。在 Go 中,Type 不需要明確使用 implement 關鍵字來說明它實作了哪個 interface,只要它符合了該 interface 中所定義的 method signature,就等於自動實作了該 interface。

舉例來說,定義「狗」的 interface 包含方法「走路」、「吠」,只要有一個 Type 它能夠提供「走路」和「吠」的方法,那個這個 Type 就自動實作(implement)了「狗」這個 interface,不需要額外使用 implement 關鍵字。

另外,任何資料型別,只要實作了該 interface 之後,都可以被視為該 interface 的 type(polymorphism)。

interface 可以被賦值

  • interface 沒有被賦值前,其 type 和 value 都會是 nil
  • interface 被賦值後,它的型別值會變成實作它的 Type 的型別和值

interface 可以被想成是帶有 (value, type) 的元組(tuple),當我們呼叫某個 interface value 的方法時,實際上就是將該 value 去執行與該 type 相同名稱的方法(method)

  • interface 的變數「動態值(dynamic value / concrete value)」會是實作此 interface 的 Type 的 value
  • interface 的變數「動態型別(dynamic type / concrete type)」會是實作此 interface 的 Type 的型別
  • interface 沒有「靜態值(static value)」
  • interface 的「靜態型別(static type)」,則是該 interface 的本身,例如 type Shape interface{},這個 interface 所建立的變數,其靜態型別即是 Shape

interface 的 dynamic type 又稱作 concrete type,因為當我們想要存取該 interface 的型別時,它回傳的會是 dynamic value,原本的 static type 會被隱藏。

從下面的例子可以看到,目前的 interface 因為尚未被賦值,所以會回傳的是 zero value,從這裡可以看到,interface 的 Type 和 value 的 zero value 都是 nil

// interface 沒有被賦值前,其 type 和 value 都會是 `nil`
type Shape interface {
Area() float64
Perimeter() float64
}

func main() {
var s Shape
fmt.Println("value of s is", s) // value of s is <nil>
fmt.Printf("type of s is %T\n", s) // type of s is <nil>
}

如果 interface 有賦值的話,則可以看到顯示的 dynamic type 和 dynamic value 會是實作該 interface 的 Type 的 method 和 value:

  • Rect type 實作了 Shape interface
  • 當一個 type 實作(implement)了某個 interface 後,該 Type 產生的變數除了會是原本的 Type 外,也同時屬於該 interface type,及 polymorphism
  • 當把 Rect 作為 Shape interface 的值後,Shape 的 Type(dynamic type)會變成 Rect、Value(dynamic value)會變成 Rect 的值({3, 5}
// interface 被賦值後,它的型別值會變成實作它的 Type 的型別和值
type Shape interface {
Area() float64
}

// Rect 實作了 Shape interface
// Rect 所建立的變數同時會符合 Rect(Struct Type)和 Shape(Interface Type)
type Rect struct {
width float64
height float64
}

func (r Rect) Area() float64 {
return r.height * r.width
}

func main() {
var s Shape = Rect{3, 5}
fmt.Printf("(%T, %v) \n", s, s) // (main.Rect, {3 5})
fmt.Println(s.Area()) // 可以直接用 Shape interface 來呼叫方法
}

範例二:

// https://tour.golang.org/methods/11

type I interface {
M()
}

// Type T 實作了 I interface
type T struct {
S string
}
func (t *T) M() {
fmt.Println(t.S)
}

// Type F 實作了 I interface
type F float64
func (f F) M() {
fmt.Println(f)
}

func main() {
var i I

i = &T{"Hello"} // 把 type T 的值賦予給變數 i
fmt.Printf("(%v, %T)\n", i, i) // i 的 dynamic value 是 &{Hello}、 dynamic type 是 *main.T
i.M() // 意思是將 type T 對應的 value (&{Hello}) 來執行 type T 對應的 M 方法

i = F(math.Pi) // 把 type F 的值賦予給變數 i
fmt.Printf("(%v, %T)\n", i, i) // i 的 dynamic value 是 3.141、dynamic type 是 main.F
i.M() // 意思是將 type F 對應的 value (3.1415) 去執行 type F 對應的 M 方法
}

Interface 的 polymorphism

當一個 type 實作了 interface 後,這個 type 所建立的變數除了屬於原本的 type 之外,也屬於這個 interface 的 type,一個變數同時符合多個型別就稱作 polymorphism(多型)

舉例來說,這裡先定義了 Salaried 這個 interface type,接著 Salary 這個 struct type 實作了 Salaried 中定義的 method signatures,因此 Salary 這個 struct type 也同時符合了 Salaried interface type,這樣的行為稱作 polymorphism

// https://medium.com/rungo/structures-in-go-76377cc106a2
type Salaried interface {
getSalary() int
}

// Salary 實作了 getSalary() 的方法,因此可以算是 Salaried type(polymorphism)
type Salary struct {
basic, insurance, allowance int
}

func (s Salary) getSalary() int {
return s.basic + s.insurance + s.allowance
}

type Employee struct {
firstName, lastName string
salary Salaried
}

func main() {
ross := Employee{
firstName: "Ross",
lastName: "Geller",
salary: Salary{
1100, 50, 50,
},
}

fmt.Println("Ross's salary is", ross.salary.getSalary())
}

Interface 會隱性的被 implement

一個 Type 可以透過實作(implement)某一 interface 中的方法來實踐該 interface。舉例來說:

type I interface {
M()
}

type T struct {
S string
}

// 這個方法指的是 type T 會實作 interface I
// 但並不需要開發者主動去宣告它
func (t T) M() {
fmt.Println(t.S)
}

func main() {
// i 的 dynamic type 是 T、dynamic value 是 "hello"
var i I = T{"hello"}
i.M()
}

同時符合多個 interfaces 的 Type

一個 type 可以也可能同時符合多個 interface,以下面的程式碼為例:

  • Cube type 同時實作了 Shape 和 Object interface
  • 可以把 Cube 指派給 Shape 或 Object interface 所建立的變數
  • Shape interface 所建立的變數 s 可以使用 s.Area() 的方法;Object interface 所建立的變數 o 可以使用 o.Volume() 的方法
  • 雖然變數 so 的 dynamic type 都是 Cube,但其底層的 Static Type 不同,前者是 Shape interface、後者是 Object interface,因此雖然它們的 dynamic type 都是 Cube type,但是並不能使用 s.Volume()o.Area()
// 程式來源:https://medium.com/rungo/interfaces-in-go-ab1601159b3a
type Shape interface {
Area() float64
}

type Object interface {
Volume() float64
}

type Cube struct {
side float64
}

func (c Cube) Area() float64 {
return 6 * (c.side * c.side)
}

func (c Cube) Volume() float64 {
return c.side * c.side * c.side
}

func main() {
c := Cube{3}
var s Shape = c
var o Object = c
fmt.Println("volume of s of interface type Shape is", s.Area()) // 54
fmt.Printf("Shape (%T, %v) \n", s, s) //Shape (main.Cube, {3})
fmt.Println("area of o of interface type Object is", o.Volume()) // 27
fmt.Printf("Object (%T, %v) \n", o, o) // Object (main.Cube, {3})
}

Type Assertions

雖然 so 的 dynamic type 都是 Cube,但因為它們底層的 static type 並不相同,前者是 Shape 後者是 Object,因此不能呼叫 s.Volume()o.Area() 的方法。為了避免呼叫到底層並不對應的 static type,可以使用 type assertion 的方式:

value := i.(Type)        // 得到實作 interface 的 Type 的值
  • i : type interface
  • Type:實作該 interface 的 type
func main() {

var s Shape = Cube{3}

// 原本不能執行 s.Volume(),但把 s 轉換成 Cube 後得到 c,即可使用 c.Volume()
c := s.(Cube) // i.(T) 可以把 interface(i) 當中的 T 的值取出來

fmt.Println("volume of s of interface type Shape is", c.Area()) // 54
fmt.Printf("Shape (%T, %v) \n", c, c) //Shape (main.Cube, {3})
fmt.Println("area of o of interface type Object is", c.Volume()) // 27
fmt.Printf("Object (%T, %v) \n", c, c) // Object (main.Cube, {3})
}

然而,如果「Cube Type 沒有實作 interface 的方法」,或者「雖然 Cube Type 有實作 interface 的方法,但 i 並沒有該 Type 的 concrete value 的話」都會報錯。例如:

// 由於 s 沒有被實際賦職,因此 c 沒有 dynamic/concrete value(nil)
// 因此使用 c.Area() 時會出現 panic
func main() {
var s Shape
c := s.(Cube)
fmt.Println(c.Area()) // panic: interface conversion: main.Shape is nil, not main.Cube
}

為了避免這種情況可以使用:

value, ok := i.(Type)
  • ok :當 i 有 dynamic type 和 dynamic value 時,ok 會是 truevalue 則會是 dynamic value;否則 ok 會是 falsevalue 會是該 Type 的 zero value。

將變數的 interface 轉換成另一個 interface

v := i.(I)    // i 是原本的 interface,I 轉變成的 interface

Type Assertions 除了用來確保某一個 interface 是否有 dynamic value / concrete value 之外,也可以用來將一個變數從原本的 interface 轉成另一個 interface:

// Person interface
type Person interface {
getFullName() string
}

// Salaried interface
type Salaried interface {
getSalary() int
}

// Employee struct
type Employee struct {
firstName string
lastName string
salary int
}

// make Employee implements Person interface
func (e Employee) getFullName() string {
return e.firstName + " " + e.lastName
}

// make Employee implements Salaried interface
func (e Employee) getSalary() int {
return e.salary
}

func main() {
// johnPerson 原本是 Person interface,只能使用 Person interface 中的 getFullName 方法
var johnPerson Person = Employee{"John", "Adams", 2000}
fmt.Printf("full name: %v \n", johnPerson.getFullName())

// 使用 i.(I),可以把原本屬於 Person interface 的 johnPerson 轉成 Salaried interface
johnSalary := johnPerson.(Salaried)
fmt.Printf("salary : %v \n", johnSalary.getSalary())
}

Embedding interfaces

在 Go 中,我們可以將兩個 interface 組合成一個新的 interface:

// 程式碼來源:https://medium.com/rungo/interfaces-in-go-ab1601159b3a
type Shape interface {
Area() float64
}

type Object interface {
Volume() float64
}

// `Material` interface 是由 Shape 和 Object 組合而成
type Material interface {
Shape
Object
}

// 也可以 embedding 其他 interface 並加上 methods
type AreaPrice interface {
Shape
Object
Price() (int, error)
}
  • Material interface 是由 Shape 和 Object 組合而成
  • AreaPrice interface 是由 ShapeObjectPrice method 組成

Interfaces 試圖解決的問題

問題:共用相同邏輯但帶入不同型別參數的函式

如果某一個函式內部運作的邏輯相同,我們是否還需要只因為參數型別的不同而撰寫不同的 function 呢?舉例來說,同樣是 shuffle 這個方法,是否會因為傳入的型別不同,而需要建立多種不同的函式?

Interfaces 的使用

在程式中的任何 type,只要這個 type 的函式有符合到該 interface 的定義,就可以歸類到該 interface 底下

範例一:polymorphism

在這個例子中,square type 的方法 area() 因為符合 shape interface 的定義,所以 square type 也一併被歸類在 shape interface 中(polymorphism):

// STEP 1:定義 Shape 這個 interface
type Shape interface {
area() int
}

// STEP 2:定義 square type
type Square struct {
sideLength int
}

// STEP 3:定義 area 這個 Square type 的 methods
// 因為 Square type 的 area method 符合 Shape interface 的規範
// 所以 Square type 同樣屬於 Shape interface
func (s Square) area() int {
return s.sideLength * s.sideLength
}

func main() {
// STEP 5:定義 Square type 的變數,ten 了符合 Square Type 之外,也同時符合 Shape Interface
ten := Square{sideLength: 10}

// STEP 6:printArea 中可以帶入 Shape type
// 因為 tne 現在同時屬於 Shape interface,所以可以放入 printArea 這個 function
printArea(ten)
}

// STEP 4:printArea 這個 function 可以帶入 Shape interface 作為參數
func printArea(s Shape) {
fmt.Println(s.area())
}

範例二

// STEP 1:定義 Bot type,它本質上是 interface
type Bot interface {
// 在程式中的任何 type,只要是名稱為 getGreeting 而且回傳 string 的函式
// 將自動升級變成 "Bot" 這個 type 的成員
getGreeting() string
}

// STEP 2:宣告兩個 struct type
type EnglishBot struct{}
type SpanishBot struct{}

// STEP 3:此 receiver function 名稱為 getGreeting 且回傳 string,因此屬於 Bot type
func (EnglishBot) getGreeting() string {
// VERY custom logic for generating an english greeting
return "Hi There!"
}

// STEP 4:此 receiver function 名稱為 getGreeting 且回傳 string,因此屬於 Bot type
func (SpanishBot) getGreeting() string {
return "Hola!"
}

func main() {
eb := EnglishBot{}
sb := SpanishBot{}

// STEP 6:現在 eb 和 sb 都算是 Bot type
printGreeting(eb)
printGreeting(sb)
}

// STEP 5:printGreeting 可以傳入 Bot interface
func printGreeting(b Bot) {
fmt.Println(b.getGreeting())
}

範例三

// STEP 1: 函式名稱為 Abs 且回傳 float64 即屬於 Abser type
type Abser interface {
Abs() float64
}

// STEP 2:定義一個 MyFloat type 且其 receiver function 符合 Abser interface 的規範
// MyFloat 會屬於 Abser type
type MyFloat float64

func (f MyFloat) Abs() float64 {
if f < 0 {
return float64(-f)
}
return float64(f)
}

// STEP 3:定義一個 Vertex type 且其 receiver function 符合 Abser interface 的規範
// Vertex 會屬於 Abser type
type Vertex struct {
X, Y float64
}

func (v *Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
var a Abser
f := MyFloat(-math.Sqrt2)
v := Vertex{3, 4}

a = f // MyFloat 可以 implement Abser type
fmt.Println(a.Abs()) // 1.4142

a = &v // *Vertex 可以 implement Abser type
fmt.Println(a.Abs()) // 5

// Cannot use 'v' (type Vertex) as type Abser.
// Vertex 不能 implement Abser type,因為 Abs 這個方法有 pointer receiver
a = v
}

範例四

// 程式來源:https://coolshell.cn/articles/21128.html
type Country struct {
Name string
}

type City struct {
Name string
}

type Stringable interface {
ToString() string
}

func (c Country) ToString() string {
return "Country = " + c.Name
}

func (c City) ToString() string {
return "City = " + c.Name
}

func PrintStr(p Stringable) {
fmt.Println(p.ToString())
}

func main() {
d1 := Country{"USA"}
d2 := City{"Los Angles"}
PrintStr(d1)
PrintStr(d2)
}

範例五:以 io.Reader 這個 interface 為例

如果它不是 interfaces 的話,為了輸出各種不同的 input,會需要建立多個接受不同型別作為參數的 function:

Imgur

透過 Interface 的使用,將可以簡化成下面這樣:

Imgur

使用 Interface 建立自己的 function

Stringer Interface 為例

Stringer @ golang

可以看到原本的 Stringer Interface 長這樣:

type Stringer interface {
String() string
}

也就是說,任何 Type 底下只要有 String() 這個方法且回傳 string,都會被歸到 Stringer interface。

幫 Person type 客製化自己的 String 方法

因此,我們可以建立一個 type,並為它添加 String() 方法後,它就會被歸類在 Stinger interface:

type Person struct {
Name string
Age int
}

// Person 這個 type 有 String 方法,且會回傳 string,因此可被歸在 Stringer interface
func (p Person) String() string {
return fmt.Sprintf("%v (%v years)", p.Name, p.Age)
}

func main() {
a := Person{"Arthur", 42} // ({Name:Arthur Age:42}, main.Person)
z := Person{"Zaphod Beeblebrox", 9001} // ({Name:Zaphod Beeblebrox Age:9001}, main.Person)

fmt.Println(a, z) // 會呼叫到 a.String() 和 z.String()
// Arthur (42 years) Zaphod Beeblebrox (9001 years)
}

幫 IPAddr type 客製化自己的 String 方法

// A Tour of Go: Exercise: Stringers
// https://tour.golang.org/methods/18

type IPAddr [4]byte

// String() string 符合 Stringer interface
func (ip IPAddr) String() string {
var ips []string
for _, ipNumber := range ip {
ips = append(ips, strconv.Itoa(int(ipNumber)))
}
return strings.Join(ips, ",")
}

func main() {
hosts := map[string]IPAddr{
"loopback": {127, 0, 0, 1},
"googleDNS": {8, 8, 8, 8},
}

for name, ip := range hosts {
fmt.Printf("%v: %v\n", name, ip)
}
}

Writer Interface 為例

// STEP 1:建立一個 logWriter 的 type
type logWriter struct{}

func main() {
resp, err := http.Get("https://pjchender.github.io")
if err != nil {
fmt.Printf("Error: %v", err)
os.Exit(1)
}

// STEP 3:建立 logWriter
lw := logWriter{}

// STEP 4:因為 logWriter 已經歸類在 Writer Interface,所以可以帶入 io.Copy 內
io.Copy(lw, resp.Body)
}

// STEP 2:根據 Writer Interface 的定義(https://golang.org/pkg/io/#Writer)
// 來撰寫 logWriter 的 Write function
// 如此,它將會被歸類在 Writer Interface 內
func (logWriter) Write(bs []byte) (int, error) {
fmt.Println(string(bs))
fmt.Println("Just wrote this many bytes: ", len(bs))
return len(bs), nil
}

使用慣例

當 interface 中只有一個 method 時,則會把這個 interface 的名稱以該 method 的名稱加上 -er 做結尾,例如:

// 這個 interface 中只有 Save 這個方法,所以 interface 取名做 saver
type saver interface {
Save() error
}

// 這個 interface 中只有 Write 這個方法,所以 interface 取名做 writer
type writer interface {
Write() error
}

其他

Empty interface (any)

沒有定義任何方法的 interface 稱作 empty interface,由於所有的 types 都能夠實作 empty interface,因此它的值會是 any type,也可以使用 any 這個 keyword,意思是一樣的:

type I interface{} // 等同於 any

func main() {
var i I
describe(i) // (<nil>, <nil>)

i = 42
describe(i) //(42, int)

i = "hello"
describe(i) // (hello, string)

}

func describe(i I) {
fmt.Printf("(%v, %T)\n", i, i)
}

Type assertions

透過 type assertion ,可以判斷某個資料是否符合某個型別,若符合,則取得其值:

// 斷定 interface 的 concrete type 是 T,並將 T 的 value 指派到變數 t
v := someData.(T) // 如果型別不正確會直接 panic

// 如果要檢測某 interface 是否包含某一個 type,則需要接收兩個回傳值- underlying value 和 assertion 是否成功
v, ok := someData.(T)
// 如果 someData 的型別是 T,則 someData 會得到 underlying value,而 ok 會是 true
// 如果 someData 的型別不是 T,則 someData 會得到 type T 的 zero value,且 ok 會是 false

v, ok := someData.(int)

範例:

type user struct {
username string
password string
}

func printSomething(someData interface{}) {
v, ok := someData.(user)
if ok {
fmt.Printf("username is '%v' and password is %v\n", v.username, v.password)
}
}

Type switches

type switches @ A Tour of Go

type switch 很適合做為某一個會接收多種型別方法的寫法,在該方法中透過 type switch 的方式來根據不同的型別回傳不同的內容或執行不同的行為,其語法是 switch someData.(type){}

提示

使用 Type Switch 不只可以用來判斷 Basic Types,也可以用來判斷自訂的 Type:

type user struct {
username string
password string
}

func printSomething(someData interface{}) {
switch v := someData.(type) {
case string:
fmt.Printf("'%v' is string\n", v)
case int:
fmt.Printf("'%v' is int\n", v)
case user:
// 也可以用來判斷自訂的 Type,例如這裡的 user
fmt.Printf("username is '%v' and password is %v\n", v.username, v.password)
default:
fmt.Printf("%v is unhandled type\n", v)
}
}

type switch 和一般的 switch 語法相同,只是 switch 判斷的內容是使用 type assertion(value.(type))、在 case 的地方則是判斷某一 interface value 的型別:

注意

不同於 Type Assertions 中 value.(T) 中的 T 指的是型別,Type switch 中 value.(type)type 是固定的關鍵字,只能用 type ,不能用其他字,且只能在 switch 中使用。

Generic Type

在 Golang 中,Generic Type 的使用方式如下:

// T 是 Generic Type,後面要放該泛型可以接受的型別
func add[T any](a, b T) T {}

// 限制 T 可以接受的型別
func add[T int|float64|string](a, b T) T{
return a + b
}

// 多個 Generic Types
func multiple[T any, K any](a, b T) K {}

參考