Go 语言入门(二)方法和接口

写在前面

在学习 Go 语言之前,我自己是有一定的 Java 和 C++ 基础的,这篇文章主要是基于A tour of Go编写的,主要是希望记录一下自己的学习历程,加深自己的理解

方法

Go 语言中是没有「类」这个概念的,但我们可以为变量定义方法,例如对结构体定义方法,达到类似于类的情况。这里我们先对 Go 中的方法进行一个定义:

什么是方法

「方法」:一类带特殊的接收者参数的函数

对于方法,「接受者参数」位于func关键字和方法名之间:

// 定义一个结构体
type Vertex struct {
X, Y float64
} // 这里有一个接受者参数 v
func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
} func main() {
v := Vertex{3, 4}
// 我们可以直接调用 v 的 Abs() 方法
fmt.Println(v.Abs())
}

当然,我们也可以直接将接受者 v 作为一个参数传入,那么这就是一个普通的函数了,它们可以实现相同的功能:

func Abs(v Vertex) float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

接受者参数

在上面,我们定义了一个「接受者参数」为结构体VertexAbs()方法,我们可以为任意类型的变量声明方法

这里我们使用type关键字定义一个变量类型「MyFloat」:

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

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

指针接受者

对于其它语言有所了解的话,我们知道在函数实际上是对参数的拷贝进行操作;又由于「指针」的存在,有「实参」和「形参」之分。

在 Go 中同样有这两者的存在,对于某种类型 T:

  • 如果接受者参数的类型为T,则是「形参」,函数中的修改不会修改原来的元素;

  • 如果接受者参数的类型为*T,则是「实参」,可以在函数中直接修改它指向的元素,这样能够同步修改原元素。

看下面的例子:

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}
// 对于方法,Go 可以自动在值和指针之间转换
// 因此这里等价于 (&v).Scale(10)
v.Scale(10)
fmt.Println(v.Abs())
}

当我们定义方法的时候,Go 语言会帮我们自动在值和指针之间转换;类似于上面,如果方法需求的是一个值,但我们传入的是指针,Go 也能够帮我们自动将其转换为值。但是使用函数时不会自动转换

因此我们可以直接使用v.Scale(10),虽然传入的不是指针而是值,但这里的执行结果仍为 50;如果我们将Scale()方法的接受者参数改为v Vertext,那么Scale函数中传入的是形参,只是对拷贝进行修改,运行后 v 的元素值不变,执行结果为 5。

通常来说,所有给定类型的方法都应该有值或指针接收者,但并不应该二者混用:当某一类型的所有方法接收者都是指针时,每个方法都会对变量本身进行修改;如果我们将值接收者和指针接收者混用,那在某一次调用指针接收者的方法后,对于后续的值接收者方法,可能会对本身值产生不应该的修改。

方法变量与表达式

Go 中我们可以将「使用方法」和「调用方法」两个操作分开。我们可以为一个方法变量赋值,让它成为一个函数,把方法绑定到特定接收者上,这样只需要提供参数而不需要提供接收者就可以调用:

拿上面的 Scale 方法做例子:

v := Vertex{3, 4}
scale := v.Scale
// 等价于 fmt.Println(v.Scale(10))
fmt.Println(scale(10))

有时,我们还希望能够灵活选择方法接收者,这时,我们可以将这个方法赋值为一个方法表达式。在调用方法表达式时,必须选择接收者,且将其作为第一个形参,之后则像原方法一样进行调用即可。

type Point struct{ X, Y float64 }

// Point 的两个方法
func (p Point) Add(q Point) { return Point{p.X + q.X, p.Y + q.Y} }
func (p Point) Sub(q Point) { return Point{p.X - q.X, p.Y - q.Y} } func (path Path) TranslateBy(offset Point, add bool) {
// 方法表达式
var op func(p, q Point) Point
// 根据 add 值的不同为方法表达式 op 进行赋值
if add {
op = Point.Add
} else {
op = Point.Sub
}
for i := range path {
path[i] = op(path[i], offset)
}
}

接口

不同于 Java 中的接口,在 Go 中,接口是一种抽象类型。它就像一种「约定」,所有的接口类型都能引用其提供的所有方法。

「接口类型」:由一组方法签名定义的集合

type Abser interface {
Abs() float64
}

如何使用接口

如果一个类型实现了一个接口要求的所有方法,那么这个类型就实现了这个接口。

我们看下面代码的例子:

package main

import (
"fmt"
"math"
) // 接口 Abser,包含方法 Abs()
type Abser interface {
Abs() float64
} // MyFloat 实现了 Abs() 方法
type MyFloat float64
// 无需使用 implements 关键字
func (f MyFloat) Abs() float64 {
if f < 0 {
return float64(-f)
}
return float64(f)
} // *Vertex 实现了 Abs() 方法
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.Sqrt(2))
v := Vertex{3, 4} a = f // a MyFloat 实现了 Abser
a = &v // a *Vertex 实现了 Abser // 这里被注释的语句会报错
// v 是一个 Vertex(而不是 *Vertex),没有实现 Abser。
// a := v fmt.Println(a.Abs())
fmt.Println(b.Abs())
}

上例代码中,类型MyFloat和类型*Vertex都实现了Abs()方法,因此可以给接口类型Abser赋值;而Vertex未实现,如果用它来赋值则会报错。

同时,我们还可以从上面代码中看到,在实现Abser接口的方法时,我们并没有像 Java 中实现接口一样用implements关键字显式声明,这样也鼓励程序员对接口要有明确的定义。

接口值

「接口」作为一种抽象类型,它也是一个值,可以像其它值一样进行传递,也就是可以作为参数或是返回值。

下面我们定义一个接口I,类型*TM实现了这个接口,然后我们将接口I作为参数传入函数desribe()中:

package main

import (
"fmt"
"math"
) type I interface {
M()
} // 实现 I 的类型 *T
type T struct {
S string
} func (t *T) M() {
fmt.Println(t.S)
} // 实现 I 的类型 F
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

可以看到,传入的接口参数会以底层的类型和值进行打印。

底层值为 nil 的接口值

如果接口的具体值nil,然后调用这个接口的方法,在一些语言中(如 Java)将会触发「空指针异常」,但在 Go 中则能够正确打印出 nil。

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

我们使用前例的接口I和类型*T,作出测试如下:

1.「接口具体值」为 nil

func main() {
var i I var t *T
i = t
describe(i)
i.M()
}

执行结果如下:

(<nil>, *main.T)
<nil>

可以看到,在调用 i 的方法M()时,Go 能够正常地打印出<nil>值而不会报错。

2.「接口」自身为 nil

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

执行上面的语句,describe()方法能够打印出(<nil>, <nil>),表示接口自身为nil,自然值也为nil;而在对接口 i 进行方法调用时则会抛出异常(因为 Go 不知道应该调用哪个具体方法的类型)。

空接口

「空接口」:定义了 0 个方法的接口

interface{}

空接口有什么作用呢?它能够接受任何类型的值(因为空接口对方法没有要求),因此我们可以用空接口来处理未知类型的值。例如,fmt.Print可接受类型为interface{}的任意数量的参数。

下面的例子可以有效地帮我们进行理解:

func main() {
var i interface{}
describe(i)
// 运行结果: (<nil>, <nil>) i = 42
describe(i)
// 运行结果: (42, int) i = "hello"
describe(i)
// 运行结果: (hello, string)
} func describe(i interface{}) {
fmt.Printf("(%v, %T)\n", i, i)
}

上面的describe()函数便以空接口作为参数,因此可以接受任何类型(包括 nil)的值。

类型断言

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

t := i.(T)

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

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

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

t, ok := i.(T)

这个「双赋值」和映射中的很相似:

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

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

下面我们用一个例子总结一下接口的「类型断言」:

package main

import "fmt"

func main() {
var i interface{} = "hello" s := i.(string)
fmt.Println(s)
// 执行结果: hello s, ok := i.(string)
fmt.Println(s, ok)
// 执行结果: hello true f, ok := i.(float64)
fmt.Println(f, ok)
// 执行结果: 0 false f = i.(float64)
fmt.Println(f)
// 报错(panic)
}

类型选择

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

想象一下,如果我们需要根据变量的不同类型来对其进行特定操作,我们首先想到的便是在 if-else 中套用之前的类型断言:

if v, ok := i.(T); ok {
// v 的类型为 T
} else if v, ok := i.(S); ok {
// v 的类型为 s
} else {
// 没有匹配,v 与 i 的类型相同
}

Go 中套用swtich语句,能够简单地针对给定接口值所存储的值的类型进行比较,也就是我们可以这样简化上面的代码:

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 的接口类型和值相同。

注意:「类型选择」只对接口类型适用,我们不能对其他类型的值来使用。

嵌入interface

Go 里面真正吸引人的是它内置的逻辑语法,就像我们在学习 Struct 时学习的匿名字段,多么的优雅啊,那么相同的逻辑引入到 interface 里面,那不是更加完美了。如果一个 interface1 作为 interface2 的一个嵌入字段,那么 interface2 隐式的包含了 interface1 里面的 method。

我们可以看到源码包container/heap里面有这样的一个定义:

type Interface interface {
sort.Interface //嵌入字段sort.Interface
Push(x interface{}) //a Push method to push elements into the heap
Pop() interface{} //a Pop elements that pops elements from the heap
}

我们看到sort.Interface其实就是嵌入字段,把sort.Interface的所有method给隐式的包含进来了。也就是下面三个方法:

type Interface interface {
// Len is the number of elements in the collection.
Len() int
// Less returns whether the element with index i should sort
// before the element with index j.
Less(i, j int) bool
// Swap swaps the elements with indexes i and j.
Swap(i, j int)
}

另一个例子就是 io 包下面的io.ReadWriter,它包含了 io 包下面的 Reader 和 Writer 两个 interface:

// io.ReadWriter
type ReadWriter interface {
Reader
Writer
}

常用接口:Stringer

fmt包中定义的Stringer是最普遍的接口之一,它类似于 Java 中的toString()方法,我们可以通过实现它来自定义在如何输出调用者。

fmt包中具体定义如下:

type Stringer interface {
String() string
}

下面我们通过实现Stringer接口,来自定义输出结构体Person的值:

type Person struct {
Name string
Age int
} func (p Person) String() string {
return fmt.Sprintf("Person: %v (%v years)", p.Name, p.Age)
} func main() {
a := Person{"Arthur Dent", 42}
z := Person{"Zaphod Beeblebrox", 9001}
fmt.Println(a)
fmt.Println(z)
}

输出结果如下:

Person: Arthur Dent (42 years)
Person: Zaphod Beeblebrox (9001 years)

常用接口:error

Go 中用error值来表示错误状态。与fmt.Stringer类似,error类型是一个内建接口:

type error interface {
Error() string
}

fmt.Stringer类似,fmt包在打印错误值时也会满足我们实现(或者默认的)error,因此我们可以通过实现Error()方法来自定义需要打印的错误信息。

那么,如何获取是否产生了错误呢?Go 中用的还是熟悉的「双赋值」.

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

  • error为 nil 时,表示执行成功,没有错误

  • 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接口,我们实现一个开方的函数,并让传入参数为负数的时候产生错误:

package main

import (
"fmt"
"math"
) type ErrNegativeSqrt float64 func (e ErrNegativeSqrt) Error() string {
// 注意:这里要 float64(e),不然会产生死循环
return fmt.Sprint(float64(e))
} func Sqrt(x float64) (float64, error) {
if x < 0 {
return 0, ErrNegativeSqrt(x)
}
return math.Sqrt(x), nil
} func main() {
fmt.Println(Sqrt(2))
fmt.Println(Sqrt(-2))
}

运行结果如下:

1.4142135623730951 <nil>
0 -2

这里需要解释一下为什么在Error()方法中不能直接打印e而要打印float64(e)

因为fmt包在输出时也会试图匹配error,e 变量通过实现Error()的接口函数成为了error类型,在fmt.Sprint(e)时就会调用e.Error()来输出错误的字符串信息,也就是下面的代码是等价的:

func (e MyError) Error() string {
return fmt.Printf(e)
}
// e 是一个 error,上面的语句实际上是这样的
// 这也就产生来死循环
func (e MyError) Error() string {
return fmt.Printf(e.Error())
}

常用接口:Reader

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

执行结果如下:

// 第一次循环
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!"
// 第三次循环,遇到 EOF 异常,表示读取完毕,结束循环
n = 0 err = EOF b = [101 97 100 101 114 33 32 82]
b[:n] = ""

我们可以通过「A tour of Go」的在线练习来练习关于 Reader 接口的使用。

下面我们实现一个Reader类型,它产生一个 ASCII 字符 'A' 的无限流。

package main

import "golang.org/x/tour/reader"

type MyReader struct{}

// TODO: 给 MyReader 添加一个 Read([]byte) (int, error) 方法

func (r MyReader) Read(b []byte) (int, error) {
// 1.填充字节切片
b[0] = 'A'
// 2.返回填充的字符数和错误值
return 1, nil
}
func main() {
reader.Validate(MyReader{})
}

常用接口:Image

image包定义了Image接口:

package 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包定义。这里可以了解更多关于image包的信息。

Go 语言入门(二)方法和接口的更多相关文章

  1. Go 语言中的方法,接口和嵌入类型

    https://studygolang.com/articles/1113 概述 在 Go 语言中,如果一个结构体和一个嵌入字段同时实现了相同的接口会发生什么呢?我们猜一下,可能有两个问题: 编译器会 ...

  2. R语言入门二

    一.R语言应知常用函数 1.getwd() 函数:获取工作目录(同eclipse设置workspace类似),直接在R软件中使用,如下图: 2.setwd(dir=”工作目录”) 函数:设置R软件RS ...

  3. go语言入门(二)

    Go 语言变量 Go 语言变量名由字母.数字.下划线组成,其中首个字母不能为数字. 声明变量的一般形式是使用 var 关键字: var identifier type 变量声明 第一种,指定变量类型, ...

  4. Go语言练习之方法,接口,并发

    多练练,有感觉了就写实际的东东. package main import ( "fmt" "math" "os" "time&qu ...

  5. JAVAEE——Mybatis第一天:入门、jdbc存在的问题、架构介绍、入门程序、Dao的开发方法、接口的动态代理方式、SqlMapConfig.xml文件说明

    1. 学习计划 第一天: 1.Mybatis的介绍 2.Mybatis的入门 a) 使用jdbc操作数据库存在的问题 b) Mybatis的架构 c) Mybatis的入门程序 3.Dao的开发方法 ...

  6. mock server 实现get方法的接口(二)

    mock server 实现get方法的接口(二) 下面是实现查询品牌的接口demo: 1.当response数据量小的时候,可以直接使用json, mock会自动设置headers为applicat ...

  7. 【Go语言入门系列】(九)写这些就是为了搞懂怎么用接口

    [Go语言入门系列]前面的文章: [Go语言入门系列](六)再探函数 [Go语言入门系列](七)如何使用Go的方法? [Go语言入门系列](八)Go语言是不是面向对象语言? 1. 引入例子 如果你使用 ...

  8. [二] java8 函数式接口详解 函数接口详解 lambda表达式 匿名函数 方法引用使用含义 函数式接口实例 如何定义函数式接口

    函数式接口详细定义 package java.lang; import java.lang.annotation.*; /** * An informative annotation type use ...

  9. 初识 go 语言:方法,接口及并发

    目录 方法,接口及并发 方法 接口 并发 信道 结束语 前言: go语言的第四篇文章,主要讲述go语言中的方法,包括指针,结构体,数组,切片,映射,函数闭包等,每个都提供了示例,可直接运行. 方法,接 ...

随机推荐

  1. Python 渗透测试编程技术方法与实践 ------全书整理

    1.整个渗透测试的工作阶段 ( 1 )前期与客户的交流阶段.( 2 )情报的收集阶段.( 3 )威胁建模阶段.( 4 )漏洞分析阶段.( 5 )漏洞利用阶段.( 6 )后渗透攻击阶段.( 7 )报告阶 ...

  2. Codeforces #364 (Div. 2) D. As Fa(数学公式推导 或者二分)

    数学推导的博客 http://codeforces.com/contest/701/problem/D  题目 推导的思路就是 : 让每个人乘车的时间相等 ,让每个人走路的时间相等. 在图上可以这么表 ...

  3. SATB的标记问题解决之道与G1垃圾收集模式系统详解及最佳实践

    继续接着上一次https://www.cnblogs.com/webor2006/p/11148282.html的理论学习,上一次学习到了这: 接着继续: SATB详解: 对于三色算法在concurr ...

  4. 模拟webpack 实现自己的打包工具

    本框架模拟webpack打包工具 详细代码个步骤请看git地址:https://github.com/jiangzhenfei/easy-webpack 创建package.json { " ...

  5. WebStorm 简单搭建NodeJs服务

    开始使用 WebStorm 搭建( WebStorm 请自行安装...... ) 在 项目 根目录 新建个 app.js 开始 编写 app,js // 引入 HTTP 模块 const http = ...

  6. Visual Studio Code IDE + Docker实现PHP Xdebug调试

    一.Docker中安装配置Xdebug 通过phpinfo()输出当前安装的PHP版本信息,将信息拷贝到https://xdebug.org/wizard.php相应输入框中,系统会自动检测并推荐合适 ...

  7. C语言入坑指南-缓冲区溢出

    前言 缓冲区溢出通常指的是向缓冲区写入了超过缓冲区所能保存的最大数据量的数据.如果说之前所提到的一些问题可能只是影响部分功能的实现,那么缓冲区溢出将可能会造成程序运行终止,被不安全代码攻击等严重问题, ...

  8. redis--基于内存的高速缓存,NoSql的典型代表

    NoSql入门和概述 入门概述 为什么要使用NoSql? 1.单机mysql的美好年代 在早些年以前,那时候网站的访问量不大,用单个数据库完全可以应付.而且那个时候,绝大部分都是LAMP架构:Linu ...

  9. java.lang.NoClassDefFoundError: org/apache/poi/ss/usermodel/Workbook] with root cause

    一.问题描述 使用POI上传excel,本地可正常运行,开发服务器上报错. 二.异常信息如下: 2019-05-05 17:00:22,349 ERROR [http-nio-8080-exec-7] ...

  10. ElementUI 之 DatePicker 日期限制范围 disabledDate

    需求: 时间选择器,只能选择 2000 年 - 至今的年份. <el-date-picker v-model="year" type="year" :pi ...