1. 程式人生 > >go語言的一些基礎總結

go語言的一些基礎總結

1、型別轉換
使用type為一個型別定義一個別名。
type  width  int8
這裡 int8 就是width的底層型別,二者可以相互轉換(當然是強制型別轉換)
2、型別 T(或 *T)上的所有方法的集合叫做型別 T(或 *T)的方法集。
因為方法是函式,所以同樣的,不允許方法過載,即對於一個型別只能有一個給定名稱的方法。但是如果基於接收者型別,是有過載的:具有同樣名字的方法可以在 2 個或多個不同的接收者型別上存在


func (a *denseMatrix) Add(b Matrix) Matrix
func (a *sparseMatrix) Add(b Matrix) Matrix


如上面那樣,雖然都是Add方法,但是因為接收者不同,所以是允許的。


別名型別不能有它原始型別(底層)上已經定義過的方法。


3、型別和作用在它上面定義的方法必須在同一個包裡定義,這就是為什麼不能在 int、float 或類似這些的型別上定義方法。試圖在 int 型別上定義方法會得到一個編譯錯誤:
cannot define new methods on non-local type int
比如想在 time.Time 上定義如下方法:
func (t time.Time) first3Chars() string {
return time.LocalTime().String()[0:3]
}
型別在其他的,或是非本地的包裡定義,在它上面定義方法都會得到和上面同樣的錯誤。


4、指標接收者和值接收者
       指標方法和值方法都可以在指標或非指標上被呼叫


5、方法和未匯出欄位
go語言規定:如果一個型別名稱的首字母是大寫的話,那麼這個型別是匯出的,也就是說我們可以在其他包中訪問。反之則是不可匯出的。
package person
type Person struct {
firstName string
lastName  string
}
如上,型別Person是匯出的,但是它的欄位 firstName 和 lastName 並沒有被匯出,因為這兩個欄位的首字母是小寫,也就是說,我們在其他包中可以訪問 Person 這個型別,但是不能訪問它的 firstName 和 lastName 欄位。


6、結構體可以包含一個或多個 匿名(或內嵌)欄位,即這些欄位沒有顯式的名字,只有欄位的型別是必須的,此時型別就是欄位的名字。匿名欄位本身可以是一個結構體型別,即 結構體可以包含內嵌結構體。
       Go 語言中的繼承是通過內嵌或組合來實現的,所以可以說,在 Go 語言中,相比較於繼承,組合更受青睞。
內嵌匿名結構體的話,可能會造成命名衝突:當兩個欄位擁有相同的名字(可能是繼承來的名字)時該怎麼辦呢?
  1. 外層名字會覆蓋內層名字(但是兩者的記憶體空間都保留),這提供了一種過載欄位或方法的方式;
  2. 如果相同的名字在同一級別出現了兩次,如果這個名字被程式使用了,將會引發一個錯誤(不使用沒關係)。沒有辦法來解決這種問題引起的二義性,必須由程式設計師自己修正。
例子:
type A struct {a int}
type B struct {a, b int}


type C struct {A; B}
var c C;
規則 2:使用 c.a 是錯誤的,到底是 c.A.a 還是 c.B.a 呢?會導致編譯器錯誤:ambiguous DOT reference c.a disambiguate with either c.A.a or c.B.a。
type D struct {B; b float32}
var d D;
規則1:使用 d.b 是沒問題的:它是 float32,而不是 B 的 b。如果想要內層的 b 可以通過 d.B.b 得到。


7、內嵌型別的方法和繼承
當一個匿名型別被內嵌在結構體中時,匿名型別的可見方法也同樣被內嵌,這在效果上等同於外層型別 繼承 了這些方法:將父型別放在子型別中來實現亞型。這個機制提供了一種簡單的方式來模擬經典面嚮物件語言中的子類和繼承相關的效果,也類似 Ruby 中的混入(mixin)。
下面是一個示例:假定有一個 Engine 介面型別,一個 Car 結構體型別,它包含一個 Engine 型別的匿名欄位:
type Engine interface {
Start()
Stop()
}


type Car struct {
Engine
}
我們可以構建如下的程式碼:
func (c *Car) GoToWorkIn() {
// get in car
c.Start()
// drive to work
c.Stop()
// get out of car
}




內嵌將一個已存在型別的欄位和方法注入到了另一個型別裡:匿名欄位上的方法“晉升”成為了外層型別的方法。當然型別可以有隻作用於本身例項而不作用於內嵌“父”型別上的方法,
可以覆寫方法(像欄位一樣):和內嵌型別方法具有同樣名字的外層型別的方法會覆寫內嵌型別對應的方法。
因為一個結構體可以嵌入多個匿名型別,所以實際上我們可以有一個簡單版本的多重繼承,就像:type Child struct { Father; Mother}。
結構體內嵌和自己在同一個包中的結構體時,可以彼此訪問對方所有的欄位和方法。


8、介面:介面(interface)定義了一種規範,實現了該介面的所有型別都必須符合介面的規範。
介面是一種契約,實現型別必須滿足它,它描述了型別的行為,規定型別可以做什麼。介面徹底將型別能做什麼,以及如何做分離開來,使得相同介面的變數在不同的時刻表現出不同的行為,這就是多型的本質。
type Namer interface {
    Method1(param_list) return_type
    Method2(param_list) return_type
}
我們可以宣告一個介面型別的變數,var ai Namer。任何實現了Namer介面型別的變數都可以賦值給ai。
注意:不能宣告指向介面值的指標。




9、介面的匿名巢狀
  原理和struct匿名巢狀相同


10、型別斷言
如上聲明瞭一個介面型別的變數:var  ai Namer;那麼任何實現了Namer介面的變數都可以賦值給 ai,所以要有一種方法可以判斷 ai 的動態型別。那就是型別斷言:v,ok := varI.(T),其中 varI 必須是一個介面變數,用於判斷 varI 型別此時是否是一個 T 型別。
注意:如果型別斷言成功,那麼 v 就是一個 T 型別的變數,可以通過 v 來訪問 T 型別的任何屬性。




11、介面的方法集呼叫方式
弄懂下面的例子就可以了:
作用於變數上的方法(這裡的變數指的是非介面型別的其他變數)實際上是不區分變數到底是指標還是值的。當碰到介面型別值時,這會變得有點複雜,原因是介面變數中儲存的具體值是不可定址的,幸運的是,如果使用不當編譯器會給出錯誤。考慮下面的程式:
示例 11.5 methodset2.go:
package main


import (
"fmt"
)


type List []int


func (l List) Len() int {
return len(l)
}


func (l *List) Append(val int) {
*l = append(*l, val)
}


type Appender interface {
Append(int)
}


func CountInto(a Appender, start, end int) {
for i := start; i <= end; i++ {
a.Append(i)
}
}


type Lener interface {
Len() int
}


func LongEnough(l Lener) bool {
return l.Len()*10 > 42
}


func main() {
// A bare value
var lst List
// compiler error:
// cannot use lst (type List) as type Appender in argument to CountInto:
//List does not implement Appender (Append method has pointer receiver)
// CountInto(lst, 1, 10)
if LongEnough(lst) { // VALID:Identical receiver type
fmt.Printf("- lst is long enough\n")
}


// A pointer value
plst := new(List)
CountInto(plst, 1, 10) //VALID:Identical receiver type
if LongEnough(plst) {
// VALID: a *List can be dereferenced for the receiver
fmt.Printf("- plst is long enough\n")
}
}
討論
在 lst 上呼叫 CountInto 時會導致一個編譯器錯誤,因為 CountInto 需要一個 Appender,而它的方法 Append 只定義在指標上。 在 lst 上呼叫 LongEnough 是可以的因為 'Len' 定義在值上。
在 plst 上呼叫 CountInto 是可以的,因為 CountInto 需要一個 Appender,並且它的方法 Append 定義在指標上。 在 plst 上呼叫 LongEnough 也是可以的,因為指標會被自動解引用。
總結
在介面上呼叫方法時,必須有和方法定義時相同的接收者型別或者是可以從具體型別 P 直接可以辨識的:
  ● 指標方法可以通過指標呼叫
  ● 值方法可以通過值呼叫
  ● 接收者是值的方法可以通過指標呼叫,因為指標會首先被解引用
  ● 接收者是指標的方法不可以通過值呼叫,因為儲存在介面中的值沒有地址
將一個值賦值給一個介面時,編譯器會確保所有可能的介面方法都可以在此值上被呼叫,因此不正確的賦值在編譯期就會失敗。
譯註
Go 語言規範定義了介面方法集的呼叫規則:
  ● 型別 *T 的可呼叫方法集包含接受者為 *T 或 T 的所有方法集
  ● 型別 T 的可呼叫方法集包含接受者為 T 的所有方法
  ● 型別 T 的可呼叫方法集不包含接受者為 *T 的方法




12、空介面
空介面是最小的介面,不包含任何方法,對他的實現不做任何要求。
type Any interface {}
任何其他型別都預設實現了空介面。可以給一個空介面型別的變數 var val interface {} 賦任何型別的值。


每個 interface {} 變數在記憶體中佔據兩個字長:一個用來儲存它包含的型別,另一個用來儲存它包含的資料或者指向資料的指標。


13、介面到介面的賦值
一個介面的值可以賦值給另一個介面變數,只要底層型別實現了必要的方法。這個轉換是在執行時進行檢查的,轉換失敗會導致一個執行時錯誤:這是 'Go' 語言動態的一面,可以拿它和 Ruby 和 Python 這些動態語言相比較。
假定:
var ai AbsInterface // declares method Abs()
type SqrInterface interface {
    Sqr() float
}
var si SqrInterface
pp := new(Point) // say *Point implements Abs, Sqr
var empty interface{}
那麼下面的語句和型別斷言是合法的:
empty = pp                // everything satisfies empty
ai = empty.(AbsInterface) // underlying value pp implements Abs()
// (runtime failure otherwise)
si = ai.(SqrInterface) // *Point has Sqr() even though AbsInterface doesn’t
empty = si             // *Point implements empty set
// Note: statically checkable so type assertion not necessary.
下面是函式呼叫的一個例子:
type myPrintInterface interface {
print()
}


func f3(x myInterface) {
x.(myPrintInterface).print() // type assertion to myPrintInterface
}
x 轉換為 myPrintInterface 型別是完全動態的:只要 x 的底層型別(動態型別)定義了 print 方法這個呼叫就可以正常執行。






13、反射
反射是一個很重要的概念




14、介面和動態型別
在經典的面嚮物件語言(像 C++,Java 和 C#)中資料和方法被封裝為 類 的概念:類包含它們兩者,並且不能剝離。
Go 沒有類:資料(結構體或更一般的型別)和方法是一種鬆耦合的正交關係。


接收一個(或多個)介面型別作為引數的函式,實參可以是任何實現了該介面的型別。 實現了某個介面的型別可以被傳給任何以此介面為引數的函式 。


15、動態方法呼叫
像 Python,Ruby 這類語言,動態型別是延遲繫結的(在執行時進行):方法只是用引數和變數簡單地呼叫,然後在執行時才解析(它們很可能有像 responds_to 這樣的方法來檢查物件是否可以響應某個方法,但是這也意味著更大的編碼量和更多的測試工作)
Go 的實現與此相反,通常需要編譯器靜態檢查的支援:當變數被賦值給一個介面型別的變數時,編譯器會檢查其是否實現了該介面的所有函式。如果方法呼叫作用於像 interface{} 這樣的“泛型”上,你可以通過型別斷言來檢查變數是否實現了相應介面。
例如,你用不同的型別表示 XML 輸出流中的不同實體。然後我們為 XML 定義一個如下的“寫”介面(甚至可以把它定義為私有介面):
type xmlWriter interface {
WriteXML(w io.Writer) error
}
現在我們可以實現適用於該流型別的任何變數的 StreamXML 函式,並用型別斷言檢查傳入的變數是否實現了該介面;如果沒有,我們就呼叫內建的 encodeToXML 來完成相應工作:
// Exported XML streaming function.
func StreamXML(v interface{}, w io.Writer) error {
if xw, ok := v.(xmlWriter); ok {
// It’s an  xmlWriter, use method of asserted type.
return xw.WriteXML(w)
}
// No implementation, so we have to use our own function (with perhaps reflection):
return encodeToXML(v, w)
}


// Internal XML encoding function.
func encodeToXML(v interface{}, w io.Writer) error {
// ...
}




16、空介面和函式過載
我們知道Go語言中函式過載是不被允許的。在 Go 語言中函式過載可以用可變引數 ...T 作為函式最後一個引數來實現。如果我們把 T 換為空介面,那麼可以知道任何型別的變數都是滿足 T (空介面)型別的,這樣就允許我們傳遞任何數量任何型別的引數給函式,即過載的實際含義。
函式 fmt.Printf 就是這樣做的:
fmt.Printf(format string, a ...interface{}) (n int, errno error)
這個函式通過列舉 slice 型別的實參動態確定所有引數的型別。




17、介面的巢狀和繼承
① 首先下面是介面的巢狀:這個和struct的匿名巢狀是相同的。
一個介面可以包含一個或多個其他的介面,這相當於直接將這些內嵌介面的方法列舉在外層介面中一樣。
比如介面 File 包含了 ReadWrite 和 Lock 的所有方法,它還額外有一個 Close() 方法。
type ReadWrite interface {
    Read(b Buffer) bool
    Write(b Buffer) bool
}


type Lock interface {
    Lock()
    Unlock()
}


type File interface {
    ReadWrite
    Lock
    Close()
}
②  然後是介面的繼承
當一個型別包含(內嵌)另一個型別(實現了一個或多個介面)的指標時(使用指標的原因:我個人覺得應該是因為指標型別的方法集既包含了接收者是指標型別的方法,又包含了接收者是值型別的方法。),這個型別就可以使用(另一個型別)所有的介面方法。
例如:
type Task struct {
Command string
*log.Logger
}
這個型別的工廠方法像這樣:
func NewTask(command string, logger *log.Logger) *Task {
return &Task{command, logger}
}
當 log.Logger 實現了 Log() 方法後,Task 的例項 task 就可以呼叫該方法:
task.Log()
型別可以通過繼承多個介面來提供像 多重繼承 一樣的特性:
type ReaderWriter struct {
*io.Reader
*io.Writer
}
上面概述的原理被應用於整個 Go 包,多型用得越多,程式碼就相對越少。這被認為是 Go 程式設計中的重要的最佳實踐。


注意:第①條是介面的巢狀,是介面之間的巢狀。第②條是介面的繼承,是一個struct繼承兩一個struct的所有介面。