当前位置: 首页 > 科技观察

再说说Go中的接口

时间:2023-03-13 07:17:48 科技观察

本文转载自微信公众号“光城”,作者lightcity。转载本文请联系广诚公众号。1.接口在Go中使用interface关键字来声明一个接口:typeShaperinterface{Area()float64Perimeter()float64}如果我们直接使用fmt库进行输出,结果会怎样?funcmain(){varsShaperfmt.Println("valueofsis",s)fmt.Printf("typeofsis%T\n",s)}Output:valueofsistypeofsis这里引入接口的概念。有两种类型的接口。接口的静态类型就是接口本身,比如上面程序中的Shape。接口没有静态值,它们指向动态值。接口类型的变量可以保存实现该接口的类型的值。该类型的值成为接口的动态值,类型成为接口的动态类型。从上面的例子我们可以看出零值和接口都是nil类型。这是因为,此时我们声明了一个Shaper类型的变量s,但是还没有赋值。当我们使用带有接口参数的fmt包中的Println函数时,它指向接口的动态值,而Printf函数中的%T语法指的是动态类型的接口。其实接口静态类型就是Shaper。当我们使用类型来实现接口时会发生什么。typeRectstruct{widthfloat64heightfloat64}func(rRect)Area()float64{returnr.width*r.height}func(rRect)Perimeter()float64{return2*(r.width+r.height)}//mainfuncmain(){varsShaperfmt.Println("valueofsis",s)fmt.Printf("typeofsis%T\n",s)s=Rect{5.0,4.0}r:=Rect{5.0,4.0}fmt.Printf("typeofsis%T\n",s)fmt.Printf("valueofsis%v\n",s)fmt.Printf("areaofrectis%v\n",s.Area())fmt.Println("s==ris",s==r)}Output:valueofsistypeofsistypeofsismain.Rectvalueofsis{54}areaofrectis20s==ristru可以看到此时s变成了动态类型,存放了main.Rect,value变成了{5,4}。有时动态类型接口也称为具体类型,因为当我们访问接口类型时,它返回其底层动态值的类型,而其静态类型保持隐藏状态。我们可以在s上调用Area方法,因为接口Shaper定义了Area方法,而s的具体类型是Rect,它实现了Area方法。将在接口持有的动态值上调用此方法。此外,我们可以看到我们可以使用s与r进行比较,因为这两个变量都具有相同的动态类型(Rect类型的结构)和动态值{54}。然后我们使用圆来实现接口:typeCirclestruct{radiusfloat64}func(cCircle)Area()float64{return3.14*c.radius*c.radius}func(cCircle)Perimeter()float64{return2*3.14*c.radius}//mains=Circle{10}fmt.Printf("typeofsis%T\n",s)fmt.Printf("valueofsis%v\n",s)fmt.Printf("areaoofrectis%v\n",s.Area())此时输出:typeofsismain.Circlevalueofsis{10}areaorefectis314这里进一步了解接口保存的动态类型。从切片的角度来看,可以说接口通过动态保存对底层类型的引用以类似的方式工作。当我们删除Perimeter的实现时,可以看到如下错误结果。./rect.go:34:4:cannotuseRect{...}(typeRect)astypeShaperinassignment:RectdoesnotimplementShaper(missingPerimetermethod)从上面的错误中可以明显看出,要成功实现一个接口,所有使用完全签名接口声明的方法需要实施。2.空接口当一个接口没有任何方法时,它被称为空接口。这由interface{}表示。因为空接口没有方法,所以所有类型都隐式实现了这个接口。空接口的作用之一就是函数可以接收多个不同类型的参数。比如:fmt的Println函数。funcPrintln(a...interface{})(nint,errerror)Println是一个可变函数,它接受interface{}类型的参数。例如:typeMyStringstringfunceexplain(iinterface{}){fmt.Printf("type:%T,value:%v\n",i,i)}//mains:=MyString("hello")explain(s)r:=Rect{1,2}explain(r)output:type:inter.MyString,value:hellotype:inter.Rect,value:{12}可以看到空接口的类型和值是动态的。3.多个接口在下面的程序中,我们使用Area方法创建Shape接口,使用Volume方法创建Object接口。因为struct类型Cube实现了这两个方法,所以它实现了这两个接口。因此,我们可以将结构类型Cube的值赋值给Shape或Object类型的变量。typeIShapeinterface{Area()float64}typeObjectinterface{Volume()float64}typeCubestruct{sidefloat64}func(cCube)Area()float64{return6*c.side*c.side}func(cCube)Volume()float64{returnc.side*c.side*c.side}//mainc:=Cube{3}varsIShape=cvaroObject=cfmt.Println("areais",s.Area())fmt.Println("Volumeis",o.Volume())这个这种调用是没有问题的,调用各自动态类型的方法。如果是这样怎么办?fmt.Println(“areaofsofinterfacetypeIShapeis”,s.Volume())s的类型是IShape,o的静态类型是Object。因为IShape没有定义Volume方法,而Object没有定义Area方法,所以我们得到上面的错误。为了让它工作,我们需要以某种方式提取这些接口的动态值,这是一个Cube类型的结构体,Cube实现了这些方法。这可以使用类型断言来完成。4、类型断言我们可以通过i.(Type)判断接口i底层的动态值,Go会检查i的动态类型是否和type相同,并返回可能的动态值。vars1IShape=Cube{3}c1:=s1.(Cube)fmt.Println("areaofsofinterfacetypeIShapeis",c1.Volume())fmt.Println("volumeofinterfacetypeObjectis",c1.Area())这将正常工作。如果IShape不存储Cube类型,且Cube没有实现IShape,则报错:impossibletypeassertion:CubedoesnotimplementIShape(missingAreamethod)如果IShape不存储Cube类型,且Cube实现了Shape,则报错:panic:interfaceconversion:inter。IShapeisnil,notinter.Cub还好是的,语法中还有一个返回值:value,ok:=i.(Type)在上面的语法中,如果i有一个具体的类型type或者type的动态值,我们可以使用确定要检查的变量。如果不是,则ok将为false,value将为Type的零值(nil)。另外可以通过类型断言来检查接口的动态类型是否实现了其他接口,就像前面IShape的动态类型是Cube,它实现了IShape和Object接口。下例:vaule1,ok1:=s1.(Object)value2,ok2:=s1.(Skin)fmt.Printf("IShapes的动态类型值为:%v,动态类型是否实现了Object接口:%v\n",vaule1,ok1)fmt.Printf("IShapes的动态类型值为:%v,动态类型是否实现了Skin接口:%v\n",value2,ok2)输出:动态类型值IShapes的值为:{3},动态类型是否实现了Object接口:trueIShapes的动态类型值为:,动态类型是否实现了Skin接口:false类型断言不仅用于检查是否接口具有给定类型的特定值,还可以使用给定接口类型的变量转换为不同的接口类型。5.TypeSwitch在前面的空接口中,我们知道,如果使用空接口作为函数参数,那么函数可以接受任何类型,那么如果我有一个需求:当传递的数据类型是字符串时,全部改变required是大写的,其他类型不操作?对于这样的需求,我们可以使用TypeSwitch,即:i.(type)+switch。funcswitchProcess(iinterface{}){switchi.(type){casestring:fmt.Println("processstring")caseint:fmt.Println("processint")default:fmt.Printf("typeis%T\n",i)}}Output:processintprocessstring6.嵌入接口在Go中,一个接口不能实现或扩展其他接口,但我们可以通过合并两个或多个接口来创建一个新的接口。例如:这里使用Runner和Eater这两个接口组合成一个新的接口RunEater,即Embedding接口。typeRunnerinterface{run()string}typeEaterinterface{eat()string}typeRunEaterinterface{RunnerEater}typeDogstruct{ageint}func(dDog)run()string{返回“run”}func(dDog)eat()string{返回“eat”}//maind:=Dog{10}varreRunEater=dvarrRunner=dvareEater=dfmt.Printf("RunnEaterdynamictype:%T,value:%v\n",re,re)fmt.Printf("Runndynamictype:%T,value:%v\n",r,r)fmt.Printf("Eaterdynamictype:%T,value:%v\n",e,e)输出:RunnEaterdynamictype:inter.Dog,value:{10}Runndynamictype:inter.Dog,值:{10}Eaterdynamictype:inter.Dog,值:{10}7.接口比较如果底层动态值为nil,则两个接口总是相等的,这意味着两个nil接口总是相等的,所以==操作返回true。vara,binterface{}fmt.Println(a==b)//true如果这些接口不为nil,那么它们的动态类型(具体值的类型)应该相同,具体值应该相等。如果接口的动态类型是不可比较的,如slice、map、function,或者接口的具体值是一个包含这些不可比较值的复杂数据结构,如slice或array,==或!=操作将导致运行时恐慌。从https://medium.com/rungo/interfaces-in-go-ab1601159b3a学习