GO语言学习笔记-接口1
接口是什么?
在面向对象的世界中,接口的意思是:“接口定义了对象的行为”。它只表明对象应该能做什么,而具体怎么做则由对象内部实现。
Go语言中,接口是方法的集合,若某个类型实现了接口中定义的所有方法,则可以说这个类型实现了这个接口。用OOP的形式来说就是: 接口表明了类型应该有哪些方法,而类型则决定如何实现那些方法。
比如,WashingMachine
这个接口中可以定义Cleaning()
和Drying()
方法,任何类型实现了这2个方法都可以说是实现了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中并不需要这个关键字,而是如果某个类型全部定义了接口中的方法,则隐式的实现了这个接口。
第28行,我们将MyString类型的变量name
转换成了VowelsFinder接口类型,这是可以的因为MyString实现了VowelsFinder接口。接下来通过v.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行声明了SalaryCalculator
接口类型并且包含一个CalculateSalary() int
方法。
公司中有2种雇员,Permanent
和Contract
。Permanent的工资由basicpay
和pf
构成,而Contract仅有basicpay
。各自的CalculateSalary
方法在23、28行实现。Permanent和Contract都实现了SalaryCalculator
接口。
36行定义的totalExpense
方法是一种十分漂亮的接口使用案例,这个方法参数是一个由SalaryCalculator
接口类型组成的切片。49行我们把合同工和临时工这2种类型数据组成了一个切片传递给了totalExpense
方法,而totalExpense
方法通过调用各自的CalculateSalary
函数来计算相应的花费。
totalExpense
最大的优势就是可以扩展任意多的类型而不用修改本身代码。假定公司现在又有一个Freelancer
类型雇员而且有不同的薪资构成,只要它实现了SalaryCalculator
接口就可以放到切片参数中。
程序输出为Total Expense Per Month $14050.
。
接口的内部构造
一个接口内部可以认为是由元组(type, value)
构成,type
记录接口的具体类型,value
则记录接口具体类型的值。
通过代码可以更好的理解:
package main
import (
"fmt"
)
type Test interface {
Tester()
}
type MyFloat float64
func (m MyFloat) Tester() {
fmt.Println(m)
}
func describe(t Test) {
fmt.Printf("Interface type %T value %v\n", t, t)
}
func main() {
var t Test
f := MyFloat(89.7)
t = f
describe(t)
t.Tester()
}
接口Test
拥有一个Tester()
方法,并且MyFloat
实现了它,第24行我们转换MyFloat
类型的变量f
为Test
类型并赋值给t
。现在具体类型是MyFloat
并且值为89.7
。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{})
函数使用一个空接口作为参数,所以可以接受任何类型数据。我们分别传递int
、string
、struct
类型数据到函数中,输出如下:
Type = string, value = Hello World
Type = int, value = 55
Type = struct { name string }, value = {Naveen R}
类型断言
类型断言被用于提取接口的隐含值(underlying value)。
i.(T) 用来获取接口的隐含值,接口i的具体类型为T
。
一码胜千言,我们来写个类型断言的例子:
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)
}
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)
}
我们传递string
类型的变量s到assert
函数,并且尝试从中获取一个整数的值,程序将报错panic: interface conversion: interface {} is string, not int
。
解决这个问题可以使用语法
v, ok := i.(T)
如果i的类型是T,那么v将得到i的隐含值并且ok为true
。否则ok为false
并且v为T类型的零值, 程序不会报错!
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类型的零值是0。(roy注:把assert函数中的int改成string则将返回空字符串,零值意思应该是默认初始值。)
56 true
0 false
类型判断
(roy注:原文是Type Switch,内容就是使用switch
语法判断某接口的类型。)
Type Switch
用来比较某个接口的具体类型是否在多个指定的case分支中,和switch-case
语法类似,区别就是case后的类型不再是普通的值。
语法上和类型断言也很像,在i.(T)
语法中,把T用type
关键字替换掉就成了type switch
。代码如下:
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)
指定了一个type switch
,每一个case语句都用来比较i的实际类型和后面指定的类型。如果有匹配的则进行相应的输出。执行结果如下:
I am a string and my value is Naveen
I am an int and my value is 77
Unknown type
88.98是float64
类型不匹配任何一项,所以最后输出Unknow type
。
对某类型和接口进行比较也是可以的,如果我们定义了某个类型并且这个实现了某个接口,那么就可以对这2者进行比较:
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行,变量v
和Describer
接口类型进行比较。p
实现了Describer
所以满足第一个case判断,Describe
函数也被调用。输出如下:
unknown type
Naveen R is 25 years old
这篇文章至此结束,剩下的部分将在第二篇文章中讨论。