在上一篇文章中,咱們聊了聊Golang中的一些基礎的語法,如變量的定義、條件語句、循環語句等等。他們和其餘語言很類似,咱們只須要看一看它們之間的區別,就差很少能夠掌握了,因此做者稱它們爲「基礎語法」。在這篇文章中,咱們將聊一聊Golang的一些語言特性,這也是Golang和其餘語言差異比較大的地方。除此以外,還有一部份內容是關於Golang的併發,這一部分將在下一篇文章中介紹。html
在Java中,咱們已經體會過了面向對象的方便之處。咱們只須要將現實中的模型抽象出來,就成爲了一個類,類裏面定義了描述這個類的一些屬性。編程
而在Golang中,則沒有對象這一說法,由於Golang是一個面向過程的語言。可是,咱們又知道面向對象在開發中的便捷性,因此咱們在Golang中有了結構體這一類型。併發
結構體是複合類型,當須要定義類型,它由一系列屬性組成,每一個屬性都有本身的類型和值的時候,就應該使用結構體,它把數據彙集在一塊兒。
組成結構體類型的那些數據成爲字段(fields)。每一個字段都有一個類型和一個名字;在一個結構體中,字段名字必須是惟一的。函數
咱們能夠近似的認爲,一個結構體就是一個類,結構體內部的字段,就是類的屬性。學習
注意,在結構體中也遵循用大小寫來設置公有或私有的規則。若是這個結構體名字的第一個字母是大寫,則能夠被其餘包訪問,不然,只能在包內進行訪問。而結構體內的字段也同樣,也是遵循同樣的大小寫肯定可用性的規則。
對於結構體,他的定義方式以下:設計
type 結構體名 struct { 字段1 類型 字段2 類型 }
對於結構體的聲明和初始化,有如下幾種形式:指針
使用var關鍵字code
var s T s.a = 1 s.b = 2
注意,在使用了var
關鍵字以後不須要初始化,這和其餘的語言有些不一樣。Golang會自動分配內存空間,並將該內存空間設置爲默認的值,咱們只須要按需進行賦值便可。htm
使用new函數對象
type people struct { name string age int } func main() { ming := new(people) ming.name = "xiao ming" ming.age = 18 }
使用字面量
type people struct { name string age int } func main() { ming := &people{"xiao ming", 18} }
上面咱們提到了幾種結構體的聲明的方法,但其實這幾種是有些區別的。
先說結論,第一種使用var
聲明的方式,返回的是該實例的結構類型,而第二第三種,返回的是一個指向這個結構類型的一個指針,是地址。
注意,這一部分做者能夠保證是觀點是正確的。可是做者的解釋其實有些問題。這是由於做者能力有限,還沒開始研究Golang的源碼,因此不能很好的解釋「返回的是實例的結構類型」這一句話。在做者的理解中,返回類型有兩種,一種是具體的數值,一種是指向這個數值的指針。
因此,對於第二第三種返回指針的聲明形式,在咱們須要修改他的值的時候,其實應該使用的方式是:
(*ming).name = "xiao wang"
也就是說,對於指針類型的數值,應該要先用*
取值,而後再修改。
可是,在Golang中,能夠省略這一步驟,直接使用ming.name = "xiao wang"
。儘管如此,咱們應該知道這一行爲的緣由,分清楚本身所操做的對象到底是什麼類型,掌握這點對下面方法這一章節相當重要。
在上一節的內容中,咱們也提到了面向對象的優點,而Golang又是一種面向過程的語言。在上一章節中,提到了用結構體實現了對象這一律念。在這一章中,提到的是對象對應的方法。
在Go語言中有一個概念,它和方法有着一樣的名字,而且大致上意思相同,Go的 方法是做用在接收器(receiver)上的一個函數,接收器是某種類型的變量,所以方法是一種特殊類型的函數。
說白了,方法就是函數,只不過是一種比較特殊的函數。
咱們都知道,在Golang中,定義一個函數是這樣的:
func 函數名(args) 返回類型
而在此基礎上,在func
和函數名
之間,加上接受者的類型,就能夠定義一個方法。
type Vertex struct { X, Y float64 } func (v Vertex) Abs() float64 { return math.Sqrt(v.X*v.X + v.Y*v.Y) } func main() { v := Vertex{3, 4} fmt.Println(v.Abs()) }
能夠看到,咱們定義了一個Vertex
爲接收者的方法。也就是說,這個方法,僅僅能夠被Vertex
的結構體數值調用。
注意,接受者有兩種類型,即指針接收者和非指針接受者。
咱們來看下面的代碼:
type Vertex struct { X, Y float64 } func (v Vertex) test1(){ v.X++; v.Y++; } func (v *Vertex) test2(){ v.X++; v.Y++; }
在這裏咱們定義了兩個方法,test1
和test2
,他們惟一的區別就是方法名前面的接收者不一樣,一個是指針類型的,一個是值類型的。
而且,執行這兩個方法,也須要定義不一樣的結構體類型。
v1 := Vertex{1, 1} v2 := &Vertex{1, 1} v1.test1() v2.test2() fmt.Println(v1) fmt.Println(v2)
執行以後咱們能夠查看結果:
{1 1} &{2 2}
也就是說,只有指針接收者類型的方法,才能修改這個接收器的成員值,非指針接收者,方法修改的只是這個傳入的指針接收者的一個拷貝。
那麼爲何會這樣,咱們一樣拿代碼說話:
type Vertex struct { X, Y float64 } func (v Vertex) test1(){ fmt.Printf("在方法中的v的地址爲:%p\n", &v) v.X++; v.Y++; } func main() { v1 := Vertex{1, 1} fmt.Printf("本身定義的v1內存地址爲:%p\n", &v1) v1.test1() }
在上述的代碼中,我定義了一個非指針類型接收者的方法,而後打印方法外的v1和方法內的v的內存地址,結果以下:
本身定義的v1內存地址爲:0xc00000a0e0 在方法中的v的地址爲:0xc00000a100
咱們能夠看出,這兩個結構體數值的內存地址是不同的。也就是說,就算咱們修改了方法內的數值,對方法外的原變量也不能起到任何的做用,由於咱們修改的只是一個結構體數值的拷貝,沒有真正的修改的他原本的值。
可是,若是使用的是指針接收者,他們的內存地址就是同樣的了,下面看代碼:
type Vertex struct { X, Y float64 } func (v *Vertex) test2(){ fmt.Printf("在方法中的v的地址爲:%p\n", v) v.X++; v.Y++; } func main() { v1 := &Vertex{1, 1} fmt.Printf("本身定義的v1內存地址爲:%p\n", v1) v1.test2() }
執行以後的結果爲:
本身定義的v1內存地址爲:0xc00000a0e0 在方法中的v的地址爲:0xc00000a0e0
因此咱們能夠知道,使用指針接收器是能夠直接拿到原數據所在的內存地址,也就是說能夠直接修改原來的數值。這也和Java中的對象調用方法更加類似;而對於非指針,它是拷貝原來的數據。至於使用哪種,須要按照實際的業務來處理。
可是,若是是一個大對象,若是也採用拷貝的方式,將會耗費大量的內存,下降效率。
還有一點須要補充說明:不論是指針接收者仍是非指針接收者,他在接受一個對象的時候,會自動將這個對象轉換爲這個方法所須要的類型。也就是說,若是我如今有一個非指針類型的對象,去調用一個指針接收者的方法,那麼這個對象將會自動被取地址而後再被調用。
換句話說,方法的調用類型不重要,重要的是方法是怎麼定義的。
在聊接口怎麼用以前,咱們先來聊聊接口的做用。
在做者看來,接口是一種規範,一種約定。舉個例子:一個商品只要是符合某種種類的約定,遵循某種種類的規範,那麼就能夠認爲這個商品是屬於這個種類的,他會具備這個種類應有的一切功能。這樣作的目的是爲了把生產這個商品的生產者和使用這個商品的消費者分開。用編程裏面的術語來說,咱們能夠把實現和調用解耦。
下面舉個鴨子模型的例子,來自於知乎,能夠說特別的形象生動了。注意,在這裏先不研究語法,語法的問題咱們後面會提到,你只須要跟隨做者的思路去思考:
首先咱們定義一個規範,也就是說定義一個接口:
type Duck interface { Quack() // 鴨子叫 DuckGo() // 鴨子走 }
這個接口是鴨子的行爲,咱們認爲,做爲一隻鴨子,它須要會叫,會走。而後咱們再定義一隻雞:
type Chicken struct { }
假設這隻雞特別厲害,它也會像鴨子那樣叫,也會像鴨子那樣走路,那麼咱們定義一下這隻雞的行爲:
func (c Chicken) Quack() { fmt.Println("嘎嘎") } func (c Chicken) DuckGo() { fmt.Println("大搖大擺的走") }
注意,這裏只是實現了 Duck 接口方法,並無將雞類型和鴨子接口顯式綁定。這是一種非侵入式的設計。
而後咱們讓這隻雞,去叫,去像鴨子那樣走路:
func main() { c := Chicken{} var d Duck d = c d.Quack() d.DuckGo() }
執行以後咱們能夠獲得結果:
嘎嘎 大搖大擺的走
也就是說,這隻雞,他能作到鴨子能作的全部事情,那麼咱們能夠認爲,這隻雞,他就是一個鴨子。
這裏牽涉到了一個概念,任何類型的數據,它只要實現了一個接口中方法集,那麼他就屬於這個接口類型。因此,當咱們在實現一個接口的時候,須要實現這個接口下的全部方法,不然編譯將不能經過。
理解了接口是什麼以後,咱們再來聊聊語法,首先是如何定義一個接口:
type 接口名 interface { 方法1(參數) 返回類型 方法2(參數) 返回類型 ... }
這一部分和結構體的定義很類似,可是裏面的元素換成了函數,可是這個函數不須要func
關鍵字。這裏和Java中的接口類似,不須要訪問修飾符,只須要函數名參數返回類型。
定義完接口以後,咱們須要定義方法去實現這些接口。注意,這裏新定義方法的方法名,參數,返回類型,必須和接口中所定義的徹底一致。
其次,這裏的方法中的接收者,就是調用這個方法的對象類型。換句話說,這個方法想要被哪類對象執行,接收者就是誰。
還有最重要的一點,若是要實現某個接口,必需要實現這個接口的所有方法。
在調用接口的時候,咱們須要先聲明這個接口類型的變量,如咱們上面定義了一個Duck
接口,就應該聲明一個Duck
類型的變量。
var d Duck
而後咱們把實現了這個方法的接收器對象賦值給這個變量d
:
d := Chicken{}
隨後,咱們就能夠用這個變量d
,是調用那些方法了。
首先,仍是很感謝你能看到這裏,謝謝你!(鞠躬
這是《Golang入門》系列的第三篇了,也差很少要講完Golang的基本語法了。在這篇文章中是介紹了一些Golang的比較特殊的用法,但願可以對你有所幫助。
固然了,做者也只是個初學者,也纔剛剛開始學習Golang。在學習的過程當中,不免會有錯誤的理解,或者會有遺漏的地方。若是你發現了,請你留言指正我,謝謝!除此以外,若是有我講的不清楚不明白的地方,也歡迎留言,咱們一塊兒交流學習。
在下一篇中,做者將介紹一下Golang的併發。咱們下篇文章見。
再次感謝~
PS:若是有其餘的問題,也能夠在公衆號找到做者。而且,全部文章第一時間會在公衆號更新,歡迎來找做者玩~