The Laws of Reflection

原文地址

第一次翻译文章,请各路人士多多指教!

类型和接口

因为映射建设在类型的基础之上,首先我们对类型进行全新的介绍。

go是一个静态性语言,每个变量都有静态的类型,因此每个变量在编译阶段中有明确的变量类型,比如像:int、float32、MyType。。。

比如:

  1. type MyInt int
  2. var i int
  3. var j MyInt

变量i的类型为int,变量j的类型为MyInt,变量i、j具有确定的类型,虽然i、j的潜在类型是一样的,但是在没有转换的情况下他们之间不能相互赋值。

在类型中有重要的一类为接口类型(interface),接口类型为一系列方法的集合。一个接口型变量可以存储接口方法中声明的任何具体的值。像io.Reader和io.Writer是一个很好的例子,这两个接口在io包中定义。

  1. type Reader interface{
  2. Read(p []byte)(n int, err error)
  3. }
  4. type Writer interface{
  5. Writer(p []byte)(n int,er error)
  6. }

任何声明为io.Reader或者io.Writer类型的变量都可以使用Read或者Writer 方法。也就意味着io.Reader类型的变量可以赋值任何有Read方法的的变量。

  1. var r io.Reader
  2. r = os.Stdin
  3. r = bufio.NewReader(r)
  4. r = new(bytes.Buffer)

无论变量r被赋值什么类型的值,变量r的类型依旧是io.Reader。go语言是静态类型语言,并且r的类型永远是io.Reader。

在接口类型中有一个重要的极端接口类型--空接口。

interface{}

他代表一个空的方法集合并且可以被赋值为任何值,因为任何一个变量都有0个或者多个方法。

有一种错误的说法是go的接口类型是动态定义的,其实在go中他们是静态定义的,一个接口类型的变量总是有着相同类型的类型,尽管在运行过程中存储在接口类型变量的值具有不同的类型,但是接口类型的变量永远是静态的类型。

接口的表示方法

关于go中接口类型的表示方法Russ Cox大神在一篇博客中已经详细介绍[blog:http://research.swtch.com/2009/12/go-data-structures-interfaces.html]

一个接口类型的变量存储一对信息:具体值,值的类型描述。更具体一点是,值是实现接口的底层具体数据项,类型是数据项类型的完整描述。

举个例子:

  1. var r io.Reader
  2. tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
  3. if err != nil {
  4. return nil, err
  5. }
  6. r = tty

变量r包含两个数据项:值(tty),类型(os.File)。注意os.File实现的方法不仅仅是Read,即使接口类型仅包含Read方法,但是值(tty)却用于其完整的类型信息,因此我们可以按照如下方法调用

  1. var w io.Writer
  2. w = r.(io.Writer)

这条语句是一个断言语句,断言的意思是变量r中的数据项声明为io.Writer,因为我们可以将r赋值给w。执行完这条语句以后,变量w将和r一样包含值(tty)、类型(*os.File)。即使具体值可能包含很多方法,但是接口的静态类型决定什么方法可以通过接口型变量调用。

同样我们可以

  1. var empty interface{}
  2. empty = w

这个接口型变量同样包含一个数据对(tty,*os.File)。空接口可以接受任何类型的变量,并且包含我们可能用到的关于这个变量的所有信息。在这里我们不需要断言是因为w变量满足于空接口。在上一个从Reader向Writer移动数据的例子中,我们需要类型断言,因为Reader接口中不包含Writer方法

切记接口的数据对中的内容只能来自于(value , concrete type)而不能是(value, interface type),也就是接口类型不能接受接口类型的变量。

1.从接口类型到映射对象

在最底层,映射是对存储在接口内部数据对(值、类型)的解释机制。首先我们需要知道在reflect包中的两种类型Type和Value,这两种类型提供了对接口变量内部内容的访问,同时reflect.TypeOf和reflect.ValueOf两个方法检索接口类型的变量。

首先我们开始TypeOf

  1. package main
  2. import (
  3. "fmt"
  4. "reflect"
  5. )
  6. func main() {
  7. var f float64 = 13.4
  8. fmt.Println(reflect.TypeOf(f))
  9. fmt.Println("Hello, playground")
  10. }

结果

  1. float64
  2. Hello, playground

我们可以会感到奇怪这里没有接口呀?因为在程序中我们可以得知f的变量类型应为float32,不应该是什么变量类型。但是我们在golang源码中我们得知,reflect.TypeOf包含一个空接口类型的变量.

func TypeOf(i interface{})Type

当我们在调用reflect.TypeOf方法时,x首先存储在一个空的接口中,然后再作为一个参数传送到reflect.TypeOf方法中,然后该方法解压这个空的接口得到类型信息。

同样reflect.ValueOf方法,得到值。

  1. var f float64 = 13.4
  2. fmt.Println(reflect.ValueOf(f))

结果

  1. 13.4

reflect.Type和reflec.Value有许多方法让我们检查和修改它们。一个比较重要的方法是Value有一个能够返回reflect.Value的类型的方法Type。另外一个比较重要的是Type和Value都提供一个Kind方法,该方法能够返回存储数据项的字长(Uini,Floatr64,Slice等等)。同样Value方法也提供一些叫做Int、Float的方法让我们修改存储在内部的值。

  1. var f float64 = 13.44444
  2. v := reflect.ValueOf(f)
  3. fmt.Println(v)
  4. fmt.Println(v.Type())
  5. fmt.Println(v.Kind())
  6. fmt.Println(v.Float())

结果

  1. 13.444444444444445
  2. float64
  3. float64
  4. 13.444444444444445

同时有像SetInt、SetFloat之类的方法,但是我们必须谨慎的使用它们。

反射机制有两个重要的性质。首先,为了保证接口的简洁行,gettersetter两个方法是可以接受最大类型值的赋值,比如int64可以接受任何符号整数。所以值的Int方法会返回一个int64类型的值,SetInt接受int64类型的值,因此它可能转化为所涉及的实际类型。

  1. var x uint8 = 'x'
  2. v := reflect.ValueOf(x)
  3. fmt.Println("type:", v.Type()) // uint8.
  4. fmt.Println("kind is uint8: ", v.Kind() == reflect.Uint8) // true.
  5. x = uint8(v.Uint()) // v.Uint returns a uint64.

第二个特性:接口保存了数据项底层类型,而不是静态的类型,如果一个接口包含用户定义的整数类型的值,比如

  1. type MyInt int
  2. var x MyInt = 7
  3. v := reflect.ValueOf(x)

则v的Kind方法调用仍然返回的是reflect.Int,尽管x的静态类型是MyInt。也可以说,Kind`不会像Type`一样将MyInt和int当作两种类型来对待。

2.从映射对象到接口的值

像物理映射一样,Go中的映射也有其自身的相反性。

通过利用Interface的方法我们可以将interface.Value恢复至接口类型,实际上这个方法将type和value信息包装至interface类型并且返回该值。

  1. // Interface returns v's value as an interface{}.
  2. func (v Value) Interface() interface{}

因此我们可以说

  1. y := v.Interface().(float64) // y will have type float64.
  2. fmt.Println(y)

打印float64类型的值,其实是接口类型变量v的映射。

或者我们可以这样做,fmt.Println, fmt.Printf等函数的参数尽管是空的接口类型也能运行,在fmt包里面解析出type和value的方法和我们上面的例子相似。因此所有正确打印reflect.Value的方法都试通过interface的方法将值传递给格式化打印函数。

  1. fmt.Println(v.Interface())

(为什么不是fmt.Println(v)?因为通过v是reflect.Value类型.)因为我们的值底层是float64类型,因此我们甚至可以浮点类型的格式打印.

  1. fmt.Printf("value is %7.1e\n", v.Interface())

结果是

  1. 3.4e+00

因此我们不用类型断言v.Interface{}到float64类型。因为接口类型内部保存着值的信息,Printf函数能够恢复这些信息。

简单的说Interface是ValueOf的反操作,除非这个值总是静态的Interface类型。

改变接口对象,他的值必须是可改变的

第三法则比较微妙并且容易混淆,但是如果从第一准则开始看的话,那么还是比较容易理解的。

这是一条错误的语句,但是这个错误值得我们研究

  1. var x float64 = 3.4
  2. v := reflect.ValueOf(x)
  3. v.SetFloat(7.1) // Error: will panic.

如果你运行这条语句则会有下面的报错信息

  1. panic: reflect.Value.SetFloat using unaddressable value

因为变量v是不可更改的,所以提示值7.1是不可寻址的。可赋值是value的一个特性,但是并不是所以的value都具有这个特性。

CanSet方法返回该值是否是可以改变的,比如

  1. var x float64 = 3.4
  2. v := reflect.ValueOf(x)
  3. fmt.Println("settability of v:", v.CanSet())

结果是

  1. settability of v: false

如果在不可以赋值的变量上进行赋值,就回引起错误。但是到底是什么才是可以赋值的呢?

可赋值的有点像是可寻址的,但是会更严格。映射对象可以更改存储值的特性可以用来创建新的映射对象。映射对象包含原始的数据项是决定映射对象可赋值的关键。当下面代码运行时

  1. var x float64 = 3.4
  2. v := reflect.ValueOf(x)

只是将x的拷贝到reflect.ValueOf,因此reflect.ValueOf的返回值是x的复制项,而不是x本身。假如下面这条语句可以正常运行

  1. v.SetFloat(5.4)

尽管v看起来是由x创建的,但是并不会更新x的值,因为这条语句会更新x拷贝值的值,但是并不影响x本身,因此可更改的这一特性就是为了避免这种操作。

虽然这看起来很古怪,但其实这是一种很熟悉的操作。比如我们将x值赋值给一个方法

  1. f(x)

我们本身不想修改x的值,因为传入的只是x值的拷贝,但是如果我们想修改x的值,那么我们需要传送x的地址(也就是x的指针)

  1. f(&x)

这种操作是简单明了的,其实对于映射也是一样的。如果我们想通过映射修改x的值,那么我们需要传送x的指针。比如

  1. var x float64 = 3.4
  2. p := reflect.ValueOf(&x) // Note: take the address of x.
  3. fmt.Println("type of p:", p.Type())
  4. fmt.Println("settability of p:", p.CanSet())

结果

  1. type of p: *float64
  2. settability of p: false

映射对象p仍然是不可修改的,但是其实我们并不想修改p,而是*p。为了得到指针的指向,我们需要使用Elem()方法,该方法将会指向*p的值,并且将其保存到映射变量中

  1. v := p.Elem()
  2. fmt.Println("settability of v:", v.CanSet())

结果为

  1. settability of v: true

现在v是一个可修改的映射对象。并且v代表x,因此我们可以使用v.SetFloat()来修改x的值。

  1. v.SetFloat(7.1)
  2. fmt.Println(v.Interface())
  3. fmt.Println(x)

输出结果为

  1. 7.1
  2. 7.1

映射是比较难理解的,尽管我们通过映射的Values``Types隐藏了到底发生了什么操作。我们只需要记住如果想改变它的值,那在调用ValuesOf方法时应该使用指向它的指针。

Struct

在上一个例子中v并不是指向自身的指针,而是通过其他方式产生的。还有一种常用的操作就是修改结构体的某个字段,只要我们知道了结构体的地址,我们就能修改它的字段。

这有一个修改结构体变量t的例子。因为我们要修改结构体的字段,所以我们使用结构体指针创建结构体对象。我们使用typeOfT代表t的数据类型,并通过NumField方法迭代结构体的字段。主意:我们只是提取出结构体类型字段的的名字,而他们的reflect.Value对象。

  1. type T struct {
  2. A int
  3. B string
  4. }
  5. t := T{23, "skidoo"}
  6. s := reflect.ValueOf(&t).Elem()
  7. typeOfT := s.Type()
  8. for i := 0; i < s.NumField(); i++ {
  9. f := s.Field(i)
  10. fmt.Printf("%d: %s %s = %v\n", i,
  11. typeOfT.Field(i).Name, f.Type(), f.Interface())
  12. }

输出结果是

  1. 0: A int = 23
  2. 1: B string = skidoo

值得注意的是只有可导出的字段才能使可修改的。

因为s包含一个可修改的映射对象,所以我们可以修改结构体的字段

  1. s.Field(0).SetInt(77)
  2. s.Field(1).SetString("Sunset Strip")
  3. fmt.Println("t is now", t)

结果为

  1. t is now {77 Sunset Strip}

如果s是通过t创建而不是&t,那么SetInt和SetString方法都会出错,因为t的字段是不可以修改的。

原博客地址:The Go Blog|The Laws of Reflection

Golang接口(interface)三个特性(译文)的更多相关文章

  1. Golang 接口interface

    接口interface 接口是一个或多个方法签名的集合 只要某个类型拥有该接口的所有方法签名,即算实现该接口,无需显示声明实现了哪个接口,这成为Structural Typing 接口只有方法声明,没 ...

  2. 【Golang】Go 通过结构(struct) 实现接口(interface)

    一.通过结构(struct) 实现 接口(interface) 1.在了解iris框架的时候,经常看到有这样去写的使用一个空结构体作为接收器,来调用方法,有点好奇这样做有什么意义. 解释:在 Go 语 ...

  3. Golang的Interface是个什么鬼

    问题概述 Golang的interface,和别的语言是不同的.它不需要显式的implements,只要某个struct实现了interface里的所有函数,编译器会自动认为它实现了这个interfa ...

  4. C#编程利器之三:接口(Interface)【转】

    C#编程利器之三:接口(Interface) C#接口是一个让很多初学者容易迷糊的东西,用起来好象很简单,定义接口,然后在里面定义方法,通过继承与他的子类来完成具体的实现.但没有真正认识接口的作用的时 ...

  5. java之接口interface

    接口 1.多个无关的类可以实现同一个接口 2.一个类可以实现多个无关的接口 3.与继承关系类似,接口与实现类之间存在多态性 4.定义java类的语法格式 < modifier> class ...

  6. Go语言学习笔记(四)结构体struct & 接口Interface & 反射

    加 Golang学习 QQ群共同学习进步成家立业工作 ^-^ 群号:96933959 结构体struct struct 用来自定义复杂数据结构,可以包含多个字段(属性),可以嵌套: go中的struc ...

  7. JAVA之旅(七)——final关键字 , 抽象类abstract,模板方法模式,接口interface,implements,特点,扩展

    JAVA之旅(七)--final关键字 , 抽象类abstract,模板方法模式,接口interface,implements,特点,扩展 OK,我们继续学习JAVA,美滋滋的 一.final 我们来 ...

  8. Go接口interface

    目录 接口是什么? interface类型 空接口(interface{}) interface函数参数 interface变量存储的类型 类型断言 嵌入interface 接口是什么? Go 语言不 ...

  9. Go 接口(interface)

        文章转载地址:https://www.flysnow.org/2017/04/03/go-in-action-go-interface.html 1.什么是 interface? 简单的说,i ...

随机推荐

  1. 灵魂宝石 bzoj 2663

    灵魂宝石(1s 128MB)soulgem [问题描述] "作为你们本体的灵魂,为了能够更好的运用魔法,被赋予了既小巧又安全的外形" 我们知道,魔法少女的生命被存放于一个称为灵魂宝 ...

  2. JavaScript中String对象的方法介绍

    1.字符方法 1.1 charAt() 方法,返回字符串中指定位置的字符. var question = "Do you like JavaScript?"; alert(ques ...

  3. Flexible 弹性盒子模型之CSS flex-grow 属性

    实例 让第二个元素的宽度为其他元素的三倍: div:nth-of-type(1){flex-grow:1;} div:nth-of-type(2){flex-grow:3;} div:nth-of-t ...

  4. angular2之前端篇—1(node服务器分支)

    上一篇.net core和angular2之前端篇-1 使用的是dotnet模板.之所以用它,因为想用他写webapi,但是写道下一篇的时候遇到点问题,所以先写个分支测试一下.这次是用Node作为服务 ...

  5. Android Retrofit 2.0 使用-补充篇

    推荐阅读,猛戳: 1.Android MVP 实例 2.Android Retrofit 2.0使用 3.RxJava 4.RxBus 5.Android MVP+Retrofit+RxJava实践小 ...

  6. 我的MYSQL学习心得(十) 自定义存储过程和函数

    我的MYSQL学习心得(十) 自定义存储过程和函数 我的MYSQL学习心得(一) 简单语法 我的MYSQL学习心得(二) 数据类型宽度 我的MYSQL学习心得(三) 查看字段长度 我的MYSQL学习心 ...

  7. 微软Visual Studio Code 0.8.0发布,新增多种主题

    月30日,Build 开发者大会上,正式宣布了 Visual Studio Code 项目;并将其定义为:一个运行于 Mac OS X.Windows和 Linux 之上的,针对于编写现代 Web 和 ...

  8. SurfaceView 绘制分形图

    之前一直做的是应用类,这次抽时间,参考网上资料实践了下SurfaceView.目标是在页面上画一个科赫曲线的分形图. 代码如下: package com.example.fredric.demo02; ...

  9. 斗天斗地斗控件 -- 与 Flyout 控件的斗争史

    前言 在淘宝的收藏夹页面本地化过程中,收藏的宝贝和店铺的分类展示通过一个下拉菜单的方式进行展示.如果单独为此从头重写一个控件,那么不但费时费力,包括所有的动画方式都要全新设计,而且还容易出 bug.好 ...

  10. MongoDB学习笔记~数据模型属性为集合时应该为它初始化

    回到目录 今天要说一下技术点,我们在设计mongodb的数据模型时,如果属性是数组或者集合类型,我们在模型初始化时,需要为它们初始化一下,否则在数据库里将会被存储为NULL,当被存储为NULL时,我们 ...