内容简介:接着上次的继续讲接口,先回顾一下接口的用法:没有定义任何方法的接口,就是空接口:由于空接口里没有定义任何方法,任何类型都实现了空接口。也就是空接口可以被任何类型实现,空接口能够容纳任何类型。
接口
接着上次的继续讲接口,先回顾一下接口的用法:
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的副本给方法处理了。
实现一个负载均衡的调度算法,支持随机、轮训等算法
(略...)
反射
反射,可以在运行时动态的获取到变量的相关信息。需要 reflect 包:
import "reflect"
基本用法
主要是下面这2个函数:
func TypeOf(i interface{}) Type func ValueOf(i interface{}) 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
- 增加查看某人的借书记录的功能
以上所述就是小编给大家介绍的《Go语言6-接口、反射》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- Go语言反射之反射调用
- Go语言反射之类型反射
- Go语言反射之值反射
- 模块讲解----反射 (基于web路由的反射)
- 装饰器与元数据反射(4)元数据反射
- .NET/C# 反射的的性能数据,以及高性能开发建议(反射获取 Attribute 和反射调用方法)
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
进化式运营:从互联网菜鸟到绝顶高手
李少加 / 电子工业出版社 / 2016-11 / 59
互联网运营作为一个新兴的岗位,一方面它是企业的核心岗职,身负重任,另一方面,又由于其短暂的历史,缺乏成熟体系的工作方法论,而目前业界主流的运营方法却是从企业视角出发,存在极大的改进空间。 《进化式运营:从互联网菜鸟到绝顶高手》作者基于自身十年的互联网洞察、实践经验,并融合了信息论、心理学、经济学、管理学、甚至包括生态学、进化论等跨学科跨学业的知识,从无到有地构建了一套全新的互联网运营体系:基......一起来看看 《进化式运营:从互联网菜鸟到绝顶高手》 这本书的介绍吧!
Markdown 在线编辑器
Markdown 在线编辑器
HSV CMYK 转换工具
HSV CMYK互换工具