内容简介:接着上次的继续讲接口,先回顾一下接口的用法:没有定义任何方法的接口,就是空接口:由于空接口里没有定义任何方法,任何类型都实现了空接口。也就是空接口可以被任何类型实现,空接口能够容纳任何类型。
接口
接着上次的继续讲接口,先回顾一下接口的用法:
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 和反射调用方法)
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Programming Collective Intelligence
Toby Segaran / O'Reilly Media / 2007-8-26 / USD 39.99
Want to tap the power behind search rankings, product recommendations, social bookmarking, and online matchmaking? This fascinating book demonstrates how you can build Web 2.0 applications to mine the......一起来看看 《Programming Collective Intelligence》 这本书的介绍吧!