寫一個(gè)簡單的方法:
成都創(chuàng)新互聯(lián)公司-專業(yè)網(wǎng)站定制、快速模板網(wǎng)站建設(shè)、高性價(jià)比華寧網(wǎng)站開發(fā)、企業(yè)建站全套包干低至880元,成熟完善的模板庫,直接使用。一站式華寧網(wǎng)站制作公司更省心,省錢,快速模板網(wǎng)站建設(shè)找我們,業(yè)務(wù)覆蓋華寧地區(qū)。費(fèi)用合理售后完善,10年實(shí)體公司更值得信賴。
type Point struct{X, Y float64}
// 普通的函數(shù)
func Distance(p, q Point) float64 {
return math.Hypot(q.X-p.X, q.Y-p.Y)
}
// 同樣的作用,用方法實(shí)現(xiàn)
func (p Point) Distance(q Point) float64 {
return math.Hypot(q.X-p.X, q.Y-p.Y)
}
接收者:附加的參數(shù) p 稱為方法的接收者。
調(diào)用方法的時(shí)候,接收者在方法名的前面。這樣就和聲明保持一致:
p := Point{1, 2}
q := Point{4, 6}
fmt.Println(Distance(p, q)) // 函數(shù)調(diào)用
fmt.Println(p.Distance(q)) // 方法調(diào)用
選擇子:表達(dá)是 p.Distance 稱作選擇子(selector),因?yàn)樗鼮榻邮照?p 選擇合適的 Distance 方法。
對(duì)于函數(shù),它會(huì)復(fù)制每一只實(shí)參變量。如果函數(shù)需要更新一個(gè)變量,或者是因?yàn)閷?shí)參太大而需要避免復(fù)制整個(gè)實(shí)參,就需要使用指針來傳遞變量的地址。
對(duì)于方法的接受者,也可以將方法綁定到指針類型。習(xí)慣上遵循如果一個(gè)類型的任何一個(gè)方法使用指針接收者,那么所有該類型的方法都應(yīng)該使用指針接收者,即使有些方法不一定需要。
另外,為了防止混淆,不允許本身是指針的類型進(jìn)行方法聲明,會(huì)有編譯錯(cuò)誤:
type p *int
func (p) f() { /*...*/ } // 編譯錯(cuò)誤:非法的接收者類型
通常是在相同的表達(dá)式里使用和調(diào)用方法,但是把兩個(gè)操作分開也是可以的。選擇子 p.Distance 可以賦予一個(gè)方法變量,它是一個(gè)函數(shù),把方法(Point.Distance)綁定到一個(gè)接收者 p 上。函數(shù)只需要提供實(shí)參而不需要提供接收者就能夠調(diào)用:
p := Point{1, 2}
q := Point{4, 6}
distanceFromP := p.Distance // 方法變量
fmt.Println(distanceFromP(q))
這里 p.Distance 是選擇子,把它賦值給變量 distanceFromP,這個(gè)變量就是方法變量,并且這個(gè)變量是一個(gè)函數(shù)。
如果包內(nèi)的 API 調(diào)用一個(gè)函數(shù)值,并且使用者期望這個(gè)函數(shù)的行為是調(diào)用一個(gè)特定接收者的方法,方法變量就非常有用。使用方法變量還可以是代碼更加簡潔:
type Rocket struct { /* ... */ }
func (r *Rocket) Launch() { /* ... */ }
r := new(Rocket)
time.AfterFunc(10 * time.Second, func() { r.Launch() }) // 如果沒有方法變量,那么要把執(zhí)行一個(gè)方法包在一個(gè)函數(shù)里,等到函數(shù)被調(diào)用后執(zhí)行
time.AfterFunc(10 * time.Second, r.Launch) // 使用方法變量,這里 r.Launch 就是一個(gè)函數(shù),只是沒有賦值給某個(gè)變量,沒有函數(shù)名
函數(shù) time.AfterFunc 的作用是在指定的延遲后調(diào)用一個(gè)函數(shù)。上面說了,方法變量也是函數(shù)。
調(diào)用方法的時(shí)候必須提供接收者,并且按照選擇子的語法進(jìn)行調(diào)用。
方法表達(dá)式,寫成 T.f 或者 (*T.f)。
其中 T 是類型,是一種函數(shù)變量,把原來方法的接收者替換成函數(shù)的第一個(gè)形參,因此它可以像平常的函數(shù)一樣調(diào)用:
p := Point{1, 2}
q := Point{4, 6}
distance := Point.Distance // 方法表達(dá)式
fmt.Println(distance(p, q))
fmt.Printf("%T\n", distance) // "func(Point, Point) float64"
如果需要一個(gè)值來代表多個(gè)方法中的一個(gè),而方法都屬于同一個(gè)類型,方法表達(dá)式可以實(shí)現(xiàn)讓這個(gè)值所對(duì)應(yīng)的方法來處理不同的接收者。就是可以把一個(gè)方法變成一個(gè)函數(shù),函數(shù)的變量會(huì)增加一個(gè),第一個(gè)變量就是原來方法中的接收者。其實(shí)各個(gè)參數(shù)的順序還是一樣的,原本第一個(gè)參數(shù)在 func 前,現(xiàn)在移動(dòng)到了 func 后面。 p.Distance(q) 變成了 distance(p, q)。
io包定義了很多有用的接口:
io.Writer 是一個(gè)廣泛使用的接口,它負(fù)責(zé)所有可以寫入字節(jié)的抽象,包括但不限于下面列舉的這些:
接口值,就是一個(gè)接口類型的值。分兩個(gè)部分:
var w io.Writer // 聲明接口,動(dòng)態(tài)類型和動(dòng)態(tài)值都是nil
w = os.Stdout // 有動(dòng)態(tài)類型,也有動(dòng)態(tài)值
w = io.Writer(os.Stdout) // 和上面這句等價(jià),把一個(gè)具體類型顯式轉(zhuǎn)換為接口類型
w = new(bytes.Buffer) // 有動(dòng)態(tài)類型,也有動(dòng)態(tài)值
w = nil // 把動(dòng)態(tài)類型和動(dòng)態(tài)值都設(shè)置為nil,恢復(fù)到聲明時(shí)的狀態(tài)
接口值可以用 == 和 != 來比較。動(dòng)態(tài)類型一致,然后動(dòng)態(tài)值相等(使用動(dòng)態(tài)類型的 == 來比較),那么接口值相等。接口值都是nil也是相等的。
可以作為map的key,也可以作為switch語句的操作數(shù),因?yàn)榭梢员容^。
動(dòng)態(tài)值可能是不可比較的類型,比如切片。對(duì)這樣的接口進(jìn)行比較,就會(huì)Panic。把這樣的接口用作map的key或者switch語句的操作數(shù)時(shí)也同樣會(huì)Panic。所以,僅在能確認(rèn)接口值包含的動(dòng)態(tài)值可以比較時(shí),才比較接口值。
fmt 包的 %T 打印出來的就是動(dòng)態(tài)類型。在內(nèi)部實(shí)現(xiàn)中,fmt 用反射來拿到接口動(dòng)態(tài)類型的名字。
空的接口值(動(dòng)態(tài)類型和動(dòng)態(tài)值都為空)和僅僅動(dòng)態(tài)值為nil的接口值是不一樣的。
const debug = true
func main() {
var buf *bytes.Buffer
if debug {
buf = new(bytes.Buffer)
}
f(buf)
if debug {
// ...使用 buf...
}
}
// 如果 out 不是 nil,那么會(huì)向其寫入輸出的數(shù)據(jù)
func f(out io.Writer) {
// ...其他代碼...
if out != nil {
out.Write([]byte("done\n"))
}
}
這里,把一個(gè)類型為 *bytes.Buffer 的空指針賦給了 out 參數(shù),此時(shí) out 的動(dòng)態(tài)值為空。但它的動(dòng)態(tài)類型是 *bytes.Buffer。就是說 out 是一個(gè)包含空指針的非空接口,所以這里的檢查 out != nil
是 true,防御不了這種情況。
對(duì)于某些類型,比如 *os.File,空接收值是合法的。但是對(duì)于這里的 *buyes.Buffer,要求接收者不能為空,于是運(yùn)行時(shí)會(huì)Panic。
這里的解決方案是,把 main 函數(shù)中的 buf 類型修改為 io.Writer,從而避免在最開始就把一個(gè)功能不完整的值賦給一個(gè)接口:
var buf io.Writer
if debug {
buf = new(bytes.Buffer)
}
f(buf)
類型斷言是一個(gè)作用在接口值上的操作,代碼類似于x(T)
,x是一個(gè)接口類型的表達(dá)式,而T是一個(gè)類型(稱為斷言類型)。類型斷言會(huì)檢查操作數(shù)的動(dòng)態(tài)類型是否滿足指定的斷言類型。
這里有兩種可能:
具體類型
如果斷言類型T是一個(gè)具體類型,斷言類型會(huì)檢查x的動(dòng)態(tài)類型是否就是T。如果檢查成功,返回x的動(dòng)態(tài)值,返回的類型就是T。如果檢查失敗,那么操作崩潰。
接口類型
如果斷言類型T是一個(gè)接口類型,斷言類型會(huì)檢查x的動(dòng)態(tài)類型是否滿足T。如果檢查成功,動(dòng)態(tài)值并沒有提取出來,仍然是一個(gè)接口值,接口值的類型和值部分也不會(huì)變,只是結(jié)果類型為接口類型T。就是說,這里類型斷言就是一個(gè)接口值表達(dá)式,從一個(gè)接口類型變?yōu)閾碛辛硗庖惶追椒ǖ慕涌陬愋停A袅私涌谥抵袆?dòng)態(tài)類型和動(dòng)態(tài)值部分。如果檢查失敗還是會(huì)崩潰。
類型斷言可以返回兩個(gè)結(jié)果,此時(shí)操作不會(huì)因?yàn)闄z查失敗而崩潰。多出來的返回值是一個(gè)布爾型,用來指示斷言是否成功。按照慣例,一般變量名用ok。如果操作失敗,ok為false,而第一個(gè)返回值會(huì)是斷言類型的零值。
接口有兩種不同的風(fēng)格。
第一種風(fēng)格下,典型的比如:io.Reader、io.Writer、fmt.Stringer、sort.Interface、http.Handler 和 error。接口上的各種方法突出了滿足這個(gè)接口的具體類型之間的相似性,但隱藏了各個(gè)具體類型的布局和各自特有的功能。這種風(fēng)格強(qiáng)調(diào)了方法,而不是具體類型。
第二種風(fēng)格則充分利用了接口值能夠容納各種具體類型的能力,它把接口作為這些類型的聯(lián)合(union)來使用。類型斷言用來在運(yùn)行時(shí)區(qū)分這些類型并分別處理。這這種風(fēng)格中,強(qiáng)調(diào)的是滿足這個(gè)接口的具體類型,而不是這個(gè)接口的方法(經(jīng)常是沒變方法的空接口),也不注重信息隱藏。這種風(fēng)格的接口使用方式稱為可識(shí)別聯(lián)合(discriminated union)。
如果對(duì)面向?qū)ο笫煜?,這兩種風(fēng)格分別對(duì)應(yīng):
不要一開始就定義接口,每個(gè)接口卻只是一個(gè)單獨(dú)的實(shí)現(xiàn)。這種接口是不必要的抽象,還會(huì)有運(yùn)行時(shí)的成本。僅在有兩個(gè)或多個(gè)具體類型需要按統(tǒng)一的方式處理時(shí)才需要接口。
上面的建議也有特例,如果接口和類型實(shí)現(xiàn)出于依賴的原因不能放在同一個(gè)包里邊,那么一個(gè)接口只有一個(gè)具體類型實(shí)現(xiàn)也是可以的。在這種情況下,接口是一種解耦兩個(gè)包的好方式。