記得剛從Java轉Go的時候,一個用Go語言的前輩告訴我:「要少用interface{},這玩意兒很好用,可是最好不要用。」那時候個人組長打趣接話:「不會,他是從Java轉過來的,碰到個問題就想定義個類。」當時我對interface{}的第一印象也是類比Java中的Object類,咱們使用Java確定不會處處去傳Object啊。後來的事實證實,年輕人畢竟是年輕人,看着目前項目裏漫天飛的interface{},它們時而變成函數形參讓人摸不着頭腦;時而隱藏在結構體字段中變化多端。不由想起之前看到的一句話:「動態語言一時爽,重構代碼火葬場。」故而寫下此篇關於interface{}的經驗總結,供之後的本身和讀者參考。html
一個語言用的久了,不免使用者的思惟會受到這個語言的影響,interface{}做爲Go的重要特性之一,它表明的是一個相似*void
的指針,能夠指向不一樣類型的數據。因此咱們可使用它來指向任何數據,這會帶來相似與動態語言的便利性,如如下的例子:git
type BaseQuestion struct{
QuestionId int
QuestionContent string
}
type ChoiceQuestion struct{
BaseQuestion
Options []string
}
type BlankQuestion struct{
BaseQuestion
Blank string
}
func fetchQuestion(id int) (interface{} , bool) {
data1 ,ok1 := fetchFromChoiceTable(id) // 根據ID到選擇題表中找題目,返回(ChoiceQuestion)
data2 ,ok2 := fetchFromBlankTable(id) // 根據ID到填空題表中找題目,返回(BlankQuestion)
if ok1 {
return data1,ok1
}
if ok2 {
return data2,ok2
}
return nil ,false
}
複製代碼
在上面的代碼中,data1是ChoiceQuestion
類型,data2是BlankQuestion
類型。所以,咱們的interface{}指代了三種類型,分別是ChoiceQuestion
、BlankQuestion
和nil
,這裏就體現了Go和麪向對象語言的不一樣點了,在面嚮對象語言中,咱們本能夠這麼寫:github
func fetchQuestion(id int) (BaseQuestion , bool) {
...
}
複製代碼
只須要返回基類BaseQuestion
便可,須要使用子類的方法或者字段只須要向下轉型。然而在Go中,並無這種is-A
的概念,代碼會無情的提示你,返回值類型不匹配。
那麼,咱們該如何使用這個interface{}
返回值呢,咱們也不知道它是什麼類型啊。因此,你得不厭其煩的一個一個判斷:golang
func printQuestion(){
if data, ok := fetchQuestion(1001); ok {
switch v := data.(type) {
case ChoiceQuestion:
fmt.Println(v)
case BlankQuestion:
fmt.Println(v)
case nil:
fmt.Println(v)
}
fmt.Println(data)
}
}
// ------- 輸出--------
{{1001 CHOICE} [A B]}
data - &{{1001 CHOICE} [A B]}
複製代碼
EN,好像經過Go的switch-type
語法糖,判斷起來也不是很複雜嘛。若是你也這樣覺得,而且跟我同樣用了這個方法,恭喜你已經入坑了。
由於需求永遠是多變的,假如如今有個需求,須要在ChoiceQuesiton
打印時,給它的QuestionContent
字段添加前綴選擇題
,因而代碼變成如下這樣:數據庫
func printQuestion() {
if data, ok := fetchQuestion(1001); ok {
switch v := data.(type) {
case ChoiceQuestion:
v.QuestionContent = "選擇題"+ v.QuestionContent
fmt.Println(v)
...
fmt.Println(data)
}
}
// ------- 輸出--------
{{1001 選擇題CHOICE} [A B]}
data - {{1001 CHOICE} [A B]}
複製代碼
咱們獲得了不同的輸出結果,而data根本沒有變更。可能有的讀者已經猜到了,v
和data
根本不是指向同一份數據,換句話說,v := data.(type)
這條語句,會新建一個data在對應type
下的副本,咱們對v
操做影響不到data。固然,咱們能夠要求fetchFrom***Table()
返回*ChoiceQuestion
類型,這樣咱們能夠經過判斷*ChoiceQuestion
來處理數據副本問題:bash
func printQuestion() {
if data, ok := fetchQuestion(1001); ok {
switch v := data.(type) {
case *ChoiceQuestion:
v.QuestionContent = "選擇題"+ v.QuestionContent
fmt.Println(v)
...
fmt.Println(data)
}
}
// ------- 輸出--------
&{{1001 選擇題CHOICE} [A B]}
data - &{{1001 選擇題CHOICE} [A B]}
複製代碼
不過在實際項目中,你可能有不少理由不能去動fetchFrom***Table()
,也許是涉及數據庫的操做函數你沒有權限改動;也許是項目中不少地方使用了這個方法,你也不能隨便改動。這也是我沒有寫出fetchFrom***Table()
的實現的緣由,不少時候,這些方法對你只能是黑盒的。退一步講,即便方法簽名能夠改動,咱們這裏也只是列舉出了兩種題型,可能還有材料題、閱讀題、寫做題等等,若是需求要對每一個題型的QuestonContent
添加對應的題型前綴,咱們豈不是要寫出下面這種代碼:數據結構
func printQuestion() {
if data, ok := fetchQuestion(1001); ok {
switch v := data.(type) {
case *ChoiceQuestion:
v.QuestionContent = "選擇題"+ v.QuestionContent
fmt.Println(v)
case *BlankQuestion:
v.QuestionContent = "填空題"+ v.QuestionContent
fmt.Println(v)
case *MaterialQuestion:
v.QuestionContent = "材料題"+ v.QuestionContent
fmt.Println(v)
case *WritingQuestion:
v.QuestionContent = "寫做題"+ v.QuestionContent
fmt.Println(v)
...
case nil:
fmt.Println(v)
fmt.Println(data)
}
}
複製代碼
這種代碼帶來了大量的重複結構,因而可知,interface{}
的動態特性很不能適應複雜的數據結構,難道咱們就不能有更方便的操做了麼?山窮水盡之際,或許能夠回頭看看面向對象思想,也許繼承和多態能很好的解決咱們遇到的問題。函數
咱們能夠把這些題型抽成一個接口,而且讓BaseQuestion
實現這個接口。fetch
type IQuestion interface{
GetQuestionType() int
GetQuestionContent()string
AddQuestionContentPrefix(prefix string)
}
type BaseQuestion struct {
QuestionId int
QuestionContent string
QuestionType int
}
func (self *BaseQuestion) GetQuestionType() int {
return self.QuestionType
}
func (self *BaseQuestion) GetQuestionContent() string {
return self.QuestionContent
}
func (self *BaseQuestion) AddQuestionContentPrefix(prefix string) {
self.QuestionContent = prefix + self.QuestionContent
}
//修改返回值爲IQuestion
func fetchQuestion(id int) (IQuestion, bool) {
data1, ok1 := fetchFromChoiceTable(id) // 根據ID到選擇題表中找題目
data2, ok2 := fetchFromBlankTable(id) // 根據ID到選擇題表中找題目
if ok1 {
return &data1, ok1
}
if ok2 {
return &data2, ok2
}
return nil, false
}
複製代碼
無論有多少題型,只要它們包含BaseQuestion
,就能自動實現IQuestion
接口,從而,咱們能夠經過定義接口方法來控制數據。ui
func printQuestion() {
if data, ok := fetchQuestion(1002); ok {
var questionPrefix string
//須要增長題目類型,只須要添加一段case
switch data.GetQuestionType() {
case ChoiceQuestionType:
questionPrefix = "選擇題"
case BlankQuestionType:
questionPrefix = "填空題"
}
data.AddQuestionContentPrefix(questionPrefix)
fmt.Println("data - ", data)
}
}
//--------輸出--------
data - &{{1002 填空題BLANK 2} [ET AI]}
複製代碼
這種方法無疑大大減小了副本的建立數量,並且易於擴展。經過這個例子,咱們也瞭解到了Go接口的強大之處,雖然Go並非面向對象的語言,可是經過良好的接口設計,咱們徹底能夠從中窺探到面向對象思惟的影子。也難怪在Go文檔的FAQ中,對於Is Go an object-oriented language?這個問題,官方給出的答案是yes and no.
這裏還能夠多扯一句,前面說了v := data.(type)
這條語句是拷貝data的副本,但當data是接口對象時,這條語句就是接口之間的轉型而不是數據副本拷貝了。
//定義新接口
type IChoiceQuestion interface {
IQuestion
GetOptionsLen() int
}
func (self *ChoiceQuestion) GetOptionsLen() int {
return len(self.Options)
}
func showOptionsLen(data IQuestion) {
//choice和data指向同一份數據
if choice, ok := data.(IChoiceQuestion); ok {
fmt.Println("Choice has :", choice.GetOptionsLen())
}
}
//------------輸出-----------
Choice has : 2
複製代碼
nil
坑看如下代碼:
func fetchFromChoiceTable(id int) (data *ChoiceQuestion) {
if id == 1001 {
return &ChoiceQuestion{
BaseQuestion: BaseQuestion{
QuestionId: 1001,
QuestionContent: "HELLO",
},
Options: []string{"A", "B"},
}
}
return nil
}
func fetchQuestion(id int) (interface{}) {
data1 := fetchFromChoiceTable(id) // 根據ID到選擇題表中找題目
return data1
}
func sendData(data interface{}) {
fmt.Println("發送數據 ..." , data)
}
func main(){
data := fetchQuestion(1002)
if data != nil {
sendData(data)
}
}
複製代碼
一串很常見的業務代碼,咱們根據id查詢Question,爲了之後能方便的擴展,咱們使用interface{}
做爲返回值,而後根據data是否爲nil
來判斷是否是要發送這個Question
。不幸的是,無論fetchQuestion()
方法有沒有查到數據,sendData()
都會被執行。運行main()
,打印結果以下:
發送數據 ... <nil>
Process finished with exit code 0
複製代碼
要明白內中玄機,咱們須要回憶下interface{}
到底是個什麼東西,文檔上說,它是一個空接口,也就是說,一個沒有聲明任何方法的接口,那麼,接口在Go的內部又到底是怎麼表示的?我在官方文檔上找到一下幾句話:
Under the covers, interfaces are implemented as two elements, a type and a value. The value, called the interface's dynamic value, is an arbitrary concrete value and the type is that of the value. For the int value 3, an interface value contains, schematically, (int, 3).
以上的話大意是說,interface在Go底層,被表示爲一個值和值對應的類型的集合體,具體到咱們的示例代碼,fetchQuestion()
的返回值interface{}
,實際上是指(*ChoiceQuestion, data1)的集合體,若是沒查到數據,則咱們的data1爲nil,上述集合體變成(*ChoiceQuestion, nil)。而Go規定中,這樣的結構的集合體自己是非nil的,進一步的,只有(nil,nil)這樣的集合體才能被判斷爲nil。
這嚴格來講,不是interface{}
的問題,而是Go接口設計的規定,你把以上代碼中的interface{}
換成其它任意你定義的接口,都會產生此問題。因此咱們對接口的判nil
,必定要慎重,以上代碼若是改爲多返回值形式,就能徹底避免這個問題。
func fetchQuestion(id int) (interface{},bool) {
data1 := fetchFromChoiceTable(id) // 根據ID到選擇題表中找題目
if data1 != nil {
return data1,true
}
return nil,false
}
func sendData(data interface{}) {
fmt.Println("發送數據 ..." , data)
}
func main(){
if data, ok := fetchQuestion(1002); ok {
sendData(data)
}
}
複製代碼
固然,也有不少其它的辦法能夠解決,你們能夠自行探索。
零零散散寫了這麼多,有點前言不搭後語,語言不通之處還望見諒。Go做爲一個設計精巧的語言,它的成功不是沒有道理的,經過對目前遇到的幾個大問題和總結,慢慢對Go有了一點點淺薄的認識,之後碰到了相似的問題,還能夠繼續添加在文章裏。
interface{}
做爲Go中最基本的一個接口類型,能夠在代碼靈活性方面給咱們提供很大的便利,可是咱們也要認識到,接口就是對一類具體事物的抽象,而interface{}
做爲每一個結構體都實現的接口,提供了一個很是高層次的抽象,以致於咱們會丟失事物的大部分信息,因此咱們在使用interface{}
前,必定要謹慎思考,這就像相親以前提要求,你要是說只要是個女的我均可以接受,那可就別怪來的人多是高的矮的胖的瘦的美的醜的。
文中出現的代碼,能夠在示例代碼 中找到完整版。