没有理想的人不伤心

Golang - 方法与接口

2025/03/02
2
0

image.png

1 方法

1.1 为结构体类型声明方法

Go 没有类。不过可以为结构体类型定义方法。方法就是一类带特殊的 接收者 参数的函数。方法接收者在它自己的参数列表内,位于 func 关键字和方法名之间。

接收者最好选择简短的名字,最常用的就是类型名称的首字母,如 Vertex 中的 v。

能够为不同类型的接收者使用相同的方法名

type Vertex struct {
    X,Y float64
}

func(v Vertex)Abs()float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
//Abs 方法拥有一个名为 v,类型为 Vertex 的接收者
func main() {
    v:= Vertex{3,4}
    fmt.Println(v.Abs())
}

方法只是个带接收者参数的函数!

如下面这个 Abs就是正常的函数,功能并没有变化

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

1.2 为非结构体类型声明方法

如下是一个带 Abs方法的数值类型MyFloat

type MyFloat float64

func(a MyFloat)Abs()float64 {
	if a < 0 {
		return float64(-a)
	}
	return float64(a)
}

func main() {
	f:= MyFloat(-math.Sqrt2)
	fmt.Println(f.Abs())
}

接收者的类型定义和方法声明必须在同一包内;不能为**内建类型(int 等)**声明方法。

1.3 为指针接收者声明方法

对于某类型 T,接收者的类型可以用 *T 的文法

如下,为 *Vertex 定义了 Scale 方法

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函数是对原始的 Vertex 的副本进行操作,然后返回值。

若把 Abs Scale方法写为函数,则如下

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

习惯上遵循如果类型 T 的任何一个方法使用指针接收者,则所有 T 的方法都应该使用指针接收者

1.4 方法与指针重定向

上面两个程序的区别在于,指针接收者的方法进行调用时,接收者既可以是指针也可以是值

var v Vertex
v.Scale(5)  // OK
p:= &v
p.Scale(10) // OK

带指针参数的函数则必须接收一个指针。 Scale(&v,5)

原因是 go 会将语句 v.Scale(5)自动解释为 (&v).Scale(5)

相反同理:

对于值接收者的方法调用时,接收者既可以是指针也可以是值

var v Vertex
fmt.Println(v.Abs()) // OK
p:= &v
fmt.Println(p.Abs()) // OK

这种情况下,方法调用 p.Abs() 会被解释为 (*p).Abs()

而对于接受一个值做参数的函数,必须接受一个指定类型的值!而不能是指针。 fmt.Println(AbsFunc(v))

1.5 结构体内嵌组成类型的方法调用

type Point struct{ X,Y float64}
type ColoredPonit struct {
    Point	//匿名结构体字段
    Color color.RGBA
}

结构体 ColoredPonit中内嵌了 Point类型的结构体。内嵌使我们可以直接使用 ColoredPonit内的所有字段,包括 Point中的 X、Y,而不需要显示的提及 Point类型

当 Point 类型作为函数的参数时,必须显示的写出。

var cp ColoredPonit
cp.X = 1	//可以直接使用,不需要提到 Point
cp.Y = 2
fmt.Println(cp.Ponit.X,cp.Y)

对于 Point的方法也是同理,ColoredPonit类型的变量可以直接使用Point的方法,Point的方法都被纳入到ColoredPonit中。

func(p *Point)ScaleBy(factor float64){
    p.X *= factor
    p.Y *= factor
}
var cp = ColoredPonit{Point{1,1},red}
cp.ScaleBy(2)

1.6 方法变量和方法表达式

选择子 p.ScaleBy是一个函数,返回一个方法变量,可以将方法 Point.ScaleBy绑定到一个接收者 p 上,这个函数调用时不用指定其接收者,只需要传入函数的参数即可。

p:= Point{1,2}
scaleP:= p.ScaleBy	//方法变量,相当于一个函数
fmt.Println(scaleP(2))	//{2,4}
fmt.Println(scaleP(3))	//{6,12}
fmt.Println(scaleP(6))	//{36,72}

若想在包内的 API 中调用一个特定接收者的方法时,方法变量就派上用场了。

方法表达式:T.f 或(*T).f,是一种函数变量,T 是接收者的变量类型,f 是方法。可以像函数一样调用,把原来方法的接收者替换成函数的第一个参数。

通过这样得到的函数比原方法多一个参数,就是指定的接收者

p:= Point{1,2}
scale:= (*Point).ScaleBy	//方法表达式
scale(&p,2)	//函数的调用,&p 是接受者
fmt.Println(p)            // "{2 4}"
fmt.Printf("%T\n",scale) // "func(*Point,float64)"

当需要根据参数变量来选择同一类型的不同方法时,方法表达式可以调用不同的方法,以及选择不同的接收者。

如下

type Point struct{ X,Y float64 }

func(p Point)Add(q Point)Point { return Point{p.X + q.X,p.Y + q.Y} }
func(p Point)Sub(q Point)Point { return Point{p.X - q.X,p.Y - q.Y} }

type Path []Point

func(path Path)TranslateBy(offset Point,add bool) {
    var op func(p,q Point)Point	//匿名函数
    //根据不同的参数来选择调用的方法
    if add {
        op = Point.Add
    } else {
        op = Point.Sub
    }
    for i:= range path {
        //根据不同接收者来调用方法
        path[i] = op(path[i],offset)
    }
}

1.7 封装

一个对象的变量或者方法如果对调用方是不可见的话,一般就被定义为“封装”

Go 语言只有一种控制可见性的手段:大写首字母的标识符会从定义它们的包中被导出,小写字母的则不会。这种限制包内成员的方式同样适用于结构体或者一个类型的方法。因而如果我们想要封装一个对象,我们必须将其定义为一个结构体。

只用来访问或修改内部变量的函数被称为 setter 或者 getter,在命名一个 getter 方法时,我们通常会省略掉前面的 Get 前缀。这种简洁上的偏好也可以推广到各种类型的前缀比如 Fetch,Find 或者 Lookup。比如 log 包里的 Logger 类型对应的一些函数

package log
type Logger struct {
    flags  int
    prefix string
    // ...
}
func(l *Logger)Flags()int
func(l *Logger)SetFlags(flag int)
func(l *Logger)Prefix()string
func(l *Logger)SetPrefix(prefix string)

2 接口

接口类型 是由一组方法签名定义的集合。

接口类型的变量可以保存任何实现了这些方法的值。

接口的命名带上 er

//定义了一个接口
type Abser interface {
  Abs()float64
}

func main() {
  var a Abser
  f:= MyFloat(-math.Sqrt2)
  v:= Vertex{3,4}

  a = f  // a MyFloat 实现了 Abser
  a = &v // a *Vertex 实现了 Abser

  // 下面一行,v 是一个 Vertex(而不是 *Vertex)
  // 所以没有实现 Abser。
  a = v

  fmt.Println(a.Abs())
}

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

以上代码第 15 行存在一个错误。由于 Abs 方法只为 *Vertex(指针类型)定义,因此 Vertex(值类型)并未实现 Abser,所以第 15 行错误

2.1 接口与隐式实现

关于接口需要注意的是,只有当有两个或两个以上的具体类型必须以相同的方式进行处理时才需要定义接口。不要为了接口而写接口,那样只会增加不必要的抽象,导致不必要的运行时损耗。

类型通过实现一个接口的所有方法来实现该接口。既然无需专门显式声明,也就没有 “implements” 关键字。

隐式接口从接口的实现中解耦了定义,这样接口的实现可以出现在任何包中,无需提前准备。

因此,也就无需在每一个实现上增加新的接口名称,这样同时也鼓励了明确的接口定义

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

2.2 接口值

接口也是值。它们可以像其它值一样传递。

接口值可以用作函数的参数或返回值。

在内部,接口值可以看做包含值和具体类型的元组:**(value,type)**

接口值保存了一个具体底层类型的具体值。

接口值调用方法时会执行其底层类型的同名方法。

//接口定义
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)
}

//输出为
(&{Hello}, *main.T)
Hello
(3.141592653589793,main.F)
3.141592653589793

接口的实现就是将接口变量赋值为接口中包含的方法的接收者,且该接收者就是接口变量的底层变量

2.3 底层值为 nil 的接口值

即便接口内的具体值为 nil,方法仍然会被 nil 接收者调用。

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
//结构体指针未赋值,所以为空值 nil
	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>, *main.T)
<nil>
(&{hello}, *main.T)
hello

保存了 nil 具体值的接口其自身并不为空值 nil

2.4 nil 接口值(未赋值)

nil 接口值既不保存值也不保存具体类型。

为 nil 接口调用方法会产生运行时错误,因为接口的元组内并未包含能够指明该调用哪个 具体 方法的类型。

type I interface {
	M()
}

func main() {
	var i I
	describe(i)
	i.M()
}

func describe(i I) {
	fmt.Printf("(%v, %T)\n",i,i)
}

//输出
(<nil>, <nil>)
panic:runtime error:invalid memory address or nil pointer dereference
[signal 0xc0000005 code=0x0 addr=0x0 pc=0x7e51b9]

2.5 空接口

指定了零个方法的接口值被称为 空接口:**interface{}**

空接口可保存任何类型的值。(因为每个类型都至少实现了零个方法。)

空接口被用来处理未知类型的值。例如,fmt.Print 可接受类型为 interface{} 的任意数量的参数。

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

//输出
(<nil>, <nil>)
(42,int)
(hello,string)

空接口作为 map 的值,使用空接口实现可以保存任意值的字典。

// 空接口作为 map 值
var studentInfo = make(map[string]interface{})
studentInfo["name"] = "李白"
studentInfo["age"] = 18
studentInfo["married"] = false
fmt.Println(studentInfo)

2.6 接口嵌套

接口与接口间可以通过嵌套创造出新的接口。嵌套得到的接口的使用与普通接口一样

// Sayer 接口
type Sayer interface {
    say()
}

// Mover 接口
type Mover interface {
    move()
}

// 接口嵌套
type animal interface {
    Sayer
    Mover
}

type cat struct {
    name string
}

func(c cat)say() {
    fmt.Println("喵喵喵")
}

func(c cat)move() {
    fmt.Println("猫会动")
}

func main() {
    var x animal
    x = cat{name: "花花"}
    x.move()
    x.say()
}

2.7 接口类型断言

类型断言 提供了访问接口变量底层具体值的方式。

t:= i.(T)

该语句断言:接口值 i 保存了具体类型 T,并将其底层类型为 T 的值赋予变量 t。

若 i 并未保存 T 类型的值,该语句就会触发一个 panic。

为了判断一个接口值是否保存了一个特定的类型,类型断言可返回两个值:其底层值以及一个报告断言是否成功的布尔值。

**t,ok := i.(T)**

若 i 保存了一个 T,那么 t 将会是其底层值,而 ok 为 true。

否则,ok 将为 false 而 t 将为 T** 类型的零值**,程序并不会产生**panic**

请注意这种语法和读取一个映射时的相同之处。

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) // 报错(panic)
	fmt.Println(f)
}

//输出
hello
hello true
0 false
panic:interface conversion:interface {} is string,not float64

2.8 接口类型选择

类型选择 是一种按顺序从几个类型断言中选择分支的结构。

类型选择与一般的 switch 语句相似,不过类型选择中的 case 为类型(而非值), 它们针对给定接口值所存储的值的类型进行比较。

switch v:= i.(type) {
case T:
    // v 的类型为 T
case S:
    // v 的类型为 S
default:
    // 没有匹配,v 与 i 的类型相同
}

类型选择中的声明与类型断言 i.(T)的语法相同,只是具体类型 T 被替换成了关键字 type

此选择语句判断接口值 i 保存的值类型是 T 还是 S。在 T 或 S 的情况下,变量 v 会分别按 T 或 S 类型保存 i 拥有的值。在默认(即没有匹配)的情况下,变量 v 与 i 的接口类型和值相同。

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

//输出
Twice 21 is 42
"hello"is 5 bytes long
I don't know about type bool!

2.9 常用接口

2.9.1 Stringer

fmt包中定义的 Stringer是最普遍的接口之一。

type Stringer interface {
    String()string
}

Stringer 是一个可以用字符串描述自己的类型。fmt 包(还有很多包)都通过此接口来打印值。

type Person struct {
	Name string
	Age  int
}
//persom 类型实现了 Stringer 接口
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)
}

//输出
Arthur Dent(42 years)Zaphod Beeblebrox(9001 years)

2.9.2 error

Go 程序使用 error 值来表示错误状态。

与 fmt.Stringer 类似,error 类型是一个内建接口:

type error interface {
    Error()string
}

(与 fmt.Stringer 类似,fmt 包也会根据对 error 的实现来打印值。)

通常函数会返回一个 error 值,调用它的代码应当判断这个错误是否等于 nil 来进行错误处理。

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 时表示成功;非 nil 的 error 表示失败。

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

//输出
at 2024-03-06 20:04:28.4684658 +0800 CST m=+0.002122601,it didn't work

2.9.3 Reader

io 包指定了 io.Reader 接口,它表示从数据流的末尾进行读取。

Go 标准库包含了该接口的许多实现,包括文件、网络连接、压缩和加密等等。

io.Reader 接口有一个 Read 方法:

func(T)Read(b []byte) (n int,err error)

Read 用数据填充给定的字节切片并返回填充的字节数和错误值。在遇到数据流的结尾时,它会返回一个 io.EOF 错误。

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

//输出
n = 8 err = <nil> b = [72 101 108 108 111 44 32 82]
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] = ""

2.9.4 Image

image 包定义了 Image 接口:

type Image interface {
    ColorModel()color.Model
    Bounds()Rectangle
    At(x,y int)color.Color
}

注意: Bounds 方法的返回值 Rectangle 实际上是一个 image.Rectangle,它在 image包中声明

color.Colorcolor.Model 类型也是接口,但是通常因为直接使用预定义的实现 image.RGBAimage.RGBAModel 而被忽视了。这些接口和类型由 image/color 包定义。

func main() {
	m:= image.NewRGBA(image.Rect(0,0,100,100))
	fmt.Println(m.Bounds())
	fmt.Println(m.At(0,0).RGBA())
}