为了账号安全,请及时绑定邮箱和手机立即绑定

Go语言6-接口、反射

标签:
Go


接口

接着上次的继续讲接口,先回顾一下接口的用法:

package main

import "fmt"

// 定义接口

type Car interface {

    GetName() string

    Run()

}

// 定义结构体

type Tesla struct {

    Name string

}

// 实现接口的GetName()方法

func (t *Tesla) GetName() string {

    return t.Name

}

// 实现接口的Run()方法

func (t *Tesla) Run() {

    fmt.Printf("%s is running\n", t.Name)

}

func main() {

    var c Car

    var t Tesla = Tesla{"Tesla Model S"}

    c = &t  // 上面是用指针*Tesla实现了接口的方法,这里要传地址

    /*  或者在定义的时候,就定义结构体指针

    var t *Tesla = &Tesla{"Tesla Model X"}

    c = t

    */

    fmt.Println(c.GetName())

    c.Run()

}

强调一下:interface 类型默认是一个指针

空接口

没有定义任何方法的接口,就是空接口:

type Empty interface{}  // 定义了一个接口类型 Empty,里面没有任何方法

var e1 Empty  // e1 就是一个空接口

var e2 interface{}  // e2 也是空接口,这里跳过了接口类型的定义,在定义接口的同时把接口类型一起做了

由于空接口里没有定义任何方法,任何类型都实现了空接口。也就是空接口可以被任何类型实现,空接口能够容纳任何类型。

package main

import "fmt"

func main(){

    var e interface{}  // 定义一个空接口

    var n int

    e = n  // n可以给e赋值,因为n实现了e。这样接口就能存储它具体的实现类

    //n = e  // 反过来就不行,

    fmt.Printf("%T %T\n", n, e)  // 通过接口也能获取到它的实现类

}

之前一直使用的 fmt.Println() ,什么类型都可以往里传。这个函数接收的参数是这样的:

func(a ...interface{}) (n int, err error)

这里单看参数类型,就是空接口,任何类型都实现了空接口,所以任何类型都能作为参数。

类型转换

空接口也是个类型,类型转换的用法是一样的,不要遇到了大括号就看不懂了:

var i int  // 定义一个int类型

j := int32(i)  // 转成int32

k := interface{}(i)  // 转成空接口类型

对自定义结构体排序

排序使用 sort 包。包里提供了 Sort 方法可以对接口进行排序。

func Sort(data Interface)

Sort 对 data 进行排序。它调用一次 data.Len 来决定排序的长度 n,调用 data.Less 和 data.Swap 的开销为 O(n*log(n))。此排序为不稳定排序。 

这里是对接口进行排序,所以传入的 data 参数需要实现接口里的方法,接口的定义如下:

type Interface interface {

    // Len is the number of elements in the collection.

    // Len 为集合内元素的总数

    Len() int

    // Less reports whether the element with

    // index i should sort before the element with index j.

    //

    // Less 返回索引为 i 的元素是否应排在索引为 j 的元素之前。

    Less(i, j int) bool

    // Swap swaps the elements with indexes i and j.

    // Swap 交换索引为 i 和 j 的元素

    Swap(i, j int)

}

所以要对你的自定义结构体进行排序,首先定义一个该结构体类型的切片类型,然后实现上面的3个方法。之后就可以插入数据然后进行排序了:

package main

import (

    "fmt"

    "sort"

)

// 自定义的结构体

type Animal struct {

    Type   string

    Weitht int

}

// 新定义一个切片的类型,下面对这个类型实现interface要求的3个方法

// 直接用 []Animal Go不认,这里应该是起了个别名

type AnimalSlice []Animal

// 对自定义的切片类型实现Sort的接口要求的3个方法

func (a AnimalSlice) Len() int {

    return len(a)

}

func (a AnimalSlice) Less(i, j int) bool {

    return a[i].Weitht < a[j].Weitht

}

func (a AnimalSlice) Swap(i, j int) {

    a[i], a[j] = a[j], a[i]

}

func main() {

    var tiger Animal = Animal{"Tiger", 200}

    var dog Animal = Animal{"Dog", 20}

    var cat Animal = Animal{"Cat", 15}

    var elephant Animal = Animal{"Elephant", 4000}

    // 这里的切片要用自定义的类型,别名被认为是两个不同的类型,只有这个实现了接口的方法

    var data AnimalSlice

    data = append(data, tiger)

    data = append(data, dog)

    data = append(data, cat)

    data = append(data, elephant)

    fmt.Println(data)

    sort.Sort(data)

    fmt.Println(data)

}

接口嵌套

一个接口可以嵌套另外的接口:

type ReadWrite interface {

    Read(b Buffer) bool

    Write(b Buffer) bool

}

type Lock interface {

    Lock()

    Unlock

}

type File interface {

    ReadWrite

    Lock

    Close

}

嵌套的用法类似结构体的继承。这样如果已经有了一些接口,只要把这些接口组合一下,就又产生了一些新的接口了,不用去重复定义。

再来写个例子,主要是熟悉对接口编程的思路,顺便用到了接口的嵌套:

package main

import "fmt"

// 定义一个接口

type Reader interface {

    Read()

}

// 再定义一个接口

type Writer interface {

    Write(s string)

}

// 定义第三个接口,嵌套上面的两个接口

type ReadWriter interface {

    Reader

    Writer

}

// 定义一个结构体

type file struct {

    content string

}

func (f *file) Read(){

    fmt.Println(f.content)

}

func (f *file) Write(s string){

    f.content = s

    fmt.Println("写入数据:", s)

}

// 这个函数是对接口进行操作,上面的file类型实现了接口的所有方法

func CheckChange(rw ReadWriter, s string) {

    rw.Read()

    rw.Write(s)

    rw.Read()

}

func main() {

    var f file = file{"Hello"}

    CheckChange(&f, "How are you")

}

上面写的 CheckChange() 方法,只有是实现了 ReadWriter 这个接口的任何类型,都可以用这个函数来调用。

类型断言

类型断言,由于接口是一般类型,不知道具体的类型。

之前的例子里的函数,定义的入参是接口。而实际传入的是某个实现了接口类型的具体类型。比如上面的例子 CheckChange() 方法接收的参数主要是实现了 ReadWriter 接口的任何类型都可以。可以是例子里的自定义类型 file。也可以是别的类型比如再自定义一个 message。这样在函数接收参数后,是不知道这个参数的具体类型的。有些场景,你需要知道这个接口指向的具体类型是什么。

可以把接口类型转成具体类型,如果要转成具体类型,可以采用以下方法进行转换:

package main

import "fmt"

func main() {

    var i int = 10  // 这个是int

    var j interface{}  // 这个是空接口

    fmt.Printf("%T %v\n", j, j)  // 还没给j赋值,现在j只是一个空指针,默认类型和默认值都是nil

    j = i  // 任何类型都可以给空接口赋值,如果i是参数传入的,现在并不知道i的类型

    fmt.Printf("%T %v\n", j, j)  // 可以打印查看现在的j的类型和值都是和i一样的,但是代码层面还是不知道具体类型

    res := j.(int)  // 转成int类型,如果不是int类型会报错

    //res := j.(int32)  // 转成int32,由于类型不对,会报错

    fmt.Printf("%T %v\n", res, res)

}

上面在 res := j.(int) 这句做类型转换之前,打印 j 的类型的时候已经看到类型是 int 了,但是其实 j 的类型在代码层面还不知道。需要执行这句类型转换把类型转成 int 。下面的函数接收空接口,但是内部要做加法,只有将参数转成数值类型后,才能做加法:

func add(a interface{}){

    b := a.(int)  // 只有做了类型转换,才能做下面的加法

    b++

    c := a

    fmt.Printf("%T %v\n", c, c)  // 虽然能打印出类型,但是代码层面这个的类型还是interface{}

    //c++  // 这句还不能执行,现在c的类型是interface{},只有数值类型能做加法

}

上面是不带检查的,如果类型转换不成功,会报错。下面是带检查的类型断言:

package main

import "fmt"

type Num struct {

    n int

}

func main() {

    var i Num = Num{1}

    var j interface{}

    j = i

    res, ok := j.(int)  // 带检查的类型断言

    fmt.Println(res, ok)  // ok是false,类型不对,res的值就是转换类型的默认值

    var k interface{}

    k = i

    res2, ok := k.(Num)  // 这次类型是对的

    fmt.Println(res2, ok)  // ok是true

}

判断类型

除了类型断言,还有这个方法可以判断类型。

下面的函数可以判断传入参数的类型:

package main

import "fmt"

func classifier(items ...interface{}) {

    for i, v := range items {

        switch v.(type) {

        case bool:

            fmt.Println("bool", i)

        case float64:

            fmt.Println("float64", i)

        case int:

            fmt.Println("int", i)

        case nil:

            fmt.Println("nil", i)

        case string:

            fmt.Println("string", i)

        default:

            fmt.Println("unknow", i)

        }

    }

}

func main() {

    classifier(1, "", nil, 1.234, true, int32(5))

}

/* 执行结果

PS H:\Go\src\go_dev\day6\interface\classifier> go run main.go

int 0

string 1

nil 2

float64 3

bool 4

unknow 5

PS H:\Go\src\go_dev\day6\interface\classifier>

*/

这里用到了 v.(type) ,这个必须与 switch case 联合使用,如果写在 switch 外面,编译器会报错。

判断是否实现了指定接口

语法如下:

v, ok := interface{}(实例).(接口名)

先要把类型转成空接口,然后再判断是否实现了指定的接口。

示例:

package main

import "fmt"

// 定义一个结构体

type Example struct{

    Name string

}

// 这是一个接口

type IF1 interface{

    Hello()

}

// 这是另一个接口

type IF2 interface{

    Hi()

}

// 实现了接口 IF1 的方法

func (e Example) Hello(){

    fmt.Println("Hello")

}

func main(){

    var e Example = Example{"TEST"}  // 这里可以不做初始化的,不初始化也是有默认值的,srting型就是空

    v, ok := interface{}(e).(IF1)

    fmt.Println(v, ok)

    v2, ok := interface{}(e).(IF2)

    fmt.Println(v2, ok)

}

/* 执行结果

PS H:\Go\src\go_dev\day6\interface\is_if> go run main.go

{TEST} true

<nil> false

PS H:\Go\src\go_dev\day6\interface\is_if>

*/

接口示例

实现一个通用的链表类

重点要实现尾插法,头插法的当前节点不用移动,始终是头节点就行了。而尾插法要有一个当前节点的指针始终指向最后的一个节点。示例:

// go_dev\day6\interface\link\link\link.go

package link

import (

    "fmt"

)

type Link struct{

    Data interface{}  // 数据是空接口,所以是通用类型

    Next *Link

}

// 头插法,p需要传指针,因为方法里需要改变p的值

// 但是p本身也是个指针,所以接收的类型是指针的指针

func (l *Link) AddNodeHead(data interface{}, p **Link){

    var node Link

    node.Data = data

    node.Next = (*p).Next

    (*p).Next = &node

}

// 尾插法

func (l *Link) AddNodeTail(data interface{}, p **Link){

    var node Link

    node.Data = data

    (*p).Next = &node

    (*p) = &node

}

// 遍历链表的方法,打印当前节点以及之后的所有的节点

func (l *Link) Trans(){

    for l != nil {

        fmt.Println(*l)

        l = l.Next

    }

}

// go_dev\day6\interface\link\main\main.go

package main

import (

    "../link"

)

func main(){

    var intLink link.Link  // 别名,后面都用intLink

    head := intLink  // head是头节点

    p := &head  // p是指向当前节点的指针,注意结构体是值类型

    // 插入节点

    for i := 0; i < 10; i++ {

        node := intLink

        node.Data = i

        // 插入节点的方法,需改改变p本真的值,这里就要把p的地址传进去

        // 由于p本身已经是个指针了,再传指针的地址,那个变量就是指针的指针

        //intLink.AddNodeHead(node, &p)  // 头插法

        intLink.AddNodeTail(node, &p)  // 尾插法

    }

    head.Trans()  // 从头节点遍历链表

}

这个例子用了指针的指针。因为结构体是值类型,指向当前节点的变量p需要是一个指针类型。然而在添加节点的方法里(主要是尾插法),需要改变p的值,将p重新指向新插入的节点。这就要求必须把p的地址传进来,这样就是指针的指针了。

其实也可以不用那么做,不在方法里改变p的值,而是给方法添加一个返回值,返回最新的当前节点。这样就需要在调用方法的时候获取返回值然后赋值给p,就是在方法外改变p的值,这样就可以传p的副本给方法处理了。

实现一个负载均衡的调度算法,支持随机、轮训等算法

// go_dev\day6\interface\balance\balancd\balance.go

// 定义接口、结构体,以及对结构体的封装

package balance

import "fmt"

type Balancer interface {

    DoBalance([]*Instance) (*Instance, error)

}

// 封装:里面的字段都是小写,这样外部不可见,就无法查看也无法修改,甚至无法创建

// 在对结构体做什么样的操作,就再写方法来实现

// 这样就可以把这个结构体封装好,只能用提供的方法进行有限的操作

type Instance struct {

    host string

    port int

}

// 结构体里的字段都是小写,外部不可见,外部调用构造函数创建结构体

func NewInstance(host string, port int) *Instance {

    return &Instance{host, port}

}

// 还是因为封装,这里再提供方法可以查询

func (i *Instance) GetHost() string{

    return i.host

}

func (i *Instance) GetPort() int{

    return i.port

}

func (i *Instance) String() string{

    return fmt.Sprintf("%v:%v", i.host, i.port)

}

// go_dev\day6\interface\balance\balancd\random.go

// 随机算法

package balance

import (

    "errors"

    "math/rand"

    "time"

)

func init() {

    rand.Seed(time.Now().UnixNano())

}

// 虽然是个空结构体,但是需要一个结构体类型,然后去实现Balancer接口里的方法

type RandomBalance struct{

}

func (b *RandomBalance) DoBalance(objs []*Instance) (obj *Instance, err error) {

    if len(objs) == 0 {

        err = errors.New("没有传入任何实例")

        return

    }

    l := len(objs)

    i := rand.Intn(l)

    obj = objs[i]

    return

}

// go_dev\day6\interface\balance\balancd\round_robin.go

// 轮训算法

package balance

import (

    "errors"

)

type RoundRobinBalance struct{

    Index int

}

func (b *RoundRobinBalance) DoBalance(objs []*Instance) (obj *Instance, err error) {

    if len(objs) == 0 {

        err = errors.New("没有传入任何实例")

        return

    }

    l := len(objs)

    if b.Index >= l {

        b.Index = 0

    }

    obj = objs[b.Index]

    b.Index = (b.Index + 1) % l

    return

}

// go_dev\day6\interface\balance\main\main.go

// 使用和测试上面写的balance包

package main

import (

    "../balance"

    "fmt"

    "os"

    "strconv"

)

func main(){

    var objs []*balance.Instance

    for i := 0; i < 5; i++ {

        host := "Host" + strconv.Itoa(i)

        port := 80 + i

        obj := balance.NewInstance(host, port)

        objs = append(objs, obj)

    }

    var balancer balance.Balancer

    var arg = "round"

    if len(os.Args) > 1 {

        arg = os.Args[1]

    }

    if arg == "random" {

        balancer = &balance.RandomBalance{}

    } else if arg == "round" {

        balancer = &balance.RoundRobinBalance{}

    } else {

        arg = "random"

        // 不同的参数对应不同的负载均衡算法,生成不同的实例

        // 直接再通过实例调用里面的方法,是不需要用到接口的

        // 在这里,要用balancer这个变量来接收不同的结构体类型,就需要用到接口了

        // 只要实现了接口的里DoBalance()方法,就可以赋值给接口,之后就用接口统一调用这个方法

        balancer = &balance.RandomBalance{}

    }

    fmt.Println("负载均衡算法:", arg)

    // 调用10次,检查负载均衡的效果

    for i := 0; i < 10; i++{

        obj, err := balancer.DoBalance(objs)  // 通过接口统一调用方法

        if err != nil {

            fmt.Println("异常:", err)

            continue

        }

        fmt.Println(obj)

    }

}

反射

反射,可以在运行时动态的获取到变量的相关信息。需要 reflect 包:

import "reflect"

基本用法

主要是下面这2个函数:

func TypeOf(i interface{}) Type : 获取变量的类型,返回 reflect.Type 类型

func ValueOf(i interface{}) Value : 获取变量的值,返回 reflect.Value 类型

package main

import (

    "fmt"

    "reflect"

)

func test(a interface{}){

    t := reflect.TypeOf(a)

    fmt.Println(t)

    v := reflect.ValueOf(a)

    fmt.Println(v)

}

func main(){

    n := 100

    test(n)

}

/* 执行结果

PS H:\Go\src\go_dev\day6\reflect\beginning> go run main.go

int

100

PS H:\Go\src\go_dev\day6\reflect\beginning>

*/

在 reflect.Value 里提供了很多方法。大多数情况下,都是要先获取到 reflect.Value 类型,然后再调用对应的方法来实现。

获取类别(kind)

类型(type)和类别(kind),原生的类型两个的名字应该是一样了。不过自定义类型比如结构体,type就是我们自定义的名字,而kind就是struct。

要获取kind,首先是用上面的方法获取到 reflect.Value 类型,然后调用 Kind 方法,返回 reflect.Kind 类型:

func (v Value) Kind() Kind

具体用法:

package main

import (

    "fmt"

    "reflect"

)

type Example struct{}  // 自定义结构体,看下类型和类别

func main(){

    a1 := 10

    t1 := reflect.TypeOf(a1)

    v1 := reflect.ValueOf(a1)

    k1 := v1.Kind()

    fmt.Println(t1, k1)  // 原生类型的类别看不出来

    a2 := Example{}

    t2 := reflect.TypeOf(a2)

    v2 := reflect.ValueOf(a2)

    k2 := v2.Kind()

    fmt.Println(t2, k2)  // 自定义结构体的类型是自定义的名字,类别是struct

}

/* 执行结果

PS H:\Go\src\go_dev\day6\reflect\kind> go run main.go

int int

main.Example struct

reflect.Kind string

*/

示例中我们最后看到的是打印输出的效果。上面的两个 Kind() 方法的返回值的类型是 reflect.Kind ,这是包里定义的常量。如果要进行比较的话,这样比较:

k1 == reflect.Struct

k2 == reflect.String

另外,返回的类型并不是字符串类型。返回的是包里定义的常量上面已经讲过了。如果要获取类型的字符串名称,可以用 reflect.Kind 类型的 String() 方法:

func (k Kind) String() string

转成空接口

用法:

func (v Value) Interface() (i interface{})

示例:

package main

import (

    "fmt"

    "reflect"

)

type Student struct{

    Name string

    Age int

}

func main(){

    var s Student = Student{"Adam", 18}

    t := reflect.ValueOf(s)

    tif := t.Interface()  // 调用Interface()方法,返回空接口类型

    // 类型断言,必须要用空接口调用

    if stu, ok := tif.(Student); ok{

        fmt.Printf("%T %v\n", stu, stu)

    }

}

获取、设置变量

通过反射获取变量的值:

func (v Value) Float() float64

func (v Value) Int() int64

func (v Value) Bool() bool

func (v Value) String() string

通过反射设置变量的值:

func (v Value) SetFloat(x float64)

func (v Value) SetInt(x int64)

func (v Value) SetBool(x bool)

func (v Value) SetString(x string)

如果要设置的是一个值类型,那么肯定是要传地址的。但是传地址之后,转成了Value类型后就无法再用星号取到指针指向的内容了。这里提供下面的 Elem() 方法。

取指针指向的值:

func (v Value) Elem() Value

示例:

package main

import (

    "fmt"

    "reflect"

)

func get(x interface{}){

    v := reflect.ValueOf(x)

    res := v.Int()

    fmt.Printf("%T %v\n", res, res)

}

func set(x interface{}){

    v := reflect.ValueOf(x)  // x如果是个指针,*x是可以用的

    // 但是通过ValueOf()方法获得的v就不是指针了,没法用*v

    // 所以有了下面的Elem()方法,效果就是我们想要的*v的效果

    v.Elem().SetInt(2)  // 先要用Elem获取到指针指向的内容,然后才能Set

}

func main(){

    var n int = 1

    get(n)

    set(&n)  // 这里肯定是要地址的

    get(n)

}

操作结构体

返回结构体里字段、方法的数量:

func (v Value) NumField() int

func (v Value) NumMethod() int

示例:

package main

import (

    "fmt"

    "reflect"

)

type Student struct{

    Name string

    Age int

    Score float32

}

func TestStruct(x interface{}){

    v := reflect.ValueOf(x)

    if k := v.Kind(); k != reflect.Struct {

        fmt.Println(v, "不是结构体")

        return

    }

    fmt.Println(v, "是结构体")

    numOfField := v.NumField()

    fmt.Println("结构体里的字段数量:",numOfField)

    numOfMethod := v.NumMethod()

    fmt.Println("结构体里的方法数量:",numOfMethod)

}

func main() {

    TestStruct(1)  // 传个非结构体测试一下效果

    var a Student = Student{"Adam", 17, 92.5}

    TestStruct(a)

}

获取对应的字段、方法

通过下标获取:

func (v Value) Field(i int) Value

func (v Value) Method(i int) Value

还有通过名字获取:

func (v Value) FieldByName(name string) Value

func (v Value) FieldByNameFunc(match func(string) bool) Value

func (v Value) MethodByName(name string) Value

调用方法:

用上面的方法获取到方法后,再 .Call(nil) 就可以执行了。没有参数的话传 nil 就好了。Call只接收1个参数,把方法需要的所有参数都转成 Value 类型然后放在一个切片里传给 Call 执行。返回值也是切片,里面所有的值都是 Value 类型:

func (v Value) Call(in []Value) []Value

上面2句可以写一行里,比如下面这样,调用第一个方法,没有参数,不要返回值:

v.Method(0).Call(nil)

Type 接口的操作

这里用的是TypeOf() 方法,不要和上面的搞混了。返回值是 reflect.Type 类型,这是一个接口类型:

type Type interface {}

接口里的方法比较多,具体去官网看吧:https://go-zh.org/pkg/reflect/#Type

获取字段的Tag对应的内容

json序列化是用Tag替换字段名的实现,利用的也是这里的反射。

通过接口的 Field(i int) StructField 方法,传入下标获取到的是一个 StructField 结构体:

type StructField struct {

    // Name is the field name.

    // PkgPath is the package path that qualifies a lower case (unexported)

    // field name.  It is empty for upper case (exported) field names.

    // See http://golang.org/ref/spec#Uniqueness_of_identifiers

    Name    string

    PkgPath string

    Type      Type      // field type

    Tag       StructTag // field tag string

    Offset    uintptr   // offset within struct, in bytes

    Index     []int     // index sequence for Type.FieldByIndex

    Anonymous bool      // is an embedded field

}

结构体里有一个字段是 Tag ,类型是 StructTag 。这是一个字符串类型的别名,不过里面实现了一些方法。调用 StructTag 的 Get 方法,传入Tag的key,就能返回Tag里对应的value:

func (tag StructTag) Get(key string) string

完整的代码,抄官网的示例( https://go-zh.org/pkg/reflect/#example_StructTag ):

package main

import (

    "fmt"

    "reflect"

)

func main() {

    type S struct {

        F string `species:"gopher" color:"blue"`

    }

    s := S{}

    st := reflect.TypeOf(s)  // 注意这里是TypeOf,返回值是 Type 接口

    field := st.Field(0)  // Type 接口里的方法,返回 StructField 结构体。

    // StructField结构体里面的Tag字段是 StructTag 一个 string 类型的别名

    // StructTag里实现了Get方法,下面就是调用该方法通过key获取到value

    fmt.Println(field.Tag.Get("color"), field.Tag.Get("species"))

}

json序列化操作的时候,就是利用了反射的方法,获取到tag里json这个key对应的value,替换原本的字段名。

课后作业

实现一个图书管理系统v2,增加以下功能:

增加用户登录、注册功能

增加借书过期的图书界面

增加显示热门图书的功能,被借次数最多的Top10

增加查看某人的借书记录的功能

©著作权归作者所有:来自51CTO博客作者骑士救兵的原创作品,如需转载,请注明出处,否则将追究法律责任


点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消