6. 让 iota 从 a +1 开始增量 在前面的例子中同时也产生了一个我已经遇到过许多次的 bug。假设你有一个新的结构体,有一个State 字段: 1 2 3 4 5 | type T struct {
Name string
Port int
State State
}
|
现在如果基于 T 创建一个新的变量,然后输出,你会得到奇怪的结果(http://play.golang.org/p/LPG2RF3y39): 1 2 3 4 5 6 | func main() {
t := T{Name: "example" , Port: 6666 }
fmt.Printf( "t %+v\n" , t)
}
|
看到 bug 了吗?State 字段没有初始化,Go 默认使用对应类型的零值进行填充。由于State 是一个整数,零值也就是0 ,但在我们的例子中它表示Running 。 那么如何知道 State 被初始化了?还是它真得是在Running 模式?没有办法区分它们,那么这就会产生未知的、不可预测的 bug。不过,修复这个很容易,只要让 iota 从 +1 开始(http://play.golang.org/p/VyAq-3OItv): 1 2 3 4 5 6 | const (
Running State = iota + 1
Stopped
Rebooting
Terminated
)
|
现在t 变量将默认输出Unknown ,不是吗? : 1 2 3 4 5 6 | func main() {
t := T{Name: "example" , Port: 6666 }
fmt.Printf( "t %+v\n" , t)
}
|
不过让 iota 从零值开始也是一种解决办法。例如,你可以引入一个新的状态叫做Unknown ,将其修改为: 1 2 3 4 5 6 7 | const (
Unknown State = iota
Running
Stopped
Rebooting
Terminated
)
|
7. 返回函数调用 我已经看过很多代码例如(http://play.golang.org/p/8Rz1EJwFTZ): 1 2 3 4 5 6 7 8 | func bar() (string, error) {
v, err := foo()
if err != nil {
return "" , err
}
return v, nil
}
|
然而,你只需要: 1 2 3 | func bar() (string, error) {
return foo()
}
|
更简单也更容易阅读(当然,除非你要对某些内部的值做一些记录)。
8. 把 slice、map 等定义为自定义类型 将 slice 或 map 定义成自定义类型可以让代码维护起来更加容易。假设有一个Server 类型和一个返回服务器列表的函数: 1 2 3 4 5 6 7 8 9 10 11 12 | type Server struct {
Name string
}
func ListServers() []Server {
return []Server{
{Name: "Server1" },
{Name: "Server2" },
{Name: "Foo1" },
{Name: "Foo2" },
}
}
|
现在假设需要获取某些特定名字的服务器。需要对 ListServers() 做一些改动,增加筛选条件: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | func ListServers(name string) []Server {
servers := []Server{
{Name: "Server1" },
{Name: "Server2" },
{Name: "Foo1" },
{Name: "Foo2" },
}
if name == "" {
return servers
}
filtered := make([]Server, 0 )
for _, server := range servers {
if strings.Contains(server.Name, name) {
filtered = append(filtered, server)
}
}
return filtered
}
|
现在可以用这个来筛选有字符串Foo 的服务器: 1 2 3 4 5 6 | func main() {
servers := ListServers( "Foo" )
fmt.Printf( "servers %+v\n" , servers)
}
|
显然这个函数能够正常工作。不过它的弹性并不好。如果你想对服务器集合引入其他逻辑的话会如何呢?例如检查所有服务器的状态,为每个服务器创建一个数据库记录,用其他字段进行筛选等等…… 现在引入一个叫做Servers 的新类型,并且修改原始版本的 ListServers() 返回这个新类型: 1 2 3 4 5 6 7 8 9 10 11 | type Servers []Server
func ListServers() Servers {
return []Server{
{Name: "Server1" },
{Name: "Server2" },
{Name: "Foo1" },
{Name: "Foo2" },
}
}
|
现在需要做的是只要为Servers 类型添加一个新的Filter() 方法: 1 2 3 4 5 6 7 8 9 10 11 12 13 | func (s Servers) Filter(name string) Servers {
filtered := make(Servers, 0 )
for _, server := range s {
if strings.Contains(server.Name, name) {
filtered = append(filtered, server)
}
}
return filtered
}
|
现在可以针对字符串Foo 筛选服务器: 1 2 3 4 5 | func main() {
servers := ListServers()
servers = servers.Filter( "Foo" )
fmt.Printf( "servers %+v\n" , servers)
}
|
哈!看到你的代码是多么的简单了吗?还想对服务器的状态进行检查?或者为每个服务器添加一条数据库记录?没问题,添加以下新方法即可: 1 2 3 4 | func (s Servers) Check()
func (s Servers) AddRecord()
func (s Servers) Len()
...
|
9. withContext 封装函数 有时对于函数会有一些重复劳动,例如锁/解锁,初始化一个新的局部上下文,准备初始化变量等等……这里有一个例子: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | func foo() {
mu.Lock()
defer mu.Unlock()
}
func bar() {
mu.Lock()
defer mu.Unlock()
}
func qux() {
mu.Lock()
defer mu.Unlock()
}
|
如果你想要修改某个内容,你需要对所有的都进行修改。如果它是一个常见的任务,那么最好创建一个叫做withContext 的函数。这个函数的输入参数是另一个函数,并用调用者提供的上下文来调用它: 1 2 3 4 5 6 | func withLockContext(fn func()) {
mu.Lock
defer mu.Unlock()
fn()
}
|
只需要将之前的函数用这个进行封装: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | func foo() {
withLockContext(func() {
})
}
func bar() {
withLockContext(func() {
})
}
func qux() {
withLockContext(func() {
})
}
|
不要光想着加锁的情形。对此来说最好的用例是数据库链接。现在对 withContext 函数作一些小小的改动: 1 2 3 4 5 6 | func withDBContext(fn func(db DB) error) error {
dbConn := NewDB()
return fn(dbConn)
}
|
如你所见,它获取一个连接,然后传递给提供的参数,并且在调用函数的时候返回错误。你需要做的只是: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | func foo() {
withDBContext(func(db *DB) error {
})
}
func bar() {
withDBContext(func(db *DB) error {
})
}
func qux() {
withDBContext(func(db *DB) error {
})
}
|
你在考虑一个不同的场景,例如作一些预初始化?没问题,只需要将它们加到withDBContext 就可以了。这对于测试也同样有效。 这个方法有个缺陷,它增加了缩进并且更难阅读。再次提示,永远寻找最简单的解决方案。
10. 为访问 map 增加 setter,getters 如果你重度使用 map 读写数据,那么就为其添加 getter 和 setter 吧。通过 getter 和 setter 你可以将逻辑封分别装到函数里。这里最常见的错误就是并发访问。如果你在某个 goroutein 里有这样的代码: 还有这个: 会发生什么?你们中的大多数应当已经非常熟悉这样的竞态了。简单来说这个竞态是由于 map 默认并非线程安全。不过你可以用互斥量来保护它们: 1 2 3 | mu.Lock()
m[ "foo" ] = "bar"
mu.Unlock()
|
以及: 1 2 3 | mu.Lock()
delete (m, "foo" )
mu.Unlock()
|
假设你在其他地方也使用这个 map。你必须把互斥量放得到处都是!然而通过 getter 和 setter 函数就可以很容易的避免这个问题: 1 2 3 4 5 6 7 8 9 10 | func Put(key, value string) {
mu.Lock()
m[key] = value
mu.Unlock()
}
func Delete(key string) {
mu.Lock()
delete (m, key)
mu.Unlock()
}
|
使用接口可以对这一过程做进一步的改进。你可以将实现完全隐藏起来。只使用一个简单的、设计良好的接口,然后让包的用户使用它们: 1 2 3 4 5 | type Storage interface {
Delete(key string)
Get(key string) string
Put(key, value string)
}
|
这只是个例子,不过你应该能体会到。对于底层的实现使用什么都没关系。不光是使用接口本身很简单,而且还解决了暴露内部数据结构带来的大量的问题。 但是得承认,有时只是为了同时对若干个变量加锁就使用接口会有些过分。理解你的程序,并且在你需要的时候使用这些改进。
总结 抽象永远都不是容易的事情。有时,最简单的就是你已经实现的方法。要知道,不要让你的代码看起来很聪明。Go 天生就是个简单的语言,在大多数情况下只会有一种方法来作某事。简单是力量的源泉,也是为什么在人的层面它表现的如此有弹性。
如果必要的话,使用这些基数。例如将[]Server 转化为Servers 是另一种抽象,仅在你有一个合理的理由的情况下这么做。不过有一些技术,如 iota 从 1 开始计数总是有用的。再次提醒,永远保持简单。
特别感谢 Cihangir Savas、Andrew Gerrand、Ben Johnson 和 Damian Gryski 提供的极具价值的反馈和建议。 原文出处: Fatih Arslan. 译文出处:mikespook |