learn Go with tests学习笔记(6)——Pointers & errors

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实现fmtpackage的一个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

具体操作:

  1. import errors into your code.
  2. errors.New creates a new error 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
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

洞爷湖dyh

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值