欢迎来到Golang系列教程 的第18部分,这是接口教程 2 小部分的第 1 部分。
什么是接口?
在面向对象世界接口的一般定义是“接口定义一个对象的行为”。它只指定对象应该做什么。获取这个行为(实现细节)的方式是由对象决定。
在Go中,接口是一组方法的签名。当类型提供了接口中所有方法的定义时,我们说实现了这个接口。这个面向对象世界很相似。接口指定类型应该有什么方法和类型决定如何实现这些方法。
例如WashingMachine可以是拥有 Cleaning() 和 Drying() 方法签名的接口。任何提供 Cleaning() 和 Drying() 方法签名定义的类型,就叫实现了 WashingMachine 接口。
声明和实现接口
让我们直接进行程序,创建一个接口并实现它。
package main
import (
"fmt"
)
//interface definition
type VowelsFinder interface {
FindVowels() []rune
}
type MyString string
//MyString implements VowelsFinder
func (ms MyString) FindVowels() []rune {
var vowels []rune
for _, rune := range ms {
if rune == 'a' || rune == 'e' || rune == 'i' || rune == 'o' || rune == 'u' {
vowels = append(vowels, rune)
}
}
return vowels
}
func main() {
name := MyString("Sam Anderson")
var v VowelsFinder
v = name // possible since MyString implements VowelsFinder
fmt.Printf("Vowels are %c", v.FindVowels())
}
在上面程序的第 8 行创建了一个名为 VowelsFinder
的接口类型,它有一个方法 FindVowels() []rune
。
在下一行,创建了 MyString
类型。
在第 15 行我们给接口器类型 MyString
增加方法 FindVowels() []rune
。现在就说 MyString
实现了 VowelsFinder
接口。这和其他语言截然不同,比如Java,类必须使用关键字 implements
显式声明它实现一个接口。这在 go 中是不需要的,如果一个类型包含在接口中声明的所有方法,go 接口被隐式实现。
在第 28 行,我们将 MyString
类型的 name
赋值给 VowelFinder
类型的 v 。这是可以的,因为 MyString
实现了 VowelFinder
接口。在下一行在 MyString
类型上调用 FindVowels 方法打印字符串 Sam Anderson
中所有的元音。这个程序输出 Vowels are [a e o]
。
恭喜,你已经创建并实现了你第一个接口。
接口的实际使用
上面程序教了我们如何创建并实现接口,但它并没有真正的展示接口的使用。如果我们在上面的程序中使用 name.FindVowels()
代替 v.FindVowels()
,它也会工作但没有使用已经创建的接口。
现在让我们来看一下接口的实际用例。
我们将写一个简单的程序,来基于一个公司雇员的个人薪水计算公司的总费用。为了简短,我们假设所有的花费是美元。
package main
import (
"fmt"
)
type SalaryCalculator interface {
CalculateSalary() int
}
type Permanent struct {
empId int
basicpay int
pf int
}
type Contract struct {
empId int
basicpay int
}
//salary of permanent employee is sum of basic pay and pf
func (p Permanent) CalculateSalary() int {
return p.basicpay + p.pf
}
//salary of contract employee is the basic pay alone
func (c Contract) CalculateSalary() int {
return c.basicpay
}
/*
total expense is calculated by iterating though the SalaryCalculator slice and summing
the salaries of the individual employees
*/
func totalExpense(s []SalaryCalculator) {
expense := 0
for _, v := range s {
expense = expense + v.CalculateSalary()
}
fmt.Printf("Total Expense Per Month $%d", expense)
}
func main() {
pemp1 := Permanent{1, 5000, 20}
pemp2 := Permanent{2, 6000, 30}
cemp1 := Contract{3, 3000}
employees := []SalaryCalculator{pemp1, pemp2, cemp1}
totalExpense(employees)
}
在上面程序的第 7 行声明了包含一个方法 CalculateSalary() int
的 SalaryCalculator
接口类型。
在公司中,有两类雇员,在第 11 行和 17 行被结构体定义的 Permanent
和 Contract
。长期雇员的薪水是 basicpay
和 pf
的和,而合同雇员的薪水只有其他的 basicpay
。它们相应的 CalculateSalary
方法分别在第 23 行和 28 行表述。通过声明这个方法,Permanent
和 Contract
都实现了 SalaryCalculator
接口。
在 36 行声明的 totalExpense
函数表达了使用接口的美妙之处。这个方法使用一个 SalaryCalculator 接口切片作为参数,在 49 行,我们向 totalExpense
函数传递包含 Permanent
和 Contract
类型的切片。totalExpense
函数通过调用相应类型的 CalculateSalary
方法计算花费。这在 39 行完成。
这个程序的最大优势是 totalExpense
可以通过增加任何新的雇员类型来扩展而不用任何代码。我们说公司增加了一个使用不同薪水结构的新的雇员类型 Freelancer
。Freelancer
可以公通过切片参数传给 totalExpense
,甚至不用修改 totalExpense
函数的一行代码。这个方法将执行它就的操作,因为 Freelancer
也实现了 SalaryCalculator
接口。
这个程序输出 Total Expense Per Month $14050
。
接口内部表示
接口可以被认为是元组 (type, value)
的内部表示。 type
是接口的基础具体类型,value
保存具体类型的值.
我们来写一个程序以更好的理解它。
package main
import (
"fmt"
)
type Tester interface {
Test()
}
type MyFloat float64
func (m MyFloat) Test() {
fmt.Println(m)
}
func describe(t Tester) {
fmt.Printf("Interface type %T value %v\n", t, t)
}
func main() {
var t Tester
f := MyFloat(89.7)
t = f
describe(t)
t.Test()
}
Tester 接口有一个方法 Test()
,MyFloat 类型实现了这个接口。在 24 行,我们将 MyFloat
类型的变量 f
赋值给 Tester
类型的 t
。现在 t 的具体类型是 MyFloat
,t 的值是 89.7
。第 17 行的 describe
函数打印它的值和接口的具体类型。程序输出
Interface type main.MyFloat value 89.7
89.7
空接口
没有任何方法的接口被称为空接口。它表示为 interface{}
。由于空接口没有方法,所有的类型实现空接口。
package main
import (
"fmt"
)
func describe(i interface{}) {
fmt.Printf("Type = %T, value = %v\n", i, i)
}
func main() {
s := "Hello World"
describe(s)
i := 55
describe(i)
strt := struct {
name string
}{
name: "Naveen R",
}
describe(strt)
}
在上面程序中,在 7 行,函数 describe(i interface{})
使用一个空接口作为参数,因此它可以被传递任何参数。
我们分别在 13, 15 和 21 行向 describe
函数传递 string, int 和 结构体,程序打印
Type = string, value = Hello World
Type = int, value = 55
Type = struct { name string }, value = {Naveen R}
类型断言
类型断言用于提取接口底层的值。
**i.(T)**是用来获取具体类型为 T
的接口 i
的底层值的语法。
程序胜千言,我们写一个类型断言的程序
package main
import (
"fmt"
)
func assert(i interface{}) {
s := i.(int) //get the underlying int value from i
fmt.Println(s)
}
func main() {
var s interface{} = 56
assert(s)
}
在 12 行 s
的具体类型是 int
。我们在第 8 行使用语法 i.(int)
来获取 i 的底层的值。这个程序打印 56
。
如果上面程序的具体类型不是 int 会发生什么?我们来找出它。
package main
import (
"fmt"
)
func assert(i interface{}) {
s := i.(int)
fmt.Println(s)
}
func main() {
var s interface{} = "Steven Paul"
assert(s)
}
在上面程序我们向函数 assert
传递具体类型为 string
的 s
,试图从它提取 int 值。这个程序将使用消息 panic: interface conversion: interface {} is string, not int
的恐慌。
为了解决这个总是我们使用语法
v, ok := i.(T)
如果 i
的具体类型为 T
,那么 v
将会有 i
的底层的值,同时 ok
的值是 true。
如果 i
的具体类型不是 T
。ok
是 false,同时 v
将有一个类型 T
的零值,程序没有恐慌(panic)。
package main
import (
"fmt"
)
func assert(i interface{}) {
v, ok := i.(int)
fmt.Println(v, ok)
}
func main() {
var s interface{} = 56
assert(s)
var i interface{} = "Steven Paul"
assert(i)
}
当将 Steven Paul
传递给函数 assert
。ok
将是 false,由于 i
的具体类型不是 int
,同时 v
的值将是 0 ,它是 int 的零值,程序将打印
56 true
0 false
类型开关(Switch)
类型开关用来将接口的具体类型与各个在 case 语句指定的多个类型相比较。它和 switch case 很相。唯一不同的是 cases 指定的是类型而不是正常 switch 中的值。
类型开关的语法类似于类型断言。在类型断言 i.(T)
的语法中,类型 T
应该被类型开关的关键字 type
替换。让我们在下面的程序中看下它是如何工作的。
package main
import (
"fmt"
)
func findType(i interface{}) {
switch i.(type) {
case string:
fmt.Printf("I am a string and my value is %s\n", i.(string))
case int:
fmt.Printf("I am an int and my value is %d\n", i.(int))
default:
fmt.Printf("Unknown type\n")
}
}
func main() {
findType("Naveen")
findType(77)
findType(89.98)
}
在上面程序的第 8 行,switch i.(type)
指定一个类型开关。每个 case 语句将 i
的具体类型与指定类型相比较。如果匹配任何 case,相应的语句被打印。程序输出
I am a string and my value is Naveen
I am an int and my value is 77
Unknown type
在 20 行89.98是 float64
类型,与任何 case 都不匹配,因此在最下面的那行打印 Unknown type
。
比较一个类型与接口也是可以的。如果我们有一个类型并且类型实现了一个接口。可以比较这个类型与它实现的接口。
程序可以更加清晰
package main
import "fmt"
type Describer interface {
Describe()
}
type Person struct {
name string
age int
}
func (p Person) Describe() {
fmt.Printf("%s is %d years old", p.name, p.age)
}
func findType(i interface{}) {
switch v := i.(type) {
case Describer:
v.Describe()
default:
fmt.Printf("unknown type\n")
}
}
func main() {
findType("Naveen")
p := Person{
name: "Naveen R",
age: 25,
}
findType(p)
}
在上面程序中,结构体 Person
实现了 Describer
接口。在 19 行的 case 语句,v
与 Describer
接口类型比较。p
实现了 Describer
。因此在 32 行,控制命令 findType(p)
,这个 case 满足这种情况,Describe()
方法被调用。
这个程序输出
unknown type
Naveen R is 25 years old
这将我们带到了接口第一部分的结尾,我们将在接口第二部分继续讨论。
**下一教程 - 接口 - II