• 接口的朦胧之美
  • 发布于 2个月前
  • 236 热度
    0 评论
一. 什么是接口
所谓接口,其实就是一个约定,约定了被描述对象的一种或多种行为。怎么理解这句话呢?我们先来看两个例子:
  // 定义两种人群
  type Chinese struct {}
  type Englishmen struct {}
 
  func (c Chinese) Speck() string {
      return "中文"
  }
  func (c Chinese) Environment() string {
      return "中国社会"
  }
 
  func (e Englishmen) Speck() string {
      return "English"
  }
  func (e Englishmen) Environment() string {
      return "British Society"
  }
 
  func main() {
      c := Chinese{}
      fmt.Printf("%T生在%s, 说%s\n", c, c.Environment(), c.Speck())
      e := Englishmen{}
      fmt.Printf("%T生在%s, 说%s\n", e, e.Environment(), e.Speck())
  }
运行结果:
main.Chinese生在中国社会, 说中文
main.Englishmen生在British Society, 说English
当人的类型变多时,比如再加上美国人,韩国人,日本人...... 我们会发现,他们之间存在着一定的共性。比如,上述例子中的 Speck 和 Environment:无论是哪个国家的人,都有自己的语言和生长环境。而无论哪个国家的人,本质都属于人类。

所以,我们将这两种属性放在一块,约定成一个接口 Person,再来实现上面的例子:
  // 定义一个接口类型,接口里有两个方法:人的生长环境和语言
  type Person interface {
      Environment() string// 接口里面的方法是抽象方法,可以没有具体的实现
      Speck() string
  }
  // 堆代码 duidaima.com
  type Chinese struct {}
  type Englishmen struct {}
 
  func (c Chinese) Speck() string {
      return "中文"
  }
  func (c Chinese) Environment() string {
      return "中国社会"
  }
 
  func (e Englishmen) Speck() string {
      return "English"
  }
  func (e Englishmen) Environment() string {
      return "British Society"
  }
 
  func main() {
      var p Person
      c := Chinese{}
      p = c
      fmt.Printf("%T生在%s, 说%s\n", p, p.Environment(), p.Speck())
      e := Englishmen{}
      p = e
      fmt.Printf("%T生在%s, 说%s\n", p, p.Environment(), p.Speck())
  }
实现了接口 Person 里的所有方法,相当于实现了该接口。实现该接口后,Chinese 和 Enlishmen 类型的变量可以赋值给 Person 类型的 p。这就是具体类型(本例中为 Chinese 和 Englishmen)实现了某个接口(Person),并把具体的类型赋值给接口的过程。具体类型还可以换成任意表达式,比如 Go 语言中常用的文件流写入方法:
  var w io.Writer
  w = os.Stdout // *os.File 实现了 Write 方法
  w = new(bytes.Buffer) // *bytes.Buffer 实现了 Write 方法
二. 空接口的妙用
前面已经提到,将某个类型或者表达式赋值给接口时,需要实现这个接口的所有方法。
  var 张三 信徒 // 定义变量张三,类型为信徒
  李四有着了和张三一样的信仰(实现了信仰这个方法)
  张三 = 李四 // 张三接受李四,因为李四也是一个信徒
那么对于空接口呢?我们知道,空接口不包含任何方法,所以,Go 语言中,空接口可以接受任何类型的赋值:
  var 张三 空信仰的人
  李四实现了某个信仰
  张三 = 李四 // 张三接受李四,因为张三没有信仰
切换成 Go 语言表达式,空接口可以被赋值为任意的数据类型:
  var any interface{} // any 为一个空接口
  any = true // bool 值
  any = 12.34 // 浮点数
  any = "hello world" // 字符串
  any = map[string]int // map 集合
三. 类型断言
综上所述,我们可以发现一个接口的值分为两个部分:一个具体类型和该类型的一个值,这两者称为接口的动态类型和动态值。既然空接口可以接收任何类型的值,那么如何将这些值还原成它们本身的类型呢?Go 语言中的类型断言会帮你完成这个操作,语法为 x.{T},其中 x 是一个接口类型的表达式,T 是一个具体的类型:
 var w io.Writer
 w = os.Stdout
 f := w.(*os.File) // 类型断言,将 w 转换成 *os.File 类型
 c := w.(*bytes.Buffer) // 转换失败:系统崩溃退出
如上所示,当一个接口不能转换成某种具体的类型时,会直接发生崩溃。这时,我们可以在类型断言的接收处添加一个变量,去判断类型转换是否失败:
 f, ok := w.(*os.File) // 转换成功,ok == true
 c, ok := w.(*bytes.Buffer) // 转换失败, ok == false, c == nil
四. 类型分支
当不确定接口是何种具体类型时,可以用 switch 语句判断,我们把这种判断语句叫做类型分支:
 func ensureInteType(x interface{}) {
 switch v := x.(type) {
     case string:
         fmt.Printf("x is a string,value is %s\n", v)
     case int:
         fmt.Printf("x is a int is %d\n", v)
     case bool:
         fmt.Printf("x is a bool is %v\n", v)
     default:
         fmt.Println("unsupport type!")
     }
 }
Go 语言中最常用的类型分支是 database/sql 包中 Query 和 Exec 函数,它们支持数据库做增删改查的操作。
 import "database/sql"
 
 type Person struct {
     ID   int
     Name string
     age  int
 }
 func queryID(db sql.DB, age int, name string) {
     querySQL := "select * from Person where age = ? and name = ?"
     values := make([]interface{}, 0)
     values = append(values, age, name)
     rows, err := db.Query(querySQL, values...)
     if err != nil {
    fmt.Println("sql exec failed")
         return
 }
     for rows.next{
         ...
    }
 }
如上所示,我们在做 SQL 查询时,会加入条件判断。但很常见的情况是,这些判断条件字段的数据类型都是不一样的。比如第 9 行写的那样,Person 表里的 age 字段是一个 int 类型的值,而 name 是 string 类型的(上面的函数模拟了数据库的查询操作,运行的前置条件是运行机器安装了 MySQL,并新建了一个 Person 表)。

sql 包里的 Query 函数把查询语句中的每一个 “?" 都替换成了相应参数值对应的 SQL  字面量(用 ? 替换入参字面量是必要的,因为你不知道入参的类型和值是什么,会不会有不恰当的引号来控制你的查询,这样做可以防止一定程度的 SQL 注入攻击)。

同样地,当执行增、删、改操作时,用 Exec 函数:
 updateSQL := "update Person set age = ? where name = ?"
 values := make([]interface{}, 0)
 _, err := db.Exec(updateSQL, values...)
 if err != nil {
     fmt.Println("sql exec failed")
     return
 }
这都是由于在 Query 和 Exec 函数底层实现的代码中,都用到了类型分支(有兴趣可以去看看 sql 包底层实现的代码,这里就不展示出来了)。

五. 总结
接口是对类型行为的概括与抽象,和 Java 中的多态机制很类似。Go 接口的存在,让我们可以写出更加灵活与通用的函数。这些函数会基于绑定类型的不同,呈现出不同的行为。

接口是抽象的,不仅会造成很多难以理解的代码,而且运行时的成本也不低。所以,只有在不确定数据类型,或者其它必要的情况下用接口来处理。具体类型有独特的美,接口亦有它抽象和朦胧的美。

接口和具体类型在同样的场景下不可同时使用,但它们可以相互转换。这种相爱相杀的设计,也许正是 Go 语言得天独厚的魅力所在吧。
用户评论