GO千练——方法和接口

73 阅读8分钟

方法

方法概念

Go 没有类,但是 GO 可以为结构体类型定义方法,请记住:方法只是个带接收者参数的函数。 方法就是一类带特殊的 接收者 参数的函数。方法接收者在它自己的参数列表内,位于 func 关键字和方法名之间。请参考如下 nextTenYears() 方法的定义:

package main

import "fmt"

type Person struct {
	Name string
	Age  int
}

func (p Person) nextTenYears() int {
	return p.Age + 10
}

func main() {
	p := Person{"张三", 10}

	fmt.Printf("%v 10 年后 %v 岁", p.Name, p.nextTenYears())
}

方法特殊接受者

除了给结构体定义方法,还可以为非结构体类型声明方法。

但是我们只能为在同一包内定义的类型的接收者声明方法,而不能为其它包内定义的类型(包括 int 之类的内建类型)的接收者声明方法。

内置类型

package main

import "fmt"

type MyInt int

func (m MyInt) Abs() int {
	if m < 0 {
		return int(-m)
	}

	return int(m)
}

func main() {
	m := MyInt(-10)
	fmt.Printf("%v 的绝对值是 %v", m, m.Abs())

}

指针接收者

GO 可以为指针接收者声明方法。 这意味着对于某类型 T,接收者的类型可以用 *T 的文法。(注意: T 不能是像 *int 这样的指针)

package main

import "fmt"

type Person struct {
	Name string
	Age  int
}

func (p *Person) nextTenYearsNew() {
	p.Age = p.Age + 10
}

func main() {
	p := Person{"张三", 20}
	fmt.Println(p)

	p.nextTenYearsNew()
	fmt.Println(p)
}

方法与指针重定向

对比

通过比较 nextTenYears() 和 nextTenYearsNew() 方法对比,会发现: 如果参数为指针类型的话,则带指针参数的函数必须接受一个指针,无法接受一个值。 如果接收者为指针的方法被调用时,接收者既能为值又能为指针。 原因:Go 会将语句 p.nextTenYearsNew() 解释为 (&p).nextTenYearsNew() 执行。

package main

import "fmt"

type Person struct {
	Name string
	Age  int
}

func nextTenYears(p *Person) {
	(*p).Age = (*p).Age + 10
}

func (p *Person) nextTenYearsNew() {
	p.Age = p.Age + 10
}

func main() {
	p := Person{"张三", 20}
	fmt.Println(p)

	// 新年龄设置值
	nextTenYears(&p)
	fmt.Println(p)

	//如下下会报:cannot use p (variable of type Person) as type *Person in argument to nextTenYears
	// nextTenYears(p)

	// 方法内部指针直接设置新年龄,无需返回
	p.nextTenYearsNew()
	fmt.Println(p)

	// 这样是编译通过并运行成功的
	p1 := &p
	p1.nextTenYearsNew()
	fmt.Println(p)
}

// 打印输出
{张三 20}
{张三 30}
{张三 40}
{张三 50}

相反的: 接受一个值作为参数的函数必须接受一个指定类型的值。 而以值为接收者的方法被调用时,接收者既能为值又能为指针。 原因:方法调用 p.nextTenYearsNew() 会被解释为 (*p).nextTenYearsNew()。

package main

import "fmt"

type Person struct {
	Name string
	Age  int
}

// 由于值类型是值传递,则需要返回值
func nextTenYears(p Person) int {
	return p.Age + 10
}

// 由于值类型是值传递,则需要返回值
func (p Person) nextTenYearsNew() int {
	return p.Age + 10
}

func main() {
	p := Person{"张三", 20}
	fmt.Println(p)

	// 需要返回新年龄设置值
	p.Age = nextTenYears(p)
	fmt.Println(p)

	//如下下会报:cannot use &p (value of type *Person) as type Person in argument to nextTenYears
	// p.Age = nextTenYears(&p)

	// 方法内部指针直接设置新年龄,无需返回
	p.Age = p.nextTenYearsNew()
	fmt.Println(p)

	// 这样是编译通过并运行成功的
	p1 := &p
	p.Age = p1.nextTenYearsNew()
	fmt.Println(p)
}

结论

我们应该选择值还是指针作为接收者呢?

我们应该选择指针,原因有二: 首先,方法能够修改其接收者指向的值。 其次,这样可以避免在每次调用方法时复制该值。若值的类型为大型结构体时,这样做会更加高效。

接口

接口概念

Go 语言提供了另外一种数据类型,即接口接口类型 是由一组方法签名定义的集合,它把所有的具有共性的方法定义在一起,任何其它类型只要实现了这些方法就是实现了这个接口。

/* 定义接口 */
type interface_name interface {
   method_name1 [return_type]
   method_name2 [return_type]
   method_name3 [return_type]
   ...
   method_namen [return_type]
}

/* 定义结构体 */
type struct_name struct {
   /* variables */
}

/* 实现接口方法 */
func (struct_name_variable struct_name) method_name1() [return_type] {
   /* 方法实现 */
}

// ...

func (struct_name_variable struct_name) method_namen() [return_type] {
   /* 方法实现*/
}

实例代码:

package main

import "fmt"

type Phone interface {
	call()
}

type IPhone struct {
}

type Android struct {
}

func (iphone IPhone) call() {
	fmt.Println("苹果手机打电话...")
}

func (android Android) call() {
	fmt.Println("安卓手机打电话...")
}

func main() {
	iphone := new(IPhone)
	iphone.call()

	android := new(Android)
	android.call()
}

接口值

在 GO 中接口也是值。它们可以像其它值一样传递。接口值可以用作函数的参数或返回值。

接口值可以看做包含值和具体类型的元组:

// value 具体值
// type 具体类型
(value, type)

接口值保存了一个具体底层类型的具体值。接口值调用方法时会执行其底层类型的同名方法。

package main

import "fmt"

type Phone interface {
	call()
}

type IPhone struct {
}

type Android struct {
}

func (iphone IPhone) call() {
	fmt.Println("苹果手机打电话...")
}

func (android Android) call() {
	fmt.Println("安卓手机打电话...")
}

func main() {
	var phone Phone
	phone = new(IPhone)
    // 打印接口底层的类型
	fmt.Printf("phone 的类型:%T\n", phone)
	phone.call()
	
	fmt.Println()

	phone = new(Android)
    // 打印接口底层的类型
	fmt.Printf("phone 的类型:%T\n", phone)

	phone.call()
}

底层值为 nil 的接口值

即便接口内的具体值为 nil,方法仍然会被 nil 接收者调用。 在一些语言中,这会触发一个空指针异常,但在 Go 中通常会写一些方法来优雅地处理它

package main

import "fmt"

type Phone interface {
	call()
}

type IPhone struct {
	Name string
}

func (iphone *IPhone) call() {
	if iphone == nil {
		fmt.Println("iphone 是 nil")
	} else {
		fmt.Printf("%v 手机打电话...\n", iphone.Name)
	}
}
func main() {
	var iphone *IPhone
	// 打印接口底层的类型
	fmt.Printf("phone  的值:%v ,类型:%T\n", iphone, iphone)
	iphone.call()
}

注意: 保存了 nil 具体值的接口其自身并不为 nil。

nil 接口值

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

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

package main

import "fmt"

type Phone interface {
	call()
}

func main() {
	var phone Phone
	// 打印接口底层的类型
	fmt.Printf("phone  的值:%v ,类型:%T\n", phone, phone)
}

// 打印输出
phone  的值:<nil> ,类型:<nil>

空接口

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

// 空接口
interface{}

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

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

package main

import "fmt"

func main() {
	var i interface{}
	describe(i)

	i = 20
	describe(i)

	i = "GO"
	describe(i)
}

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

// 打印输出
(<nil>, <nil>)
(20, int)
(GO, string)

类型断言

GO 类型断言提供了访问接口值底层具体值的方式。结构如下:

// 该语句断言接口值 i 保存了具体类型 T,并将其底层类型为 T 的值赋予变量 t。
// 若 i 并未保存 T 类型的值,该语句就会触发一个恐慌(个人理解类似 Java 的异常)。
t := i.(T)

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

// 若 i 保存了一个 T,那么 t 将会是其底层值,而 ok 为 true。
// 否则,ok 将为 false 而 t 将为 T 类型的零值,程序并不会产生恐慌(个人理解类似 Java 的异常)。
t, ok := i.(T)

实例代码:

package main

import "fmt"

func main() {
	var i interface{} = "GO"

	s := i.(string)
	fmt.Println(s)

	s, ok := i.(string)
	fmt.Println(s, ok)

	f, ok := i.(float64)
	fmt.Println(f, ok)

    // 报错(panic)
	f = i.(float64) 
	fmt.Println(f)
}

// 打印结果
GO
GO true
0 false
panic: interface conversion: interface {} is string, not float64

类型选择

GO 类型选择是一种按顺序从几个类型断言中选择分支的结构。 类型选择与一般的 switch 语句相似,不过类型选择中的 case 为类型(而非值), 它们针对给定接口值所存储的值的类型进行比较。

// 类型选择中的声明与类型断言 i.(T) 的语法相同,只是具体类型 T 被替换成了关键字 type。
switch v := i.(type) {
    case T:
    // v 的类型为 T
    case S:
    // v 的类型为 S
    default:
    // 没有匹配,v 与 i 的类型相同
}
package main

import "fmt"

func switchType(i interface{}) {
	switch i.(type) {
	case int:
		fmt.Printf("i 是int类型,值为:%v\n", i)
	case string:
		fmt.Printf("i 是string类型,值为:%v\n", i)
	default:
		fmt.Printf("这是 default 分支,值为:%v\n", i)

	}
}

func main() {
	switchType(10)
	switchType("go")
	switchType(true)
}

错误Error

Go 程序使用 error 值来表示错误状态。 error 类型是一个内建接口:

type error interface {
    Error() string
}

error 的实例:

package main

import (
	"fmt"
	"time"
)

type MyError struct {
	Time time.Time
	Msg  string
}

func (e *MyError) Error() string {
	return fmt.Sprintf("%v, %s", e.Time, e.Msg)
}

func run() *MyError {
	return &MyError{time.Now(), "这是一个错误!"}
}

func main() {
	if e := run(); e != nil {
		fmt.Println(e)
	}
}

数据流读取Reader

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

package main

import (
	"fmt"
	"io"
	"strings"
)

func main() {
	r := strings.NewReader("Hello, Go!")
	b := make([]byte, 8)

	for {
		n, e := r.Read(b)
		fmt.Printf("本次读取的字符数:%v,err为:%v, 字符数组:%v\n", n, e, b)
		fmt.Printf("本次读取的字符串是:%q\n\n", b[:n])
		if e == io.EOF {
			break
		}
	}
}

// 打印输出
本次读取的字符数:8,err为:<nil>, 字符数组:[72 101 108 108 111 44 32 71]
本次读取的字符串是:"Hello, G"

本次读取的字符数:2,err为:<nil>, 字符数组:[111 33 108 108 111 44 32 71]
本次读取的字符串是:"o!"

本次读取的字符数:0,err为:EOF, 字符数组:[111 33 108 108 111 44 32 71]
本次读取的字符串是:""

图像Image

image 包定义了 Image 接口:

package image

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

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

实例:

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

请关注公众号【Java千练】,更多干货文章等你来看!

qrcode_for_gh_e39063348296_258.jpg