方法
Go 没有类。不过你可以为结构体类型定义方法。
方法就是一类带特殊的 接收者 参数的函数。
方法接收者在它自己的参数列表内,位于 func 关键字和方法名之间。
在此例中,Abs 方法拥有一个名为 v,类型为 Vertex 的接收者。
package main
import (
"fmt"
"math"
)
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}
fmt.Println(v.Abs())
}
其实方法可以看作一个特殊的函数,只是多了一个接收者参数罢了
下面这个代码和上面的功能实现一致
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))
}
那么是不是只有结构体类型才可以作为接收者呢,其实不然,你也可以为非结构体类型声明方法。
还要注意的是你只能为在同一包内定义的类型的接收者声明方法,而不能为其它包内定义的类型(包括 int 之类的内建类型)的接收者声明方法。
(注:就是接收者的类型定义和方法声明必须在同一包内;不能为内建类型声明方法。)
下面的例子就在包内定义了一个float64类型的接收者
package main
import (
"fmt"
"math"
)
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())
}
指针接收者
除了结构体和基础数据类型,我们还可以为指针接收者声明方法
这意味着对于某类型 T,接收者的类型可以用 *T 的文法。(但是T不能是 * int这样的)
当我们使用指针接收者的时候,我们可以改变指针指向的对象内部的元素,但是如果仅仅是值接收者的话,我们的修改操作仅仅是对对象元素副本进行的修改而已,不会影响其本身
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())
fmt.Println(v.X)
fmt.Println(v.Y)
}
// 50 30 40
// 如果把(v Vertex)里的去掉
//输出会变成 5 3 4
方法与指针重定向
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{4, 3}
p.Scale(3)
ScaleFunc(p, 8)
fmt.Println(v, p)
}
比较上述代码的Scale()方法和ScaleFunc()函数,我们可以发现其实带指针参数的函数必须接受一个指针,而以指针为接收者的方法被调用时,接收者既能为值又能为指针
1.这是因为在Go里面以指针为接收者的方法会隐式将语句 v.Scale(2) 解释为 (&v).Scale(2)。
2.值可以隐式转换为指针,那么指针是否也可以被隐式转换成值呢?——当然可以
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.Abs())
fmt.Println(AbsFunc(*p))
}
上述代码我们可以看出,p作为一个指针照样可以使用Abs()方法,但是AbsFunc()函数里的参数必须是Vertex类型,所以可以得出
1.接受一个值作为参数的函数必须接受一个指定类型的值
2.以值为接收者的方法被调用时,接收者既能为值又能为指针
这种情况下,方法调用 p.Abs() 会被解释为 (*p).Abs()
接口interface
Go语言中的接口和Java的抽象接口十分相似,里面其实就一个或者多个方法的集合,而且这些方法都没有具体的方法体。(个人理解)
package main
import (
"fmt"
"math"
)
type Abser interface {
Abs() float64
}
func main() {
var a Abser
f := MyFloat(-math.Sqrt2)
v := Vertex{3, 4}
a = f // a MyFloat 实现了 Abser
fmt.Println(a.Abs())
a = &v // a *Vertex 实现了 Abser
// 下面一行,v 是一个 Vertex(而不是 *Vertex)
// 所以没有实现 Abser。
/*
a = v
fmt.Println(a.Abs())
这两行代码会报错Vertex类型不能转换成Myfloat结构体类型
*/
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)
}
接口的隐式实现
Go里面实现接口不像Java一样需要‘implements’关键字,Go里面实现接口是不需要显式声明的,这样的隐式实现让接口的可以在任何包里面定义,是代码解耦的一种体现。
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)
}
上述代码分别有两个方法都实现了接口中的M()方法,在调用的时候会根据传入的接收者的数据类型自动寻找相同数据类型的方法去调用。
接收者值为nil的接口实现
即便接口内的具体值为 nil,方法仍然会被 nil 接收者调用。
在一些语言中,这会触发一个空指针异常,但在 Go 中通常会写一些方法来优雅地处理它(如本例中的 M 方法)。
package main
import "fmt"
type I interface {
M()
}
type T struct {
S string
}
// M()方法的实现涵盖了nil值和非nil时的两种返回结果
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, *main.T)
nil
(&{hello}, *main.T)
hello
需要记住的是,如果是nil值去直接调用接口里的方法,会产生运行时错误(空指针异常/非法内存地址)
空接口——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)
}
上面的代码其实就是泛型编程的一种体现,空接口由于实现了0个方法,所以任何类型的方法其实都相当于实现了这个接口,所以可以用来处理未知类型的方法和值
输出
(nil, nil)
(42, int)
(hello, string)
类型断言
Type Assertion(中文名叫:类型断言),通过它可以做到以下几件事情
1.检查 i 是否为 nil
2.检查 i 存储的值是否为某个类型
为了 判断 一个接口值是否保存了一个特定的类型,类型断言可返回两个值:其底层值以及一个报告断言是否成功的布尔值。
t, ok = i.(T)
如果i里面存储了T类型的数据,那么t会获取到i里面的T类型值,ok为True
否则t就会是T类型的零值,ok为false
(这种格式程序不会发生panic)
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) // 报错(panic)
fmt.Println(f)
}
类型选择
类型选择 是一种按顺序从几个类型断言中选择分支的结构。
类型选择与一般的 switch 语句相似,不过类型选择中的 case 为类型(而非具体的值), 它们针对给定接口值所存储的值的类型进行比较。
switch v := i.(type) {
case T:
// v 的类型为 T
case S:
// v 的类型为 S
default:
// 没有匹配,v 与 i 的类型相同
}
类型选择的文法和类型断言的i.(T)很像,只是把具体类型T换成了关键字type
package main
import "fmt"
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!
(如果类型对上了,v就会自动赋予相应的值)
Stringer
fmt 包中定义的 Stringer 是最普遍的接口之一。
type Stringer interface {
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)\n", p.Name, p.Age)
}
func main() {
a := Person{"Arthur Dent", 42}
z := Person{"Zaphod Beeblebrox", 9001}
fmt.Println(a, z)
}
上述例子实现(重写)了Stringer里的String()方法,在调用fmt.Println(a, z)方法的时候其实底层是调用接收者为Person类型的String()方法,输出结果如下
Arthur Dent已经工作了 (42 years)
Zaphod Beeblebrox已经工作了 (9001 years)
练习
练习:Stringer
通过让 IPAddr 类型实现 fmt.Stringer 来打印点号分隔的地址。
例如,IPAddr{1, 2, 3, 4} 应当打印为 “1.2.3.4”。
package main
import "fmt"
type IPAddr [4]byte
// TODO: 给 IPAddr 添加一个 "String() string" 方法
func (ip IPAddr) String() string{
return fmt.Sprintf("%v.%v.%v.%v",ip[0],ip[1],ip[2],ip[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)
}
}
Error
Go 程序使用 error 值来表示错误状态。
与 fmt.Stringer 类似,error 类型是一个内建接口:
type error interface {
Error() string
}
error接口只有一个方法,我们只需要实现这个方法,就可以实现error接口了,所以我们自定义error处理方法可行性还是很高的
我们可以定义一个fileError
type fileError struct{
}
func (f fileError) Error() string{
return "文件出错"
}
Go语言(golang)的错误设计——是通过返回值的方式,来强迫调用者对错误进行处理,要么你忽略,要么你处理(处理也可以是继续返回给调用者),对于golang这种设计方式,我们会在代码中写大量的if判断,以便做出决定。
i, err := strconv.Atoi("42")
if err != nil {
fmt.Printf("couldn't convert number: %v\n", err)
return
}
fmt.Println("Converted integer:", i)
err为nil的时候代表成功,不为nil代表失败,需要我们进行错误处理
自定义error接口
上面我们自定义了一个fileError实现了error接口,我们可以测试下效果
type fileError struct{
}
func (f fileError) Error() string{
return "文件出错"
}
func openFile() ([]byte,error) {
return nil,fileError{}
}
func main(){
content,err := openFile()
// 如果有err,就把错误打印,否则打印内容
if err != nil{
fmt.Println(err)
}
else{
fmt.Println(content)
}
}
// 输出是"文件出错"
如何定位error在哪里发生
因为Go语言提供的错误太简单了,以至于简单到我们无法更好的处理问题,甚至不能为我们处理错误,提供更有用的信息,所以诞生了很多对错误处理的库,github.com/pkg/errors是比较简洁的一样,并且功能非常强大,受到了大量开发者的欢迎,使用者很多。
它的使用非常简单,如果我们要新生成一个错误,可以使用New函数,生成的错误,自带调用堆栈信息。
func New(message string) error{}
如果是一个现成的error,我们可以使用3个函数对其进行包装
//只附加新的信息
func WithMessage(err error, message string) error
//只附加调用堆栈信息
func WithStack(err error) error
//同时附加堆栈和信息
func Wrap(err error, message string) error
其实上面的包装,很类似于Java的异常包装,被包装的error,其实就是Cause,在前面的章节提到错误的根本原因,就是这个Cause。所以这个错误处理库为我们提供了Cause函数让我们可以获得最根本的错误原因。
func Cause(err error) error {
type causer interface {
Cause() error
}
for err != nil {
cause, ok := err.(causer)
// 当ok为true时,表示一直可以找到error,一直调用Cause方法
// 当ok为false时,表示当前error已经是最底层的了,结束循环
if !ok {
break
}
err = cause.Cause()
}
return err
}
我们使用for循环一直找到最底层的error,找到error后使用fmt.Printf()输出即可