Go的方法和接口

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())
}

五、指针和函数

这里AbsScale方法被重写,作为函数。

再次,试着移除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)。就像下面的例子。

在这个例子中,ScaleAbs都带有一个类型为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语句测试是否一个接口值持有一个TS。在每一个TS用例中,变量v将分别时TS类型,并持有从i获取的值。在默认用例中(这儿没有匹配上),变量 vi具有相同的接口类型和值。

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)讲会让程序进入无限循环。你应该避免这个,通过转换efmt.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.Colorcolor.Model类型也是接口,但是我们通过使用预定义的实现color.RGBAcolor.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)
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值