go interface 使用 与 marshal 使用

Interface 基本使用

// _Interfaces_ are named collections of method
// signatures.

package main

import "fmt"
import "math"

// Here's a basic interface for geometric shapes.
type geometry interface {
    area() float64
    perim() float64
}

// For our example we'll implement this interface on
// `rect` and `circle` types.
type rect struct {
    width, height float64
}
type circle struct {
    radius float64
}

// To implement an interface in Go, we just need to
// implement all the methods in the interface. Here we
// implement `geometry` on `rect`s.
func (r rect) area() float64 {
    return r.width * r.height
}
func (r rect) perim() float64 {
    return 2*r.width + 2*r.height
}

// The implementation for `circle`s.
func (c circle) area() float64 {
    return math.Pi * c.radius * c.radius
}
func (c circle) perim() float64 {
    return 2 * math.Pi * c.radius
}

// If a variable has an interface type, then we can call
// methods that are in the named interface. Here's a
// generic `measure` function taking advantage of this
// to work on any `geometry`.
func measure(g geometry) {
    fmt.Println(g)
    fmt.Println(g.area())
    fmt.Println(g.perim())
}

func main() {
    r := rect{width: 3, height: 4}
    c := circle{radius: 5}

    // The `circle` and `rect` struct types both
    // implement the `geometry` interface so we can use
    // instances of
    // these structs as arguments to `measure`.
    measure(r)
    measure(c)
}

这个例子的官方解释是:

矩形和圆形都实现了几何图形的接口

我来做几点解释:

  • measure 的参数是一个 geometry 的接口(interface)
  • 当调用 measure 函数的时候会有一次类型转换,将实参转换为形参接口类型
  • 这个转换过程是在编译期间完成的,编译器会检测方法列表,当实参方法列表是形参方法列表的超集时,此次转换成功

空 Interface

上面那个例子是有方法的interface作为参数,但是很多时候还会出现一种空interface

 空interface(interface{})不包含任何的method,因此所有的类型都实现了空interface。

https://tiancaiamao.gitbooks.io/go-internals/content/zh/07.2.html interface 原理

因此空Interface就很像C语言里面的空指针。

comma-ok

但是不同的是,接口包含的是方法。并且空接口也不是直接强制转换为其他接口的,而是通过如下方法:

func Disconnect(usb interface{}){   //注意,这里是空接口
        switch v:=usb.(type) {
        case PhoneConnect:
                fmt.Println(" Phone device Disconnected from",v.name)
        case TVConnect:
                fmt.Println("TV device Disconnected from",v.name)
        default:
                fmt.Println("Unknown device ...")
        }
}
func main(){
        a := PhoneConnect{"IPhone"}
        b := TVConnect{"ChuangWei"}
        Disconnect(a)
        Disconnect(b)
}

这种方式被称为Comma-ok断言

Comma-ok断言的语法是:value, ok := element.(T)。element必须是接口类型的变量,T是普通类型

如果element是T类型的数据,那么断言成功(interface{} 的 _type 字段匹配),转换为value对象。否则OK被置为false

还有一种switch语法就如同上面例子中使用的一样。

interface slice

interface slice 与 万能类型 empty interface 是不一样的,可以直接将任何类型的值传给万能类型,但是不能将任何类型的 slice 直接传给 interface slice

举例说明:

func MethodTakeinSlice(in []interface{}){...}
...
slice := []int{1,2,3}
MethodTakeinSlice(slice)   // 会报错 cann't use slic (type []int) as type []interface{}

marshal

一个简单的介绍 https://www.jb51.net/article/130778.htm

http://cizixs.com/2016/12/19/golang-json-guide

使用过程中需要注意的几点:

  1. marshal 过程中,对应的struct结构体必须首字母大写!
  2. unmarshal 函数传递的是 地址!需要加上&符号

记一次 json marshal 过程

某次测试的过程中遇到这样一个问题:

我们的某个接口,访问网页,获取到了返回值,将其 unmarshal 为了一种特殊的结构体

req, err := http.NewRequest(method, t.endpoint+urlPath, bytes.NewReader(bodyData))
resp, err := t.client.Do(req)   // resp 为 *http.Response 类型
//type Response struct {
//      Code    int         `json:"code"`
//      Message string      `json:"message"`
//      Data    interface{} `json:"data"` // api-specified response result
//}

res := new(hh.Response)
err = hh.UnmarshalStream(marshaler.NewJsonMarshaler(), resp.Body, res)
// res 为 Response 类型

然后我需要将 res.Data 转换成我需要的结构体数组类型。

一开始试来试去,弄出一种方法:

es_status := types.ES_health{}
json.Unmarshal([]byte(resp.Data.(string)), &es_status)

也就是先将 resp.Data 声明为string(居然没有报错),然后转换为 []byte,最后unmarshal。

这个方法对于这个例子还是OK的。

但是,当返回值并不是直接的结构体,而是结构体数组的时候,这种方法就行不通了。

例如:

var val []model.Flavor
json.Unmarshal([]byte(resp.Data.(string)), &val)

报错

panic: interface conversion: interface {} is map[string]interface {}, not string

省去分析过程了,直接上结论

结论

marshal 会将 []byte 代表的 json 数据转换为我们想要的结构体,但是一旦我们转换的目标结构体中包含 interface{} 字段的时候,marshal 便无法分析这段数据的类型,将其转换为默认的 map[string]interface{}。也就是这个 interface{} 数据指针的 _type 成员未被设置。因此之后的 comma-ok 断言会报错。

解决的方式有两种:

  1. 将其作为 map[string]interface{} 使用,并且调用库mapstructure(需要下载)将 interface{} 转换成我们需要的结构体
for _, v := range res.Data.([]interface{}){
    val2 := model.Flavor{}
    if err := mapstructure.Decode(v, &val2); err != nil {
        t.Fatal(err)
    }
    t.Log(val2)
}
  1. 先将这个 interface{} 使用marshal 函数转换为 []byte,然后再调用 unmarshal 将其转换为我们需要的 struct,也就是说,unmarshal 的时候传入我们的 struct 变量,这样unmarshal 函数就会帮我们填充 _type 字段了
var val []model.Flavor
v, err:= json.Marshal(res.Data)
if err!= nil {
    t.Fatal(err)
}
json.Unmarshal(v, &val)
t.Log(val)

由于第一种方法会引入额外的依赖,因此第二种方式更优一点。

我觉得应该有直接修改 interface{} 的_type 字段的方法(就像强行转换空指针,虽然感觉很危险),留待以后学习吧。

参考链接:

Go语言Interface漫谈

GO语言Comma-ok断言