用了9年的C++,1年的C#,最近改用go语言开发,深感go语言的设计简单,其设计宗旨也是less is more,它极大的加快了开发速度。
go语言吸取了很多现代语言的优点,一个比较重要的特性就是基于接口编程,函数是程序世界的第一公民,这个有点像scalar语言。实现这个接口的语言原型是interface。
基于接口编程
C++中不支持接口,接口的实现方式是用纯虚函数来实现的,C#具有接口,但是它认为接口是对象的一个能力,这是一个很大的进步,但是它不大灵活,比如
public Interface IFile
{
public int Read(string filePath, int len)
public int Write(string filePath, int len)
}
这个接口实现从一个文件中读取len个字节数据,返回实际的数据,以及写入len个字节数据,返回实际写入的数据个数
public class CDataNode:IFile
{
public int Read(string filePath, int len){...}
public int Write(string filePath, int len){...}
}
这样CDataNode就可以在支持IFile接口的地方直接使用
这样的代码写了很久,但是如果发现IFile这个粒度太大了,有些地方只需要Read,有些地方需要Write,在C#中只能是
public Interface IRead
public Interface IWrite
public Interface IFile:IRead,IWrite
在go中不需要这样,只需要再声明IRead和IWrite接口,IFile可以直接转换到IRead和IWrite,它更加灵活。
package main
import (
"fmt"
)
type iFile interface {
Read(int) int
Write(int) int
}
type rwFile struct {
}
func (rw rwFile) Read(int) int {
fmt.Println("rwFile read")
return 0
}
func (rw rwFile) Write(int) int {
fmt.Println("rwFile write")
return 0
}
type iRead interface {
Read(int) int
}
type iWrite interface {
Write(int) int
}
func TestInterface() {
var rwInterface iFile = rwFile{}
rwInterface.Read(0)
rwInterface.Write(0)
var rInterface iRead = rwInterface
rInterface.Read(0)
var wInterface iWrite = rwInterface
wInterface.Write(0)
}
模块之间依赖接口,并且go是不支持继承,只支持组合,继承是一种强关系,一旦定下来就很难改动,或者说改动成本非常之高,组合是一种弱关系,更加松散一点。
继承和多态特性
继承和多态是面向对象设计一个非常好的特性,它可以更好的抽象框架,让模块之间依赖于接口,而不是依赖于具体实现。
package main
import (
"fmt"
)
type iAnimal interface {
speak() string
}
type dog struct {
}
func (d dog) speak() string {
return "Woof!"
}
type cat struct {
}
func (c cat) speak() string {
return "Meow!"
}
type coder struct {
}
func (c coder) speak() string {
return "Hello world!"
}
type superCoder struct {
coder//如果没有给出变量名字,编译器生成一个coder coder的变量,然后它的语法糖是针对这种变量的访问,直接可以访问其内部成员变量,而不需要经过.coder来访问。
}
func (s superCoder) sayHi() {
fmt.Println("super coder say hi")
}
func Polymorphic() {
animals := []iAnimal{dog{}, cat{}, coder{}, superCoder{}}
for _, animal := range animals {
fmt.Println(animal.speak())
}
}
依赖于接口来实现,只要实现了这个接口就可以认为赋值给这个口,实现动态绑定。superCoder在这里可以认为是继承与coder,但是它增加了新的方法SayHi,原有的speak方法没有变化,所以它也不需要重写。
go中是没有继承的,匿名成员变量也只是一个语法糖而已,并不是继承
type cat struct{
age int
}
type dog struct{
cat
}
var c cat = dog{}//编译错误
var pc *cat = &dog{}//编译错误
var pc *cat = (*cat)(&dog{})//编译错误
在C++中是可以用基类指针指向子类的对象的,但是go中它认为是两种类型,不能进行转换。
从根本上讲,struct在go中是没有虚表指针,这种特性只有interface才有,所以它是两种独立的类型,不能相互转换
interface的使用
interface特性是:
* 是一组函数签名的集合
* 是一种类型
interface{}
interface{}是一类特殊的接口,因为它没有实现任何类型,所以所有类型变量都可以赋值给它,有点像C#的object,它是所有对象的基类。
package main
import (
"fmt"
)
func PrintAll(vals []interface{}) {
for _, val := range vals {
fmt.Println(val)
}
}
func main() {
names := []string{"stanley", "david", "oscar"}
PrintAll(names)
}
最开始我很奇怪为什么不能编译通过,后来仔细想想就是应该编译不过,这里PrintAll接收一个“[]interface{}”类型,输入的参数是一个”[]string”类型,两个是不同的类型,不能进行赋值。所以正确的做法是
package main
import (
"fmt"
)
func PrintAll(vals []interface{}) {
for _, val := range vals {
fmt.Println(val)
}
}
func main() {
names := []string{"stanley", "david", "oscar"}
//PrintAll(names)
vals := make([]interface{}, len(names))
for i, v := range names {
vals[i] = v
}
PrintAll(vals)
}
因为[]interface{}也是一种类型,所以它是可以赋值给interface{}的.
interface{}用法
有一个需求,将一个json解析称为一个map,因为数据格式多变,采用interface{},调用库函数UnmarshalJSON。
package main
import (
"encoding/json"
"fmt"
"reflect"
)
// start with a string representation of our JSON data
var input = `
{
"created_at": "Thu May 31 00:00:01 +0000 2012"
}
`
func main() {
// our target will be of type map[string]interface{}, which is a
// pretty generic type that will give us a hashtable whose keys
// are strings, and whose values are of type interface{}
var val map[string]interface{}
if err := json.Unmarshal([]byte(input), &val); err != nil {
panic(err)
}
fmt.Println(val)
for k, v := range val {
fmt.Println(k, reflect.TypeOf(v))
}
}
返回结果
map[created_at:Thu May 31 00:00:01 +0000 2012]
created_at string
注意到这里的时间是一种特殊格式的time,和普通的不一样,这样返回的结果是string类型而不是time类型.及时将map的数据改成time也会有问题,因为它不是一种标准的time格式,会抛出异常。
可以考虑实现json的接口
type Unmarshaler interface {
UnmarshalJSON([]byte) error
}
func (t *Timestamp) UnmarshalJSON(b []byte) error {
v, err := time.Parse(time.RubyDate, string(b[1:len(b)-1]))
if err != nil {
return err
}
*t = Timestamp(v)
return nil
}
上面的map就能够正常,返回的类型也是time类型。
## interface{}一般可以作为函数的入口参数,但是最好不要作为一个函数的返回值。
有一个需求,要设计一个API,将http request的请求转化为某中数据类型格式,格式有可能有很多种。
因为返回的数据格式没有定,采用interface{}格式最好。
GetEntity(*http.Request) (interface{}, error)
caller:
switch v:Get(r).type(){
int:
...
string:
...
}
这样做非常不好,因为每次增加一个新类型,调用方就需要改变,不满足开闭原则,正确的做法是
type Entity interface {
UnmarshalHTTP(*http.Request) error
}
func GetEntity(r *http.Request, v Entity) error {
return v.UnmarshalHTTP(r)
}
caller:
var u User
if err := GetEntity(req, &u); err != nil {
// ...
}
其中User需要实现这个接口
func (u *User) UnmarshalHTTP(r *http.Request) error {
// ...
}
将interface{}作为参数,而不是返回值,这样做的好处增加新的类型只需要实现Entity接口就可以了,再增加一个对这种类型的调用,而不需要修改GetEntity的内部实现。
interface的底层实现
我们先看一个例子,从这个例子来看怎么实现的
type Stringer interface {
String() string
}
type Binary uint64
func (i Binary) String() string {
return strconv.Uitob64(i.Get(), 2)
}
func (i Binary) Get() uint64 {
return uint64(i)
}
创建一个uint64,初始值为200
b := Binary(200)
其内存模型如下:
此时将b赋值给一个接口:
s := Stringer(b)//如果不能转换会抛出exception
interface s由两个成员,
* 第一个是itable,有点类似C++的vtable,保存了一系列的函数对象,第一个是type,通过它可以找到它所属于哪个类型,这个是所有interface的itable第一个值,其它的就是这个interface类型需要的函数对应的实际实现,我们可以看到String()的Binary实现。Binary中实现了Get函数,因为它不是Stringer的接口,所以就没有放在这里。
itable是如何生成?它是在运行时动态生成的,首先每种类型,编译器都会为其生成meta,这个meta里包含了所有它支持的函数以及成员变量,当一个对象要赋值给interface时,runtime会遍历整个对象的函数签名找到匹配这个interface的函数签名,如果全部能够匹配则继续执行,否则就会抛出异常(如果不检查成功失败的话),同理将一个interface赋值给另外一个interface.
假设变量的类型有m个函数,interface有n个需要支持的签名,则采用算法每个都匹配一遍,复杂度为O(mn),go中它会将每个函数签名集合进行签名,这样做匹配的时候复杂度只为O(m+n).
* 另外一个是实际的数据data,这里它会创建一个新的值,所以b的修改是不会影响接口的
func (i *Binary) String() string {
return strconv.Uitob64(i.Get(), 2)
}
ps := Stringer(&b)
这种情况ps中的data部分保存的就是指向b的指针,因为在做转换的时候传进来的也是指针。
总结
- go语言所有的赋值都是值拷贝
- interface是一种函数集合的类型
- interface的itable中保存了这个interface的具体实现,idata中保存了赋值数据的拷贝
- interface作为参数的入口函数,而不应该作为返回值。
有疑问加站长微信联系(非本文作者)