概述
我們将用幾節來學習Go語言基礎,本文結構如下:
1. 方法
指針 vs. 值
2.接口與其它類型
接口
類型轉換
接口轉換與類型斷言
通用性
接口和方法
3.空白辨別符
多重指派中的空白辨別符
未使用的導入和變量
為副作用而導入
接口檢查
4.内嵌
複制
1.方法
指針 vs. 值
我們可以為任何已命名的類型(除了指針或接口)定義方法;
接收者可不必為結構體。
對于接收器,可以采用 指針或者指 ,通過下面的示例,我們先聲明一個類型,再為它指定一個 值 類型的接收器。
type ByteSlice []byte
func (slice ByteSlice) Append(data []byte) []byte {
// 主體省略
return 切片傳回值
}
複制
注意上面的 方法 的傳回值是個切片,它仍然需要傳回更新後的切片。為了消除這種不便,我們可通過 将一個 指針 作為該方法的接收者, 示例:
func (p *ByteSlice) Append(data []byte) {
slice := *p
// 主體省略,但沒有 return。
*p = slice
}
複制
這次的寫法就不需要傳回值了, 仿照 io.Writer 方法,我們繼續改進它:
func (p *ByteSlice) Write(data []byte) (n int, err error) {
slice := *p
// 主體省略。
*p = slice
return len(data), nil
}
複制
注意,這次它傳回了兩個值,一個表示長度,一個表示錯誤。
它滿足了标準的 io.Writer 接口,這将非常實用。 例如,我們可以通過列印将内容寫入。
var b ByteSlice
fmt.Fprintf(&b, "This hour has %d days\n", 7) // 注意這裡傳入位址
複制
以指針或值為接收者的差別在于:值方法可通過指針和值調用, 而指針方法隻能通過指針來調用。
指針方法可以修改接收者;通過值調用它們會導緻方法接收到該值的副本, 是以任何修改都将被丢棄。不過有個友善的例外:若該值是可尋址的, 那麼該語言就會自動插入取址操作符來對付一般的通過值調用的指針方法。在我們的例子中,變量 b 是可尋址的,是以我們隻需通過 b.Write 來調用它的 Write 方法,編譯器會将它重寫為 (&b).Write。
2. 接口與其它類型
2.1 接口
接口為指定對象的行為提供了一種方法約定:
- 如果某樣東西可以完成這個, 那麼它就可以用在這裡。
比如:通過實作 String 方法,我們可以自定義列印函數。實作了 Write 方法的對象可被 Fprintf 使用來列印輸出。
一種類型可以實作多個接口。
例如一個實作了 sort.Interface 接口的集合就可通過 sort 包中的例程進行排序。該接口包括 Len()、Less(i, j int) bool 以及 Swap(i, j int),還有個自定義的格式化字元串函數。
type Sequence []int
// Methods required by sort.Interface.
// sort.Interface 所需的方法。
func (s Sequence) Len() int {
return len(s)
}
func (s Sequence) Less(i, j int) bool {
return s[i] < s[j]
}
func (s Sequence) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
// Method for printing - sorts the elements before printing.
// 用于列印的方法 - 在列印前對元素進行排序。
func (s Sequence) String() string {
sort.Sort(s)
str := "["
for i, elem := range s {
if i > 0 {
str += " "
}
str += fmt.Sprint(elem)
}
return str + "]"
}
複制
2.2 類型轉換
先看一個例子:
func (s Sequence) String() string {
sort.Sort(s)
return fmt.Sprint([]int(s)) //注意這裡
}
複制
注意上面的第三行,将 Sequence 轉換為 []int 後,就能共享 []int 的 已實作的功能(被格式化輸出)。
轉換過程并不會建立新值,它隻是值暫讓現有的時看起來有個新類型而已。 (還有些合法轉換則會建立新值,如從整數轉換為浮點數等。)
2.3 接口轉換與類型斷言
類型選擇
有時候要先判斷後再安全轉換,用到類型選擇:它接受一個“接口“,在選擇 (switch)中根據類型的不同來選擇對應的情況(case), 然後再轉換為該種類型,示例:
type Stringer interface {
String() string
}
var value interface{} // 調用者提供的值。
switch str := value.(type) {
case string:
return str
case Stringer:
return str.String()
}
複制
類型斷言
有時候用到對單一類型判斷,就用到類型斷言。格式如下:
value.(typeName)
複制
上面這句的 結果會傳回:擁有靜态類型 typeName 的新值。示例:
str := value.(string)
複制
會将 value 轉換成 string 後傳回, str 是 字元串類型。
逗号, ok
如果不能轉成字元串,這裡就會錯誤崩潰。為避免這種情況, 需使用“逗号, ok”慣用方式,它能安全地判斷該值是否為字元串:
str, ok := value.(string)
if ok {
fmt.Printf("字元串值為 %q\n", str)
} else {
fmt.Printf("該值非字元串\n")
}
複制
若類型斷言失敗,str 将繼續存在且為字元串類型,但它将擁有零值,即空字元串。
補充說明個的完整示例,像下面的 if-else 寫法,它和上面說的”類型選擇“”是一樣:
if str, ok := value.(string); ok {
return str
} else if str, ok := value.(Stringer); ok {
return str.String()
}
複制
2.4 通用性
若某種現有的類型僅實作了一個接口,且除此之外并無可導出的方法,則該類型本身就無需導出。 僅導出該接口能讓我們更專注于其行為而非實作,其它屬性不同的實作則能鏡像該原始類型的行為。
這也能夠避免為每個通用接口的執行個體重複編寫文檔。
2.5 接口和方法
[]
3. 空白辨別符
空白辨別符可被賦予或聲明為任何類型的任何值,而其值會被無害地丢棄。
3.1 多重指派中的空白辨別符
如果某個函數傳回多個值,我們使用多重指派接收它,而其中某個變量不會被程式使用, 那麼用空白辨別符來代替該變量可避免建立無用的變量,并能清楚地表明該值将被丢棄。
if _, err := os.Stat(path); os.IsNotExist(err) {
fmt.Printf("%s does not exist\n", path)
}
複制
3.2 未使用的導入和變量
導入了某個包,或聲明了某個變量而不使用它,就會産生編譯錯誤。但實際編碼過程中還會遇到 占位 的情況,先寫一半代碼通過編譯再說。這時為了通過編譯還得删除 占位 代碼實在麻煩,這時,空白辨別符有用了,示例:
package main
import (
"fmt"
"io"
"log"
"os"
)
func main() {
fd, err := os.Open("test.go")
if err != nil {
log.Fatal(err)
}
// TODO: use fd.
}
複制
上面的代碼無法通過編譯,我們使用空白辨別符後,示例如下:
package main
import (
"fmt"
"io"
"log"
"os"
)
var _ = fmt.Printf // For debugging; delete when done. // 用于調試,結束時删除。
var _ io.Reader // For debugging; delete when done. // 用于調試,結束時删除。
func main() {
fd, err := os.Open("test.go")
if err != nil {
log.Fatal(err)
}
// TODO: use fd.
_ = fd
}
複制
注意,它使用了空白辨別符來 調用了已導入包中的方法。使用空白辨別符接收 未使用的變量 fd 來關閉未使用變量錯誤。
3.3 為副作用而導入
有時導入某個包隻是為了其副作用, 而沒有任何明确的使用。
隻為了其副作用來哦導入該包, 隻需将包重命名為空白辨別符:
import _ "net/http/pprof"
複制
上面的例子中,在
net/http/pprof
包的
init
函數中記錄了HTTP處理程式的調試資訊。它有個可導出的API, 但大部分用戶端隻需要該處理程式的記錄和通過Web通路資料。
這種導入格式能明确表示該包是為其副作用而導入的,因為沒有其它使用該包的可能: 在此檔案中,它沒有名字。(若它有名字而我們沒有使用,編譯器就會拒絕該程式。)
3.4 接口檢查
若隻需要判斷某個類型是否是實作了某個接口,而不需要實際使用,可以使用空白辨別符來忽略類型斷言的值:
if _, ok := val.(json.Marshaler); ok {
fmt.Printf("value %v of type %T implements json.Marshaler\n", val, val)
}
複制
4. 内嵌
Go并不提供典型的,類型驅動的子類化概念,但通過 将類型 内嵌到結構體或接口中, 它就能“借鑒”部分實作。
4.1 接口内嵌
簡單了解就是:将接口 嵌入到接口中。
接口内嵌非常簡單,示例:
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
複制
内嵌後:
// ReadWriter 接口結合了 Reader 和 Writer 接口。
type ReadWriter interface { // 這是個接口
Reader // 隻有類型,沒有變量
Writer // 隻有類型,沒有變量
}
複制
注意上面的寫法,Reader 和 Writer 隻有類型,沒有變量
正如它看起來那樣:ReadWriter 能夠做任何 Reader 和 Writer 可以做到的事情,它是内嵌接口的聯合體 (它們必須是不相交的方法集)。
隻有接口能被嵌入到接口中。
4.2 結構體内嵌
4.2.1 結構體内嵌變量
我們先說 繁瑣的實作如下:
内嵌的元素為指向
結構體的指針
,當然它們在使用前必須被初始化為指向有效結構體的指針。 ReadWriter 結構體可通過如下方式定義:
type ReadWriter struct { // 這裡是結構體
reader *Reader // 注意這裡有變量
writer *Writer
}
複制
但為了提升該字段的方法并滿足 io 接口,我們同樣需要提供轉發的方法, 就像這樣:
func (rw *ReadWriter) Read(p []byte) (n int, err error) {
return rw.reader.Read(p)
}
複制
如上,為了獲得 子變量 的能力(功能),我們不得不在外部類寫個同樣的名字的方法作為轉發到内部變量來調用。
然而,通過直接内嵌 “
結構體類型
”,我們就能避免如此繁瑣,看下文。
4.2.2 結構體内嵌類型
// ReadWriter 存儲了指向 Reader 和 Writer 的指針。
// 它實作了 io.ReadWriter。
type ReadWriter struct { // 這裡是結構體
*Reader // *bufio.Reader 注意這裡是類型
*Writer // *bufio.Writer
}
複制
在這裡,内嵌類型的方法可以直接引用,這意味着 ReadWriter 不僅包括 Reader 和 Writer 的方法,它還同時滿足下列三個接口: Reader、 Writer 以及 ReadWriter
- 當内嵌一個類型時,該類型的方法會成為外部類型的方法,
- 但當它們被調用時,該方法的接收者是内部類型,而非外部的。
在我們的例子中,當 ReadWriter 的 Read 方法被調用時,它與之前寫的轉發方法具有同樣的效果;接收者是 ReadWriter 的 reader 字段,而非 ReadWriter 本身。
4.2.3 混合變量和類型的内嵌
這個例子展示了一個内嵌字段和一個正常的命名字段:
type Job struct {
Command string
*log.Logger
}
複制
Job 類型現在有了 Log、Logf 和 *log.Logger 的其它方法。雖然可以為 Logger 提供一個字段名,但不必這麼做。現在,一旦初始化後,我們就能調用 Job 的 Log 了:
job.Log("starting now...")
複制
Logger 是 Job 結構體的正常字段, 是以我們可在 Job 的構造函數中,通過一般的方式來初始化它,就像這樣:
func NewJob(command string, logger *log.Logger) *Job {
return &Job{command, logger}
}
或通過複合字面來構造:
job := &Job{command, log.New(os.Stderr, "Job: ", log.Ldate)}
複制
若我們需要直接引用内嵌字段,可以忽略包限定名,直接将該字段的類型名作為字段名。
若我們需要通路 Job 類型的變量 job 的 *log.Logger, 可以直接寫作 job.Logger。
func (job *Job) Logf(format string, args ...interface{}) {
job.Logger.Logf("%q: %s", job.Command, fmt.Sprintf(format, args...))
}
複制
4.2.4 命名沖突規則
内嵌類型會引入命名沖突的問題,但解決規則卻很簡單: 上層優先覆寫下層。
若相同的嵌套層級上出現同名沖突,通常會産生一個錯誤。比如當 Job 結構體中包含名為 Logger 的字段或方法,再将 log.Logger 内嵌到其中的話就會産生錯誤。然而,若重名永遠不會在該類型定義之外的程式中使用,那就不會出錯。 這種限定能夠在外部嵌套類型發生修改時提供某種保護。
是以,就算添加的字段與另一個子類型中的字段相沖突,隻要這兩個相同的字段永遠不會被使用就沒問題。