寫Go代碼的你是否遇到過這些問題?弄明白了嗎?

這幾天整理了一些 Go 語言的小知識點,在這裡分享給大家,大家在工作過程中多多少少都會遇到的,希望有幫助。多看幾遍下面這些題,大家在敲代碼時會少走很多彎路的。

我們看下面的代碼:

寫Go代碼的你是否遇到過這些問題?弄明白了嗎?

map

大家想一想這裡輸出的結果是什麼?012 or 333?是的,你肯定答對了,這裡打印出來的是333。這個其實很簡單,對於指針熟悉的同學都會明白,首先 map 的 key 是 int 類型,而 value 是 int類型的指針,在 for 循環中,變量 i 的值從0到3,但是 i 的指針沒有變,而且最終的 i 的值是 3,3 < 3 是不成立的,所以此時 i 的值為3並且 for 循環結束,因此 m 的所有 value 都是3了。那麼如果我們想打印出來結果是 0、1、2應該怎麼弄呢?

這裡我說兩個簡單的方案:第一個是將 map 的 value 也轉換成 int 類型,另外一個就是在 for 循環中,增加一個臨時變量,保存 i 的值,然後將臨時變量的指針存入 m 中。這兩種方法都可以,主要看實際情況中 map 需要是什麼的類型了。

這裡還有一點關於 map 的初始化的一個建議,

m := make(map[int]*int)
m1 := make(map[int]*int,10)
var m2 map[int]*int

這裡我列出了三種 map 初始化的方式,第一種和第二種的區別是有默認大小,第三種知識聲明瞭變量,第三種我們在使用的時候需要注意的是,此時並不能向 m2 中添加,需要再使用 make 函數初始化一下再使用。對於第一種和第二種我們可以直接使用,但是這兩種方法對於我們的性能有很大影響,對於第一種,我們默認大小就是0,當添加元素時,會重新分配空間,也就是說,當此時 map 的空間不能存下新的元素時,系統會重新分配空間,這樣使程序的性能大大的下降,這裡和 slice 很類似,slice 會在空間不夠的情況下再尋找當前大小二倍的空間,這個過程很消耗時間性能的。所以在我們已知或者預計大小時,在初始化時就指定大小。

我們接著看下一個題:

type T struct {
name string
}
func main() {
m := map[string]T{"x": {"one"}}
m["x"].name = "two"
print(m["x"])
}

大家思考一下這裡,上面的代碼是輸出什麼結果?或者還是編譯不通過呢?

聰明的你肯定又對了,這裡是編譯不通過的:cannot assign to struct field m["x"].name in map。首先按照我們的邏輯看是沒有問題的,但是我們需要記住的一點就是:map中的元素不是變量,因此不能尋址。map 作為一個封裝好的數據結構,由於它底層可能會由於數據擴張而進行遷移,所以拒絕直接尋址,避免產生野指針,map 中的key在不存在的時候,賦值語句其實會進行新的k-v值的插入,所以拒絕直接尋址結構體內的字段,以防結構體不存在的時候可能造成的錯誤。

那麼我們如何解決這個問題呢?我的建議是可以將 map 的 value 改為指針類型,這時是可以的,編譯通過,而且也是我們需要的值。

接著看:

寫Go代碼的你是否遇到過這些問題?弄明白了嗎?

接口

這裡大家覺得 Value struct 是否實現了 Copyable 接口?

首先我們要知道所有 struct 都實現了empty interface{} 接口,所以 Value 也實現了 empty interface{},這是沒錯的,但是即使如此,我們也不可以認為 Value 實現了 Copyable 接口,官方給出的原因是:In Go method types must match exactly,也就是說一定要完全匹配。在上面的例子中,我們將 Copy() 的返回值改為 interface{} 之後,就可以了。

我們接著往下看:

寫Go代碼的你是否遇到過這些問題?弄明白了嗎?

接口

大家覺得上面的 T 和 T2 哪一個實現了 Equaler 接口呢?

通過 Copyable 的例子,相信大家這裡都有自己的答案了,沒錯,這裡 T2 實現了 Equaler 接口。這兩個例子主要就是告訴大家,Go 中實現接口時需要注意這一點,雖然這種情況在其他語言裡可以通過,但是 Go 是不允許的?

這裡額外再說一點,Go 中是沒有 implements 關鍵字的,想要實現某個接口,只需要實現了接口裡所有的方法即可,在一些大的項目中,我們的一個 struct 是很容易實現很多接口的,即使某些接口不是我們所需要的,那麼我們如何知道這個 struct 需要實現哪些接口呢?這裡官方給出了兩個建議:第一個就是:

type T struct{}
var _ I = T{}
var _ I = (*T)(nil)

在這裡我們斷言 T 實現了 I 接口,而且這樣寫還有一個好處就是如果 T 沒有實現 I 接口,這裡編譯是不通過的。

還有一種方法:

type Fooer interface {
Foo()
ImplementsFooer()
}

type Bar struct{}
func (b Bar) ImplementsFooer() {}
func (b Bar) Foo() {}

就是在我們的接口中增加一個獨有的方法,而方法名字就是見名知意,如果要實現這個接口,我們就必須有這個方法,如果我們一個 struct 需要實現多個接口,這個 struct 就需要具有多個這個見名知意的方法。

以上這兩種方法都有各自的好處,第一個可以看出來 struct 實現了哪些接口,而且如果沒有實現的話,編譯不通過。第二個好處就是見名知意,我看到這個函數我就知道這個 struct 實現了哪個(哪些)接口。任何事務都是沒有完美的,這樣寫也是有缺點的,增加了代碼量,如果一個 struct 實現了50個接口,那麼就需要在每個接口中都實現見名知意的方法,而且 struct 都要實現。具體情況就看大家需求吧。在我之前關於 Go 語言接口的文章中,有很多人評論這種實現接口的方式不利於閱讀代碼,所以在這裡給大家一點建議,這種方式確實是利於閱讀代碼的。

好了,接著下一題:

func returnsError() error {
var p *MyError = nil
if bad() {
p = ErrBad
}
return p
}

這裡說明一下,MyError 實現了 error 接口。大家覺得這樣的代碼有什麼問題嗎?

if err != nil 這樣的代碼相信大家已經敲了無數次了,在這個例子中,我們使用自己的錯誤類型,但是返回值是 error 類型。這時在函數調用的地方我們如果還用 if err != nil 的方式來判斷就不可以了,這樣返回的永遠都不是 nil。這個問題就需要大家對 Go 語言的 interface 有一定的瞭解了(大家可以看我的文章:go語言的interface為什麼好用?),這裡再簡單說一下,對於接口我們判斷是否為 nil,需要接口的 type 和 value 都是 nil 才可以,這個例子中,返回值 p ,它的 Type 是 myError,Value 雖然是 nil,但是 p 並不是 nil。如果大家一定要使用自己的錯誤類型,在返回的地方一定要注意一下,我們可以直接使用 return nil 代替現在的寫法。

我們看最後一段代碼:

type T1 int
type T2 int
var t1 T1
var x = T2(t1)
var st1 []T1
var sx = ([]T2)(st1)

大家覺得上面的代碼哪裡有問題?

這個主要是類型轉換的問題,T1、T2 的底層都是 int 類型,我們可以將變量 t1 直接強制轉換成 T2 類型,這是沒有問題的。但是對於 st1 是 t1 類型的數組,這裡我們就不可以直接強制轉換為 T2 類型的數組了。這裡也是大家寫代碼是需要注意的一點。

好了,今天就說這麼多吧,這些題都不是很難,但是在工作中都是非常容易遇到的,有時候我們遇到了需要很長時間查閱資料等才弄明白,如果能事先明白這些,工作中就會少走彎路。上面的例子有很多是來自 Go 語言的官方網站,大家也可以去自行閱讀,關於 Go 語言的起源等,官方都給出瞭解釋。

如果有更好的答案或者題歡迎評論討論,感謝閱讀,歡迎轉發~


分享到:


相關文章: