Go的方法和接口
文章目录
一、Methods(方法)
Go没有类(classes)。可以,你能在类型上定义方法。
方法就是带有特定接收器参数的函数。
接受者出现在它自己参数列表中,在func
关键字和方法名之间。
在这个例子中,Ads
方法有一个类型为Vertex
的接受者,命名为v
。
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 main() {
v := Vertex{1.1, 34.1}
fmt.Println(v.Abs())
}
二、方法和函数
记住:方法仅仅是一个带有接收器参数的函数。
这使用常规函数编写的Ads
,在功能上没有变化。
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 main() {
v := Vertex{3, 4}
fmt.Println(Abs(v))
}
三、在类型上声明一个方法
你也能在一个非struct的类型上声明一个方法。
在这个例子中,我们看到一个数值类型MyFloat
带有一个Abs
方法。
你仅能声明一个方法带有一个接收器,它的类型和方法是定义在相同的包里。你不能声明一个方法,带有一个接受器,而接收器的类型声明在另一个包里(包括内置的类型,比如int)。
package main
import (
"fmt"
"math"
)
type MyFloat float64
func (f MyFloat) Abs() float64 {
if f < 0 {
return float64(-f)
}
return float64(f)
}
func main() {
fmt.Println(math.Sqrt2)
// f := MyFloat(-math.Sqrt2)
f := MyFloat(-3)
fmt.Println(f.Abs())
}
四、指针接收器
你能声明一个带指针接收器的方法。
这意味着接收器类型有字面量语法*T
,对于某个类型T。(此外,T本身不能是*int之类的指针)
例如,这里的Scale
方法定义了一个*Vertex
。
带有指针接收器的方法能够修改指针接收器指向的值(就像Scale
这里做的这样)。因为方法经常需要修改它们的接收器,指针接收器比值接收器更常用。
试着从Scale
函数声明移除*
,观察函数的行为有哪些变化。
带有值接收器,Scale
方法是基于原始数据Vertex
值的拷贝进行操作(这与任何其他函数参数的行为相同)。那个Scale
方法必须有一个指针接收器用于改变声明在main
函数的Vertex
的值,
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 (v *Vertex) Scale(f float64) {
v.X = v.X * f
v.Y = v.Y * f
}
func main() {
v := Vertex{3, 4}
v.Scale(10)
fmt.Println(v.Abs())
}
五、指针和函数
这里Abs
和Scale
方法被重写,作为函数。
再次,试着移除Scale函数参数上的*
。你能看到行为有什么变化?为了编译示例你还要改变哪些内容?
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) {
v.X = v.X * f
v.Y = v.Y * f
}
func main() {
v := Vertex{3, 4}
Scale(&v, 10)
fmt.Println(Abs(v))
}
六、方法和指针的间接法
对比前面两个程序,你可能注意到带有指针参数的函数,必须获取一个指针:
var v Vertex
ScaleFunc(v, 5) // 编译错误
ScaleFunc(&v, 5) // 通过
然而带有指针接收器的方法,当它被调用的时候,既可以获取一个值作为接收器,也可以获取一个指针作为接收器。
var v Vertex
v.Scale(5) // 通过
p := &v
p.Scale(10) // 通过
对于v.Scale(5)
这个语句,即便v是一个值而不是一个指针,带有指针接收器的方法也能自动被调用。也就是说,为了方便,Go把语句v.Scale(5)
解释为(&v).Scale(5)
,因为Scale
方法有一个指针接收器。
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{3, 4}
p.Scale(3)
ScaleFunc(p, 10)
fmt.Println(v, p)
}
同样的事情发生在相反的方向。
带有一个值参数的函数,必须获取一个指定类型的值。
var v Vertex
fmt.Println(AbsFunc(v)) // 通过
fmt.Println(AbsFunc(&v)) // 编译失败
然而带有一个值接收器的方法,当它们被调用的时候,既可以接受一个值,也可以接受一个指针作为接收器。
var v Vertex
fmt.Println(v.Abs()) // 通过
p := &v
fmt.Println(p.Abs()) // 通过
在这种情况下,方法调用p.Abs()
是被解释为(*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())
fmt.Println(AbsFunc(v))
p := &Vertex{4, 3}
fmt.Println(p, *p)
fmt.Println(p.Abs())
fmt.Println(AbsFunc(*p))
}
七、选择值或指针接收器
这儿有两个原因使用指针接收器。
第一个原因是:以便于方法能够修改接收器指向的值。
第二个原因是:为了避免在每次方法调用的时候对值进行拷贝。这点非常有效,如果接收器是一个非常大的结构(struct)。就像下面的例子。
在这个例子中,Scale
和Abs
都带有一个类型为Vertex
的接收器,虽然Abs
方法不需要修改它的接收器。
通常来说,在给定的类型上,所有的方法应该有一个值接收器,或者有一个指针接收器,但是不能混合两者。
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("在调用Sacle之前:%v,Abs:%v\n", v, v.Abs())
v.Scale(5)
fmt.Printf("在调用Scale之后:%+v, Abs:%v\n", v, v.Abs())
}
八、Interfaces(接口)
一个接口类型是被定义,是作为方法签名的集合。
一个接口类型的值能够持有实现这些方法的任何值。
注意:在示例代码的22行有一个错误,Vertex
(值类型)不能实现Abser
,因为Abs
方法是仅仅定义在*Vertex
(指针类型)。
package main
import (
"fmt"
"math"
)
type Abser interface {
Abs() float64
}
type MyFloat float64
func (f MyFloat) Abs() float64 {
if f < 0 {
return float64(-f)
}
return float64(f)
}
type Vertex struct {
X, Y float64
}
func (v *Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
func main() {
var a Abser
f := MyFloat(-math.Sqrt2)
v := Vertex{4, 3}
a = f // MyFloat 实现了 Abser
a = &v // 一个 *Vertex 实现了 Abser
// 在这一行,v是一个Vertex(不是一个 *Verterx),并且没有实现 Abser
a = v
fmt.Println(a.Abs())
}
九、接口是隐式的实现
一个类型通过实现它的方法来实现接口。没有明确的意图声明,没有"implements"关键字。
隐式接口让接口从实现中解藕。然后可以在没有预先安排的情况下出现在任何包中。
package main
import "fmt"
type I interface {
M()
}
type T struct {
S string
}
// 这个方法意味着类型T实现了接口I,
// 但是不需要明确的声明
func (t T) M() {
fmt.Println(t.S)
}
func main() {
var i I = T{"hello"}
i.M()
}
十、接口值
在底层,接口值可以被认为是一个值和一个具体类型的元祖:
(value, type)
一个接口值,持有一个指定底层具体类型的值。
在一个接口值上调用方法,将执行它底层类型相同名称的方法。
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)
}
十一、带有nil底层值的接口值
如果在接口本身的内部的具体值是一个nil,将使用nil接受器调用该方法。
在一些其他语言中,这将触发空指针异常。但是在 Go 中,编写优雅地处理被 nil 接收器调用的方法是很常见的(就像例子中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)
}
十二、Nil的接口值
一个nil接口既不持有值,也不持有类型。
在 nil 接口上调用方法是一个运行时错误,因为接口元组内没有类型来指示要调用哪个具体方法。
package main
import "fmt"
type I interface {
M()
}
func main() {
var i I
describe(i)
i.M()
}
func describe(i I) {
fmt.Printf("%v, %T\n", i, i)
}
十三、空接口
指定零个方法的接口被称为空接口。
interface{}
一个空接口能够持有任何类型的值。(每个类型至少零个方法)
空接口被代码使用,能够处理未知类型的值。例如fmt.Println
获取任意数量类型为interface{}
的参数。
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)
}
十四、类型断言
类型断言提供了访问一个接口值的底层具体值。
t := i.(T)
这个语句断言接口值i
,持有具体类型T,分配底层T值给变量t。
如果i没有持有T值,这个语句将触发恐慌。
为了测试一个接口值是否持有一个具体值,一个类型断言能够返回两个值:底层值和一个布尔值,报告这个断言是否通过。
t, ok := i.(T)
如果i持有一个T,t将是这个底层值,ok将为true。
如果没有,ok将是false,t将T类型的零值,并且没有恐慌发生。
注意,这个于从map中读取数据有点相似。
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) // 发生恐慌
fmt.Println(f)
}
十五、类型switch
类型switch是一种允许串联多个类型断言的结构。
一个类型switch就像一个switch语句,但是用例使用一个具体的类型(而不是值),并且这些值是可以同持有给定接口值的类型进行比较。
switch v : = i.(type) {
case T:
// 这里的v是T类型
case S:
// 这里的v时S类型
default:
// 没有匹配,这里的v和i拥有相同的类型
}
在类型switch的声明中,它们和断言i.(T)
拥有相同的语法,但是具体的T
,是被用关键词type
所代替。
这个switch语句测试是否一个接口值持有一个T
或S
。在每一个T
或S
用例中,变量v
将分别时T
或S
类型,并持有从i
获取的值。在默认用例中(这儿没有匹配上),变量 v
和i
具有相同的接口类型和值。
package main
import "fmt"
func do(i interface{}) {
switch v := i.(type) {
case int:
fmt.Printf("两倍的%v是%v\n", v, v*2)
case string:
fmt.Printf("%q有%v个字节\n", v, len(v))
default:
fmt.Printf("我不知道的类型:%T\n", v)
}
}
func main() {
do(21)
do("hello")
do(true)
}
十六、Stringers
一个无处不在的接口是Stringer
,被定义在fmt
包中。
type Stringer type {
String() string
}
一个Stringer
是一个类型,能够描述它自己作为字符串。fmt
包(和很多其它的包),寻找这个接口是为了打印值。
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{"小明", 21}
z := Person{"小红", 26}
fmt.Println(a, z)
}
十七、练习:Stringer
创建一个IPAddr
类,让其实现fmt.Stringer
,将地址打印为虚型四边形。
例如,IPAddr{1,2,3,4}
,应该打印"1.2.3.4"
package main
import (
"fmt"
)
type IPAddr [4]byte
// 添加一个"String() string"方法到IPAddr上
func (ipAddr IPAddr) String() string {
return fmt.Sprintf("%v.%v.%v.%v", ipAddr[0], ipAddr[1], ipAddr[2], ipAddr[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)
}
}
十八、Errors 错误
Go使用error
值表示错误状态。
error
类型类似于fmt.Stringer
是内置的接口。
type error interface {
Error() string
}
和fmt.Stringer
一样,fmt
包在打印值的时候,会查看error
接口。
函数通常返回一个error
值,在调用代码的时候,通过测试error
是否等于nil
来处理错误。
i, err := strconv.Atoi("42")
if err!= nil {
fmt.Printf("不能转换数字:%v\n", err)
return
}
fmt.Printf("转换整数:%v\n", i)
一个为nil
的error代表成功,一个非nil的error代表失败。
package main
import (
"fmt"
"time"
)
type MyError struct {
When time.Time
What string
}
func (e *MyError) Error() string {
return fmt.Sprintf("当前时间 %v, %s", e.When, e.What)
}
func run() error {
return &MyError{time.Now(), "它不能工作"}
}
func main() {
if err := run(); err != nil {
fmt.Println(err)
}
}
十九、练习:Errors
从早期的练习中拷贝Sqrt
函数,修改它,让它返回一个error值。
当给出一个负数的时候Sqrt
应该给出一个非nil的值,因为它不支持复数。
创建一个新类型:
type ErrNegativeSqrt float64
并且创造一个错误,通过给它一个:
func (e ErrNegativeSqrt) Error() string
例如ErrNegativeSqrt(-2).Error
的方法应该返回“cannot Sqrt negative number: -2”。
注意:在Error
方法里面调用fmt.Sprint(e)
讲会让程序进入无限循环。你应该避免这个,通过转换e
:fmt.Sprint(float64(e))
。为什么?
fmt.Sprint(e)将调用e.Error(),为了将e转换为字符串。因此,进入无限循环。
将你的Sqrt
函数变成一个ErrNegativeSqrt
值,当给出一个负数的时候。
package main
import (
"fmt"
"math"
)
type ErrNegativeSqrt float64
func (e ErrNegativeSqrt) Error() string {
// return fmt.Sprintln("cannot Sqrt negative number:", e) // 会造成无限循环
return fmt.Sprintln("cannot Sqrt negative number:", float64(e))
}
func Sqrt(x float64) (float64, error) {
if x < 0 {
return 0, ErrNegativeSqrt(x)
}
res := 1.0
var b float64
for {
b, res = res, res-(res*res-x)/(2*res)
if math.Abs(res-b) < 1e-6 {
return res, nil
}
}
return res, nil
}
func main() {
fmt.Println(Sqrt(-2))
}
二十、Readers
在io
包中,定义了io.Reader
接口,它代表数据流的读取端。
Go的标准库包含了很多这个接口的实现,包括文件、网络连接、压缩机、密码等等。
这个io.Reader
接口有一个Read方法:
func (T) Read(b []byte) (n int, err error)
Read
填充给定的字节切片数据,并返回填充的字节数量和一个错误值。当流结束的时候它返回一个io.EOF
错误。
这个例子创建一个strings.Reader
,并一次消耗它输出的8个字节。
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
}
}
}
二十一、练习:Readers
实现一个Reader
类型,它发出一个无限循环流ASCLL字符“A”。
package main
import (
"fmt"
"golang.org/x/tour/reader"
)
type MyReader struct{}
func (r MyReader) Read(b []byte) (int, error) {
b[0] = 'A'
// b = append(b, 'A')
return 1, nil
}
func main() {
b := make([]byte, 0)
b = append(b, 'A')
fmt.Printf("%q\n", b)
reader.Validate(MyReader{})
}
二十二、练习:rot13Reader
一个非常常见的模式是:一个io.Reader
包装另一个io.Reader
,用某种方式修改这个流。
例如,gzip.NewReader
函数获取一个io.Reader
(一个压缩流数据),并返回一个*gzip.Reader
,它也实现了io.Reader
(一个解压数据流)。
实现一个rot13Reader
,它实现了io.Reader
,并从一个io.Reader
读取数据,修改这个流,将 rot13 替换密码应用于所有字母字符。
这个rot13Reader
类型已经提供给你。通过实现Read
方法把它变成io.Reader
。
package main
import (
"io"
"os"
"strings"
)
type rot13Reader struct {
r io.Reader
}
func (rot *rot13Reader) Read(b []byte) (n int, err error) {
n, err = rot.r.Read(b)
if err != nil {
return
}
for i := 0; i < n; i++ {
if b[i] >= 'A' && b[i] <= 'Z' {
b[i] = 'A' + ((b[i]-'A')+13)%26
} else if b[i] >= 'a' && b[i] <= 'z' {
b[i] = 'a' + ((b[i]-'a')+13)%26
}
}
return
}
func main() {
s := strings.NewReader("Lbh penpxrq gur pbqr!")
r := rot13Reader{s}
io.Copy(os.Stdout, &r)
}
二十三、Images
image
页面定义了一个Image
接口:
package image
type Image interface {
ColorModel() color.Model
Bounds() Rectangle
At(x, y int) color.Color
}
注意,返回值为Rectangel
的方法Bounds
,实际上是一个image.Rectangle
,因为这个声明在image
包内部。
看文档获取更详细的内容:image 。
color.Color
和color.Model
类型也是接口,但是我们通过使用预定义的实现color.RGBA
和color.RGBAModel
来忽略它们。这些接口和类型定义在image/color页面。
package main
import (
"fmt"
"image"
)
func main() {
m := image.NewRGBA(image.Rect(0, 0, 100, 100))
fmt.Println(m.Bounds())
fmt.Println(m.At(0, 0).RGBA())
}
二十四、练习:Images
还记得你早期写的图片生成(exercise-slices.go)程序吗?让我们写另外一个,但是这一次它将返回一个image.Image
实现,代替数据切片。
定义你自己的Image
类型,实现必须的方法,并且调用pic.ShowImage
。
Bounds
应该返回一个image.Rectangle
,就像image.Rect(0, 0, w, h)
。
ColorModel
应该返回color.RGBModel
。
At
应该返回一个颜色,在最后图片生成的值v
对应着color.RGBA{v, v, 255,255}
。
package main
import (
"image"
"image/color"
"golang.org/x/tour/pic"
)
type Image struct {
width, height int
color uint8
}
func (i Image) Bounds() image.Rectangle {
return image.Rect(0, 0, i.width, i.height)
}
func (i Image) ColorModel() color.Model {
return color.RGBAModel
}
func (i Image) At(x, y int) color.Color {
return color.RGBA{i.color + uint8(x), i.color + uint8(y), 255, 255}
}
func main() {
m := Image{100, 100, 100}
pic.ShowImage(m)
}