深入學習golang(3)—型別方法

YY哥發表於2014-10-03

型別方法

1. 給型別定義方法

在Go語言中,我們可以給任何型別(包括內建型別,但不包括指標和介面)定義方法。例如,在實際程式設計中,我們經常使用[ ]byte的切片,我們可以定義一個新的型別:

type ByteSlice []byte

然後我們就可以定義方法了。例如,假如我們不想使用內建的append函式,我們可以實現一個自己的append方法:

func Append(slice, data[]byte) []byte {

    l := len(slice)

    if l + len(data) > cap(slice) {  // reallocate

        // Allocate double what's needed, for future growth.

        newSlice := make([]byte, (l+len(data))*2)

        // The copy function is predeclared and works for any slice type.

        copy(newSlice, slice)

        slice = newSlice

    }

    slice = slice[0:l+len(data)]

    for i, c := range data {

        slice[l+i] = c

    }

    return slice

}

我們可以在Append實現自己的記憶體擴充套件策略。這個新的型別與[ ]byte沒有其它的區別,只是它多了一個Append方法:

        var a ByteSlice = []byte{1,2,3}

        b := []byte{4}

        a.Append(b) //won't change a

        fmt.Println(a)

        a = a.Append(b)

        fmt.Println(a);

輸出:

[1 2 3]

[1 2 3 4]

注意,上面的Append方法只能通過ByteSlice呼叫,而不能通過[ ]byte的方式呼叫。另外,為了得到更新後的值,必須將更新後的值做為返回值返回,這種做法比較笨拙,我們可以換一種更優美的方式實現Append方法:

func (p *ByteSlice) Append(data[]byte) {

    slice := *p

    l := len(slice)

    if l + len(data) > cap(slice) {  // reallocate

        // Allocate double what's needed, for future growth.

        newSlice := make([]byte, (l+len(data))*2)

        // The copy function is predeclared and works for any slice type.

        copy(newSlice, slice)

        slice = newSlice

    }

    slice = slice[0:l+len(data)]

    for i, c := range data {

        slice[l+i] = c

    }

    *p = slice

}

 通過使用指標的方式,可以達到修改物件本身的目的:

        var a ByteSlice = []byte{1,2,3}

        var c ByteSlice = []byte{1,2,3}

        b := []byte{4}

        (&a).Append(b)

        c.Append(b)

        fmt.Println(a)

        fmt.Println(c)

輸出:

[1 2 3 4]

[1 2 3 4]

 

實際上,我們可以更進一步,我們可以將函式修改成標準Write方法的樣子:

func (p *ByteSlice) Write(data []byte) (n int, err error) {

    slice := *p

    l := len(slice)

    if l + len(data) > cap(slice) {  // reallocate

        // Allocate double what's needed, for future growth.

        newSlice := make([]byte, (l+len(data))*2)

        // The copy function is predeclared and works for any slice type.

        copy(newSlice, slice)

        slice = newSlice

    }

    slice = slice[0:l+len(data)]

    for i, c := range data {

        slice[l+i] = c

    }

    *p = slice

    return len(data), nil

}

  

這樣型別*ByteSlice就會滿足標準介面io.Writer:

package io

type Writer interface {

Write(p []byte) (n int, err error)

}

 

這樣我們就可以列印到該型別的變數中:

        var b ByteSlice

        fmt.Fprintf(&b, "aa%dbb", 7)

        fmt.Println(b)

輸出:

[97 97 55 98 98]

注意,這裡必須傳遞&b給fmt.Fprintf,如果傳遞b,則編譯時會報下面的錯誤:

cannot use b (type ByteSlice) as type io.Writer in argument to fmt.Fprintf:

       ByteSlice does not implement io.Writer (Write method has pointer receiver)

 Go語言規範有這樣的規定:

The method set of any other named type T consists of all methods with receiver type T. The method set of the corresponding pointer type *T is the set of all methods with receiver *T or T (that is, it also contains the method set of T).

參見這裡。通俗點來說,就是指標型別(*T)的物件包含的接收者為T的方法,反之,則不包含。<effective go>中有這樣的描述:

We pass the address of a ByteSlice because only *ByteSlice satisfies io.Writer. The rule about pointers vs. values for receivers is that value methods can be invoked on pointers and values, but pointer methods can only be invoked on pointers.

我們這裡只定義了(p *ByteSlice) Write方法,而ByteSlice並沒有實現介面io.Write,所以就會報上面的錯誤。注意,這裡的描述有一個上下文,就是給介面賦值。

 

那為什麼在Append的示例中,(&a).Append(b)和c.Append(b)都是OK的呢?因為這裡與介面無關。我們不能再以C++的思維來理解Go,因為Go中的物件沒有this指標。更直白的說,物件本身是作為引數顯式傳遞的。所以,即使c.Append(b),Go也會傳遞&c給Append方法。

 

不管怎麼樣,我覺得這裡還是很讓人迷糊的。

 

2. 值方法與指標方法

 

上一節中,我們看到了值方法(value method,receiver為value)與指標方法(pointer method,receiver與pointer)的區別,

 

func (s *MyStruct) pointerMethod() { } // method on pointer

func (s MyStruct)  valueMethod()   { } // method on value

 

那麼什麼時候用值方法,什麼時候用指標方法呢?主要考慮以下一些因素:

 

(1)如果方法需要修改receiver,那麼必須使用指標方法;

 

(2)如果receiver是一個很大的結構體,考慮到效率,應該使用指標方法;

 

(3)一致性。如果一些方法必須是指標receiver,那麼其它方法也應該使用指標receiver;

 

(4)對於一些基本型別、切片、或者小的結構體,使用value receiver效率會更高一些。

 

詳細參考這裡

 

 

 

3. 示例

 

這種給原生資料型別增加方法的做法,在Go語言程式設計中很常見,來看一下http.Header:

// A Header represents the key-value pairs in an HTTP header.

type Header map[string][]string

 

// Add adds the key, value pair to the header.

// It appends to any existing values associated with key.

func (h Header) Add(key, value string) {

    textproto.MIMEHeader(h).Add(key, value)

}




作者:YY哥 
出處:http://www.cnblogs.com/hustcat/ 
本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連線,否則保留追究法律責任的權利。

相關文章