golang接口---中(代码片段)

大忽悠爱忽悠 大忽悠爱忽悠     2023-03-15     435

关键词:

GoLang接口---中


引言

GoLang接口—上

上一篇文章中,我们对接口的基本使用和底层实现做了简单的了解,本文对接口的一些使用技巧做相关陈述。


接口的类型断言

一个接口类型的变量 varI 中可以包含任何类型的值,必须有一种方式来检测它的 动态 类型,即运行时在变量中存储的值的实际类型。

func main() 
	var shaper Shaper

	if rand.Intn(10) > 5 
		shaper = Circleradius: 12.1
	 else 

		shaper = Squareside: 12.1
	

	println(shaper)


显然,上面这段程序,只有在运行时才能确定shaper接口中存储的值的类型

那么,有没有什么方法可以来判断当前shaper接口中存储的值的类型到底是什么呢?

通常我们可以使用 类型断言 来测试在某个时刻 varI 是否包含类型 T 的值:

v := varI.(T)       // unchecked type assertion

varI 必须是一个接口变量,否则编译器会报错:

invalid type assertion: varI.(T) (non-interface type (type of varI) on left)

类型断言可能是无效的,虽然编译器会尽力检查转换是否有效,但是它不可能预见所有的可能性。如果转换在程序运行时失败会导致错误发生。更安全的方式是使用以下形式来进行类型断言:

if v, ok := varI.(T); ok   // checked type assertion
    Process(v)
    return

// varI is not of type T

如果转换合法,vvarI 转换到类型 T 的值,ok 会是 true;否则 v 是类型 T 的零值,okfalse,也没有运行时错误发生。


这里 接口变量.(接口实现类的类型) 的操作可以理解为将父类类型强制转换为子类类型后返回,但是转换的前提是,实现类必须实现了当前接口的所有方法才行,否则go编译会报错


实例演示

package main

import (
	"fmt"
)

type Square struct 
	side float32



func (sq Square) Area() float32 
	return sq.side * sq.side


type Shaper interface 
	Area() float32


func main() 
	var shaper Shaper
    //调用方法为结构体---此时接口中保存的值类型为结构体
	shaper = Squareside: 250
	
	testValueType(shaper)
	testPointerType(shaper)
    //调用方为指针--此时接口中保存的值类型为指针
	shaper = &Squareside: 520

	testValueType(shaper)
	testPointerType(shaper)


func testValueType(shaper Shaper) 
	println("Test ValueType...")
	//判断接口中保存的值类型是否为结构体
	if square, ok := shaper.(Square); ok 
		fmt.Println("当前Shaper接口中保存的值类型为Square, ", square)
	 else 
		fmt.Println("当前Shaper接口中保存的值类型不是Square")
	

	fmt.Println("-----------------------------------------------")


func testPointerType(shaper Shaper) 
	println("Test PointerType...")
	//判断接口中保存的值类型是否为指针
	if square, ok := shaper.(*Square); ok 
		fmt.Println("当前Shaper接口中保存的值类型为Square, ", square)
	 else 
		fmt.Println("当前Shaper接口中保存的值类型不是Square")
	

	fmt.Println("-----------------------------------------------")



打印输出结果的具体原因,可以参考下面这幅图思考:

因为对于接口而言,当方法接受者为指针时,只有指针才能调用该方法,因此如果将上面的例子改一下:

package main

import (
	"fmt"
)

type Square struct 
	side float32


//方法接受者为指针,只有指针实现才能调用该方法
func (sq *Square) Area() float32 
	return sq.side * sq.side


type Shaper interface 
	Area() float32


func main() 
	var shaper Shaper
   
	//这里就无法结构体实现的Square就没有实现Shaper接口了
	//shaper = Squareside: 250
	//
	//testValueType(shaper)
	//testPointerType(shaper)

	shaper = &Squareside: 520

	//testValueType(shaper)
	testPointerType(shaper)


//func testValueType(shaper Shaper) 
//	println("Test ValueType...")
//这里Square结构体并没有实现Shaper接口,因此不能直接这样进行强制类型转换
//	if square, ok := shaper.(Square); ok 
//		fmt.Println("当前Shaper接口中保存的值类型为Square, ", square)
//	 else 
//		fmt.Println("当前Shaper接口中保存的值类型不是Square")
//	
//
//	fmt.Println("-----------------------------------------------")
//

func testPointerType(shaper Shaper) 
	println("Test PointerType...")
	if square, ok := shaper.(*Square); ok 
		fmt.Println("当前Shaper接口中保存的值类型为Square, ", square)
	 else 
		fmt.Println("当前Shaper接口中保存的值类型不是Square")
	

	fmt.Println("-----------------------------------------------")



类型判断:type-switch

接口变量的类型也可以使用一种特殊形式的 switch 来检测:type-switch

switch t := areaIntf.(type) 
case *Square:
    fmt.Printf("Type Square %T with value %v\\n", t, t)
case *Circle:
    fmt.Printf("Type Circle %T with value %v\\n", t, t)
case nil:
    fmt.Printf("nil value: nothing to check?\\n")
default:
    fmt.Printf("Unexpected type %T\\n", t)

变量 t 得到了 areaIntf 的值和类型, 所有 case 语句中列举的类型(nil 除外)都必须实现对应的接口(在上例中即 Shaper),如果被检测类型没有在 case 语句列举的类型中,就会执行 default 语句。

可以用 type-switch 进行运行时类型分析,但是在 type-switch 不允许有 fallthrough 。

如果仅仅是测试变量的类型,不用它的值,那么就可以不需要赋值语句,比如:

switch areaIntf.(type) 
case *Square:
    // TODO
case *Circle:
    // TODO
...
default:
    // TODO


nil 和 non-nil

我们可以通过一个例子理解『Go 语言的接口类型不是任意类型』这一句话,下面的代码在 main 函数中初始化了一个 *TestStruct 结构体指针,由于指针的零值是 nil,所以变量 s 在初始化之后也是 nil:

package main

type TestStruct struct

func NilOrNot(v interface) bool 
    return v == nil


func main() 
    var s *TestStruct
    fmt.Println(s == nil)      // #=> true
    fmt.Println(NilOrNot(s))   // #=> false


$ go run main.go
true
false

我们简单总结一下上述代码执行的结果:

  1. 将上述变量与 nil 比较会返回 true;
  2. 将上述变量传入 NilOrNot 方法并与 nil 比较会返回 false;

出现上述现象的原因是调用 NilOrNot 函数时发生了隐式的类型转换,除了向方法传入参数之外,变量的赋值也会触发隐式类型转换。在类型转换时,*TestStruct 类型会转换成 interface 类型,转换后的变量不仅包含转换前的变量,还包含变量的类型信息 TestStruct,所以转换后的变量与 nil 不相等。


空接口

空接口或者最小接口 不包含任何方法,它对实现不做任何要求:

type Any interface 

任何其他类型都实现了空接口(它不仅仅像 Java/C# 中 Object 引用类型),any 或 Any 是空接口一个很好的别名或缩写。

空接口类似 Java/C# 中所有类的基类: Object 类,二者的目标也很相近。

可以给一个空接口类型的变量 var val interface 赋任何类型的值。

示例:

package main

import "fmt"

var i = 5

var str = "ABC"

type Person struct 
    name string
    age  int


type Any interface

func main() 
    var val Any
    val = 5
    fmt.Printf("val has the value: %v\\n", val)
    val = str
    fmt.Printf("val has the value: %v\\n", val)
    pers1 := new(Person)
    pers1.name = "Rob Pike"
    pers1.age = 55
    val = pers1
    fmt.Printf("val has the value: %v\\n", val)
    switch t := val.(type) 
    case int:
        fmt.Printf("Type int %T\\n", t)
    case string:
        fmt.Printf("Type string %T\\n", t)
    case bool:
        fmt.Printf("Type boolean %T\\n", t)
    case *Person:
        fmt.Printf("Type pointer to Person %T\\n", t)
    default:
        fmt.Printf("Unexpected type %T", t)
    

输出:

val has the value: 5
val has the value: ABC
val has the value: &Rob Pike 55
Type pointer to Person *main.Person

在上面的例子中,接口变量 val 被依次赋予一个 int,string 和 Person 实例的值,然后使用 type-switch 来测试它的实际类型。每个 interface 变量在内存中占据两个字长:一个用来存储它包含的类型,另一个用来存储它包含的数据或者指向数据的指针。


构建通用类型或包含不同类型变量的数组

通过使用空接口。让我们给空接口定一个别名类型 Element:type Element interface

然后定义一个容器类型的结构体 Vector,它包含一个 Element 类型元素的切片:

type Vector struct 
    a []Element

Vector 里能放任何类型的变量,因为任何类型都实现了空接口,实际上 Vector 里放的每个元素可以是不同类型的变量。我们为它定义一个 At() 方法用于返回第 i 个元素:

func (p *Vector) At(i int) Element 
    return p.a[i]

再定一个 Set() 方法用于设置第 i 个元素的值:

func (p *Vector) Set(i int, e Element) 
    p.a[i] = e

Vector 中存储的所有元素都是 Element 类型,要得到它们的原始类型(unboxing:拆箱)需要用到类型断言。

TODO:The compiler rejects assertions guaranteed to fail,类型断言总是在运行时才执行,因此它会产生运行时错误。


复制数据切片至空接口切片

假设你有一个 myType 类型的数据切片,你想将切片中的数据复制到一个空接口切片中,类似:

var dataSlice []myType = FuncReturnSlice()
var interfaceSlice []interface = dataSlice

可惜不能这么做,编译时会出错:cannot use dataSlice (type []myType) as type []interface in assignment。

原因是它们俩在内存中的布局是不一样的。

必须使用 for-range 语句来一个一个显式地赋值:

var dataSlice []myType = FuncReturnSlice()
var interfaceSlice []interface = make([]interface, len(dataSlice))
for i, d := range dataSlice 
    interfaceSlice[i] = d


通用类型的节点数据结构

诸如列表和树这样的数据结构,在它们的定义中使用了一种叫节点的递归结构体类型,节点包含一个某种类型的数据字段。现在可以使用空接口作为数据字段的类型,这样我们就能写出通用的代码。下面是实现一个二叉树的部分代码:通用定义、用于创建空节点的 NewNode 方法,及设置数据的 SetData 方法。

package main
import "fmt"
type Node struct 
    le   *Node
    data interface
    ri   *Node

func NewNode(left, right *Node) *Node 
    return &Nodeleft, nil, right

func (n *Node) SetData(data interface) 
    n.data = data

func main() 
    root := NewNode(nil, nil)
    root.SetData("root node")
    // make child (leaf) nodes:
    a := NewNode(nil, nil)
    a.SetData("left node")
    b := NewNode(nil, nil)
    b.SetData("right node")
    root.le = a
    root.ri = b
    fmt.Printf("%v\\n", root) // Output: &0x125275f0 root node 0x125275e0


接口到接口

一个接口的值可以赋值给另一个接口变量,只要底层类型实现了必要的方法。这个转换是在运行时进行检查的,转换失败会导致一个运行时错误:这是 Go 语言动态的一面,可以拿它和 Ruby 和 Python 这些动态语言相比较。

假定:

var ai AbsInterface // declares method Abs()
type SqrInterface interface 
    Sqr() float

var si SqrInterface
pp := new(Point) // say *Point implements Abs, Sqr
var empty interface

那么下面的语句和类型断言是合法的:

empty = pp                // everything satisfies empty
ai = empty.(AbsInterface) // underlying value pp implements Abs()
// (runtime failure otherwise)
si = ai.(SqrInterface) // *Point has Sqr() even though AbsInterface doesn’t
empty = si             // *Point implements empty set
// Note: statically checkable so type assertion not necessary.

下面是函数调用的一个例子:

type myPrintInterface interface 
    print()

func f3(x myInterface) 
    x.(myPrintInterface).print() // type assertion to myPrintInterface

x 转换为 myPrintInterface 类型是完全动态的:只要 x 的底层类型(动态类型)定义了 print 方法这个调用就可以正常运行


参考

  • Go入门指南
  • Go语言设计与实现

golang接口的使用(练习一)(代码片段)

...接口要求的所有函数,我们就说这个类实现了这个接口。golang接口赋值实现方式一:将对象实例赋值给接口packagemainimport"fmt"//定义一个Animal接口,实现飞和跑的功能typeAnimalinterfaceFly()Run()//定义一个Bird类typeBirdstruct//通过类实现接... 查看详情

golang中接口interface和struct结构类的分析(代码片段)

再golang中,我们要充分理解interface和struct这两种数据类型。为此,我们需要优先理解type的作用。type是golang语言中定义数据类型的唯一关键字。对于type中的匿名成员和指针成员,这里先不讲,重点讲解interface和struct这两种特殊的... 查看详情

golang接口(代码片段)

查看详情

golang接口---中(代码片段)

GoLang接口---中引言接口的类型断言实例演示类型判断:type-switchnil和non-nil空接口构建通用类型或包含不同类型变量的数组复制数据切片至空接口切片通用类型的节点数据结构接口到接口参考引言GoLang接口—上上一篇文章中... 查看详情

golang实现接口(代码片段)

查看详情

golang使用接口和类型断言在go中使用任意键名解析json对象(代码片段)

查看详情

golang面向对象编程—接口(代码片段)

接口基本介绍接口快速入门基本语法接口使用注意事项基本介绍在Go语言中接口(interface)是一种类型,一种抽象的类型。interface是一组method的集合,是duck-typeprogramming的一种体现。接口做的事情就像是定义一个协... 查看详情

golang去示例代码4-接口(代码片段)

查看详情

golang自定义接口和实现接口(代码片段)

1/*2定义:3type接口名interface4方法名(可选:参数列表)可选:返回值列表||(可选:返回值列表)56例:typeWriterinterface7Write(p[]byte)(nint,errerror)89typeObjecterinterface//定义接口10say(classint,valuestring)(bbool,errerror)1112实现接口:131:接口的方法与实现接口... 查看详情

golang声明接口内联的语法(代码片段)

查看详情

golang接口---上(代码片段)

GoLang接口---上定义隐式接口类型接口底层数据接口类型结构体itab结构体接口嵌套接口指针和接口golang中的值方法和指针方法参考代码必须能够被人阅读,只是机器恰好可以执行定义Go语言不是一种“传统”的面向对象编程语... 查看详情

golang将接口值与#golang中的nil进行比较(代码片段)

查看详情

golang去http接口简单例子(代码片段)

查看详情

golanginterface接口详解(代码片段)

前言在之前的文章中我们说过,golang是通过结构体(struct)-方法(method)-接口(interface)的组合使用来实现面向对象的思想。在前文Golang复合类型和Golangmethod方法详解已经详细介绍过struct和method,本文将介绍golang面向对象的另一... 查看详情

golang学习笔记5——接口(代码片段)

接口的声明golang中的接口声明方式如下:type接口名interface 方法名(参数)返回值例子://Writer接口typeWriterinterface //Write方法,参数为一个字符串 Write(sstring)//Stringer接口typeStringerinterface //String方法,参数为空, 查看详情

golang✔️走进go语言✔️第十五课递归&接口(代码片段)

【Golang】✔️走进Go语言✔️第十五课递归&接口概述递归实现阶乘斐波那契数列接口概述Golang是一个跨平台的新生编程语言.今天小白就带大家一起携手走进Golang的世界.(第15课)递归递归(Recursion)就是在运行的过程中自己调用自... 查看详情

golang✔️走进go语言✔️第十五课递归&接口(代码片段)

【Golang】✔️走进Go语言✔️第十五课递归&接口概述递归实现阶乘斐波那契数列接口概述Golang是一个跨平台的新生编程语言.今天小白就带大家一起携手走进Golang的世界.(第15课)递归递归(Recursion)就是在运行的过程中自己调用自... 查看详情

golang中函数结构体,将函数转换为接口(代码片段)

函数结构体,将函数转换为接口定义一个函数类型F,并且实现接口A的方法,然后在这个方法中调用自己。这是Go语言中将其他函数转换为接口A的常用技巧(参数返回值定义与F一致)实现一个动态生成的“回调函数”,比如缓存... 查看详情