Pointers & errors
文章目录
指针 Pointer
In the previous example we accessed fields directly with the field name, however in our very secure wallet we don’t want to expose our inner state to the rest of the world. We want to control access via methods.通过method来获取类里面的某个属性值可以有效地保护数据隐私。
In Go if a symbol (variables, types, functions et al) starts with a lowercase symbol then it is private outside the package it’s defined in.钱包里的数据这种field就应该使用小写字母开头。而各种操作method需要被外部调用,首字母应该大写。
type Wallet struct {
balance int
}
func (w Wallet) Deposit(amount int) {
w.balance += amount
}
func (w Wallet) Balance() int {
return w.balance
}
问题引入
这时候发生了一些问题:
func TestWallet(t *testing.T) {
wallet := Wallet{}
wallet.Deposit(10)
got := wallet.Balance()
want := 10
if got != want {
t.Errorf("got %d want %d", got, want)
}
}
然而运行go test
,会发现报错:
wallet_test.go:15: got 0 want 10
我们明明有使用Deposit
方法添加了10个bitcoin,为什么得到的balance仍然是0?
In Go, when you call a function or a method the arguments are copied. 也就是说当我们调用func (w Wallet) Deposit(amount int)
,也就是当我们call Deposit
from w
时,被传进去的这个w
是“真正”的w
的一份拷贝。要想验证这一点很简单,在test文件和Deposit
方法里分别打印wallet
的地址就会发现这两个地址不一样。So when we change the value of the balance inside the code, we are working on a copy of what came from the test. Therefore the balance in the test is unchanged.
You can find out what the address of that bit of memory with &myVal
. 一般这时候使用%v
作为格式化占位符。
解决问题
更改Deposit方法,改为传入Wallet
类型的地址(指针pointer)
func (w *Wallet) Deposit(amount int) {
w.balance += amount
}
func (w *Wallet) Balance() int {
return w.balance
}
隐式dereference
这样修改以后会发现test通过。但是有一个奇怪的事情,那就是按照道理,我们在Deposit
的函数体里面也需要dereference w
这个pointer从而得到它指向的具体value,像下面这样:
func (w *Wallet) Deposit(amount int) {
(*w).balance += amount
}
我们产生这样的疑问基于Go by Example中有这样一个例子:
func zeroptr(iptr *int) {
*iptr = 0
//通过指针dereference到iptr所指向的具体int变量,从而改变变量的值为0
}
zeroptr
has an *int
parameter, meaning that it takes an int pointer. The *iptr
code in the function body then dereferences the pointer from its memory address to the current value at that address. Assigning a value to a dereferenced pointer changes the value at the referenced address.
实际上我们把代码改成我们认为应该的样子的话运行结果也是完全正确,但是Go的开发者认为这样写有点笨拙,所以允许我们只写w.balance
,毕竟我们已经在传递参数列表里面明确了传进来的是一个指针,计算机也应该心领神会XD,而不需要显式地dereference。These pointers to structs even have their own name: struct pointers and they are automatically dereferenced.
另外Balance
方法实际上是不需要传递地址的,因为不需要改变wallet内部的变量值。但是习惯上you should keep your method receiver types the same for consistency.
类型重定义
Go lets you create new types from existing ones. 类型重定义的语法是type MyName OriginalType
type Bitcoin int
type Wallet struct {
balance Bitcoin
}
func (w *Wallet) Deposit(amount Bitcoin) {
w.balance += amount
}
func (w *Wallet) Balance() Bitcoin {
return w.balance
}
func TestWallet(t *testing.T) {
wallet := Wallet{}
//To make Bitcoin you just use the syntax Bitcoin(999).
wallet.Deposit(Bitcoin(10))
got := wallet.Balance()
want := Bitcoin(10)
if got != want {
t.Errorf("got %d want %d", got, want)
}
}
Stringer
接口
当我们重新定义了一个类型之后,可以用它作为receiver实现fmt
package的一个Stringer
接口,它可以让你定义使用格式化字符串%s
如何打印你的类型。
Stringer
接口:
type Stringer interface {
String() string
}
为Bitcoin
实现上述接口:
func (b Bitcoin) String() string {
return fmt.Sprintf("%d BTC", b)
}
//可以发现为重定义类型创建method的语法和为结构体创建method一样
//接下来我们就可以把test里的%d改为%s了,这样错误信息中就会输出"got 10 BTC, want 20 BTC"这样的错误
Error
在go中,如果你想要表示出现了error,习惯上你的函数应该返回一个err
给调用者检查并act on。目前我们就碰到了相应的情况——取出Bitcoin的Withdraw
方法,需要在取出额度大于存款额度时给出错误信息,所以我们需要让Withdraw
方法返回一个error,然后我们在调用它以后需要check这个error是否为nil
。
nil
is synonymous with null
from other programming languages. Errors can be nil
because the return type of Withdraw
will be error
, which is an interface. If you see a function that takes arguments or returns values that are interfaces, they can be nillable. 和null
一样,值为nil
的变量无法被access出来,会发生runtime panic。
具体操作:
- import
errors
into your code. errors.New
creates a newerror
with a message of your choosing.
func (w *Wallet) Withdraw(amount Bitcoin) error {
if amount > w.balance {
return errors.New("oh no")
}
w.balance -= amount
return nil
}
assertError := func(t testing.TB, got error, want string) {
t.Helper()
if got == nil {
//We've introduced t.Fatal which will stop the test if it is called.Without this the test would carry on to the next step and panic because of a nil pointer.
t.Fatal("didn't get an error but wanted one")
}
if got.Error() != want {
t.Errorf("got %q, want %q", got, want)
}
}
t.Run("withdraw insufficient funds", func(t *testing.T) {
startingBalance := Bitcoin(20)
wallet := Wallet{startingBalance}
err := wallet.Withdraw(Bitcoin(100))
assertError(t, err, "cannot withdraw, insufficient funds")
assertBalance(t, wallet, startingBalance)
})
还可以改进的是直接定义global变量,避免过多重复很长的字符串:
var ErrInsufficientFunds = errors.New("cannot withdraw, insufficient funds")
总结
Pointers
- Go在你传入一个变量进函数或者方法时会进行copy, 所以如果你想写一个让变量突变的函数那就需要使用指向想要操作的变量的pointer。
- The fact that Go takes a copy of values is useful a lot of the time but sometimes you won’t want your system to make a copy of something, in which case you need to pass a reference. Examples include referencing very large data structures or things where only one instance is necessary (like database connection pools).
nil
- Pointers can be nil
- When a function returns a pointer to something, you need to make sure you check if it’s nil or you might raise a runtime exception - the compiler won’t help you here.
- Useful for when you want to describe a value that could be missing
Errors
- Errors are the way to signify failure when calling a function/method.
- By listening to our tests we concluded that checking for a string in an error would result in a flaky test. So we refactored our implementation to use a meaningful value instead and this resulted in easier to test code and concluded this would be easier for users of our API too.
- This is not the end of the story with error handling, you can do more sophisticated things but this is just an intro. Later sections will cover more strategies.
- Don’t just check errors, handle them gracefully
类型重定义
- Useful for adding more domain specific meaning to values
- Can let you implement interfaces