1循集、方法
Go 沒(méi)有類掸哑。不過(guò)你可以為結(jié)構(gòu)體類型定義方法。
方法就是一類帶特殊的 **接收者** 參數(shù)的函數(shù)筋栋。
方法接收者在它自己的參數(shù)列表內(nèi)炊汤,位于 func 關(guān)鍵字和方法名之間。
在下面的例子中弊攘,Abs 方法擁有一個(gè)名為 v抢腐,類型為 Vertex 的接收者。
package main
import (
"fmt"
"math"
)
type Vertex struct {
X, Y float64
}
func (v Vertex) Abs() float64 { //如果不用方法 直接寫(xiě) func Abs(v Vertex) float64 則運(yùn)行錯(cuò)誤
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
func main() {
v := Vertex{3, 4}
fmt.Println(v.Abs()) // 運(yùn)行結(jié)果 5
}
- 方法只是個(gè)帶接收者參數(shù)的函數(shù)襟交。
現(xiàn)在這個(gè) Abs 的寫(xiě)法就是個(gè)正常的函數(shù)迈倍,功能并沒(méi)有什么變化。
//將Abs 改寫(xiě)成函數(shù)的形式
package main
import (
"fmt"
"math"
)
type Vertex struct {
X, Y float64
}
func Abs(v Vertex) float64 { // 方法這里應(yīng)寫(xiě)成 func (v Vertex) Abs() float64
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
func main() {
v := Vertex{3, 4}
fmt.Println(Abs(v)) // 方法的話 這里是是 fmt.Println(v.Abs())
}
1.2 方法續(xù)
也可以為非結(jié)構(gòu)體類型聲明方法捣域。
在此例中啼染,我們看到了一個(gè)帶 Abs 方法的數(shù)值類型 MyFloat宴合。
你只能為在同一包內(nèi)定義的類型的接收者聲明方法,而不能為其它包內(nèi)定義的類型(包括 int 之類的內(nèi)建類型)的接收者聲明方法迹鹅。
(譯注:就是接收者的類型定義和方法聲明必須在同一包內(nèi)卦洽;不能為內(nèi)建類型聲明方法。)
package main
import (
"fmt"
"math"
)
type MyFloat float64
func (f MyFloat) Abs() float64 {
if f < 0 {
return float64(-f)
}
return float64(f)
}
func main() {
f := MyFloat(-math.Sqrt2)
fmt.Println(f.Abs())
}
// 運(yùn)行結(jié)果:1.4142135623730951
2斜棚、指針
2.1 指針接受者
指針接收者的方法可以修改接收者指向的值
你可以為指針接收者聲明方法逐样。
這意味著對(duì)于某類型 T,接收者的類型可以用 *T 的文法打肝。(此外脂新,T 不能是像 *int 這樣的指針。)
例如粗梭,這里為 *Vertex 定義了 Scale 方法争便。
**指針接收者的方法可以修改接收者指向的值(就像 Scale 在這做的)。由于方法經(jīng)常需要修改它的接收者断医,指針接收者比值接收者更常用滞乙。 **
試著移除第 16 行 Scale 函數(shù)聲明中的 *,觀察此程序的行為如何變化鉴嗤。
若使用值接收者斩启,那么 Scale 方法會(huì)對(duì)原始 Vertex 值的副本進(jìn)行操作。(對(duì)于函數(shù)的其它參數(shù)也是如此醉锅。)Scale 方法必須用指針接受者來(lái)更改 main 函數(shù)中聲明的 Vertex 的值兔簇。
package main
import (
"fmt"
"math"
)
type Vertex struct {
X, Y float64
}
func (v Vertex) Abs() float64 { // 這里Abs 是函數(shù)名 一般會(huì)用大寫(xiě) 當(dāng)然可以換成別的字母
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
func (v *Vertex) Scale(f float64) { //這個(gè)地方如果去掉 * 則運(yùn)行結(jié)果變成 5
v.X = v.X * f
v.Y = v.Y * f
}
func main() {
v := Vertex{3, 4}
v.Scale(10)
fmt.Println(v.Abs())
}
//運(yùn)行結(jié)果: 50
2.2 指針與函數(shù)
把 Abs 和 Scale 方法重寫(xiě)為函數(shù)
package main
import (
"fmt"
"math"
)
type Vertex struct {
X, Y float64
}
func Abs(v Vertex) float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
func Scale(v *Vertex, f float64) { // 如果去掉 * 則編譯不通過(guò)
v.X = v.X * f
v.Y = v.Y * f
}
func main() {
v := Vertex{3, 4}
Scale(&v, 10)
fmt.Println(Abs(v))
}
//運(yùn)行結(jié)果:50
2.3 方法與指針重定向
比較前兩個(gè)程序,你大概會(huì)注意到帶指針參數(shù)的函數(shù)必須接受一個(gè)指針:
var v Vertex
ScaleFunc(v, 5) // 編譯錯(cuò)誤硬耍!
ScaleFunc(&v, 5) // OK
而以指針為接收者的方法被調(diào)用時(shí)垄琐,接收者既能為值又能為指針:
var v Vertex
v.Scale(5) // OK
p := &v
p.Scale(10) // OK
對(duì)于語(yǔ)句 v.Scale(5),即便 v 是個(gè)值而非指針经柴,帶指針接收者的方法也能被直接調(diào)用狸窘。 也就是說(shuō),由于 Scale 方法有一個(gè)指針接收者坯认,為方便起見(jiàn)翻擒,Go 會(huì)將語(yǔ)句 v.Scale(5) 解釋為 (&v).Scale(5)。
//舉個(gè)例子
package main
import "fmt"
type Vertex struct {
X, Y float64
}
func (v *Vertex) Scale(f float64) {
v.X = v.X * f
v.Y = v.Y * f
}
func ScaleFunc(v *Vertex, f float64) {
v.X = v.X * f
v.Y = v.Y * f
}
func main() {
v := Vertex{3, 4}
v.Scale(2)
ScaleFunc(&v, 10)
p := &Vertex{4, 3}
p.Scale(3)
ScaleFunc(p, 8)
fmt.Println(v, p)
}
//運(yùn)行結(jié)果:{60 80} &{96 72}
方法與指針重定向(續(xù))
同樣的事情也發(fā)生在相反的方向牛哺。
接受一個(gè)值作為參數(shù)的函數(shù)必須接受一個(gè)指定類型的值:
var v Vertex
fmt.Println(AbsFunc(v)) // OK
fmt.Println(AbsFunc(&v)) // 編譯錯(cuò)誤陋气!
而以值為接收者的方法被調(diào)用時(shí),接收者既能為值又能為指針:
var v Vertex
fmt.Println(v.Abs()) // OK
p := &v
fmt.Println(p.Abs()) // OK
這種情況下荆隘,方法調(diào)用 p.Abs() 會(huì)被解釋為 (*p).Abs()恩伺。
package main
import (
"fmt"
"math"
)
type Vertex struct {
X, Y float64
}
func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
func AbsFunc(v Vertex) float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
func main() {
v := Vertex{3, 4}
fmt.Println(v.Abs()) //方法調(diào)用用v.Abs()
fmt.Println(AbsFunc(v)) //函數(shù)調(diào)用用AbsFunc(v)
p := &Vertex{4, 3}
fmt.Println(p.Abs())
fmt.Println(AbsFunc(*p))
}
// 運(yùn)行結(jié)果:
5
5
5
5
2.4 選擇值或指針作為接收者
**使用指針接收者的原因有二:
首先,方法能夠修改其接收者指向的值椰拒。
其次晶渠,這樣可以避免在每次調(diào)用方法時(shí)復(fù)制該值凰荚。若值的類型為大型結(jié)構(gòu)體時(shí),這樣做會(huì)更加高效褒脯。 **
在本例中便瑟,Scale 和 Abs 接收者的類型為 *Vertex,即便 Abs 并不需要修改其接收者番川。
通常來(lái)說(shuō)到涂,所有給定類型的方法都應(yīng)該有值或指針接收者,但并不應(yīng)該二者混用颁督。(我們會(huì)在接下來(lái)幾頁(yè)中明白為什么践啄。)
package main
import (
"fmt"
"math"
)
type Vertex struct {
X, Y float64
}
func (v *Vertex) Scale(f float64) {
v.X = v.X * f
v.Y = v.Y * f
}
func (v *Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
func main() {
v := &Vertex{3, 4}
fmt.Printf("Before scaling: %+v, Abs: %v\n", v, v.Abs())
v.Scale(5)
fmt.Printf("After scaling: %+v, Abs: %v\n", v, v.Abs())
}
// 運(yùn)行結(jié)果:
Before scaling: &{X:3 Y:4}, Abs: 5
After scaling: &{X:15 Y:20}, Abs: 25
3、接口
3.1 接口介紹
**接口類型 是由一組方法簽名定義的集合沉御。 **
接口類型的變量可以保存任何實(shí)現(xiàn)了這些方法的值屿讽。
3.2 接口與隱式實(shí)現(xiàn)
類型通過(guò)實(shí)現(xiàn)一個(gè)接口的所有方法來(lái)實(shí)現(xiàn)該接口。既然無(wú)需專門顯式聲明吠裆,也就沒(méi)有“implements”關(guān)鍵字伐谈。
隱式接口從接口的實(shí)現(xiàn)中解耦了定義,這樣接口的實(shí)現(xiàn)可以出現(xiàn)在任何包中试疙,無(wú)需提前準(zhǔn)備诵棵。
因此,也就無(wú)需在每一個(gè)實(shí)現(xiàn)上增加新的接口名稱祝旷,這樣同時(shí)也鼓勵(lì)了明確的接口定義履澳。
package main
import "fmt"
type I interface {
M()
}
type T struct {
S string
}
// 此方法表示類型 T 實(shí)現(xiàn)了接口 I,但我們無(wú)需顯式聲明此事缓屠。
func (t T) M() {
fmt.Println(t.S)
}
func main() {
var i I = T{"hello"}
i.M()
}
//運(yùn)行結(jié)果:hello
3.3 接口值
接口也是值奇昙。它們可以像其它值一樣傳遞。
接口值可以用作函數(shù)的參數(shù)或返回值敌完。
在內(nèi)部,接口值可以看做包含值和具體類型的元組:
(value, type)
接口值保存了一個(gè)具體底層類型的具體值羊初。
接口值調(diào)用方法時(shí)會(huì)執(zhí)行其底層類型的同名方法
package main
import (
"fmt"
"math"
)
type I interface {
M()
}
type T struct {
S string
}
func (t *T) M() {
fmt.Println(t.S)
}
type F float64
func (f F) M() {
fmt.Println(f)
}
func main() {
var i I
i = &T{"Hello"}
describe(i)
i.M()
i = F(math.Pi)
describe(i)
i.M()
}
func describe(i I) {
fmt.Printf("(%v, %T)\n", i, i)
}
--------------------------------------------------------------------
//運(yùn)行結(jié)果:
(&{Hello}, *main.T)
Hello
(3.141592653589793, main.F)
3.141592653589793
3.4 底層值為nil的接口值
即便接口內(nèi)的具體值為 nil滨溉,方法仍然會(huì)被 nil 接收者調(diào)用。
在一些語(yǔ)言中长赞,這會(huì)觸發(fā)一個(gè)空指針異常晦攒,但在 Go 中通常會(huì)寫(xiě)一些方法來(lái)優(yōu)雅地處理它(如本例中的 M 方法)。
注意: 保存了 nil 具體值的接口其自身并不為 nil得哆。
package main
import "fmt"
type I interface {
M()
}
type T struct {
S string
}
func (t *T) M() {
if t == nil {
fmt.Println("<nil>")
return
}
fmt.Println(t.S)
}
func main() {
var i I
var t *T
i = t
describe(i)
i.M()
i = &T{"hello"}
describe(i)
i.M()
}
func describe(i I) {
fmt.Printf("(%v, %T)\n", i, i)
}
-------------------------------------------------------------------------
//輸出結(jié)果:
(<nil>, *main.T)
<nil>
(&{hello}, *main.T)
hello
3.5 nil接口值
nil 接口值既不保存值也不保存具體類型脯颜。
3.6 空接口
指定了零個(gè)方法的接口值被稱為 空接口:
interface{}
空接口可保存任何類型的值。(因?yàn)槊總€(gè)類型都至少實(shí)現(xiàn)了零個(gè)方法贩据。)
空接口被用來(lái)處理未知類型的值栋操。例如闸餐,fmt.Print 可接受類型為 interface{} 的任意數(shù)量的參數(shù)。
package main
import "fmt"
func main() {
var i interface{}
describe(i)
i = 42
describe(i)
i = "hello"
describe(i)
}
func describe(i interface{}) {
fmt.Printf("(%v, %T)\n", i, i)
}
------------------------------------------------------------------------
//運(yùn)行結(jié)果:
(<nil>, <nil>)
(42, int)
(hello, string)
4矾芙、類型
4.1 類型斷言
類型斷言 提供了訪問(wèn)接口值底層具體值的方式舍沙。
t := i.(T)
該語(yǔ)句斷言接口值 i 保存了具體類型 T,并將其底層類型為 T 的值賦予變量 t剔宪。
若 i 并未保存 T 類型的值拂铡,該語(yǔ)句就會(huì)觸發(fā)一個(gè)恐慌。
為了 判斷 一個(gè)接口值是否保存了一個(gè)特定的類型葱绒,類型斷言可返回兩個(gè)值:其底層值以及一個(gè)報(bào)告斷言是否成功的布爾值感帅。
t, ok := i.(T)
若 i 保存了一個(gè) T,那么 t 將會(huì)是其底層值地淀,而 ok 為 true留瞳。
否則,ok 將為 false 而 t 將為 T 類型的零值骚秦,程序并不會(huì)產(chǎn)生恐慌她倘。
請(qǐng)注意這種語(yǔ)法和讀取一個(gè)映射時(shí)的相同之處。
package main
import "fmt"
func main() {
var i interface{} = "hello"
s := i.(string)
fmt.Println(s)
s, ok := i.(string)
fmt.Println(s, ok)
f, ok := i.(float64)
fmt.Println(f, ok)
f = i.(float64) // 報(bào)錯(cuò)(panic)
fmt.Println(f)
}
運(yùn)行結(jié)果:
hello true
0 false
panic: interface conversion: interface {} is string, not float64
4.2 類型選擇
類型選擇 是一種按順序從幾個(gè)類型斷言中選擇分支的結(jié)構(gòu)作箍。
類型選擇與一般的 switch 語(yǔ)句相似硬梁,不過(guò)類型選擇中的 case 為類型(而非值), 它們針對(duì)給定接口值所存儲(chǔ)的值的類型進(jìn)行比較胞得。
switch v := i.(type) {
case T:
// v 的類型為 T
case S:
// v 的類型為 S
default:
// 沒(méi)有匹配荧止,v 與 i 的類型相同
}
類型選擇中的聲明與類型斷言 i.(T) 的語(yǔ)法相同,只是具體類型 T 被替換成了關(guān)鍵字 type阶剑。
此選擇語(yǔ)句判斷接口值 i 保存的值類型是 T 還是 S跃巡。在 T 或 S 的情況下,變量 v 會(huì)分別按 T 或 S 類型保存 i 擁有的值牧愁。在默認(rèn)(即沒(méi)有匹配)的情況下素邪,變量 v 與 i 的接口類型和值相同。
package main
import "fmt"
func do(i interface{}) {
switch v := i.(type) {
case int:
fmt.Printf("Twice %v is %v\n", v, v*2)
case string:
fmt.Printf("%q is %v bytes long\n", v, len(v))
default:
fmt.Printf("I don't know about type %T!\n", v)
}
}
func main() {
do(21)
do("hello")
do(true)
}
---------------------------------------------------------------------------------
//運(yùn)行結(jié)果:
Twice 21 is 42
"hello" is 5 bytes long
I don't know about type bool!
5猪半、Stringer
fmt
包中定義的 Stringer
最普遍的接口之一兔朦。
type Stringer interface {
String() string
}
Stringer
是一個(gè)可以用字符串描述自己的類型。fmt
包(還有很多包)都通過(guò)此接口來(lái)打印值磨确。
package main
import "fmt"
type Person struct {
Name string
Age int
}
func (p Person) String() string {
return fmt.Sprintf("%v (%v years)", p.Name, p.Age)
}
func main() {
a := Person{"Arthur Dent", 42}
z := Person{"Zaphod Beeblebrox", 9001}
fmt.Println(a, z)
}
// 運(yùn)行結(jié)果:
Arthur Dent (42 years) Zaphod Beeblebrox (9001 years)
練習(xí):Stringer
通過(guò)讓 IPAddr 類型實(shí)現(xiàn) fmt.Stringer 來(lái)打印點(diǎn)號(hào)分隔的地址沽甥。
例如,IPAddr{1, 2, 3, 4} 應(yīng)當(dāng)打印為 "1.2.3.4"乏奥。
package main
import "fmt"
type IPAddr [4]byte
// TODO: Add a "String() string" method to IPAddr.
func (p IPAddr) String() string {
return fmt.Sprintf("%v.%v.%v.%v", p[0], p[1], p[2], p[3]);
}
func main() {
hosts := map[string]IPAddr{
"loopback": {127, 0, 0, 1},
"googleDNS": {8, 8, 8, 8},
}
for name, ip := range hosts {
fmt.Printf("%v: %v\n", name, ip)
}
}
----------------------------------------------------
// 運(yùn)行結(jié)果:
loopback: 127.0.0.1
googleDNS:8.8.8.8
6摆舟、錯(cuò)誤
Go 程序使用 error 值來(lái)表示錯(cuò)誤狀態(tài)。
與 fmt.Stringer 類似,error 類型是一個(gè)內(nèi)建接口:
type error interface {
Error() string
}
(與 fmt.Stringer 類似恨诱,fmt 包在打印值時(shí)也會(huì)滿足 error媳瞪。)
通常函數(shù)會(huì)返回一個(gè) error 值,調(diào)用的它的代碼應(yīng)當(dāng)判斷這個(gè)錯(cuò)誤是否等于 nil 來(lái)進(jìn)行錯(cuò)誤處理胡野。
i, err := strconv.Atoi("42")
if err != nil {
fmt.Printf("couldn't convert number: %v\n", err)
return
}
fmt.Println("Converted integer:", i)
error 為 nil 時(shí)表示成功材失;非 nil 的 error 表示失敗。
package main
import (
"fmt"
"time"
)
type MyError struct {
When time.Time
What string
}
func (e *MyError) Error() string {
return fmt.Sprintf("at %v, %s",
e.When, e.What)
}
func run() error {
return &MyError{
time.Now(),
"it didn't work",
}
}
func main() {
if err := run(); err != nil {
fmt.Println(err)
}
}
// 運(yùn)行結(jié)果: at 2019-05-22 17:57:45.797632243 +0800 CST m=+0.000720070, it didn't work
練習(xí):錯(cuò)誤
從之前的練習(xí)中復(fù)制 Sqrt
函數(shù)硫豆,修改它使其返回 error
值龙巨。
Sqrt
接受到一個(gè)負(fù)數(shù)時(shí),應(yīng)當(dāng)返回一個(gè)非 nil 的錯(cuò)誤值熊响。復(fù)數(shù)同樣也不被支持旨别。
創(chuàng)建一個(gè)新的類型
type ErrNegativeSqrt float64
并為其實(shí)現(xiàn)
func (e ErrNegativeSqrt) Error() string
方法使其擁有 error
值,通過(guò) ErrNegativeSqrt(-2).Error()
調(diào)用該方法應(yīng)返回 "cannot Sqrt negative number: -2"
汗茄。
注意: 在 Error
方法內(nèi)調(diào)用 fmt.Sprint(e)
會(huì)讓程序陷入死循環(huán)秸弛。可以通過(guò)先轉(zhuǎn)換 e
來(lái)避免這個(gè)問(wèn)題:fmt.Sprint(float64(e))
洪碳。這是為什么呢递览?
修改 Sqrt
函數(shù),使其接受一個(gè)負(fù)數(shù)時(shí)瞳腌,返回 ErrNegativeSqrt
值绞铃。
package main
import (
"fmt"
"math"
)
func Sqrt(x float64) (float64, error) {
if x < 0 {
return 0, ErrNegativeSqrt(x)
}
z := float64(1)
for {
y := z - (z*z-x)/(2*z)
if math.Abs(y-z) < 1e-10 {
return y, nil
}
z = y
}
return z, nil
}
type ErrNegativeSqrt float64
func (e ErrNegativeSqrt) Error() string {
return fmt.Sprintf("cannot Sqrt negative number: %v", float64(e))
}
func main() {
fmt.Println(Sqrt(2)) // 1.4142135623730951 <nil>
fmt.Println(Sqrt(-2)) // 0 cannot Sqrt negative number: -2
}
7、Reader
**io 包指定了 io.Reader 接口嫂侍,它表示從數(shù)據(jù)流的末尾進(jìn)行讀取儿捧。 **
io.Reader 接口有一個(gè) Read 方法:
func (T) Read(b []byte) (n int, err error)
Read 用數(shù)據(jù)填充給定的字節(jié)切片并返回填充的字節(jié)數(shù)和錯(cuò)誤值。在遇到數(shù)據(jù)流的結(jié)尾時(shí)挑宠,它會(huì)返回一個(gè) io.EOF 錯(cuò)誤菲盾。
示例代碼創(chuàng)建了一個(gè) strings.Reader
并以每次 8 字節(jié)的速度讀取它的輸出。
package main
import (
"fmt"
"io"
"strings"
)
func main() {
r := strings.NewReader("Hello, Reader!")
b := make([]byte, 8)
for {
n, err := r.Read(b)
fmt.Printf("n = %v err = %v b = %v\n", n, err, b)
fmt.Printf("b[:n] = %q\n", b[:n])
if err == io.EOF {
break
}
}
}
運(yùn)行結(jié)果:
b[:n] = "Hello, R"
n = 6 err = <nil> b = [101 97 100 101 114 33 32 82]
b[:n] = "eader!"
n = 0 err = EOF b = [101 97 100 101 114 33 32 82]
b[:n] = ""
8各淀、圖像
image
包定義了 Image
接口:
package image
type Image interface {
ColorModel() color.Model
Bounds() Rectangle
At(x, y int) color.Color
}
注意: Bounds
方法的返回值 Rectangle
實(shí)際上是一個(gè) image.Rectangle
懒鉴,它在 image
包中聲明。
color.Color
和 color.Model
類型也是接口揪阿,但是通常因?yàn)橹苯邮褂妙A(yù)定義的實(shí)現(xiàn) image.RGBA
和 image.RGBAModel
而被忽視了疗我。這些接口和類型由 image/color
包定義。
package main
import (
"fmt"
"image"
)
func main() {
m := image.NewRGBA(image.Rect(0, 0, 100, 100))
fmt.Println(m.Bounds()) //(0,0)-(100,100)
fmt.Println(m.At(0, 0).RGBA()) //0 0 0 0
}