Go:接口和反射

news/2024/12/26 14:06:15/

接口

定义

在传统的面向对象的语言中,是会存在类和继承的概念的,但是Go并没有

那Go如何实现类似的方法呢?它提供了接口的概念,可以实现很多面向对象的特性

接口定义会实现一组方法集,但是这些方法不包含实现的代码,他们是抽象的概念,接口里也不能有变量

用如下的方式来定义接口

type Namer interface {Method1(param_list) return_typeMethod2(param_list) return_type...
}

上面的这个Namer就是一个典型的接口类型

接口的名字由方法名加 er 后缀组成,例如 Printer、Reader、Writer、Logger、Converter 等等。还有一些不常用的方式(当后缀 er 不合适时),比如 Recoverable,此时接口名以 able 结尾,或者以 I 开头

不像大多数面向对象编程语言,在 Go 语言中接口可以有值,一个接口类型的变量或一个 接口值 :var ai Namer,ai 是一个多字(multiword)数据结构,它的值是 nil。它本质上是一个指针,虽然不完全是一回事。指向接口值的指针是非法的,它们不仅一点用也没有,还会导致代码错误。

此处的方法指针表是通过运行时反射能力构建的。

类型(比如结构体)可以实现某个接口的方法集;这个实现可以描述为,该类型的变量上的每一个具体方法所组成的集合,包含了该接口的方法集。实现了 Namer 接口的类型的变量可以赋值给 ai(即 receiver 的值),方法表指针(method table ptr)就指向了当前的方法实现。当另一个实现了 Namer 接口的类型的变量被赋给 ai,receiver 的值和方法表指针也会相应改变

类型不需要显式声明它实现了某个接口:接口被隐式地实现。多个类型可以实现同一个接口

实现某个接口的类型(除了实现接口方法外)可以有其他的方法

一个类型可以实现多个接口

比如以下面的代码为定义

type test1Shaper interface {Area() float32
}type test1Square struct {side float32
}type test1Circle struct {r float32
}func (sq test1Square) Area() float32 {return sq.side * sq.side
}func (cr test1Circle) Area() float32 {return 3.14 * cr.r * cr.r
}func test1() {sq := test1Square{10}cr := test1Circle{5}var areaInterface test1ShaperareaInterface = sqfmt.Println(areaInterface.Area())areaInterface = crfmt.Println(areaInterface.Area())
}

再看这个例子

package mainimport "fmt"type Shaper interface {Area() float32
}type Square struct {side float32
}func (sq *Square) Area() float32 {return sq.side * sq.side
}type Rectangle struct {length, width float32
}func (r Rectangle) Area() float32 {return r.length * r.width
}func main() {r := Rectangle{5, 3} // Area() of Rectangle needs a valueq := &Square{5}      // Area() of Square needs a pointer// shapes := []Shaper{Shaper(r), Shaper(q)}// or shortershapes := []Shaper{r, q}fmt.Println("Looping through shapes for area ...")for n, _ := range shapes {fmt.Println("Shape details: ", shapes[n])fmt.Println("Area of this shape is: ", shapes[n].Area())}
}

在调用 shapes[n].Area() 这个时,只知道 shapes[n] 是一个 Shaper 对象,最后它摇身一变成为了一个 Square 或 Rectangle 对象,并且表现出了相对应的行为

一个标准库的例子

io 包里有一个接口类型 Reader:

type Reader interface {Read(p []byte) (n int, err error)
}

定义变量 r var r io.Reader

那么就可以写如下的代码:

	var r io.Readerr = os.Stdin    // see 12.1r = bufio.NewReader(r)r = new(bytes.Buffer)f,_ := os.Open("test.txt")r = bufio.NewReader(f)

上面 r 右边的类型都实现了 Read() 方法,并且有相同的方法签名,r 的静态类型是 io.Reader

接口嵌套接口

一个接口可以包含一个或多个其他的接口,这相当于直接将这些内嵌接口的方法列举在外层接口中一样。

比如接口 File 包含了 ReadWrite 和 Lock 的所有方法,它还额外有一个 Close() 方法

type ReadWrite interface {Read(b Buffer) boolWrite(b Buffer) bool
}type Lock interface {Lock()Unlock()
}type File interface {ReadWriteLockClose()
}

类型断言:检测和转换接口变量的类型

一个接口类型的变量 varI 中可以包含任何类型的值,必须有一种方式来检测它的 动态 类型,即运行时在变量中存储的值的实际类型。在执行过程中动态类型可能会有所不同,但是它总是可以分配给接口变量本身的类型。通常我们可以使用 类型断言 来测试在某个时刻 varI 是否包含类型 T 的值

比如可以是这样

package mainimport ("fmt""math"
)type Square struct {side float32
}type Circle struct {radius float32
}type Shaper interface {Area() float32
}func main() {var areaIntf Shapersq1 := new(Square)sq1.side = 5areaIntf = sq1// Is Square the type of areaIntf?if t, ok := areaIntf.(*Square); ok {fmt.Printf("The type of areaIntf is: %T\n", t)}if u, ok := areaIntf.(*Circle); ok {fmt.Printf("The type of areaIntf is: %T\n", u)} else {fmt.Println("areaIntf does not contain a variable of type Circle")}
}func (sq *Square) Area() float32 {return sq.side * sq.side
}func (ci *Circle) Area() float32 {return ci.radius * ci.radius * math.Pi
}

类型判断

接口变量的类型也可以使用一种特殊形式的 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)
}

11.6 使用方法集与接口

package mainimport ("fmt"
)type List []intfunc (l List) Len() int {return len(l)
}func (l *List) Append(val int) {*l = append(*l, val)
}type Appender interface {Append(int)
}func CountInto(a Appender, start, end int) {for i := start; i <= end; i++ {a.Append(i)}
}type Lener interface {Len() int
}func LongEnough(l Lener) bool {return l.Len()*10 > 42
}func main() {// A bare valuevar lst List// compiler error:// cannot use lst (type List) as type Appender in argument to CountInto://       List does not implement Appender (Append method has pointer receiver)// CountInto(lst, 1, 10)if LongEnough(lst) { // VALID: Identical receiver typefmt.Printf("- lst is long enough\n")}// A pointer valueplst := new(List)CountInto(plst, 1, 10) // VALID: Identical receiver typeif LongEnough(plst) {// VALID: a *List can be dereferenced for the receiverfmt.Printf("- plst is long enough\n")}
}

讨论

lst 上调用 CountInto 时会导致一个编译器错误,因为 CountInto 需要一个 Appender,而它的方法 Append 只定义在指针上。 在 lst 上调用 LongEnough 是可以的,因为 Len 定义在值上。

plst 上调用 CountInto 是可以的,因为 CountInto 需要一个 Appender,并且它的方法 Append 定义在指针上。 在 plst 上调用 LongEnough 也是可以的,因为指针会被自动解引用。

总结

在接口上调用方法时,必须有和方法定义时相同的接收者类型或者是可以根据具体类型 P 直接辨识的:

  • 指针方法可以通过指针调用
  • 值方法可以通过值调用
  • 接收者是值的方法可以通过指针调用,因为指针会首先被解引用
  • 接收者是指针的方法不可以通过值调用,因为存储在接口中的值没有地址

将一个值赋值给一个接口时,编译器会确保所有可能的接口方法都可以在此值上被调用,因此不正确的赋值在编译期就会失败。

译注

Go 语言规范定义了接口方法集的调用规则:

  • 类型 *T 的可调用方法集包含接受者为 *TT 的所有方法集
  • 类型 T 的可调用方法集包含接受者为 T 的所有方法
  • 类型 T 的可调用方法集包含接受者为 *T 的方法

具体例子展示

来看下sort包当中对于接口部分的运用是怎样的:

要对一组数字或字符串排序,只需要实现三个方法:反映元素个数的 Len() 方法、比较第 i 和 j 个元素的 Less(i, j) 方法以及交换第 i 和 j 个元素的 Swap(i, j) 方法

于是可以写出如下所示的代码

func Sort(data Sorter) {for pass := 1; pass < data.Len(); pass++ {for i := 0;i < data.Len() - pass; i++ {if data.Less(i+1, i) {data.Swap(i, i + 1)}}}
}

而在这个实现中,在Sorter中实际上就会声明了对应的这些方法

type Sorter interface {Len() intLess(i, j int) boolSwap(i, j int)
}

所以,这句意味着,假设此时我们要对于一个int类型的数组来进行排序,那么就意味着要在这个int类型的数组上实现对应的接口方法,这样才能让标准库在调用Sorter的时候可以找到对应的方法,例如下所示:

type IntArray []int
func (p IntArray) Len() int           { return len(p) }
func (p IntArray) Less(i, j int) bool { return p[i] < p[j] }
func (p IntArray) Swap(i, j int)      { p[i], p[j] = p[j], p[i] }

这样的,就可以写出如下的代码,来进行一个合理的接口调用的过程:

data := []int{74, 59, 238, -784, 9845, 959, 905, 0, 0, 42, 7586, -5467984, 7586}
a := sort.IntArray(data) //conversion to type IntArray from package sort
sort.Sort(a)

相同的原理,可以实现其他类型数据的接口调用,这里我们假设自定义一个结构体,根据结构体中的相关字段来进行排序:

type test2S struct {name  stringscore int
}type TsArray []test2Sfunc (ts TsArray) Len() int {return len(ts)
}func (ts TsArray) Less(i, j int) bool {return ts[i].score < ts[j].score
}func (ts TsArray) Swap(i, j int) {ts[i], ts[j] = ts[j], ts[i]
}func test2() {data := []test2S{{"jack", 80}, {"keven", 90}, {"joe", 70}}fmt.Println("排序前: ", data)//sort.Sort(data) 错误的调用,因为Sort的接收值是一个interface变量,所以要通过data创建出它对应的interface变量sort.Sort(TsArray(data))fmt.Println("排序后: ", data)
}

运行结果为

排序前:  [{jack 80} {keven 90} {joe 70}]
排序后:  [{joe 70} {jack 80} {keven 90}]

空接口

概念

不包含任何方法,对于实现没有任何要求

type Any interface {}

任何其他类型都实现了空接口,可以给一个空接口类型的变量 var val interface {} 赋任何类型的值

这就意味着,空接口支持可以接受任何类型的变量,这在实际的开发中是很有意义的,比如可以产生如下的代码

func test3() {testFunc := func(any interface{}) {switch v := any.(type) {case bool:fmt.Println("bool type", v)case int:fmt.Println("int type", v)case string:fmt.Println("string type", v)default:fmt.Println("other type", v)}}testFunc(1)testFunc(1.2)testFunc("hello world")
}

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

假设你有一个 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
}

通用类型的节点数据结构

假设有现在的场景:

type node struct {next *nodeprev *nodedata interface{}
}func test4() {root := &node{nil, nil, "hello root"}root.next = &node{nil, root, 10}root.prev = &node{root, nil, 20}fmt.Println(root.prev.data, root.data, root.next.data)
}

接口到接口

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

比如,给出下面的代码

type test5S struct {firstname stringlastname  string
}func (ts *test5S) print2() {fmt.Println(ts.firstname, ts.lastname)
}type test5PrintInterface interface {print1()
}type test5MyInterface interface {print2()
}func t5func(x test5MyInterface) {if p, ok := x.(test5PrintInterface); ok {p.print1()} else {fmt.Println("error")}
}func test5() {ts := &test5S{"bob", "joe"}t5func(ts)
}

从这个就能看出问题,对于ts变量来说,他实现了test5MyInterface接口,但是实际上没有实现test5PrintInterface接口的内容,因此这里的转换是失败的,所以就要加一个类似于上面的检测的过程

反射包

来看看反射的概念:

反射是用程序检查其所拥有的结构,尤其是类型的一种能力;这是元编程的一种形式。反射可以在运行时检查类型和变量,例如:它的大小、它的方法以及它能“动态地”调用这些方法。这对于没有源代码的包尤其有用。这是一个强大的工具,除非真得有必要,否则应当避免使用或小心使用

变量的最基本信息就是类型和值:反射包的 Type 用来表示一个 Go 类型,反射包的 Value 为 Go 值提供了反射接口

两个简单的函数,reflect.TypeOf 和 reflect.ValueOf,返回被检查对象的类型和值。例如,x 被定义为:var x float64 = 3.4,那么 reflect.TypeOf(x) 返回 float64,reflect.ValueOf(x) 返回

实际上,反射是通过检查一个接口的值,变量首先被转换成空接口。这从下面两个函数签名能够很明显的看出来:

func TypeOf(i interface{}) Type
func ValueOf(i interface{}) Value

接口的值包含一个 type 和 value。

反射可以从接口值反射到对象,也可以从对象反射回接口值。

reflect.Typereflect.Value 都有许多方法用于检查和操作它们。一个重要的例子是 Value 有一个 Type() 方法返回 reflect.ValueType 类型。另一个是 TypeValue 都有 Kind() 方法返回一个常量来表示类型:UintFloat64Slice 等等。同样 Value 有叫做 Int()Float() 的方法可以获取存储在内部的值(跟 int64float64 一样)

下面给出如下的示例代码

func test6() {var f float64v := reflect.ValueOf(f)fmt.Println(v)k := v.Kind()fmt.Println(k)fmt.Println(reflect.Float64)
}

通过反射修改(设置)值

先看这个代码

func test7() {var x float64 = 2.3v := reflect.ValueOf(x)fmt.Println("can be set?", v.CanSet())
}

这里表示,现在通过反射拿到了x的类型,现在如果想直接进行设置它的值,是不被允许的,原因在于:当 v := reflect.ValueOf(x) 函数通过传递一个 x 拷贝创建了 v,那么 v 的改变并不能更改原始的 x

所以这里实际上需要的是,使用一个&类型,因此可以改造成这样

func test8() {var x float64 = 2.3v := reflect.ValueOf(&x)fmt.Println("can be set?", v.CanSet())
}

但是这样依旧不能设置,这是因为&x的值,相当于是一个float类型的指针,想要在代码中直接对于指针进行设置,很明显是不成功的,所以就要想办法来获取到指针对应的值

所以可以这样进行设置,使用一个Elem函数,这样就会自动来使用指针对应的值

func test9() {var x float64 = 2.3v := reflect.ValueOf(&x)v = v.Elem()fmt.Println("can be set?", v.CanSet())v.SetFloat(20.1)fmt.Println(v)fmt.Println(x)fmt.Println(v.Interface())
}

反射结构

有些时候需要反射一个结构类型。NumField() 方法返回结构内的字段数量;通过一个 for 循环用索引取得每个字段的值 Field(i)

给出如下的示例代码

type test10S1 struct {s1, s2, s3 string
}type test10S2 struct {s1, s2 stringi1     int
}func (ts test10S1) String() string {return ts.s1 + "->" + ts.s2 + "->" + ts.s3
}func (ts test10S2) String() string {return ts.s1 + "->" + ts.s2 + "->" + strconv.Itoa(ts.i1)
}func test10Func(s1 interface{}) {typ := reflect.TypeOf(s1)val := reflect.ValueOf(s1)knd := val.Kind()fmt.Println(typ, val, knd)for i := 0; i < val.NumField(); i++ {fmt.Println(i, val.Field(i))}
}func test10() {var s1 interface{} = test10S1{"hello", "go", "s1"}var s2 interface{} = test10S2{"hello", "hello", 10}test10Func(s1)test10Func(s2)
}

但是在这样的情景下,如果要进行修改值的操作,是不被允许的,比如

reflect.ValueOf(&ts).Elem().Field(0).SetString("hee")

这是因为,这个结构体当中的字段没有被导出,应该改成大写才能被修改,我们修改结构体为这样:

type test10S1 struct {S1, s2, s3 string
}type test10S2 struct {S1, s2 stringi1     int
}

此时再次运行,就好了


http://www.ppmy.cn/news/1544882.html

相关文章

【机器学习导引】ch4-决策树

基本流程 两个需要解决的问题 属性顺序&#xff1a; 问题&#xff1a;哪些属性在前面&#xff0c;哪些属性在后面&#xff1f;这个问题指的是在处理数据或进行排序时&#xff0c;需要确定属性的排列顺序&#xff0c;以便更好地进行数据处理或分析。 属性选择&#xff1a; 问题…

C++中,如何找到一个vector中最大的元素

动态规划中&#xff0c;经常需要找到一个线性表中最大的元素&#xff0c;C 最常用的是vector&#xff0c;而不是 C 中的数组&#xff0c;虽然结构更加复杂&#xff0c;但是用起来更方便。就连 C 创始人 Bjarne Stroustrup 都推荐使用vector&#xff0c;如下是《A Tour of C Thi…

什么是分布式光伏发电?设备构成、应用形式讲解

分布式光伏发电系统&#xff0c;又称分散式发电或分布式供能&#xff0c;是指在用户现场或靠近用电现场配置较小的光伏发电供电系统&#xff0c;以满足特定用户的需求&#xff0c;支持现存配电网的经济运行&#xff0c;或者同时满足这两个方面的要求。 分布式光伏发电由哪些设备…

python-21-理解python切片这一概念

python-21-理解python切片这一概念 一.简介 在python基础系列还有一个概念&#xff0c;python切片&#xff0c;切片这一使用频率特别多&#xff0c;大量python实例、真实项目中也是频繁出现&#xff0c;所以把这一概念单独整理出来&#xff0c;以便大家学习和复习&#xff01…

golang 中map使用的一些坑

golang 中map使用的一些坑 1、使用map[string]interface{}&#xff0c;类型断言[]int失败 接收下游的数据是用json转为map[string]any go a : "{\"a\":\"1\",\"b\":[123]}" var marshal map[string]any json.Unmarshal([]byte(a), &…

Python数据分析案例61——信贷风控评分卡模型(A卡)(scorecardpy 全面解析)

案例背景 虽然在效果上&#xff0c;传统的逻辑回归模型通常不如现代的机器学习模型&#xff0c;但在风控领域&#xff0c;解释性至关重要。逻辑回归的解释性是这些“黑箱”模型所无法比拟的&#xff0c;因此&#xff0c;研究传统的评分卡模型依然是有意义的。 传统的评分卡模型…

uni-app 封装图表功能

文章目录 需求分析1. 秋云 uchars2. Echarts 需求 在 uni-app 中使用图表功能&#xff0c;两种推荐的图表工具 分析 在 Dcloud市场 搜索Echarts关键词&#xff0c;会出现几款图表工具&#xff0c;通过大家的下载量&#xff0c;可以看到秋云这个库是比较受欢迎的&#xff0c;其…

el-tree展开子节点后宽度没有撑开,溢出内容隐藏了,不显示横向滚动条

html结构如下 <div class"tree-div"><el-tree><template #default"{ node, data }"><div class"node-item">...</div></template></el-tree></div> css代码(scss) .tree-div {width: 300px;…