golang接口
什么是接口?
在面向对象领域,接口的一般定义是:接口定义了对象的行为。它只指定了对象应该做些什么。至于怎么做,取决于对象本身。
也就是说:接口定义了一些行为(method: method name,method input,method output方法名称,输入,输出),而不规定具体的method是怎么做的。 接口成为了一种接口类型。就如一种模板和约定的形式存在。
具体实现类型:也就是说,实现了接口中所有已定义的方法的类型。就可以说这个类型是某个接口类型的实现类型。 接口类型的实现类型可以有多个。只要这些实现类型都保证实现了方法即可。
运用场景:
- 我们希望能够对某一类对象做统一的动作。那可以定义接口类型。然后各个具体类型去实现自己不同的动作(method)。而我们最终只需要通过接口类型的方法来统一编码即可。
接口的声明与实现
一个定义接口类型和具体实现类型的例子:
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 行,我们给接受者类型(Receiver Type) MyString 添加了方法 FindVowels() []rune。现在,我们称 MyString 实现了 VowelsFinder 接口。这就和其他语言(如 Java)很不同,其他一些语言要求一个类使用 implement 关键字,来显式地声明该类实现了接口。而在 Go 中,并不需要这样。如果一个类型包含了接口中声明的所有方法,那么它就隐式地实现了 Go 接口。
在第 28 行,v 的类型为 VowelsFinder,name 的类型为 MyString,我们把 name 赋值给了 v。由于 MyString 实现了 VowelFinder,因此这是合法的。在下一行,v.FindVowels()调用了 MyString 类型的 FindVowels 方法,打印字符串 Sam Anderson 里所有的元音。该程序输出 Vowels are [a e o]。
接口的实际用途
在生活场景中相当于是,给某一类型(人或事件)进行了归类。形成一个统一的规范,约定(或一个代表)。在我们需要管理的时候,统一按照标准或统一对接这个代表即可。而无需针对到这个归类下的所有每个具体(人或事件)。
而这个在编码的过程的体现就是,我们同样会使用代码来抽象生活中的某一类型(人或事件)。同样需要对这些类型做管理动作,或使用。而增加了这一层关系之后。对于编码来说,就是解除了依赖关系。(解耦)。使用者只需针对标准。当标准变化,操作者只需修改一次代码,接口变化。而某个具体类型下的具体方法,将下方到每个具体类型,去实现。
一个使用接口的实际例子:
我们编写一个简单程序,根据公司员工的个人薪资,计算公司的总支出。为了简单起见,我们假定支出的单位都是美元。
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 { //Permanent的实现
return p.basicpay + p.pf
}
//salary of contract employee is the basic pay alone
func (c Contract) CalculateSalary() int { //Contract的实现
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() //操作者针对类型的统一操作。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。
在公司里,我们有两类员工,即第 11 行和第 17 行定义的结构体:Permanent 和 Contract。长期员工(Permanent)的薪资是 basicpay 与 pf 相加之和,而合同员工(Contract)只有基本工资 basicpay。在第 23 行和第 28 行中,方法 CalculateSalary 分别实现了以上关系。由于 Permanent 和 Contract 都声明了该方法,因此它们都实现了 SalaryCalculator接口。
第 36 行声明的 totalExpense 方法体现出了接口的妙用。该方法接收一个 SalaryCalculator 接口的切片([]SalaryCalculator)作为参数。在第 49 行,我们向 totalExpense 方法传递了一个包含 Permanent 和 Contact 类型的切片。在第 39 行中,通过调用不同类型对应的 CalculateSalary 方法,totalExpense 可以计算得到支出。
这样做最大的优点是:totalExpense 可以扩展新的员工类型,而不需要修改任何代码。假如公司增加了一种新的员工类型 Freelancer,它有着不同的薪资结构。Freelancer只需传递到 totalExpense 的切片参数中,无需 totalExpense 方法本身进行修改。只要 Freelancer 也实现了 SalaryCalculator 接口,totalExpense 就能够实现其功能。
该程序输出 Total Expense Per Month $14050。
接口的内部表示
我们可以把接口看作内部的一个元组 (type, value)。 type 是接口底层的具体类型(Concrete 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 //interface值。 一个接口类型变量,能够存放任何实现了此接口的具体类型的值。如t=f
describe(t)
t.Tester()
}
输出:
Interface type main.MyFloat value 89.7
89.7
所以这里的含义就是,interface内部保存着(具体类型,具体类型值)的元组关系。也就是说,其实是能够从接口类型中获取到具体类型和具体值。
空接口
没有包含方法的接口称为空接口。空接口表示为 interface{}。由于空接口没有方法,因此所有类型都实现了空接口。
空interface在我们需要存储任意类型的数值的时候相当有用,因为它可以存储任意类型的数值。它有点类似于C语言的void*类型。
一个函数把interface{}作为参数,那么他可以接受任意类型的值作为参数,如果一个函数返回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 和 struct。该程序打印:
Type = string, value = Hello World
Type = int, value = 55
Type = struct { name string }, value = {Naveen R}
PS:是否可以理解为所有的类型都是基于空接口,构建出来的。
类型断言
类型断言:像是针对某个接口类型的值,向下去具体实现类型的值。向下转型,接口类型转到具体的实现实例类型上。
使用的方法:x.(T)。语法上它看起来像x.(T)被称为断言类型。x 是一个接口类型。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 //通过空接口,定义一个int类型的变量。
assert(s)
}
------
另外一个示例:
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 函数,试图从它提取出 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 类型的零值,此时程序不会报错。
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)
}
当给 assert 函数传递 Steven Paul 时,由于 i 的具体类型不是 int,ok 赋值为 false,而 v 赋值为 0(int 的零值)。该程序打印:
56 true
另外一个断言的参考示例:
package main
import (
"fmt"
)
//定义一个接口
type Person interface {
Say()
}
//定义一个类型
type Tsh struct {
name string
}
//实现接口的方法
func (tsh *Tsh) Say() {
fmt.Println("我是 Tsh类型的方法,我是", tsh.name)
}
//测试方法
//传递的参数是Person类型
func test(p Person) {
//注意:!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
//如果要使用它的实现实例Tsh的name属性,那么就需要类型断言
name := p.(*Tsh).name // p是接口类型Person *Tsh 是一个具体实现类型。 p.(*Tsh) 这样取到具体类型的值。
fmt.Println(name)
}
func main() {
//实例化
tsh := Tsh{
name: "陶士涵",
}
//传入测试方法
test(&tsh)
}
类型选择(Type Switch)
类型选择用于将接口的具体类型与很多 case 语句所指定的类型进行比较。它与一般的 switch 语句类似。唯一的区别在于类型选择指定的是类型,而一般的 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
还可以将一个类型和接口相比较。如果一个类型实现了接口,那么该类型与其实现的接口就可以互相比较。
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) { //i.(type) 类型选择
case Describer: //实际类型对象传入i之后,类型判断出来为Describer类型。因为实现了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,因此满足了该 case 语句,于是当程序运行到第 32 行的 findType(p) 时,程序调用了 Describe() 方法。
该程序输出:
unknown type
Naveen R is 25 years old
参考链接:
https://blog.csdn.net/cbmljs/article/details/83119447
https://blog.csdn.net/a595364628/article/details/54598227