前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。
类型和接口
Go 是静态类型语言。每一个变量都有一个静态的类型,即在编译时类型已知且固定:比如 int
、float32
。
接口类型
接口类型是类型的一个重要类别,它表示固定的方法集。接口变量可以存储任何具体值(非接口),只要该值实现接口的方法即可。如:
// Reader 是封装基本 Read 方法的接口 type Reader interface { Read(p []byte) (n int, err error) } // Writer 是封装基本 Write 方法的接口 type Writer interface { Write(p []byte) (n int, err error) }
任何实现了 Read(p []byte) (n int, err error)
方法的类型都被称为实现了 Reader
接口(Writer
同理)。这意味着
Reader
可以保存实现了 Read
方法的任何值:
var r io.Reader r = os.Stdin r = bufio.NewReader(r) r = new(bytes.Buffer)
需要明确的是,不管 r
可能包含什么具体值,r
的类型始终是 io.Reader
:Go 是静态类型的语言,而 r
的静态类型是 io.Reader
。
空接口
接口类型的一个非常重要的示例是空接口:
interface{}
它表示空的方法集,并且任何值都满足空接口,因为任何值都有零个或者多个方法。
有人说 Go 的接口是动态类型的,但这会产生误导。接口是静态类型的:接口类型的变量始终具有相同的类型,即使在运行时存储在接口变量中的值可能会更改类型,该值也始终满足接口的要求。
接口的表示形式
**接口类型的变量存储了一对值:分配给该变量的具体值,以及该值的类型描述。**更确切地说,该值是实现接口的基础具体数据项,而类型描述了该数据项的完整类型。例如:
var r io.Reader tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0) if err != nil { return nil, err } r = tty
r
中包含了 (value, type)
对,即 (tty, *os.File)
。请注意,类型 *os.File
实现的方法不只有 Read
;
尽管接口仅提供对 Read
方法的访问,但是其内部的值仍包含有关该值的所有类型信息。这就是为什么我们可以做下面的事情:
var w io.Writer w = r.(io.Writer)
因为 r
的具体类型里面包含了 Write
方法,而 r
里面包含的值依然持有它原来的值,所以这个断言是没有问题的。
一个重要的细节是,接口内始终保存
(值, 具体类型)
形式的元素对,而不会有(值, 接口类型)
的形式。接口内部不持有接口值。
反射
反射第一定律:从接口值反射出反射对象
反射对象主要有两类:reflect.Type
、reflect.Value
从底层讲,反射只是一种检查存储在接口变量中的值和类型对的机制。首先,我们需要了解反射包的两个类型:Type
和 Value
,
通过这两个类型可以访问接口变量的内容。还有两个函数 reflect.TypeOf
和 reflect.ValueOf
,它们可以从接口值中取出
reflect.Type
和 reflect.Value
。(另外,从 reflect.Value
可以很容易地获取到 reflect.Type
,但是让我们暂时将 Value
和 Type
的概念分开。)
package main import ( "fmt" "reflect" ) func main() { var x float64 = 3.4 // 打印 type: float64 fmt.Println("type:", reflect.TypeOf(x)) }
上面的代码看起来像将 float64
类型的变量 x
传递给了 reflect.TypeOf
,而不是传递的接口值。但实际上,传递的是接口;
// TypeOf 返回 interface{} 中值的反射类型 func TypeOf(i interface{}) Type
当我们调用 reflect.TypeOf(x)
时,x
先被存在一个空接口中,然后再作为参数传递;reflect.TypeOf
从该空接口中恢复类型信息。
相应的,reflect.ValueOf
函数会恢复值信息。
var x float64 = 3.4 // value: <float64 Value> fmt.Println("value:", reflect.ValueOf(x).String())
reflect.Type
和 reflect.Value
都有许多方法可以让我们执行检查和操作:
Value
具有Type
方法,该方法返回reflect.Value
的Type
类型。Type
和Value
都有一个Kind
方法,该方法返回 go 的类型(语言本身的类型,而不是自定义的类型)Value
的很多方法,名字类似于Int
和Float64
,可以让我们获取存储在里面的值。- 还有诸如
SetInt
和SetFloat
之类的方法,可以修改接口的值。
反射第二定律:从反射对象到接口值
给定 reflect.Value
,我们可以使用 Interface()
方法恢复接口值;
实际上,该方法将类型和值信息打包回接口表示形式并返回结果:
//接口返回v的值作为接口{}。 func (v Value) Interface() interface{}
结果,我们可以说
y := v.Interface().(float64) // y的类型为float64 fmt.Println(y)
打印反射对象 v
表示的 float64
值。一种更简洁的写法是:
// fmt.Println 本身就接受 interface{} 参数 fmt.Println(y)
反射第三定律:要修改反射对象,该值必须可设置
不可设置的例子:
var x float64 = 3.4 v:= reflect.ValueOf(x) // panic: reflect.Value.SetFloat using unaddressable value v.SetFloat(7.1)//错误:会panic错误。
因为调用 reflect.ValueOf(x)
的时候,函数只拿到了 x
的副本,而不是 x
变量本身,如果我们在函数内部修改了 x
那也只是修改了副本而已。
Value
的 CanSet
方法报告 Value
的可设置性:
var x float64 = 3.4 v:= reflect.ValueOf(x) // false fmt.Println("settability of v:", v.CanSet())
如果我们想修改它,可以在反射的时候,直接使用 x
的指针:
var x float64 = 3.4 p := reflect.ValueOf(&x) // 注意:取 x 的地址 // type of p: *float64 // settability of p: false fmt.Println("type of p:", p.Type()) fmt.Println("settability of p:", p.CanSet())
我们注意到,这里我们使用了指针,但依然是不能设置其值。这是因为反射对象 p
是不可设置的,实际上我们想要设置的不是 p
,而是 *p
。为了获取 p
指向的内容,我们调用 Value
值的 Elem
方法,该方法指向指针:
v := p.Elem() // settability of v: true fmt.Println("settability of v:", v.CanSet())
现在,v
是一个可设置的反射对象了,我们可以使用 v.SetFloat
来修改 x
的值了:
v.SetFloat(7.1) // 7.1 fmt.Println(v.Interface()) // 7.1 fmt.Println(x)
反射值需要变量的地址才能修改其表示的值。
结构体
在下面的例子中,我们使用结构体的地址创建反射对象,因为稍后将要对其进行修改。然后我们将 typeOfT
设置为其反射类型,
并使用简单的方法调用对字段进行迭代。
type T struct { A int B string } t := T{23, "skidoo"} s := reflect.ValueOf(&t).Elem() typeOfT := s.Type() for i := 0; i < s.NumField(); i++ { f := s.Field(i) fmt.Printf("%d: %s %s = %v.", i, typeOfT.Field(i).Name, f.Type(), f.Interface()) }
0: A int = 23 1: B string = skidoo
此处传递的内容还涉及可设置性的另一点:
T
的字段名是大写(已导出),因为只能设置结构体的导出字段。
因为 s
包含可设置的反射对象,所以我们可以修改结构的字段:
s.Field(0).SetInt(77) s.Field(1).SetString("Sunset Strip") fmt.Println("t is now", t)
如果我们修改代码从
t
而不是&t
创建s
,则对SeteInt
和SetString
的调用将失败,因为无法设置t
的字段。
结论
反射定律:
- 反射可以从接口值到反射对象
- 反射可以从反射对象到接口值
- 要修改反射对象,该值必须可设置。