CrazyAirhead

疯狂的傻瓜,傻瓜也疯狂——傻方能执著,疯狂才专注!

0%

背景

公司使用Hbase indexer做二级索引,最近在做数据统计时发现,数据存在有缺失的情况。在网上查找时发现可能是Hbase Indexer的一个BUG,详情看这里。大意是说修改read-row=”never”或者修改源码。我们使用的组件不是完全开源的Hbase Indexer,有被提供商做了部分调整,为了保险起见,自己还是做了一次测试。同时也是进一步了解Hbase和Habse Indexer的机会。

验证方案

  1. 把ES的qc索引做为数据来源,数据量比较大
  2. 写测试程序从ES拉数据,100万条。
  3. 调整不同的read-row方式
  4. 修改程序验证部分更新。

    验证准备

    创建Hbase表

    1
    create 'qc',{NAME =>'d', REPLICATION_SCOPE =>1}
    此处需要,注意设置REPLICATION_SCOPE为1,第一次验证时未开启。如果未开启的情况,可以进行如下操作:
    1
    2
    3
    disable 'qc'
    alter 'qc',{NAME =>'d', REPLICATION_SCOPE =>1}
    enable 'qc'

    配置Hbase Indexer

    之前已经有配置,拷贝一份及可
    1
    2
    3
    cd  /opt/morphline_config
    cp -a xyz.xml qc.xml
    cp -a xyz.conf qc.conf

修改qc.xml

1
2
3
4
5
<indexer table="qc" unique-key-field="rowkey"
unique-key-formatter="com.ngdata.hbaseindexer.uniquekey.StringUniqueKeyFormatter"
mapper="com.ngdata.hbaseindexer.morphline.MorphlineResultToSolrMapper" read-row="never">
<param name="morphlineFile" value="/opt/morphline_config/qc.conf" />
</indexer>

修改qc.conf

可以把文件下载回来修改

1
sz qc.conf

通过’rz’可以上传修改后的配置文件。

调整后的qc.conf

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
morphlines : [
{
id : morphline1
importCommands : ["org.kitesdk.morphline.**", "com.ngdata.**"]

commands : [
{
extractHBaseCells {
mappings : [
{
inputColumn : "d:_id"
outputField : "_id"
type : string
isAllowEmpty : true
source : value
},
{
inputColumn : "d:code"
outputField : "code"
type : string
isAllowEmpty : true
source : value
},
{
inputColumn : "d:errorCode"
outputField : "errorCode"
type : string
isAllowEmpty : true
source : value
},
{
inputColumn : "d:errorMsg"
outputField : "errorMsg"
type : string
isAllowEmpty : true
source : value
},
{
inputColumn : "d:eventNo"
outputField : "eventNo"
type : string
isAllowEmpty : true
source : value
},
{
inputColumn : "d:eventTime"
outputField : "eventTime"
type : string
isAllowEmpty : true
source : value
},
{
inputColumn : "d:orgCode"
outputField : "orgCode"
type : string
isAllowEmpty : true
source : value
},
{
inputColumn : "d:patientId"
outputField : "patientId"
type : string
isAllowEmpty : true
source : value
},
{
inputColumn : "d:receiveTime"
outputField : "receiveTime"
type : string
isAllowEmpty : true
source : value
},
{
inputColumn : "d:rowKey"
outputField : "rowKey"
type : string
isAllowEmpty : true
source : value
},
{
inputColumn : "d:table"
outputField : "table"
type : string
isAllowEmpty : true
source : value
},
{
inputColumn : "d:value"
outputField : "value"
type : string
isAllowEmpty : true
source : value
},
{
inputColumn : "d:version"
outputField : "version"
type : string
isAllowEmpty : true
source : value
}
]
}
}

{ logTrace { format : "output record: {}", args : ["@{}"] } }
]
}
]

添加映射

1
hbase-indexer add-indexer -c qc.xml -n qc -z node1,node3,node2  -cp solr.zk=node1:2181,node3:2181,node2:2181/solr -cp solr.collection=qc

检查配置是否生效

1
hbase-indexer list-indexers -dump

删除映射

如果配置没有生效的情况,最好先删掉映射后重新添加。

1
hbase-indexer delete-indexer --name 'qc'

重新拉取Hbase数据

1
nohup hadoop jar /opt/hbase-indexer/latest/tools/hbase-indexer-mr-1.6-ngdata-job.jar  --conf /etc/hbase/conf/hbase-site.xml -D 'mapred.child.java.opts=-Xmx500m' --hbase-indexer-file /opt/morphline_config/qc.xml --zk-host node1/solr --collection qc  --reduce 0 &

配置Solr

修改配置

之前有配置的,可直接拷贝一份开始配置

1
2
cd /root
cp -a xyz qc

修改后的scheme.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xmlversion="1.0"encoding="UTF-8"standalone="no"?>
<schemaname="qc"version="1.5">
<field indexed="true" name="_version_" stored="true" type="long"/>
<field indexed="true"name="_root_"stored="false" type="string"/>
<field indexed="true"multiValued="false"name="_id"stored="true" type="string"/>
<field indexed="true" multiValued="false" name="rowkey" stored="true" type="string"/>
<field indexed="true" name="code" stored="false" type="string"/>
<field indexed="true" name="errorCode" stored="true" type="string"/>
<field indexed="true" name="errorMsg" stored="true" type="string"/>
<field indexed="true" name="eventNo" stored="true" type="string"/>
<field indexed="true" name="eventTime" stored="true" type="string"/>
<field indexed="true" name="orgCode" stored="true" type="string"/>
<field indexed="true" name="patientId" stored="true" type="string"/>
<field indexed="true" name="receiveTime" stored="false" type="string"/>
<field indexed="true" name="rowKey" stored="true" type="string"/>
<field indexed="true" name="table" stored="true" type="string"/>
<field indexed="true" name="value" stored="true" type="string"/>
<field indexed="true" name="version" stored="true" type="string"/>
<uniqueKey>rowkey</uniqueKey>
</schema>

此处注意配置rowkey字段,之前一就因为rowkey没有导致创建索引失败

上传配置

1
/opt/solr/latest/server/scripts/cloud-scripts/zkcli.sh -zkhost node1:2181/solr -cmd upconfig --confdir /root/qc/conf/ --confname qc

创建索引

1
/opt/solr/latest/bin/solr create_collection -c qc -d /root/qc/conf/ -n qc

修改ES配置

拉取测试数据时,提示只有10000的窗口数据,需要设计max_result_window,修改方法如下:

1
2
3
4
curl -XPOST 'http://xx:9200/qc/_close'
curl -XPUT 'http://xx:9200/qc/_settings?preserve_existing=true' -d '{"max_result_window" : "1000000"}'
curl -XGET 'http://xx:9200/qc/_settings?preserve_existing=true'
curl -XPOST 'http://xx:9200/qc/_open'

测试

read-row为never

  1. 测试数据为100万条,全量数据更新。
    数据测试,导入数据时出现服务连接问题,中间出现Hbase Indexer异常停止,重启后,数据能对上。
  2. 测试数据为100万条,部分数据更新。
    数据字段出现丢失情况,
  3. 验证配置字段顺序问题。

    清理Hbase数据

    1
    truncate 'qc'

    清理Solr数据

    1
    2
    hdfs dfs -rm -r /solr/qc
    hdfs dfs -ls /solr
    验证的情况,与配置文件的顺序无关

验证重跑MapRedurce

删除solr中qc的记录

1
2
<delete><query>*:*</query></delete>
<commit/>

重跑

1
nohup hadoop jar /opt/hbase-indexer/latest/tools/hbase-indexer-mr-1.6-ngdata-job.jar  --conf /etc/hbase/conf/hbase-site.xml -D 'mapred.child.java.opts=-Xmx500m' --hbase-indexer-file /opt/morphline_config/qc.xml --zk-host node1/solr --collection qc  --reduce 0 &

测试时发现可以正常拉取数据,正式线发现不能拉取到之前遗漏的数据。

解决方案

  1. 修改read-row为never
  2. 重新检查写入Hbase相关代码,确保数据是整条记录更新(即需要合并旧数据的方式进行更新)

注意事项,采用read-row为never时,只会从WAL中获取数据去更新Solr,也就是说如果,数据只更新部分,Solr也只会有最后更新的那部分数据。
3. 通过写程序将缺失数据提取出来重新更新Hbase,该工作已让郑维协助处理,保持跟进。

参考链接

Lily HBase Indexer同步HBase二级索引到Solr丢失数据的问题分析

使用Notepad++有挺长一段时间了。因为打开速度快,经常会用它来编绎有一些小文本,或者文件的格式化。在一次升级后发现Json Viwer插件不见了。
当时也没太在意,换了一台电脑用了。最近因为查问题需要格式化的JSON也多了起来,就想查查到底是怎么回事,顺便记录下处理方式。

网上一搜还有不少碰到这个问题的人notepad++64位添加Plugin Manager。大意是64位已经不提供plugin manager了。可以通过https://github.com/bruderstein/nppPluginManager/releases下载Plugin Manager。下载之后,用覆盖的方式粘贴plugins和updater文件夹,重新启动就可以。

启动之后提示,32位Notepad++不能运行64位插件。索性重新下载了个新版本的Notepad++64位,plugin manager已经改名为Plugin Admin了。下载我自己需要的插件,一切正常。

第六章 - 并发(Chapter 6 - Concurrency)

Go is often described as a concurrent-friendly language. The reason for this is that it provides a simple syntax over two powerful mechanisms: goroutines and channels.

Go常被描述为是一种适用于并发的语言。是因为它在两个强大的机制提供了简法的语法支持:go协程通道

Go协程(Goroutines)

A goroutine is similar to a thread, but it is scheduled by Go, not the OS. Code that runs in a goroutine can run concurrently with other code. Let’s look at an example:

一个Go协程和一个线程类似,只不这它是由Go,而不是系统来调度的。在协程中的代码可以和其他代码并发执行。让我们看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import (
"fmt"
"time"
)

func main() {
fmt.Println("start")
go process()
time.Sleep(time.Millisecond * 10) // this is bad, don't do this! 这样不好,不能这么做!
fmt.Println("done")
}

func process() {
fmt.Println("processing")
}

There are a few interesting things going on here, but the most important is how we start a goroutine. We simply use the go keyword followed by the function we want to execute. If we just want to run a bit of code, such as the above, we can use an anonymous function. Do note that anonymous functions aren’t only used with goroutines, however.

这里有几个有趣的地方,但最重要的是我们如何开启一个Go协程。我们只是简单的使用了go关键字后紧跟我们需的执行的函数。如果我们只是要运行一小段代码,比如上面的例子,我们可以使用匿名函数。但是记住,匿名函数不只适用于Go协程。

1
2
3
go func() {
fmt.Println("processing")
}()

Goroutines are easy to create and have little overhead. Multiple goroutines will end up running on the same underlying OS thread. This is often called an M:N threading model because we have M application threads (goroutines) running on N OS threads. The result is that a goroutine has a fraction of overhead (a few KB) than OS threads. On modern hardware, it’s possible to have millions of goroutines.

Go协程创建简单和开销小。多个Go协程最终会运行在一个系统线程中。这通常称为M:N线程模型,因为我们有M个应用线程(Go协程)运行在N个系统线程上。结果就是,一个Go协程的开销比系统线程小(一般都是几KB)。在现代的硬件上,有可能创建成千上万个Go协程。

Furthermore, the complexity of mapping and scheduling is hidden. We just say this code should run concurrently and let Go worry about making it happen.

此外,因为隐藏了映射和调度的复杂性。我们只需要说这段代码需要并发执行,然后让Go自己来运行它。

If we go back to our example, you’ll notice that we had to Sleep for a few milliseconds. That’s because the main process exits before the goroutine gets a chance to execute (the process doesn’t wait until all goroutines are finished before exiting). To solve this, we need to coordinate our code.

回到我们的例子中,你将会注意到我们使用了Sleep让程序等待了几毫秒。这是让主进程在退出前有机会去执行协程(主进程退出时不会等待所有协程都执行结束)。为了解决这个问题,我们必须让代码协同。

同步(Synchronization)

Creating goroutines is trivial, and they are so cheap that we can start many; however, concurrent code needs to be coordinated. To help with this problem, Go provides channels. Before we look at channels, I think it’s important to understand a little bit about the basics of concurrent programming.

创建Go协程是容易的,而且他们的开销很小,所以我们可以开启很多Go协程;但是并发代码需要协同。为了帮助我们解决这个问题,Go提供了通道。在我们继续通道之前,我觉得有必要先了解一些并发编程的基础知识。

Writing concurrent code requires that you pay specific attention to where and how you read and write values. In some ways, it’s like programming without a garbage collector – it requires that you think about your data from a new angle, always watchful for possible danger. Consider:

在编写并发执行的代码时,你需要特别的注意在哪里和如何读写一个值。出于某些原因,例如没有垃圾回收的语言,需要你从一个新的角度去考虑你的数据,总是警惕着可能存在的危险。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"fmt"
"time"
)

var counter = 0

func main() {
for i := 0; i < 2; i++ {
go incr()
}
time.Sleep(time.Millisecond * 10)
}

func incr() {
counter++
fmt.Println(counter)
}

What do you think the output will be?

你觉得输出的会是什么呢?

If you think the output is 1, 2 you’re both right and wrong. It’s true that if you run the above code, you’ll very likely get that output. However, the reality is that the behavior is undefined. Why? Because we potentially have multiple (two in this case) goroutines writing to the same variable, counter, at the same time. Or, just as bad, one goroutine would be reading counter while another writes to it.

如果你觉得输出是12,不能说你对或者错。如果你运行上面的代码,你很有可能得到那样的输出。但是,实际上这个输出是不确定的。为什么?因为我们可能有多个(这里是2个)Go协程同时写同一个变量counter。或者更糟的情况是一个协程正在读counter,而另一个协程正在写counter

Is that really a danger? Yes, absolutely. counter++ might seem like a simple line of code, but it actually gets broken down into multiple assembly statements – the exact nature is dependent on the platform that you’re running. It’s true that, in this example, the most likely case is things will run just fine. However, another possible outcome would be that they both see counter when its equal to 0 and you get an output of 1, 1. There are worse possibilities, such as system crashes or accessing an arbitrary piece of data and incrementing it!

这很危险吗?是的,绝对的。counter++似乎看起来只是一行简单的代码,但是实际上它被拆分为很多汇编指令————具体依赖于你运行的软件和硬件平台。是的,在上面的例子中,确实在大多数情况下运行良好。但是,其他一些平台可能的输出结果是1, 1,因为两个协程看到的counter都是0。还有更糟的情况是,比如系统崩溃或者访问到一个随机值并递增它。

The only concurrent thing you can safely do to a variable is to read from it. You can have as many readers as you want, but writes need to be synchronized. There are various ways to do this, including using some truly atomic operations that rely on special CPU instructions. However, the most common approach is to use a mutex:

在并发编程中维一安全的事情就是读一个变量。无论你想读多少次都可以,但是写变量时必须是同步的。有几种方式来实现,包括一些在特定CPU架构上真正的原子操作。但是,最常见的方式就是用互斥锁:

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
26
package main

import (
"fmt"
"time"
"sync"
)

var (
counter = 0
lock sync.Mutex
)

func main() {
for i := 0; i < 2; i++ {
go incr()
}
time.Sleep(time.Millisecond * 10)
}

func incr() {
lock.Lock()
defer lock.Unlock()
counter++
fmt.Println(counter)
}

A mutex serializes access to the code under lock. The reason we simply define our lock as lock sync.Mutex is because the default value of a sync.Mutex is unlocked.

互斥锁会顺序化有锁的代码的访问。因为sync.Mutex默认值是未锁状态,所以我们简单的定义了一个锁lock sync.Mutex

Seems simple enough? The example above is deceptive. There’s a whole class of serious bugs that can arise when doing concurrent programming. First of all, it isn’t always so obvious what code needs to be protected. While it might be tempting to use coarse locks (locks that cover a large amount of code), that undermines the very reason we’re doing concurrent programming in the first place. We generally want fine locks; else, we end up with a ten-lane highway that suddenly turns into a one-lane road.

看起来足够简单?上面的例子有欺骗性。在并发编程时,会碰到一系列很严重的bug。首先,那些需要被保护代码通常都不是这么明显。虽然它可能是想使用一个粗锁(涵盖了大量代码的锁),但这破坏了并发编程首要原则。我们需要适度的锁,或者说,我们最终由一个10快车道的突然转变成一个单车道。

The other problem has to do with deadlocks. With a single lock, this isn’t a problem, but if you’re using two or more locks around the same code, it’s dangerously easy to have situations where goroutineA holds lockA but needs access to lockB, while goroutineB holds lockB but needs access to lockA.

另一个问题是如何处理死锁。只有一个锁的时候,这不是问题,但是如果你在相同的代码中使用2个或者更多的锁,就很容易出现一种危险的情况,即协程A拥有锁lockA,想去访问锁lockB,同时协程B拥有lockB并需要访问锁lockA

It actually is possible to deadlock with a single lock, if we forget to release it. This isn’t as dangerous as a multi-lock deadlock (because those are really tough to spot), but just so you can see what happens, try running:

实际上使用一个锁也有可能发生死锁,如果我们忘记释放它时。但是这和多个锁引起的死锁为比起来,危害性不大(因为这真的很少出现),但只是想让你看会发生什么,试试下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import (
"time"
"sync"
)

var (
lock sync.Mutex
)

func main() {
go func() { lock.Lock() }()
time.Sleep(time.Millisecond * 10)
lock.Lock()
}

There’s more to concurrent programming than what we’ve seen so far. For one thing, there’s another common mutex called a read-write mutex. This exposes two locking functions: one to lock for reading and one to lock for writing. This distinction allows multiple simultaneous readers while ensuring that writing is exclusive. In Go, sync.RWMutex is such a lock. In addition to the Lock and Unlock methods of a sync.Mutex, it also exposes RLock and RUnlock methods; where R stands for Read. While read-write mutexes are commonly used, they place an additional burden on developers: we must now pay attention to not only when we’re accessing data, but also how.

接下来我们会介绍更多的并发编程。一方面,另一个常见的互斥锁叫读写互斥锁。它主要提供2中锁功能:一个读锁定和一个写锁定。在Go中,sync.RWMutex就是这种锁。另外sync.Mutex结构不但提供了LockUnlock方法,也提供了RLockRLock方法,这里的R代表。虽然读写锁很常用,但是他们也给开发者带来一些额外的负担:我们不但要关注我们何时访问数据,而且也要关注如何访问。

Furthermore, part of concurrent programming isn’t so much about serializing access across the narrowest possible piece of code; it’s also about coordinating multiple goroutines. For example, sleeping for 10 milliseconds isn’t a particularly elegant solution. What if a goroutine takes more than 10 milliseconds? What if it takes less and we’re just wasting cycles? Also, what if instead of just waiting for goroutines to finish, we want to tell one hey, I have new data for you to process!?

此外,部分并发编程不只是通过为数不多代码按顺序的访问变量,也需要协调多个go协程。例如,休眠10毫秒不是一种优雅的方法。如果一个Go协程运行的时间超过10毫秒呢?如果Go协程运行时间少于10毫秒,我们只是浪费了cpu?又或者可以等待Go协程运行完毕,我们告诉另外一个Go协程嗨,我有一些新数据给你处理?

These are all things that are doable without channels. Certainly for simpler cases, I believe you should use primitives such as sync.Mutex and sync.RWMutex, but as we’ll see in the next section, channels aim at making concurrent programming cleaner and less error-prone.

所有的这些事在不使用通道的情况下也都是可以实现的。当然,对于更简单的例子,我认为你应该使用基本的功能例如sync.Mutexsync.RWMutex,但是在下一节我们将看到,通道的目的是为了使并发编程更清晰和不易出错。

通道(Channels)

The challenge with concurrent programming stems from sharing data. If your goroutines share no data, you needn’t worry about synchronizing them. That isn’t an option for all systems, however. In fact, many systems are built with the exact opposite goal in mind: to share data across multiple requests. An in-memory cache or a database, are good examples of this. This is becoming an increasingly common reality.

并发编程的最在挑战来自共享数据。如果你的Go协程没有共享数据,你不需要担心他们之间的同步。但是这不是所有系统的选择。事实上,许多系统的构建就是为了:在多个请求中共享数据。内存缓存或者数据库,都是很好的例子。这也成为越来越普遍的事实。

Channels help make concurrent programming saner by taking shared data out of the picture. A channel is a communication pipe between goroutines which is used to pass data. In other words, a goroutine that has data can pass it to another goroutine via a channel. The result is that, at any point in time, only one goroutine has access to the data.

通过共享数据规划,通道使并发编程更清晰。一个通道是一个通信管道用于Go协程之间的数据传递。换一句话来说。一个Go协程可以通过通道来把数据传递给另一个Go协程。这样做的结果就是,无论什么时间节点,都只有一个Go协程可以访问共享数据。

A channel, like everything else, has a type. This is the type of data that we’ll be passing through our channel. For example, to create a channel which can be used to pass an integer around, we’d do:

通道和其他类型一样有类型。这个类型就是我们将在通道中传递的数据类型。例如,创建一个用来传递整数的通道,我们这样做:

1
c := make(chan int)

The type of this channel is chan int. Therefore, to pass this channel to a function, our signature looks like:

这个通道的类型是chan int。因此,将这个通道传递给一个函数是,可以这样声明:

1
func worker(c chan int) { ... }

Channels support two operations: receiving and sending. We send to a channel by doing:

通道支持2种操作:接收和发送。我们可以使用下面方式往通道发送数据:

1
CHANNEL <- DATA

and receive from one by doing

然后可以使用下面方式从通道接收数据:

1
VAR := <-CHANNEL

The arrow points in the direction that data flows. When sending, the data flows into the channel. When receiving, the data flows out of the channel.

箭头的方向就是数据的流动方向。当发送数据时,数据流入通道。当发送数据时,数据是流出通道。

The final thing to know before we look at our first example is that receiving and sending to and from a channel is blocking. That is, when we receive from a channel, execution of the goroutine won’t continue until data is available. Similarly, when we send to a channel, execution won’t continue until the data is received.

最后,在看我们的第一个例子之前,从一个通道接收或者发送数据时会阻塞。也就是说,当我们从一个通道接收数据时,直到数据可用Go协程才会继续执行。类似的,往一个通道发送数据时,在数据被接收之前Go协程也不会继续执行。

Consider a system with incoming data that we want to handle in separate goroutines. This is a common requirement. If we did our data-intensive processing on the goroutine which accepts the incoming data, we’d risk timing out clients. First, we’ll write our worker. This could be a simple function, but I’ll make it part of a structure since we haven’t seen goroutines used like this before:

假设这样的一个系统,我们想通过不同的协程来处理输入数据。这是一个常见的需求。如果通过Go协程接收输入的数据并进行数据密集型处理,那么在客户端会有超时风险。首先,我们将写出我们的处理器。这是一个简单的函数,但是我会让它变成一个结构体的部分,因为我们之前从来没有这样使用过Go协程:

1
2
3
4
5
6
7
8
9
10
type Worker struct {
id int
}

func (w Worker) process(c chan int) {
for {
data := <-c
fmt.Printf("worker %d got %d\n", w.id, data)
}
}

Our worker is simple. It waits until data is available then “processes” it. Dutifully, it does this in a loop, forever waiting for more data to process.

我们的处理器很简单。它会一直等待直到数据可用并“处理”它。它通过一个循环来实现,永久等待更多的数据来处理。

To use this, the first thing we’d do is start some workers:

为了使用上面的代码,我们首先要做的是启动一些处理器:

1
2
3
4
5
c := make(chan int)
for i := 0; i < 4; i++ {
worker := Worker{id: i}
go worker.process(c)
}

And then we can give them some work:

然后我们可以给他们一些工作:

1
2
3
4
for {
c <- rand.Int()
time.Sleep(time.Millisecond * 50)
}

Here’s the complete code to make it run:

下面是完整的可执行代码:

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
26
27
28
29
30
31
package main

import (
"fmt"
"time"
"math/rand"
)

func main() {
c := make(chan int)
for i := 0; i < 5; i++ {
worker := &Worker{id: i}
go worker.process(c)
}

for {
c <- rand.Int()
time.Sleep(time.Millisecond * 50)
}
}

type Worker struct {
id int
}

func (w *Worker) process(c chan int) {
for {
data := <-c
fmt.Printf("worker %d got %d\n", w.id, data)
}
}

We don’t know which worker is going to get what data. What we do know, what Go guarantees, is that the data we send to a channel will only be received by a single receiver.

我们不知道哪个处理器将获得数据。我们所知道的是,Go确保了往一个通道发送数据时,仅有一个单独的接收器可以接受。

Notice that the only shared state is the channel, which we can safely receive from and send to concurrently. Channels provide all of the synchronization code we need and also ensure that, at any given time, only one goroutine has access to a specific piece of data.

需要指出的是通道是唯一的共享方式,通过通道我们可以并发安全的发送和接收数据。通道提供了我们需要的所有同步代码,并且也确保在任意的特定时刻只有一个Go协程可以访问一个特定的数据。

带缓存的通道(Buffered Channels)

Given the above code, what happens if we have more data coming in than we can handle? You can simulate this by changing the worker to sleep after it has received data:

在上面的代码中,如果输入的数据超过我们可以处理的数据会发生什么?你可以模拟这种场景,在处理器收到数据后执行time.Sleep

1
2
3
4
5
for {
data := <-c
fmt.Printf("worker %d got %d\n", w.id, data)
time.Sleep(time.Millisecond * 500)
}

What’s happening is that our main code, the one that accepts the user’s incoming data (which we just simulated with a random number generator) is blocking as it sends to the channel because no receiver is available.

main函数中会发什么呢?接收用户的输入数据(这里通过一个随机的数字生成器模拟)会被阻塞,因为往通道发送数据时没有可用的接收者。

In cases where you need high guarantees that the data is being processed, you probably will want to start blocking the client. In other cases, you might be willing to loosen those guarantees. There are a few popular strategies to do this. The first is to buffer the data. If no worker is available, we want to temporarily store the data in some sort of queue. Channels have this buffering capability built-in. When we created our channel with make, we can give our channel a length:

在这种情况下,你需要确保数据被处理,你可能想要让客户端阻塞。在其他情况下,你可能愿意不确保数据被处理。这里有一些流行的策略能完成此事。首先是将数据缓存起来。如果没有处理器可用,我们想将数据暂时存放在一个有序的队列中。通道内置缓存能力。当我们使用make创建一个通道时,我们可以指定通道的长度:

1
c := make(chan int, 100)

You can make this change, but you’ll notice that the processing is still choppy. Buffered channels don’t add more capacity; they merely provide a queue for pending work and a good way to deal with a sudden spike. In our example, we’re continuously pushing more data than our workers can handle.

你可以做这样的修改,但是你将注意到处理过程仍然震荡。缓冲通道没有增加处理能力,他们只是为挂起的工作提供了一个队列和应对突发尖峰的好方法。在我们示例中,我们持续不断的发送超出我们处理器可以处理的数据。

Nevertheless, we can get a sense that the buffered channel is, in fact, buffering by looking at the channel’s len:

尽管如此,事实上,我们可以查看通道的len,来了解到带缓存的通道的缓冲情况:

1
2
3
4
5
for {
c <- rand.Int()
fmt.Println(len(c))
time.Sleep(time.Millisecond * 50)
}

You can see that it grows and grows until it fills up, at which point sending to our channel start to block again.

你可以看到它的长度在不断增大,直到装满为止,此时,往通道发送的数据又开始被阻塞。

选择(Select)

Even with buffering, there comes a point where we need to start dropping messages. We can’t use up an infinite amount of memory hoping a worker frees up. For this, we use Go’s select.

即使借助缓存,有一点需要指出的是,我们需要开始丢弃一些消息,我们不能使用一个无限大的内存,并指望人工的释放它。所以我们使用Go的select

Syntactically, select looks a bit like a switch. With it, we can provide code for when the channel isn’t available to send to. First, let’s remove our channel’s buffering so that we can clearly see how select works:

在语法结构上,select看起来有点类似switch。通过select,我们能写出一些针对通道不可写情况下的代码。首先,让我们去掉我们通道的缓存,这样可以更清晰的看到select是如何工作的。

1
c := make(chan int)

Next, we change our for loop:

接下来,我们修改for循环:

1
2
3
4
5
6
7
8
9
10
for {
select {
case c <- rand.Int():
//optional code here
default:
//this can be left empty to silently drop the data
fmt.Println("dropped")
}
time.Sleep(time.Millisecond * 50)
}

We’re pushing out 20 messages per second, but our workers can only handle 10 per second; thus, half the messages get dropped.

我们每秒往通道中发送20个信息,但是我们的处理器每秒只能处理10个信息;因此,有一半的信息被丢弃。

This is only the start of what we can accomplish with select. A main purpose of select is to manage multiple channels. Given multiple channels, select will block until the first one becomes available. If no channel is available, default is executed if one is provided. A channel is randomly picked when multiple are available.

这仅仅只是我们使用select完成一些事的开始。使用select的最主要目的是通过它管理多个通道。给定多个通道,select将阻塞直到有一个通道可用。如果没有可用的通道,当提供了default语句时,执行该分支。当多个通道都可用时,选择其中的一个通道是随机的。

It’s hard to come up with a simple example that demonstrates this behavior as it’s a fairly advanced feature. The next section might help illustrate this though.

很难想出一个简单的例子来证明这种行为,因为这是一种高级特性。在下一小节可能有助于说明这个问题。

超时(Timeout)

We’ve looked at buffering messages as well as simply dropping them. Another popular option is to timeout. We’re willing to block for some time, but not forever. This is also something easy to achieve in Go. Admittedly, the syntax might be hard to follow but it’s such a neat and useful feature that I couldn’t leave it out.

我们已经学习了缓存消息和简单丢弃消息。另外一种比较流行的做法是使用超时。我们将阻塞一段时间,但不是一直阻塞。在Go中这很容易实现。老实说,这个语法有点难于接受,但是它是比较灵活和有用的特性,我基本不能没有它。

To block for a maximum amount of time, we can use the time.After function. Let’s look at it then try to peek beyond the magic. To use this, our sender becomes:

为了达到阻塞的最大时限,我们可以使用time.After函数。让我们看看它,并试着看出其中的魔法。为了使用这种方式,我们的发送器需要修改为:

1
2
3
4
5
6
7
8
for {
select {
case c <- rand.Int():
case <-time.After(time.Millisecond * 100):
fmt.Println("timed out")
}
time.Sleep(time.Millisecond * 50)
}

time.After returns a channel, so we can select from it. The channel is written to after the specified time expires. That’s it. There’s nothing more magical than that. If you’re curious, here’s what an implementation of after could look like:

time.After返回一个通道,所以我们可以对它使用select语法。当指定的时间到期时这个通道被写入。就是如此。没有其他更多的魔法了。如果你还是好奇,这里有一个after的实现:

1
2
3
4
5
6
7
8
func after(d time.Duration) chan bool {
c := make(chan bool)
go func() {
time.Sleep(d)
c <- true
}()
return c
}

Back to our select, there are a couple of things to play with. First, what happens if you add the default case back? Can you guess? Try it. If you aren’t sure what’s going on, remember that default fires immediately if no channel is available.

回到我们的select中来,还一些内容可以研究。首先,如果添加了default条件会发生会什么呢?你可以猜猜?试试。如果你不确定会发生什么,记住如果没有可用的通道default会立即被触发。

Also, time.After is a channel of type chan time.Time. In the above example, we simply discard the value that was sent to the channel. If you want though, you can receive it:

同时,time.After的通道类型是chan time.Time。上面的例子中,我们简单的丢弃了发送给通道的值。如果你相要,你可以这样接收它:

1
2
case t := <-time.After(time.Millisecond * 100):
fmt.Println("timed out at", t)

Pay close attention to our select. Notice that we’re sending to c but receiving from time.After. select works the same regardless of whether we’re receiving from, sending to, or any combination of channels:

更近一步的看我们的select。注意我们向c发送数据,但是从time.After接收数据。select对无论是接收数据,发送数据,还是其他通道的组合,都是一样对待的:

  • The first available channel is chosen.

  • If multiple channels are available, one is randomly picked.

  • If no channel is available, the default case is executed.

  • If there’s no default, select blocks.

  • 第一个可用的通道被选择。

  • 如果有多个通道可用,随机选择一个。

  • 如果没有通道可用,默认条件被执行。

  • 如果没有默认条件,选择阻塞。

Finally, it’s common to see a select inside a for. Consider:

最后,select通常在for循环中使用。例如:

1
2
3
4
5
6
7
8
9
for {
select {
case data := <-c:
fmt.Printf("worker %d got %d\n", w.id, data)
case <-time.After(time.Millisecond * 10):
fmt.Println("Break time")
time.Sleep(time.Second)
}
}

继续之前(Before You Continue)

If you’re new to the world of concurrent programming, it might all seem rather overwhelming. It categorically demands considerably more attention and care. Go aims to make it easier.

如果你是并发编程的新手,它可能显得相当庞大。它绝对是需要相当多的重视和关注。 Go的目标就是使其更容易。

Goroutines effectively abstract what’s needed to run concurrent code. Channels help eliminate some serious bugs that can happen when data is shared by eliminating the sharing of data. This doesn’t just eliminate bugs, but it changes how one approaches concurrent programming. You start to think about concurrency with respect to message passing, rather than dangerous areas of code.

Go协程有效的抽象了需要并发执行的代码。通道协助消除了可能在数据共享时的严重Bug。这不只是消除了Bug,更是改变了并发编程的开发方式。你开始使用消息传递的方式来考虑并发,而不是危险的共享代码。

Having said that, I still make extensive use of the various synchronization primitives found in the sync and sync/atomic packages. I think it’s important to be comfortable with both. I encourage you to first focus on channels, but when you see a simple example that needs a short-lived lock, consider using a mutex or read-write mutex.

虽然这么说,我仍然广泛使用的各种同步原语中发现的syncsync/atomic包。我觉得这两种情况都要适应是很重要的。我鼓励你先聚焦在通道上,但是如果你碰到只是需要短暂的多锁,建议你使用互斥锁或者读写互斥锁。

结论(Conclusion)

I recently heard Go described as a boring language. Boring because it’s easy to learn, easy to write and, most importantly, easy to read. Perhaps, I did this reality a disservice. We did spend three chapters talking about types and how to declare variables after all.

我最近听说Go被描述为一门单调的语言。单调是因为它很容易学习,很容易编写,最为重要的是,很容易读。也许,我这是在帮倒忙,我确实花了三个章节来介绍类型和如何申请变量。

If you have a background in a statically typed language, much of what we saw was probably, at best, a refresher. That Go makes pointers visible and that slices are thin wrappers around arrays probably isn’t overwhelming to seasoned Java or C# developers.

如果你在静态类型语言的背景,大多数我们看到的,充其量只是复习。同时Go的指针可见性和切片的轻量封装对经验丰富的Java的C#开发人员来说不算什么。

If you’ve mostly been making use of dynamic languages, you might feel a little different. It is a fair bit to learn. Not least of which is the various syntax around declaration and initialization. Despite being a fan of Go, I find that for all the progress towards simplicity, there’s something less than simple about it. Still, it comes down to some basic rules (like you can only declare variable once and := does declare the variable) and fundamental understanding (like new(X) or &X{} only allocate memory, but slices, maps and channels require more initialization and thus, make).

如果你更多的是使用动态语言,你可能会觉得有点不同。这一点公平的学习。不过其中最重要的是各种声明和初始化的语法。虽然是一个Go粉,我发现所有的努力都是为了简单,还有一些致简的东西。仍然,它也有一些基本的规则(比如变量申明一次和:=已经申明了变量)和基本的了解(比如new(X)&X{}只是分配了内存,但切片,字典和通道就需要使用make来分配内存和初始化)。

Beyond this, Go gives us a simple but effective way to organize our code. Interfaces, return-based error handling, defer for resource management and a simple way to achieve composition.

除此之外,Go提供了一个简洁但又高效的方式来组织我们的代码。接口,基于返回值的错误处理,用于资源管理的defer和简单的实现组合。

Last but not least is the built-in support for concurrency. There’s little to say about goroutines other than they’re effective and simple (simple to use anyway). It’s a good abstraction. Channels are more complicated. I always think it’s important to understand basics before using high-level wrappers. I do think learning about concurrent programming without channels is useful. Still, channels are implemented in a way that, to me, doesn’t feel quite like a simple abstraction. They are almost their own fundamental building block. I say this because they change how you write and think about concurrent programming. Given how hard concurrent programming can be, that is definitely a good thing.

最后但是最重要的是它内置了对并发的支持。还有一点关于Go协程的要说就是它们高效和简单(反正使用简单)。这是很好的抽象。通道会更复杂一点。我一直认为在学习高级封装之前要掌握好基础。我确认认为不使用通道来进行并发编程是有益的。但是,通道的实现方式,对我来说,不太像是一个简单的抽象。它们有自己的基础构建。我这么说是因为它们改变了你对并发编程的思考和书写方式。鉴于并发编程的难度,这绝对是一个好事。

第五章 - 特点(珍品?)(Chapter 5 - Tidbits)

In this chapter, we’ll talk about a miscellany of Go’s feature which didn’t quite fit anywhere else.

在这一章是,我们会来介绍一些Go的特性的杂烩,这些内容不太适合放在其他章节中。

错误处理(Error Handling)

Go’s preferred way to deal with errors is through return values, not exceptions. Consider the strconv.Atoi function which takes a string and tries to convert it to an integer:

Go更喜欢用返回值而不是异常的方式来处理错误。例如strconv.Atoi函数将一个字符串转换成一个整数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"fmt"
"os"
"strconv"
)

func main() {
if len(os.Args) != 2 {
os.Exit(1)
}

n, err := strconv.Atoi(os.Args[1])
if err != nil {
fmt.Println("not a valid number")
} else {
fmt.Println(n)
}
}

You can create your own error type; the only requirement is that it fulfills the contract of the built-in error interface, which is:

你可以创建你自己的错误类型;唯一的要求就是需要实现内置接口error:

1
2
3
type error interface {
Error() string
}

More commonly, we can create our own errors by importing the errors package and using it in the New function:

更为常见的是,我们可以通过导入errors包,并通过它的New函数来创建自己的错误:

1
2
3
4
5
6
7
8
9
10
11
12
import (
"errors"
)


func process(count int) error {
if count < 1 {
return errors.New("Invalid count")
}
...
return nil
}

There’s a common pattern in Go’s standard library of using error variables. For example, the io package has an EOF variable which is defined as:

在GO的标准库中,使用错误变量是一个常用的模式。例如, 在io包中有一个EOF变量是这样定义的:

1
var EOF = errors.New("EOF")

This is a package variable (it’s defined outside of a function) which is publicly accessible (upper-case first letter). Various functions can return this error, say when we’re reading from a file or STDIN. If it makes contextual sense, you should use this error, too. As consumers, we can use this singleton:

这是一个公共(大写字母开关)的包变量(它定义有函数之外)。如果我们从文件或者标准输入时失败时,我们可以返回这个错误。为了更容易理解,你也应该用这个错误。作为使用者,我们可以用这个单件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import (
"fmt"
"io"
)

func main() {
var input int
_, err := fmt.Scan(&input)
if err == io.EOF {
fmt.Println("no more input!")
}
}

As a final note, Go does have panic and recover functions. panic is like throwing an exception while recover is like catch; they are rarely used.

最后要注意的是,Go有panicrecover函数。panic像是抛出异常,而recover是捕获异常;它们不常使用。

Defer(Defer)

Even though Go has a garbage collector, some resources require that we explicitly release them. For example, we need to Close() files after we’re done with them. This sort of code is always dangerous. For one thing, as we’re writing a function, it’s easy to forget to Close something that we declared 10 lines up. For another, a function might have multiple return points. Go’s solution is the defer keyword:

虽然Go有垃圾回收机制,但是有些资源需要显示的释放它们。比如,当我们使用文件完了之后,需要调用Close()来关闭它们。这类代码总是很危险。其一,我们写一下函数的时候,如果申请一个资源超过10行,就很容易忘记Close。其二,一个函数可能会有多个返回点。Go的解决方案是使用defer关键字:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import (
"fmt"
"os"
)

func main() {
file, err := os.Open("a_file_to_read")
if err != nil {
fmt.Println(err)
return
}
defer file.Close()
// read the file
}

If you try to run the above code, you’ll probably get an error (the file doesn’t exist). The point is to show how defer works. Whatever you defer will be executed after the method returns, even if it does so violently. This lets you release resources near where it’s initialized and takes care of multiple return points.

如果你尝试运行上面的代码,你可能收到一个错误(文件不存在)。这里展示的是defer是如何工作的。无论如何在函数返回时defer都会被执行,虽然这样有点极端。但这可以让你在初始化附近释放资源和不要操心多个返回点的问题。

go fmt(go fmt)

Most programs written in Go follow the same formatting rules, namely, a tab is used to indent and braces go on the same line as their statement.

绝大多数Go写的代码遵守有一个相同的格式化规则,也就是说,使用Tab来缩进和花括号与语句同一行。

I know, you have your own style and you want to stick to it. That’s what I did for a long time, but I’m glad I eventually gave in. A big reason for this is the go fmt command. It’s easy to use and authoritative (so no one argues over meaningless preferences).

我知道你有自己的代码风格并且严格遵守它。我一直以来也是这么做的,但是我最终还是放弃了。一个最在的原因就是go fmt命令。它很容用且权威(所以没有人会为了毫无意义的偏好而争论)

When you’re inside a project, you can apply the formatting rule to it and all sub-projects via:

当你在一个工程目录下,你可以通过下面的命令将工程下所有文件使用相同的格式化规则:

1
go fmt ./...

Give it a try. It does more than indent your code; it also aligns field declarations and alphabetically orders imports.

尝试一下吧。除了缩进代码,它还会自动对齐你的声明语句并将包导入按字母顺序排序。

If初始化(Initialized If)

Go supports a slightly modified if-statement, one where a value can be initiated prior to the condition being evaluated:
Go提供了一种稍有不同的if声明,一个可以在条件执行之前声明和初始化:

1
2
3
if x := 10; count > x {
...
}

That’s a pretty silly example. More realistically, you might do something like:

这是一个非常简单的例子。更实际的例子是,你可能是这样做的:

1
2
3
if err := process(); err != nil {
return err
}

Interestingly, while the values aren’t available outside the if-statement, they are available inside any else if or else.

有趣的是,if语句中定义并初始化的值在if语句之外是不可用的,仅可以在else ifelse语句中使用。

空接口和转换(Empty Interface and Conversions)

In most object-oriented languages, a built-in base class, often named object, is the superclass for all other classes. Go, having no inheritance, doesn’t have such a superclass. What it does have is an empty interface with no methods: interface{}. Since every type implements all 0 of the empty interface’s methods, and since interfaces are implicitly implemented, every type fulfills the contract of the empty interface.

在大多数面向对象语言中,都有一种内置的基类,叫object,它是所有其他类的超类。但是go语言不支持继承,所以没有类似的超类。Go确实有一个没有任何方法的空接口:interface{}。因为接口都是隐式实现,每种类型都实现了空接口的0个方法,所以每种类型都实现了空接口的协议。

If we wanted to, we could write an add function with the following signature:

如果我们愿意,我们可以通过下面声明方式写一个add函数:

1
2
3
func add(a interface{}, b interface{}) interface{} {
...
}

To convert an interface variable to an explicit type, you use .(TYPE):

将一个空接口变量转换成一个指定的类型,你可以使用.(TYPE):

1
return a.(int) + b.(int)

Note that if the underlying type is not int, the above will result in an error.

需要注意如果底层类型示是一个int,上面的代码会导致一个错误。

You also have access to a powerful type switch:

你也可以通过switch使用强大的类型转换:

1
2
3
4
5
6
7
8
switch a.(type) {
case int:
fmt.Printf("a is now an int and equals %d\n", a)
case bool, string:
// ...
default:
// ...
}

You’ll see and probably use the empty interface more than you might first expect. Admittedly, it won’t result in clean code. Converting values back and forth is ugly and dangerous but sometimes, in a static language, it’s the only choice.

你会发现,空接口的使用会超出你的预期。诚然,这不会让代码变得简洁。来回转换值是丑陋和危险的,但有时候在静态类型语言中,这是唯一的选择。

字符串和字节数组(Strings and Byte Arrays)

Strings and byte arrays are closely related. We can easily convert one to the other:

字符串和字节数组有密切关系,我们可以轻易的将它们转换成对方:

1
2
3
stra := "the spice must flow"
byts := []byte(stra)
strb := string(byts)

In fact, this way of converting is common across various types as well. Some functions explicitly expect an int32 or an int64 or their unsigned counterparts. You might find yourself having to do things like:

事实上,这也是大多数类型的转换方式。一些函数明确指定一个int32或者int64或者相应的无符号类型。你可能会发现你自己不得不像下面这样做:

1
int64(count)

Still, when it comes to bytes and strings, it’s probably something you’ll end up doing often. Do note that when you use []byte(X) or string(X), you’re creating a copy of the data. This is necessary because strings are immutable.

然而,当涉及到字节和字符串时,这可能是你会经常做的事。当你使用[]byte(X)或者string(X)时务必注意,你创建了数据的拷贝。这是因为字符串的不可变性。

Strings are made of runes which are unicode code points. If you take the length of a string, you might not get what you expect. The following prints 3:

当字符串有由unicode字符码runes组成时。如果你计算字符串的长度时,可能得到的结果和你期待的不同。下面结果是输出3:

fmt.Println(len("椒"))

If you iterate over a string using range, you’ll get runes, not bytes. Of course, when you turn a string into a []byte you’ll get the correct data.

如果你使用range迭代一个字符串,你得到的是runes,而不是bytes。当然,你将一个字符串转换为[]byte时,你得到的数据还是正确的。

函数类型(Function Type)

Functions are first-class types:
函数是第一类类型:

1
type Add func(a int, b int) int

which can then be used anywhere – as a field type, as a parameter, as a return value.

可以在任何地方使用————可以做为一个字段,参数,返回值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import (
"fmt"
)

type Add func(a int, b int) int

func main() {
fmt.Println(process(func(a int, b int) int{
return a + b
}))
}

func process(adder Add) int {
return adder(1, 2)
}

Using functions like this can help decouple code from specific implementations much like we achieve with interfaces.

像这样使用函数可以使你在一些特定实现时减少代码的耦合性,就像使用接口实现那样。

继续之前(Before You Continue)

We looked at various aspects of programming with Go. Most notably, we saw how error handling behaves and how to release resources such as connections and open files. Many people dislike Go’s approach to error handling. It can feel like a step backwards. Sometimes, I agree. Yet, I also find that it results in code that’s easier to follow. defer is an unusual but practical approach to resource management. In fact, it isn’t tied to resource management only. You can use defer for any purpose, such as logging when a function exits.

我们已经学习了Go编程的很多内容。显而易见,我们看见了错误处理的行为和资源释放如连接或者打开文件。很多人不喜欢Go的错误处理方式。它让人觉得这是一种退步。有些时候,我同意这种说法。然而,我也发现这会导致代码更易读。defer是一种不常见但很实用的资源管理手段。事实上,它不仅仅可以进行资源管理。你可以使用defer完成任何目的,例如当一个函数退出时打印日志记录。

Certainly, we haven’t looked at all of the tidbits Go has to offer. But you should be feeling comfortable enough to tackle whatever you come across.

当然,我们还没有学习Go提供的所有花絮。但是无论你遇到什么你应该可以轻松应对。

第四章 - 代码组织和接口(Chapter 4 - Code Organization and Interfaces)

It’s now time to look at how to organize our code.

现在是时候来看看我们是怎么组织代码了。

包(Packages)

To keep more complicated libraries and systems organized, we need to learn about packages. In Go, package names follow the directory structure of your Go workspace. If we were building a shopping system, we’d probably start with a package name “shopping” and put our source files in $GOPATH/src/shopping/.

为了组织更复杂的类库和系统,我们需要了解包。在Go中,包名紧跟在工作目录结构之下。如果我们构建一个电商系统,我们可能以”shopping”命名包和把源文件存在$GOPATH/src/shopping/下。

We don’t want to put everything inside this folder though. For example, maybe we want to isolate some database logic inside its own folder. To achieve this, we create a subfolder at $GOPATH/src/shopping/db. The package name of the files within this subfolder is simply db, but to access it from another package, including the shopping package, we need to import shopping/db.

显然我们不想反所有的东西都放在这个目录。例如,我们希望在数据库目录下关联一些数据库的逻辑。要达到这个目的,我们创建了一个子目录$GOPATH/src/shopping/db。这个目录下的包名可以是简单的db,但是其他包需要访问这个包时,就需要包含shopping,我们需要这样导入shopping/db

In other words, when you name a package, via the package keyword, you provide a single value, not a complete hierarchy (e.g., “shopping” or “db”). When you import a package, you specify the complete path.

换名话来说,当你命名一个包时,可以通过package关键字,你提供一个单一的值,不是完整的层级(比如,”shopping”或者”db”)。当你导入包时,你需要指定完整的路径。

Let’s try it. Inside your Go workspace’s src folder (which we set up in Getting Started of the Introduction), create a new folder called shopping and a subfolder within it called db.

让我们来试一试,在我们的Go的工作目录下的src文件夹中(我们在入门介绍中设置的),创建一个shopping的文件夹并创建一个db的子文件夹。

Inside of shopping/db, create a file called db.go and add the following code:

shopping/db目录下,创建一个名为db.go的文件并添加如下代码:

1
2
3
4
5
6
7
8
9
10
11
package db

type Item struct {
Price float64
}

func LoadItem(id int) *Item {
return &Item{
Price: 9.001,
}
}

Notice that the name of the package is the same as the name of the folder. Also, obviously, we aren’t actually accessing the database. We’re just using this as an example to show how to organize code.

注意下包名和文件夹的名字是一样的。显然,我们也没有真下的访问数据库。我们只是用这个例子来演示如何组织代码。

Now, create a file called pricecheck.go inside of the main shopping folder. Its content is:

现在,在主目录shopping下创建一个名为pricecheck.go的文件。它的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
package shopping

import (
"shopping/db"
)

func PriceCheck(itemId int) (float64, bool) {
item := db.LoadItem(itemId)
if item == nil {
return 0, false
}
return item.Price, true
}

It’s tempting to think that importing shopping/db is somehow special because we’re inside the shopping package/folder already. In reality, you’re importing $GOPATH/src/shopping/db, which means you could just as easily import test/db so long as you had a package named db inside of your workspace’s src/test folder.

这很容易理解,导入shopping/ db有些特殊,因为我们是shopping包/文件夹中了。实际上,要导入$ GOPATH/src/shopping/db,这意味着你可以很容易地导入test/db,只要工作空间下的src/test/db的文件夹有一个db的包。

If you’re building a package, you don’t need anything more than what we’ve seen. To build an executable, you still need a main. The way I prefer to do this is to create a subfolder called main inside of shopping with a file called main.go and the following content:

如果你想建一个包,只需要我们看到的这些内容就可以了。要创建一个可执行程序,我们还需要一个main函数。我喜欢在shopping文件夹下建一个main文件夹并新增一个main.go的文件。它的内容如下:

1
2
3
4
5
6
7
8
9
10
package main

import (
"shopping"
"fmt"
)

func main() {
fmt.Println(shopping.PriceCheck(4343))
}

You can now run your code by going into your shopping project and typing:

进入shopping目录输入如下内容可以运行代码:

1
go run main/main.go

循环导入(Cyclical Imports)

As you start writing more complex systems, you’re bound to run into cyclical imports. This happens when package A imports package B but package B imports package A (either directly or indirectly through another package). This is something the compiler won’t allow.

当你开始写一些更复杂的系统时,你不免会碰到循环导入的问题。当包A的导入了包B但是包B又导入了包A(无论是直接还是间接通过其他包导入)循环导入就发生了。这是编译器不允许的。

Let’s change our shopping structure to cause the error.

让我们来修改一下shopping的结构来引发这个错误。

Move the Item definition from shopping/db/db.go into shopping/pricecheck.go. Your pricecheck.go file should now look like:

Item的定义从shopping/db/db.go移到shopping/pricecheck.go中。你的pricecheck.go应该看起来像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package shopping

import (
"shopping/db"
)

type Item struct {
Price float64
}

func PriceCheck(itemId int) (float64, bool) {
item := db.LoadItem(itemId)
if item == nil {
return 0, false
}
return item.Price, true
}

If you try to run the code, you’ll get a couple of errors from db/db.go about Item being undefined. This makes sense. Item no longer exists in the db package; it’s been moved to the shopping package. We need to change shopping/db/db.go to:

如果你尝试运行代码,你会从db/db.go中收到一些错误信息说Item未定义。这好理解。Item已经不在db包下了;这被移到了shopping包。我们需要修改shopping/db/db.go的代码为:

1
2
3
4
5
6
7
8
9
10
11
package db

import (
"shopping"
)

func LoadItem(id int) *shopping.Item {
return &shopping.Item{
Price: 9.001,
}
}

Now when you try to run the code, you’ll get a dreaded import cycle not allowed error. We solve this by introducing another package which contains shared structures. Your directory structure should look like:

现在当你尝试运行代码时,你会收到一个令人害怕的错误import cycle not allowed。我们通过引入一个包含共享结构的另一个包来解决这个问题。你的目录结构看起来像这样:

1
2
3
4
5
6
7
8
9
$GOPATH/src
- shopping
pricecheck.go
- db
db.go
- models
item.go
- main
main.go

pricecheck.go will still import shopping/db, but db.go will now import shopping/models instead of shopping, thus breaking the cycle. Since we moved the shared Item structure to shopping/models/item.go, we need to change shopping/db/db.go to reference the Item structure from models package:

pricecheck.go一样是导入shopping/db,但是db.go现在要导入shopping/models来替换shopping,这样打破了循环。因为将共享结构体Item移到了shopping/models/item.go,我们需要修改shopping/db/db.gomodel包中引用结构体Item

1
2
3
4
5
6
7
8
9
10
11
package db

import (
"shopping/models"
)

func LoadItem(id int) *models.Item {
return &models.Item{
Price: 9.001,
}
}

You’ll often need to share more than just models, so you might have other similar folder named utilities and such. The important rule about these shared packages is that they shouldn’t import anything from the shopping package or any sub-packages. In a few sections, we’ll look at interfaces which can help us untangle these types of dependencies.

你常常需要的共享结构不仅仅是models,所以你可能还有一些类似utilities这样的文件夹。最重要的原则是这些共享对象,不能导入shopping包或者它的子包。要不了几个章节,我们会看到接口是如何解开这些依赖的。

可见性(Visibility)

Go uses a simple rule to define what types and functions are visible outside of a package. If the name of the type or function starts with an uppercase letter, it’s visible. If it starts with a lowercase letter, it isn’t.

Go用一个非常简单的原则来决定一个包的类型和函数是否在包外可见。如果类型或者函数是以大写字母开头,就是可见的,如果是小写字母开头,就是不可见的。

This also applies to structure fields. If a structure field name starts with a lowercase letter, only code within the same package will be able to access them.

这对结构体的字段也是一样适用的。如果一个结构体的字段名是以小写字母开头,只有在同一个包里的代码可以访问它们。

For example, if our items.go file had a function that looked like:

例如:在我们的items.go文件中有这样的一个函数:

1
2
3
func NewItem() *Item {
// ...
}

it could be called via models.NewItem(). But if the function was named newItem, we wouldn’t be able to access it from a different package.

可以通过models.NewItem()来调用。但是如果这个函数被命名为newItem,我们就不能在其他包里面来调用它了。

Go ahead and change the name of the various functions, types and fields from the shopping code. For example, if you rename the Item's Price field to price, you should get an error.

继续修改在shopping中修改函数,类型和字段的名字。例如,如果你修改ItemPriceprice,你会收到一个编译错误。

包管理(Package Management)

The go command we’ve been using to run and build has a get subcommand which is used to fetch third-party libraries. go get supports various protocols but for this example, we’ll be getting a library from Github, meaning, you’ll need git installed on your computer.

我们用来runbuildgo命令,它还一个get的子命令用来获取第三方的类库。go get支持多种协议,但是这里,我们将从Github上获取一个类库。这是说,你要在你的电脑上安装好git

Assuming you already have git installed, from a shell/command prompt, enter:

假设你已经安装好了git,打开一个shell/命令提示符,输入:

1
go get github.com/mattn/go-sqlite3

go get fetches the remote files and stores them in your workspace. Go ahead and check your $GOPATH/src. In addition to the shopping project that we created, you’ll now see a github.com folder. Within, you’ll see a mattn folder which contains a go-sqlite3 folder.

go get获取远程文件并把它们存在你的工作目录中。到$GOPATH/src目录中检查一下。除了我们自己创建的shopping项目外,还会看到一个github.com文件夹。里面你会看到一个包含go-sqlite3文件夹的mattn文件夹。

We just talked about how to import packages that live in our workspace. To use our newly gotten go-sqlite3 package, we’d import it like so:

我们刚介绍了在工作区中如何导入包。要用我们刚刚获取到的go-sqlite3包,我们需要像这样来导入它:

1
2
3
import (
"github.com/mattn/go-sqlite3"
)

I know this looks like a URL but in reality, it’ll simply import the go-sqlite3 package which it expects to find in $GOPATH/src/github.com/mattn/go-sqlite3.

我知道这看起来像是一个URL,但实际上,如果知道是在$GOPATH/src/github.com/mattn/go-sqlite3目录,导入go-sqlite3包是很简单的。

依赖管理(Dependency Management)

go get has a couple of other tricks up its sleeve. If we go get within a project, it’ll scan all the files, looking for imports to third-party libraries and will download them. In a way, our own source code becomes a Gemfile or package.json.

go get有一些其他的戏法。如果我们在一个项目中执行go get,它会扫描所有文件并查找所有导入的第三方库,然后下载这些第三方库。某种程度上说,我们自己的源代码变成一个Gemfile或者package.json

If you call go get -u it’ll update the packages (or you can update a specific package via go get -u FULL_PACKAGE_NAME).

执行go get -u将更新你的包(或者你可以通过go get -u FULL_PACKAGE_NAME更新指定的包)

Eventually, you might find go get inadequate. For one thing, there’s no way to specify a revision, it always points to the master/head/trunk/default. This is an even larger problem if you have two projects needing different versions of the same library.

最后,你可能发现了go get的一些不足。首先,它不能指定一个修订,它会一直指向master/head/trunk/default。这是一个严重的问题,尤其当你有2个项目需要同一个库的不同版本时。

To solve this, you can use a third-party dependency management tool. They are still young, but two promising ones are goop and godep. A more complete list is available at the go-wiki.

为了解决这个问题,你可以使用一个第三方的依赖管理工具。虽然还不太成熟,但是有2个依赖管理工具比较有前景,即goopgodep。更完整的列表可以参考go-wiki

接口(Interfaces)

Interfaces are types that define a contract but not an implementation. Here’s an example:

接中是一种定义了协议但没有实现的类型。这是一个例子:

1
2
3
type Logger interface {
Log(message string)
}

You might be wondering what purpose this could possibly serve. Interfaces help decouple your code from specific implementations. For example, we might have various types of loggers:

你可能会想知道这么做有什么目的。接口可以帮的的代码从特定的实现中解藕出来。例如,我们可能有多种类型的日志:

1
2
3
type SqlLogger struct { ... }
type ConsoleLogger struct { ... }
type FileLogger struct { ... }

Yet by programming against the interface, rather than these concrete implementations, we can easily change (and test) which we use without any impact to our code.

是的,能过接口而不是这些具体的实现来编程,我们可以很容易的在不影响我们的代码的基础上修改(和测试)。

How would you use one? Just like any other type, it could be a structure’s field:

你要如何来使用?就像其他类型一样,它可以是一个结构体的字段:

1
2
3
type Server struct {
logger Logger
}

or a function parameter (or return value):

或者是一个函数的参数(或者访回值):

1
2
3
func process(logger Logger) {
logger.Log("hello!")
}

In a language like C# or Java, we have to be explicit when a class implements an interface:

在像C#或者Java的语言中,我们必须显示的申请明一个类实现了一个接口:

1
2
3
4
5
public class ConsoleLogger : Logger {
public void Logger(message string) {
Console.WriteLine(message)
}
}

In Go, this happens implicitly. If your structure has a function name Log with a string parameter and no return value, then it can be used as a Logger. This cuts down on the verboseness of using interfaces:

在Go中,这是隐式的。如果的结构体有一个名为Log的函数,有一个string的参数和没有返回值,那么它就可以当作Logger来使用。这减少了使用接口时的繁索:

1
2
3
4
type ConsoleLogger struct {}
func (l ConsoleLogger) Log(message string) {
fmt.Println(message)
}

It also tends to promote small and focused interfaces. The standard library is full of interfaces. The io package has a handful of popular ones such as io.Reader, io.Writer, and io.Closer. If you write a function that expects a parameter that you’ll only be calling Close() on, you absolutely should accept an io.Closer rather than whatever concrete type you’re using.

这也会倾向于促进接口的小巧和单一。标准库中到处都是接口。在io包中有一些流行的接口,如io.Reader, io.Writer, 和io.Closer。如果你要写一个函数需要一个参数但只调用它的Close()方法,你绝对可以使用io.Closer接口需不是任何具体的类型。

Interfaces can also participate in composition. And, interfaces themselves can be composed of other interfaces. For example, io.ReadCloser is an interface composed of the io.Reader interface as well as the io.Closer interface.

接口也可以组合。也就是说接口可以有其他接口组成。例如,io.ReadCloser就是由接口io.Readerio.Closer接口组成。

Finally, interfaces are commonly used to avoid cyclical imports. Since they don’t have implementations, they’ll have limited dependencies.

最后,接口常用于避免循环导入。由于接口没有实现,他们的依赖关系有限。

继续之前(Before You Continue)

Ultimately, how you structure your code around Go’s workspace is something that you’ll only feel comfortable with after you’ve written a couple of non-trivial projects. What’s most important for you to remember is the tight relationship between package names and your directory structure (not just within a project, but within the entire workspace).

最后,当你试着用go写一些简单的项目之后,你会习惯在go语言的工作区中组织代码的方式。最重要是的记住go语言中的包名和你的目录结构有密切关系(不仅仅在一个项目中,在整个工作空间都如此)。

The way Go handles visibility of types is straightforward and effective. It’s also consistent. There are a few things we haven’t looked at, such as constants and global variables but rest assured, their visibility is determined by the same naming rule.

go语言处理类型的可见性方法是简单有效的。也是一致的。还有一些内容我们没有介绍,例如常量和全局变量,但是不用担心,它们的可见性也是遵循同样的规则。

Finally, if you’re new to interfaces, it might take some time before you get a feel for them. However, the first time you see a function that expects something like io.Reader, you’ll find yourself thanking the author for not demanding more than he or she needed.

最后,如果你不熟悉go语言中的接口,你可能需要花一些时间去感受它们。无论如何,当你首次看见一个函数需要例如io.Reader之类的参数时,你会发现你自己感激作者的要求不是太苛刻。

第三章 - 字典 ,数组和切片(Chapter 3 - Maps, Arrays and Slices)

So far we’ve seen a number of simple types and structures. It’s now time to look at arrays, slices and maps.

目前为止我们看了些简单类型和结构体。现在是时候来看看数组,切片和字典了。

数组(Arrays)

If you come from Python, Ruby, Perl, JavaScript or PHP (and more), you’re probably used to programming with dynamic arrays. These are arrays that resize themselves as data is added to them. In Go, like many other languages, arrays are fixed. Declaring an array requires that we specify the size, and once the size is specified, it cannot grow:

如果你从Python、Ruby、Perl、JavaScript或者PHP(还有更多),你可能使用过动态数组。这些数组当数据添加进来可以调整大小。在Go中,和其他语言一样,数组是固定大小的。申请一个数组需要我们指定它的大小。申明一个数组时需要指定大小,一旦大小指定了,就不能增长了:

1
2
var scores [10]int
scores[0] = 339

The above array can hold up to 10 scores using indexes scores[0] through scores[9]. Attempts to access an out of range index in the array will result in a compiler or runtime error.

上面的这个数组从scores[0]scores[9]可以容纳10个分数。尝试访问超出数组索引的会报编译或者运行时错误。

We can initialize the array with values:

我们可以带值初始化数组:

1
scores := [4]int{9001, 9333, 212, 33}

We can use len to get the length of the array. range can be used to iterate over it:

我们可能通过len来获取数组的长度。可以用range来迭代数组。

1
2
3
for index, value := range scores {

}

Arrays are efficient but rigid. We often don’t know the number of elements we’ll be dealing with upfront. For this, we turn to slices.

数组高效但不灵活。我们常常不能预先知道有多少元素需要处理。因此,我们使用切片。

切片(Slices)

In Go, you rarely, if ever, use arrays directly. Instead, you use slices. A slice is a lightweight structure that wraps and represents a portion of an array. There are a few ways to create a slice, and we’ll go over when to use which later on. The first is a slight variation on how we created an array:

在Go中,你很少,或者根本不,直接使用数组。反而,你使用切片。切片是对数组一个轻量型封装。有几种方式来创建一个切片,我们会全部过一遍。第一种方式和创建数组有一点小小的变化。

1
scores := []int{1,4,293,4,9}

Unlike the array declaration, our slice isn’t declared with a length within the square brackets. To understand how the two are different, let’s see another way to create a slice, using make:

和申请数组不同,我们的切片没有在中括号内指定长度。要理解这两者的不同,我们用另一种方式创建切片,使用make

1
scores := make([]int, 10)

We use make instead of new because there’s more to creating a slice than just allocating the memory (which is what new does). Specifically, we have to allocate the memory for the underlying array and also initialize the slice. In the above, we initialize a slice with a length of 10 and a capacity of 10. The length is the size of the slice, the capacity is the size of the underlying array. Using make we can specify the two separately:

我们使用make来替代new因为创建一个切片比只是分配内存(new做的事情)要复杂一些。特别的,我们需要分配底层数组和初始化切片。上面的例子,我们初始化了一个长度和容量为10的切片。长度是切片的大小,容量时底层数组的大小。使用make时可以分开提定这两个值:

1
scores := make([]int, 0, 10)

This creates a slice with a length of 0 but with a capacity of 10. (If you’re paying attention, you’ll note that make and len are overloaded. Go is a language that, to the frustration of some, makes use of features which aren’t exposed for developers to use.)

这创建了一个长度为0但容量为10的切片。(如果你留心,你会注意到makelen重载的。Go有的时候令人沮丧,一些在使用的功能没有暴露给开发者使用。)

To better understand the interplay between length and capacity, let’s look at some examples:

为了更好的理解长度和容量之间的相互作用,让我们来看一个例子:

1
2
3
4
5
func main() {
scores := make([]int, 0, 10)
scores[5] = 9033
fmt.Println(scores)
}

Our first example crashes. Why? Because our slice has a length of 0. Yes, the underlying array has 10 elements, but we need to explicitly expand our slice in order to access those elements. One way to expand a slice is via append:

我们的第一个程序崩溃了。为什么呢?因我们的切片的长度为0。是的,底层数组有10个元素,但是我们需要显示的扩展切片来访问这些元素。一种扩展方式是使用append

1
2
3
4
5
func main() {
scores := make([]int, 0, 10)
scores = append(scores, 5)
fmt.Println(scores) // prints [5]
}

But that changes the intent of our original code. Appending to a slice of length 0 will set the first element. For whatever reason, our crashing code wanted to set the element at index 5. To do this, we can re-slice our slice:

上面的修改改变了我们的原始代码的意图。扩展一个长度为0的切片会设置第一个元素。无论什么原因,我们崩溃的代码想要的是修改索引为5的元素。 我们可以重切片一次我们的切片:

1
2
3
4
5
6
func main() {
scores := make([]int, 0, 10)
scores = scores[0:6]
scores[5] = 9033
fmt.Println(scores)
}

How large can we resize a slice? Up to its capacity which, in this case, is 10. You might be thinking this doesn’t actually solve the fixed-length issue of arrays. It turns out that append is pretty special. If the underlying array is full, it will create a new larger array and copy the values over (this is exactly how dynamic arrays work in PHP, Python, Ruby, JavaScript, …). This is why, in the example above that used append, we had to re-assign the value returned by append to our scores variable: append might have created a new value if the original had no more space.

我们调整切片最大是多少?这是由它的容量决定的,在本例中,是10。你可能会想这没有从本质上解决数能固定和长度的问题。事实上,append是非常特殊的。当底层数组满了,它会创建一个新的数组,并把数值拷贝过来(PHP, Python, Ruby, JavaScript等也是这么做的)。这也就是为什么,我们上面的代码,使用了append之后,我们需要把append的返回值重新赋值给scores的原因:append会产生一个新的值如果原始的空间不足。

If I told you that Go grew arrays with a 2x algorithm, can you guess what the following will output?

如果我告诉你说Go是近两倍的算法来增长数组,那下面的代码会输出什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main() {
scores := make([]int, 0, 5)
c := cap(scores)
fmt.Println(c)

for i := 0; i < 25; i++ {
scores = append(scores, i)

// if our capacity has changed,
// Go had to grow our array to accommodate the new data
if cap(scores) != c {
c = cap(scores)
fmt.Println(c)
}
}
}

The initial capacity of scores is 5. In order to hold 20 values, it’ll have to be expanded 3 times with a capacity of 10, 20 and finally 40.

scores最初的容量是5。为了容纳20个值,它将会扩展3次,分别是10,20和40。

As a final example, consider:

来思考下最后一个例子:

1
2
3
4
5
func main() {
scores := make([]int, 5)
scores = append(scores, 9332)
fmt.Println(scores)
}

Here, the output is going to be [0, 0, 0, 0, 0, 9332]. Maybe you thought it would be [9332, 0, 0, 0, 0]? To a human, that might seem logical. To a compiler, you’re telling it to append a value to a slice that already holds 5 values.

这里,输出是[0, 0, 0, 0, 0, 9332]。可能你会想它应该是[9332, 0, 0, 0, 0]?对于人来说这可能符合逻辑。对于编译器来说,你告诉它的就是要扩展一个已经有5个值的切片。

Ultimately, there are four common ways to initialize a slice:

最后,有四种常用的方式来初始化一个切片:

1
2
3
4
names := []string{"leto", "jessica", "paul"}
checks := make([]bool, 10)
var names []string
scores := make([]int, 0, 20)

When do you use which? The first one shouldn’t need much of an explanation. You use this when you know the values that you want in the array ahead of time.

何时用哪一种呢?第一种不需要过多的解释。当你知道所有的值并且你要的是数组头的时候使用。

The second one is useful when you’ll be writing into specific indexes of a slice. For example:

当你需要写入切片的指定索引时,第二种就很有用。比如:

1
2
3
4
5
6
7
func extractPowers(saiyans []*Saiyans) []int {
powers := make([]int, len(saiyans))
for index, saiyan := range saiyans {
powers[index] = saiyan.Power
}
return powers
}

The third version is a nil slice and is used in conjunction with append, when the number of elements is unknown.

当知道有多少元素的时候,就可使用第三种是一个空切片和append配合使用。

The last version lets us specify an initial capacity; useful if we have a general idea of how many elements we’ll need.

当我们对需要多少元素有多少了解时使用最后一种方式来指定初始容量。

Even when you know the size, append can be used. It’s largely a matter of preference:

即使你知道了大小,append依然可以使用。这很大程度上是一个偏好问题:

1
2
3
4
5
6
7
func extractPowers(saiyans []*Saiyans) []int {
powers := make([]int, 0, len(saiyans))
for _, saiyan := range saiyans {
powers = append(powers, saiyan.Power)
}
return powers
}

Slices as wrappers to arrays is a powerful concept. Many languages have the concept of slicing an array. Both JavaScript and Ruby arrays have a slice method. You can also get a slice in Ruby by using [START..END] or in Python via [START:END]. However, in these languages, a slice is actually a new array with the values of the original copied over. If we take Ruby, what’s the output of the following?

切片做为数组的封装是一个很有用的概念。许多语言有数组切片的概念。JavaScript和Ruby的数组都有slice方法。你可以对通过[START..END]或者在Python中用[START:END]的方式取得一个切片。但是,在这些语言中切片是对原数组的拷贝。如果我们使用Ruby,下面的代码会输出什么?

1
2
3
4
scores = [1,2,3,4,5]
slice = scores[2..4]
slice[0] = 999
puts scores

The answer is [1, 2, 3, 4, 5]. That’s because slice is a completely new array with copies of values. Now, consider the Go equivalent:

答案是[1, 2, 3, 4, 5]。那是因为slice是一个复制了所有值的全新数组。现在,来看下Go相同的代码:

1
2
3
4
scores := []int{1,2,3,4,5}
slice := scores[2:4]
slice[0] = 999
fmt.Println(scores)

The output is [1, 2, 999, 4, 5].

输出是[1, 2, 999, 4, 5]

This changes how you code. For example, a number of functions take a position parameter. In JavaScript, if we want to find the first space in a string (yes, slices work on strings too!) after the first five characters, we’d write:

这改变了你的编码方式。比如,一些函数需要一个位置参数。在JavaScript中,如果我们需要找到字符串中第五个字符之后的第一个空格(是的,切片对字符串也是有效的!), 我们这样写:

1
2
haystack = "the spice must flow";
console.log(haystack.indexOf(" ", 5));

In Go, we leverage slices:

在Go中,我们使用切片:

1
strings.Index(haystack[5:], " ")

We can see from the above example, that [X:] is shorthand for from X to the end while [:X] is shorthand for from the start to X. Unlike other languages, Go doesn’t support negative values. If we want all of the values of a slice except the last, we do:

上面的例子,我们可以看到,[X:]从X到结束的简写,就如[:X]从开始到X的简写一样。和其他语言不同的是,Go不支持反向取值(这边感觉不对)。如果我们需要切片除了最后一个值以外的所有值,我们这样来写:

1
2
scores := []int{1, 2, 3, 4, 5}
scores = scores[:len(scores)-1]

The above is the start of an efficient way to remove a value from an unsorted slice:

以上是一种快速删除未排序的切片中的某个值的方法的开头:

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
scores := []int{1, 2, 3, 4, 5}
scores = removeAtIndex(scores, 2)
fmt.Println(scores)
}

func removeAtIndex(source []int, index int) []int {
lastIndex := len(source) - 1
//swap the last value and the value we want to remove
source[index], source[lastIndex] = source[lastIndex], source[index]
return source[:lastIndex]
}

Finally, now that we know about slices, we can look at another commonly used built-in function: copy. copy is one of those functions that highlights how slices change the way we code. Normally, a method that copies values from one array to another has 5 parameters: source, sourceStart, count, destination and destinationStart. With slices, we only need two:

最后,既然我们是学习了切片,我们来看另一个常用的内置函数: copycopy是能突出切片是如何改变我们的编码方式的函数之一。通常,数组间拷贝需要5个参数:source, sourceStart, count, destinationdestinationStart。使用切片只要两个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import (
"fmt"
"math/rand"
"sort"
)

func main() {
scores := make([]int, 100)
for i := 0; i < 100; i++ {
scores[i] = int(rand.Int31n(1000))
}
sort.Ints(scores)

worst := make([]int, 5)
copy(worst, scores[:5])
fmt.Println(worst)
}

Take some time and play with the above code. Try variations. See what happens if you change copy to something like copy(worst[2:4], scores[:5]), or what if you try to copy more or less than 5 values into worst?

花一些时间来执行上面的代码。多试几次。看看会发生什么,如果将代码改为copy(worst[2:4], scores[:5]),或者要拷贝比5更多或少的值到worst

映射(Maps)

Maps in Go are what other languages call hashtables or dictionaries. They work as you expect: you define a key and value, and can get, set and delete values from it.

Go中的映射在其他语言中叫哈希表或者字典。它们都如你想的一样:你定义一个键和值,然后你可以通过映射来删改查这些值。

Maps, like slices, are created with the make function. Let’s look at an example:

映射,和切片一样,是通过make函数来创建的。让我们来看一个例子:

1
2
3
4
5
6
7
8
9
func main() {
lookup := make(map[string]int)
lookup["goku"] = 9001
power, exists := lookup["vegeta"]

// prints 0, false
// 0 is the default value for an integer
fmt.Println(power, exists)
}

To get the number of keys, we use len. To remove a value based on its key, we use delete:

我们使用len来获取有多少键。要通过键来删除一个值,我们用delete

1
2
3
4
5
// returns 1
total := len(lookup)

// has no return, can be called on a non-existing key
delete(lookup, "goku")

Maps grow dynamically. However, we can supply a second argument to make to set an initial size:

映射是动态增长的。但是,我们可以通过make的第二个参数来设置它的初始大小:

1
lookup := make(map[string]int, 100)

If you have some idea of how many keys your map will have, defining an initial size can help with performance.

如果知道有多少值,指定初始大小可以有更好的性能表现。

When you need a map as a field of a structure, you define it as:

如果想要把映射做为结构体的一个字段,我们这样定义:

1
2
3
4
type Saiyan struct {
Name string
Friends map[string]*Saiyan
}

One way to initialize the above is via:

初始化的一种方式:

1
2
3
4
5
goku := &Saiyan{
Name: "Goku",
Friends: make(map[string]*Saiyan),
}
goku.Friends["krillin"] = ... //todo load or create Krillin

There’s yet another way to declare and initialize values in Go. Like make, this approach is specific to maps and arrays. We can declare as a composite literal:

在Go中有另一种申明和初始化的方式。和make一样,这对映射和数组都是有效的。我们可以像组合文字一样申明:

1
2
3
4
lookup := map[string]int{
"goku": 9001,
"gohan": 2044,
}

We can iterate over a map using a for loop combined with the range keyword:

我们可以通过forrange关键字来迭代映射:

1
2
3
for key, value := range lookup {
...
}

Iteration over maps isn’t ordered. Each iteration over a lookup will return the key value pair in a random order.

映射的迭代器是无序的。每个迭代器随机查找键值对。

指针和值(Pointers versus Values)

We finished Chapter 2 by looking at whether you should assign and pass pointers or values. We’ll now have this same conversation with respect to array and map values. Which of these should you use?

通过了解什么时候传值或指针我们结束了第二章。在数组和映射的值方面我们将碰到相同的问题。我们应该用哪一种?

1
2
3
a := make([]Saiyan, 10)
//or
b := make([]*Saiyan, 10)

Many developers think that passing b to, or returning it from, a function is going to be more efficient. However, what’s being passed/returned is a copy of the slice, which itself is a reference. So with respect to passing/returning the slice itself, there’s no difference.

许多开发人员会想对于一个函数来说是传b还是将它做为返回值更高效。然而,这里传递或者返回的都是一个切片的拷贝,它本身就是一个引用。所以就传递或者返回这个切片而言,没有什么区别。

Where you will see a difference is when you modify the values of a slice or map. At this point, the same logic that we saw in Chapter 2 applies. So the decision on whether to define an array of pointers versus an array of values comes down to how you use the individual values, not how you use the array or map itself.

当你改变一个切片或者映射的值时,你会看见不同。在这点上,同样的逻辑,我们在第二章看到已经适用。所以是否定义一个数组指针还是一个数组值主要归结于如何使用单个值,而不是你如何使用数组或者映射本身。

继续之前(Before You Continue)

Arrays and maps in Go work much like they do in other languages. If you’re used to dynamic arrays, there might be a small adjustment, but append should solve most of your discomfort. If we peek beyond the superficial syntax of arrays, we find slices. Slices are powerful and they have a surprisingly large impact on the clarity of your code.

在Go中数组和映射的工作方式与其他语言非常像。如果你用过动态数组,可能有会有一些需要调整,但是通过append可以解决所有的不适。如果我们抛开数组表面的语法,我们就会发现切片。切片是相当强大的,使用切片对你代码的整洁性有着非常巨大的影响。

There are edge cases that we haven’t covered, but you’re not likely to run into them. And, if you do, hopefully the foundation we’ve built here will let you understand what’s going on.

这里有一些边界例子我们没有涉及到,但是你不太可能遇见这些例子。另外,如果你遇到了,希望我们已经打下的基础能让你理解这是怎么回事。

第二章 - 结构体(Chapter 2 - Structures)

Go isn’t an object-oriented (OO) language like C++, Java, Ruby and C#. It doesn’t have objects nor inheritance and thus, doesn’t have the many concepts associated with OO such as polymorphism and overloading.

和C++, Java, Ruby以及C#不一样,Go并不是面向对象的语言。它没有对象、继承和其他一些和面向对象相关的概念,比各多态和重载。

What Go does have are structures, which can be associated with methods. Go also supports a simple but effective form of composition. Overall, it results in simpler code, but there’ll be occasions where you’ll miss some of what OO has to offer. (It’s worth pointing out that composition over inheritance is an old battle cry and Go is the first language I’ve used that takes a firm stand on the issue.)

Go有的就是结构体,可以直接绑定方法。Go支持简单便高效的组合。总的来说,它带来更简洁的代码,但在一些场合中失去OO的一些特性。(有必要指出 组合优于继承 是一个老早的争议,但Go是我用过的这么多语言中第一个立场这么坚定的。)

Although Go doesn’t do OO like you may be used to, you’ll notice a lot of similarities between the definition of a structure and that of a class. A simple example is the following Saiyan structure:

虽然Go确实不是你用过的OO样,但是你会发现结构体和类之间的很多相似之处。来看一个简单的例子,结构体Saiyan

1
2
3
4
type Saiyan struct {
Name string
Power int
}

We’ll soon see how to add a method to this structure, much like you’d have methods as part of a class. Before we do that, we have to dive back into declarations.
很快我们就会看到怎么往这个结构体中添加方法,就像你要类中添加方法一样。在那之前,我们先细看下结构体的声明.

声明和初始化(Declarations and Initializations)

When we first looked at variables and declarations, we looked only at built-in types, like integers and strings. Now that we’re talking about structures, we need to expand that conversation to include pointers.

我们最初学习变量和声明的时候,我们只用到内建类型,比如整形和字符串。现在我们讲的是结构体,我们要深入这个话题,包括指针。

The simplest way to create a value of our structure is:
创建一个结构体的值最简单的方式是:

1
2
3
4
goku := Saiyan{
Name: "Goku",
Power: 9000,
}

Note: The trailing , in the above structure is required. Without it, the compiler will give an error. You’ll appreciate the required consistency, especially if you’ve used a language or format that enforces the opposite.

注意:结构体中最后一个,是必需的。没有的话,编译器会报错。你会喜欢这种一致性要求,特别是如果你使用了强制性相反的语言或格式

We don’t have to set all or even any of the fields. Both of these are valid:
我们可以不给所有或者任何一个字段赋值。下面两种方式都是正确的:

1
2
3
4
5
6
goku := Saiyan{}

// or

goku := Saiyan{Name: "Goku"}
goku.Power = 9000

Just like unassigned variables have a zero value, so do fields.

和没有赋值的变量一样,没有赋值的字段默认为0值。

Furthermore, you can skip the field name and rely on the order of the field declarations (though for the sake of clarity, you should only do this for structures with few fields):

再者,你也可以省略字段的名字,按字段的顺序进行声明(尽管为了简洁起见,你尽量在结构体只有少量字段时才使用这种方式):

1
goku := Saiyan{"Goku", 9000}

What all of the above examples do is declare a variable goku and assign a value to it.

上面所有的例子所做的事件就是声明一个变量goku并给它赋一个值。

Many times though, we don’t want a variable that is directly associated with our value but rather a variable that has a pointer to our value. A pointer is a memory address; it’s the location of where to find the actual value. It’s a level of indirection. Loosely, it’s the difference between being at a house and having directions to the house.

很多时候,我们不想变量直接相关的值,而是一个指向指针的变量。指针是内存地址,它可以定位实际的值有哪里。这是一种间接层。简单点说,这好比是在房子里还是有房子地址的区别。

Why do we want a pointer to the value, rather than the actual value? It comes down to the way Go passes arguments to a function: as copies. Knowing this, what does the following print?

为什么我们确实需要指针,而不是实际的值?这是因为Go在函数中参数的传递方式是:值。了解了这个,下面的程序会打出来什么?

1
2
3
4
5
6
7
8
9
func main() {
goku := Saiyan{"Goku", 9000}
Super(goku)
fmt.Println(goku.Power)
}

func Super(s Saiyan) {
s.Power += 10000
}

The answer is 9000, not 19000. Why? Because Super made changes to a copy of our original goku value and thus, changes made in Super weren’t reflected in the caller. To make this work as you probably expect, we need to pass a pointer to our value:

答案是9000,而示是19000。为什么呢?因为Super改变的是goku的一个拷贝的值,Super中的改变不会在调用者中显示出来。要如你期望的方式运行,我们需要传入一个指针:

1
2
3
4
5
6
7
8
9
func main() {
goku := &Saiyan{"Goku", 9000}
Super(goku)
fmt.Println(goku.Power)
}

func Super(s *Saiyan) {
s.Power += 10000
}

We made two changes. The first is the use of the & operator to get the address of our value (it’s called the address of operator). Next, we changed the type of parameter Super expects. It used to expect a value of type Saiyan but now expects an address of type *Saiyan, where *X means pointer to value of type X. There’s obviously some relation between the types Saiyan and *Saiyan, but they are two distinct types.

我们做了两处修改。第一处是使用了&操作符来获取值的地址(它被称为 取址操作符)。接下来,我们修改Super期望的参数类型。原来它期望的是Saiyan值类型,而现在期望的是*Saiyan的地址类型,此处*X是指 指向X类型的指针Saiyan*Saiyan的类型有一些明显的关联,但是它们两是不同的类型。

Note that we’re still passing a copy of goku's value to Super it just so happens that goku's value has become an address. That copy is the same address as the original, which is what that indirection buys us. Think of it as copying the directions to a restaurant. What you have is a copy, but it still points to the same restaurant as the original.

需要指出的是,我们现在传递给Super参数的仍然是goku的值拷贝。只是现在goku的值变成了一个地址。这个地址拷贝和源地址相同。可以认为它类似一个指向餐厅方向的拷贝,这就间接服务于我们。虽然是一个拷贝,但是和源地址一样,也指向同一个餐厅。

We can prove that it’s a copy by trying to change where it points to (not something you’d likely want to actually do):

我们可以通过改变它的指向来证明这是个拷贝(虽然不是你想要的):

1
2
3
4
5
6
7
8
9
func main() {
goku := &Saiyan{"Goku", 9000}
Super(goku)
fmt.Println(goku.Power)
}

func Super(s *Saiyan) {
s = &Saiyan{"Gohan", 1000}
}

The above, once again, prints 9000. This is how many languages behave, including Ruby, Python, Java and C#. Go, and to some degree C#, simply make the fact visible.

上例,依然,打印9000。这和很多语言的行为是一样的,包括Ruby,Python,Java和C#。Go和C#一定程度是要样的,让这个更显而易见。

It should also be obvious that copying a pointer is going to be cheaper than copying a complex structure. On a 64-bit machine, a pointer is 64 bits large. If we have a structure with many fields, creating copies can be expensive. The real value of pointers though is that they let you share values. Do we want Super to alter a copy of goku or alter the shared goku value itself?

很明显拷贝一个指针比拷贝一个复杂的结构体开销小多了。在64位的机器上,一个指针是64位的大小。如果我们有一个有很多字段的结构体,创建一份拷贝开销是比较大的。指针的真正价值是通过它可以共享值。我们想通过Super去改变goku的拷贝或者改变共享的goku值本身?

All this isn’t to say that you’ll always want a pointer. At the end of this chapter, after we’ve seen a bit more of what we can do with structures, we’ll re-examine the pointer-versus-value question.

所有这些不是说你一直要用指针。本章末尾,当我们学到更多结构体的内容后,我们会重新审视指针和值类型的问题。

结构体上的函数(结构体的方法)(Functions on Structures)

We can associate a method with a structure:
我们可以为结构体关联一个方法:

1
2
3
4
5
6
7
8
type Saiyan struct {
Name string
Power int
}

func (s *Saiyan) Super() {
s.Power += 10000
}

In the above code, we say that the type *Saiyan is the receiver of the Super method. We call Super like so:

上面的代码,我们说*SaiyanSuper方法的 接收器 。我们能过这样的方式调用Super方法:

1
2
3
goku := &Saiyan{"Goku", 9001}
goku.Super()
fmt.Println(goku.Power) // will print 19001

构造器(Constructors)

Structures don’t have constructors. Instead, you create a function that returns an instance of the desired type (like a factory):

结构体没有构造器。你可创建一个函数来返回一个期望类型的实例来替代(像工厂一样):

1
2
3
4
5
6
func NewSaiyan(name string, power int) *Saiyan {
return &Saiyan{
Name: name,
Power: power,
}
}

This pattern rubs a lot of developers the wrong way. On the one hand, it’s a pretty slight syntactical change; on the other, it does feel a little less compartmentalized.

这种方式导致很多开发者犯错。一方面,它有一些轻微的语法变化;另一方面,它有一点让人感觉不好区分。

Our factory doesn’t have to return a pointer; this is absolutely valid:

我们的工厂没有必要返回指针;下面的代码完全正确:

1
2
3
4
5
6
func NewSaiyan(name string, power int) Saiyan {
return Saiyan{
Name: name,
Power: power,
}
}

创建(New)

Despite the lack of constructors, Go does have a built-in new function which is used to allocate the memory required by a type. The result of new(X) is the same as &X{}:

尽管没有构造器,但是Go有内置的new函数可以用来分配一下指定类弄的内存。new(X)&X{}的效果是一样的:

1
2
3
goku := new(Saiyan)
// same as
goku := &Saiyan{}

Which you use is up to you, but you’ll find that most people prefer the latter whenever they have fields to initialize, since it tends to be easier to read:

使有哪种方式看你自己的喜好,但是你会发现当字段需要初始化时,大多数人喜欢使用后一种方式,因为这样更易读:

1
2
3
4
5
6
7
8
9
10
goku := new(Saiyan)
goku.name = "goku"
goku.power = 9001

//vs

goku := &Saiyan {
name: "goku",
power: 9000,
}

Whichever approach you choose, if you follow the factory pattern above, you can shield the rest of your code from knowing and worrying about any of the allocation details.

无论你使用哪种方式,如果你使用上面的工厂模式,接下来的代码中你可以不要了解和担心任何分配的细节。

结构体字段(Fields of a Structure)

In the example that we’ve seen so far, Saiyan has two fields Name and Power of types string and int, respectively. Fields can be of any type – including other structures and types that we haven’t explored yet such as arrays, maps, interfaces and functions.

目前为止我们看到的例子中,Saiyan有两个字段,一个字符串类型的Name和一个整型Power。字段可以是任何类型————包括其他的结构体和暂时我们没有讲到的类型,例如数组、字典、接口和函数。

For example, we could expand our definition of Saiyan:

例如,我们可以这样扩展Saiyan的定义:

1
2
3
4
5
type Saiyan struct {
Name string
Power int
Father *Saiyan
}

which we’d initialize via:

我们可以通过下面的方式初始化:

1
2
3
4
5
6
7
8
9
gohan := &Saiyan{
Name: "Gohan",
Power: 1000,
Father: &Saiyan {
Name: "Goku",
Power: 9001,
Father: nil,
},
}

组合(Composition)

Go supports composition, which is the act of including one structure into another. In some languages, this is called a trait or a mixin. Languages that don’t have an explicit composition mechanism can always do it the long way. In Java:

Go支持组合,就是将一个结构体包含在另一个之中。在一些语言中,这被叫特性或混入。没有明确的组合机制的语言,要实现这个特性就比较繁杂。在Java中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Person {
private String name;

public String getName() {
return this.name;
}
}

public class Saiyan {
// Saiyan is said to have a person
private Person person;

// we forward the call to person
public String getName() {
return this.person.getName();
}
...
}

This can get pretty tedious. Every method of Person needs to be duplicated in Saiyan. Go avoids this tediousness:

这样会相当的冗长。每个Person的方法都需要在Saiyan中复制一遍。Go可以避免这种冗长:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type Person struct {
Name string
}

func (p *Person) Introduce() {
fmt.Printf("Hi, I'm %s\n", p.Name)
}

type Saiyan struct {
*Person
Power int
}

// and to use it:
goku := &Saiyan{
Person: &Person{"Goku"},
Power: 9001,
}
goku.Introduce()

The Saiyan structure has a field of type *Person. Because we didn’t give it an explicit field name,
we can implicitly access the fields and functions of the composed type.
However, the Go compiler did give it a field name, consider the perfectly valid:

Saiyan结构体中有一个*Person类型的字段。因为我们没有给他一个显示的字段名,我们可以隐示的访问组合类型的所有字段和函数。
但出于完全有效的考虑,Go编辑器确实有给它分配一个字段名。

1
2
3
4
5
goku := &Saiyan{
Person: &Person{"Goku"},
}
fmt.Println(goku.Name)
fmt.Println(goku.Person.Name)

Both of the above will print “Goku”.

上面的两个输出都是”Goku”。

Is composition better than inheritance? Many people think that it’s a more robust way to share code.
When using inheritance, your class is tightly coupled to your superclass and you end up focusing on hierarchy rather than behavior.
组合是不优于继承?很多人认为这是一种更健壮的共享代码的方式。当使用继承时,你的类和超类捆绑在一起,你最终关注继承而不是行为。

重载(Overloading)

While overloading isn’t specific to structures, it’s worth addressing. Simply, Go doesn’t support overloading.
For this reason, you’ll see (and write) a lot of functions that look like Load, LoadById, LoadByName and so on.

值得指出的是,结构体没有重载。简而言之,Go不支持重载。因为这个原因,你会看到(和写)很多像 Load, LoadById, LoadByName这样的函数。

However, because implicit composition is really just a compiler trick, we can “overwrite” the functions of a composed type.
For example, our Saiyan structure can have its own Introduce function:
但是,因为非显示的组合是一个编辑器技巧,我们可以“重写”组合类型的函数。比如, 我们的Saiyan结构体可以有自己的Introduce方法:

1
2
3
func (s *Saiyan) Introduce() {
fmt.Printf("Hi, I'm %s. Ya!\n", s.Name)
}

The composed version is always available via s.Person.Introduce().

组合版本中使用s.Person.Introduce()也是一样的。

指针和值(Pointers versus Values)

As you write Go code, it’s natural to ask yourself should this be a value, or a pointer to a value? There are two pieces of good news.
First, the answer is the same regardless of which of the following we’re talking about:

当你写Go代码的时候,你很自然的就会问你自己*这应该是要用值还是要用指针?*下面是两个好消息。首先,下面讨论的这些话题是没有什么差别的:

  • A local variable assignment

  • Field in a structure

  • Return value from a function

  • Parameters to a function

  • The receiver of a method

  • 局部变量赋值

  • 结构体中的字段

  • 函数的返回值

  • 函数的参数

  • 方法的接收者

Secondly, if you aren’t sure, use a pointer.
其次,如果你不确定,就用指针好了。

As we already saw, passing values is a great way to make data immutable
(changes that a function makes to it won’t be reflected in the calling code).
Sometimes, this is the behavior that you’ll want but more often, it won’t be.

就如我们看到的那样,传值是一个让值不可变的好方法(函数内的改变不会影响调用代码中的值)。有些时候,我们却时希望如此,可常常不是这样的。
Even if you don’t intend to change the data, consider the cost of creating a copy of large structures.
Conversely, you might have small structures, say:

就算你不想改变值,想一下创建一个大结构体拷贝的开销。相反地,你可能有一个小结构体,例如:

1
2
3
4
type Point struct {
X int
Y int
}

In such cases, the cost of copying the structure is probably offset by being able to access X and Y directly, without any indirection.

在这种情况下,拷贝结构体的开销可以通过偏移量来直接访问XY,而不是间接访问。
Again, these are all pretty subtle cases. Unless you’re iterating over thousands or possibly tens of thousands of such points, you wouldn’t notice a difference.

再次指出,这些只是非常微妙的情况。除非你要访问成千上百个这样的点,否则你不会察觉有任何的不同。

继续之前(Before You Continue)

From a practical point of view, this chapter introduced structures, how to make an instance of a structure a receiver of a function, and added pointers to our existing knowledge of Go’s type system. The following chapters will build on what we know about structures as well as the inner workings that we’ve explored.

本章从实践的角度来看,介绍了结构体,以及如何创建方法接收器的结构体实例,并在我们现有的Go知识体系中引入了指针。
下面的章节将基于我们所知道的结构体知识来探讨其内部运行机制。

第一章 - 基础(Chapter 1 - The Basics)

Go is a compiled, statically typed language with a C-like syntax and garbage collection. What does that mean?

Go是一门静态类型、编译型语言,有类C风格的语法和垃圾回收机制。这意味着什么呢?

编译(Compilation)

Compilation is the process of translating the source code that you write into a lower level language – either assembly (as is the case with Go), or some other intermediary language (as with Java and C#).

编译将你写的源代码转换成一种更低级的语言————可能是汇编(如Go就是这样),或者其他中间语言(如Java和C#)的过程。

Compiled languages can be unpleasant to work with because compilation can be slow. It’s hard to iterate quickly if you have to spend minutes or hours waiting for code to compile. Compilation speed is one of the major design goals of Go. This is good news for people working on large projects as well as those of us used to a quick feedback cycle offered by interpreted languages.

因为编译可能很慢,使用编译型语言可能不是个令人愉快的事情。很难实现快速迭代因为你不得不花几分钟甚至几个小时的时间来等待编绎完成。编译速度是Go设计时的一个主要目标。这对于大项目的开发人员来说是个好消息,就像我们可以使用解释语言提供的快速反馈周期。

Compiled languages tend to run faster and the executable can be run without additional dependencies (at least, that’s true for languages like C, C++ and Go which compile directly to assembly).

编译型语言往往运行得更快,不需要额外的依赖也可以正常运行(至少,像C、C++和Go这样直接编译成汇编的语言来说,就是如此。)

静态类型(Static Typing)

Being statically typed means that variables must be of a specific type (int, string, bool, []byte, etc.). This is either achieved by specifying the type when the variable is declared or, in many cases, letting the compiler infer the type (we’ll look at examples shortly).

静态类型是指变量必须指定一个类型(整型、字符串、布尔、字节数组等等)。可以在申明变量的时候指定数据类型,也可以,大多数情况是让编译器来推断类型(我们将会在接下来的例子中看到)。

There’s a lot more that can be said about static typing, but I believe it’s something better understood by looking at code. If you’re used to dynamically typed languages, you might find this cumbersome. You’re not wrong, but there are advantages, especially when you pair static typing with compilation. The two are often conflated. It’s true that when you have one, you normally have the other but it isn’t a hard rule. With a rigid type system, a compiler is able to detect problems beyond mere syntactical mistakes as well as make further optimizations.

关于静态类型还有很多可以介绍,但我相信理解它更好的方式是阅读代码。如果你习惯于动态语言,你可能觉得这比较麻烦。没错,不过静态类型也有优势,尤其是和编译相结合的时候。静态类型和编译这两者经常被混为一谈。虽然这不是硬性的规定,但通常情况下,有其一就必有其二。在严格类型系统中,编译器除了能够检测出单纯的语法错误问题还能做出进一步的优化。

类C语法(C-Like Syntax)

Saying that a language has a C-like syntax means that if you’re used to any other C-like languages such as C, C++, Java, JavaScript and C#, then you’re going to find Go familiar – superficially, at least. For example, it means && is used as a boolean AND, == is used to compare equality, { and } start and end a scope, and array indexes start at 0.

说一门语言有一个类C的语法意味着,如果你使用的任何其他类似C语言,如C,C ++,Java,JavaScript以及C#,那么你会发现Go的相似之处————至少从表面上看。比如,&&表示逻辑与,==表示相等判断,{}}是作用域的开始和结束,以及数组从0开始索引。

C-like syntax also tends to mean semi-colon terminated lines and parentheses around conditions. Go does away with both of these, though parentheses are still used to control precedence. For example, an if statement looks like this:

类C语法也往往使用分号结束行和条件表达式用括号括起来。Go没用使用这两种方式,尽管依然使用括号来控制优先权。比如,一个if表达式看起来像这样:

1
2
3
if name == "Leto" {
print("the spice must flow")
}

And in more complicated cases, parentheses are still useful:

在更复杂的情况下,括号依然有用:

1
2
3
if (name == "Goku" && power > 9000) || (name == "gohan" && power < 4000)  {
print("super Saiyan")
}

Beyond this, Go is much closer to C than C# or Java - not only in terms of syntax, but in terms of purpose. That’s reflected in the terseness and simplicity of the language which will hopefully start to become obvious as you learn it.

除此之外,Go比C#或者Java更接近C,不仅在语法方面,还在用途方面。这体现在语言风格的简洁和简单,随着不断深入学习,你会越来越明显的体会到这种特性。

垃圾回收机制(Garbage Collected)

Some variables, when created, have an easy-to-define life. A variable local to a function, for example, disappears when the function exits. In other cases, it isn’t so obvious – at least to a compiler. For example, the lifetime of a variable returned by a function or referenced by other variables and objects can be tricky to determine. Without garbage collection, it’s up to developers to free the memory associated with such variables at a point where the developer knows the variable isn’t needed. How? In C, you’d literally free(str); the variable.

一些变量,在创建时就有明确的生命周期。如函数内的局部变量,当函数结束时就消失了。在另一些情况下,就没有这么明显了,起码对编译器来说是这样。比如函数中返回的变量,变量的引用和对象的引用的生命周期就很难判断了。没有垃圾回收机制的情况下,这依赖于开发人员在不需要这些变量时进行内存的释放。怎么实现?例如在c中,你需要正确的去释放一个变量的内存free(str);

Languages with garbage collectors (e.g., Ruby, Python, Java, JavaScript, C#, Go) are able to keep track of these and free them when they’re no longer used. Garbage collection adds overhead, but it also eliminates a number of devastating bugs.

有垃圾回收机制的语言(如Ruby、Python、Java、JavaScript、C#、Go)能记录变量并在不使用时进行释放。垃圾回收机制增加了开销,但也杜绝了一些破坏性的bug。

运行Go代码(Running Go Code)

Let’s start our journey by creating a simple program and learning how to compile and execute it. Open your favorite text editor and write the following code:

让我们创建一个简单的例子来学习如何编译和运行它,来开始我们的Go学习之旅。打开你最喜欢的文本编辑器,输入如下的代码:

1
2
3
4
5
package main

func main() {
println("it's over 9000!")
}

Save the file as main.go. For now, you can save it anywhere you want; we don’t need to live inside Go’s workspace for trivial examples.

将文件保存为main.go。开始,你可以将它保存在任何你想要的地方;作为简单的例子,我们还不需要深入理解Go的工作区。

Next, open a shell/command prompt and change the directory to where you saved the file. For me, that means typing cd ~/code.

接下来,打开一个shell/命令行,然后将目录切换到你保存文件的位置。对我来,输入cd ~/code就可以了。

Finally, run the program by entering:

最后,能过输入如下命令来运行程序:

1
go run main.go

If everything worked, you should see it’s over 9000!.

如果一切正常,你会看到 *it’s over 9000!*。

But wait, what about the compilation step? go run is a handy command that compiles and runs your code. It uses a temporary directory to build the program, executes it and then cleans itself up. You can see the location of the temporary file by running:

等等,那编译过程呢?go run是一个方便的编译和执行代码的命令。它使用临时目录来生成程序和运行,然后清理。通过下面的代码你可以查看临时文件所在位置:

1
go run --work main.go

To explicitly compile code, use go build:

要显示的编译代码,使用go build:

1
go build main.go

This will generate an executable main which you can run. On Linux / OSX, don’t forget that you need to prefix the executable with dot-slash, so you need to type ./main.

这会生成一个可执行的main程序。在Linux/OSX中,不要忘记在可执行文件前面加上点和反斜杠,所有你需要输入./main

While developing, you can use either go run or go build. When you deploy your code however, you’ll want to deploy a binary via go build and execute that.

在开发的时候,你可以使用go run或者go build。但当你发布的时候,你需要使用go build来生成可执行文件并运行它。

主函数(Main)

Hopefully, the code that we just executed is understandable. We’ve created a function and printed out a string with the built-in println function. Did go run know what to execute because there was only a single choice? No. In Go, the entry point to a program has to be a function called main within a package main.

但愿,我们刚刚的执行的代码是可以理解的。我们创建了一个函数,它调用内置的println函数打印一个字符串。难道是因为只有一个选择,所以go run才知道要执行什么吗?不是的,在Go语言中,程序的入口是main包中的main函数。

We’ll talk more about packages in a later chapter. For now, while we focus on understanding the basics of Go, we’ll always write our code within the main package.

后续章节我们会介绍更多包的内容。现在,为了我们着重理解Go的基础知识,我们只在main包中写代码。

If you want, you can alter the code and change the package name. Run the code via go run and you should get an error. Then, change the name back to main but use a different function name. You should see a different error message. Try making those same changes but use go build instead. Notice that the code compiles, there’s just no entry point to run it. This is perfectly normal when you are, for example, building a library.

如果你愿意,你也可以修改代码并改变包名89。并使用go run去执行,你会得到一个错误信息。然后,将包名改成main,但是函数名不叫main,再次运行代码,你会得到一个不同的错误信息。使用go build进行相同的操作,注意编译代码时,这里没有运行代码的入口点。这是很正常的,例如当你编译一个库时。

包导入(Imports)

Go has a number of built-in functions, such as println, which can be used without reference. We can’t get very far though, without making use of Go’s standard library and eventually using third-party libraries. In Go, the import keyword is used to declare the packages that are used by the code in the file.

Go有一些内建函数是不需要引入就可以直接使用,如println。不利用Go标准库和第三方类库的话,我们不能走得很远。在Go中,使用import关键字来申明代码中使用的包。

Let’s change our program:
让我们来修改下程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import (
"fmt"
"os"
)

func main() {
if len(os.Args) != 2 {
os.Exit(1)
}
fmt.Println("It's over ", os.Args[1])
}

Which you can run via:

通过下面的命令来运行它:

1
go run main.go 9000

We’re now using two of Go’s standard packages: fmt and os. We’ve also introduced another built-in function len. len returns the size of a string, or the number of values in a dictionary, or, as we see here, the number of elements in an array. If you’re wondering why we expect 2 arguments, it’s because the first argument – at index 0 – is always the path of the currently running executable. (Change the program to print it out and see for yourself.)

我们用了两个Go的标准包:fmtos。我们引入了另一个内建函数lenlen返加字符串的长度,或者字典的个数,再或者,如这个例子,数组元素的个数。如果你想知道为什么我们期望是两个参数,这是因为索引为0的第一个参数是当前可执程序的路径(你可以自己修改代码将它打印出来看看)。

You’ve probably noticed we prefix the function name with the package, e.g., fmt.Println. This is different from many other languages. We’ll learn more about packages in later chapters. For now, knowing how to import and use a package is a good start.

你可能已经注意到了函数名之前的包名了,比如:fmt.Println,这和其他很多语言不同。后续章节我们会学习更多包的内容。现在,知道如何导入和使用包就好了。

Go is strict about importing packages. It will not compile if you import a package but don’t use it. Try to run the following:

Go对包导入很严格。如果导入了包,但没有使用是不能通过编译的。试试运行下面的代码:

1
2
3
4
5
6
7
8
9
package main

import (
"fmt"
"os"
)

func main() {
}

You should get two errors about fmt and os being imported and not used. Can this get annoying? Absolutely. Over time, you’ll get used to it (it’ll still be annoying though). Go is strict about this because unused imports can slow compilation; admittedly a problem most of us don’t have to this degree.

你会看到两个错误信息,显示fmtos包被导入但是没有被使用。这会让人烦吗?绝对的。随着时间的推移,你会习惯(虽然还是烦人)。Go之所以在这点上这么严格是因为导入未使用的包会影响编译速度。不可否认的是,我们大多数人都没有这个深度。

Another thing to note is that Go’s standard library is well documented. You can head over to http://golang.org/pkg/fmt/#Println to learn more about the Println function that we used. You can click on that section header and see the source code. Also, scroll to the top to learn more about Go’s formatting capabilities.

令一个值得注意的地方就是Go的标准包的文档很完善。你可以通过http://golang.org/pkg/fmt/#Println 来学习更多我们用到过的Println的内容。你可以点击章节标题来查看源码。也可以滚动到顶部来查看更多关于Go格式化的功能。

If you’re ever stuck without internet access, you can get the documentation running locally via:
如果你不能访问网络,你可以通过下面的方面运行本地的文档:

1
godoc -http=:6060

and pointing your browser to http://localhost:6060

然后通过http://localhost:6060来浏览。

变量和声明(Variables and Declarations)

It’d be nice to begin and end our look at variables by saying you declare and assign to a variable by doing x = 4. Unfortunately, things are more complicated in Go. We’ll begin our conversation by looking at simple examples. Then, in the next chapter, we’ll expand this when we look at creating and using structures. Still, it’ll probably take some time before you truly feel comfortable with it.

通过x = 4就能声明和赋值变量 ,对你来说可能是一个好的开始和结束。不幸的是,Go中要复杂一些。我们将通过简单的例子来开始我们的话题。然后,我们会在下一章节中,在讲解分创建的使用结构体的时候,我们会展开来讲解。但是,你可以需要花一些时间来适应它。

You might be thinking Woah! What can be so complicated about this? Let’s start looking at some examples.

你可能会觉得 *哇!为什么这么复杂?*让我们来看些例子吧。

The most explicit way to deal with variable declaration and assignment in Go is also the most verbose:

在Go中最直接也是最繁索的变量声明和赋值方式是:

1
2
3
4
5
6
7
8
9
10
11
package main

import (
"fmt"
)

func main() {
var power int
power = 9000
fmt.Printf("It's over %d\n", power)
}

Here, we declare a variable power of type int. By default, Go assigns a zero value to variables. Integers are assigned 0, booleans false, strings "" and so on. Next, we assign 9000 to our power variable. We can merge the first two lines:

这里,我们声明了一个int类型的变量power。默认情况下,Go给变量赋为0值。整型赋为0,布尔型赋为false,字符串赋为""等等。接着,我们给变量power赋值为9000。我们可以合并开始的这两行:

1
var power int = 9000

Still, that’s a lot of typing. Go has a handy short variable declaration operator, :=, which can infer the type:

依然,还是需要很多的输入。Go有更方便的变量声明操作符,:=,它可以推断类型。

1
power := 9000

This is handy, and it works just as well with functions:

这很方便,对函数也同样适用:

1
2
3
4
5
6
7
func main() {
power := getPower()
}

func getPower() int {
return 9001
}

It’s important that you remember that := is used to declare the variable as well as assign a value to it. Why? Because a variable can’t be declared twice (not in the same scope anyway). If you try to run the following, you’ll get an error.

记住:=用于声明和赋值变量这点很重要。为什么呢?因为一个变量不能被声明两次(在同一个作用域中)。如果你尝试运行下面的代码,你会看到一个错误信息。

1
2
3
4
5
6
7
8
9
func main() {
power := 9000
fmt.Printf("It's over %d\n", power)

// COMPILER ERROR:
// no new variables on left side of :=
power := 9001
fmt.Printf("It's also over %d\n", power)
}

The compiler will complain with no new variables on left side of :=. This means that when we first declare a variable, we use := but on subsequent assignment, we use the assignment operator =. This makes a lot of sense, but it can be tricky for your muscle memory to remember when to switch between the two.

编译器会抱错提示 :=左边不是新的变量。这就是说我们一开始用:=来声明一个变量,接下来我们需要用=来给变量赋值。这很用意义,但是对你的记忆力来说是一个负担,因为你要记住这两者之间切换的时机。

If you read the error message closely, you’ll notice that variables is plural. That’s because Go lets you assign multiple variables (using either = or :=):

如果你仔细看错误信息,你会发现 变量用了复数形式。因为Go支持多个变量同时赋值(使用=或者:=):

1
2
3
4
func main() {
name, power := "Goku", 9000
fmt.Printf("%s's power is over %d\n", name, power)
}

As long as one of the variables is new, := can be used. Consider:

只要有一个变量是新的就可以使用:= 操作符。例如:

1
2
3
4
5
6
7
func main() {
power := 1000
fmt.Printf("default power is %d\n", power)

name, power := "Goku", 9000
fmt.Printf("%s's power is over %d\n", name, power)
}

Although power is being used twice with :=, the compiler won’t complain the second time we use it, it’ll see that the other variable, name, is a new variable and allow :=. However, you can’t change the type of power. It was declared (implicitly) as an integer and thus, can only be assigned integers.

虽然变量power使用了:=,但是编译器不会在第2次使用:=时报错,因为这里有一个新变量name,允许使用:=。但你不能改变power的类型。它已经被声明(隐式的)为整型,所以只能用整数来赋值。

For now, the last thing to know is that, like imports, Go won’t let you have unused variables. For example,

最后,和包导入一样,Go不允许未使用的变量。例如:

1
2
3
4
func main() {
name, power := "Goku", 1000
fmt.Printf("default power is %d\n", power)
}

won’t compile because name is declared but not used. Like unused imports it’ll cause some frustration, but overall I think it helps with code cleanliness and readability.

不会被编译因为变量name声明了但没有使用。和包导入一样会带来一些挫败感,但总的来说,这是为了代码的简洁和可读性。

There’s more to learn about declaration and assignments. For now, remember that you’ll use var NAME TYPE when declaring a variable to its zero value, NAME := VALUE when declaring and assigning a value, and NAME = VALUE when assigning to a previously declared variable.

声明和赋值还有内容需要学习。现在,只要记住,用var NAME TYPE来声明变量并赋0值,用NAME := VALUE声明变量并赋值,和用NAME = VALUE给已声明的变量赋值。

函数声明(Function Declarations)

This is a good time to point out that functions can return multiple values. Let’s look at three functions: one with no return value, one with one return value, and one with two return values.

现在是一个好的时机来指出函数是可以有多返回值的。让我们来看3个函数:一个没有返回值,一个有一个返回值,另一个有两个返回值。

1
2
3
4
5
6
7
8
func log(message string) {
}

func add(a int, b int) int {
}

func power(name string) (int, bool) {
}

We’d use the last one like so:
我们像这样来使用最后一个函数:

1
2
3
4
value, exists := power("goku")
if exists == false {
// handle this error case
}

Sometimes, you only care about one of the return values. In these cases, you assign the other values to _:

有时,你可能只关心其中一个返回值。在这种情况下,你可以把其他值赋为_:

1
2
3
4
_, exists := power("goku")
if exists == false {
// handle this error case
}

This is more than a convention. _, the blank identifier, is special in that the return value isn’t actually assigned. This lets you use _ over and over again regardless of the returned type.

这不仅仅是一个约定。_,空白标识符,尤其在用在返回值时它没有真正的赋值。无论返回值是什么类型你都可以使用_

Finally, there’s something else that you’re likely to run into with function declarations. If parameters share the same type, we can use a shorter syntax:

最后,你可能遇到一些不同的函数声明方式。如果函数的参数类型都相同,那么可以用以下更简洁的方式:

1
2
3
func add(a, b int) int {

}

Being able to return multiple values is something you’ll use often. You’ll also frequently use _ to discard a value. Named return values and the slightly less verbose parameter declaration aren’t that common. Still, you’ll run into all of these sooner than later so it’s important to know about them.

你会常常用到函数多返回值这个特性。你也会经常使用_去舍弃一个返回值。具名返回值和无名参数声明并不常见。但是迟早你都会遇到,最好对他们都有所了解。

继续之前(Before You Continue)

We looked at a number of small individual pieces and it probably feels disjointed at this point. We’ll slowly build larger examples and hopefully, the pieces will start to come together.

现在我们已经学习了许多的小知识点,你可能会觉得有点脱节。我们会逐步构建一个更大的例子,有望将这些小知识点串联起来。

If you’re coming from a dynamic language, the complexity around types and declarations might seem like a step backwards. I don’t disagree with you. For some systems, dynamic languages are categorically more productive.

如果你是来自动态类型语言的开发人员,你可能会觉得Go的变量类型和声明的复杂是一种倒退。我同意你的看法。对于一些系统,动态类型的语言绝对更有效率。

If you’re coming from a statically typed language, you’re probably feeling comfortable with Go. Inferred types and multiple return values are nice (though certainly not exclusive to Go). Hopefully as we learn more, you’ll appreciate the clean and terse syntax.

如果你是来自静态类型语言的开发人员,你可能会习惯使用Go。类型推断和多返回值是如此的美好(尽管这不是Go独有的)。希望随着我们不断深入的学习,你会喜欢上Go干净和简洁的语法。

入门

如果你想试试Go,你可以使用Go运行环境,它可以让你无需安装任何东西就可以在网上运行代码。这也是在Go论坛如StackOverflow中寻求帮助时,分享Go代码最常用的方式。

安装Go很简单。你可以从源码安装,但我建议你用一个已编译好的可执行文件。当你打开下载页面时,你可以看到不同平台的安装包。让我们避免使用这些并学习自己如何配置Go。然后你就会发现这不难。

除了简单的例子,Go被设计成只有你的代码在工作区时才能正常运行。这个工作区是有binpkgsrc三个子目录的文件夹。你也许会想强制Go去适应你自己的风格 - 别想。

通常,我把我的项目放在~/code目录下。例如,~/code/blog是我的博客。对于Go来说,我的工作区是~/code/go,而我Go版本的博客会在/code/go/src/blog下。因为这经常用到,我做了个符号链接,通过`/code/blog`来访问它:

ln -s ~/code/go/src/blog ~/code/blog

总之,无论你把你的项目放在哪,创建一个Go目录包含src子目录来放置你的项目。

OSX / Linux

下载你平台对应tar.gz。OSX,你需要关心go#.#.#.darwin-amd64-osx10.8.tar.gz,其中#.#.#表示Go的最新版本。

通过tar -C /usr/local -xzf go#.#.#.darwin-amd64-osx10.8.tar.gz将文件解压到/usr/local目录。

设置两个环境变量:

  1. GOPATH指向你的工作区,对我来说是$HOME/code/go
  2. 我们需要将Go可执行文件的路径添加到PATH

你可以通过shell来完成设置:

echo 'export GOPATH=$HOME/code/go' >> $HOME/.profile
echo 'export PATH=$PATH:/usr/local/go/bin' >> $HOME/.profile

为了使这些变量生效,你需要关闭并重新打开你的Shell,或者运行source $HOME/.profile命令。

输入go version,你会看到类似这样的输出go version go1.3.3

Windows

下载最新的zip文件。如果你是x64系统,你需要下载go#.#.#.windows-amd64.zip,其中#.#.#表示Go的最新版本。

将其解压到你想要的位置。c:\Go是个不错和选择。

Set up two environment variables:
设置两个环境变理:

  1. GOPATH 指向你的工作区. 可以是这样的目录c:\users\goku\work\go
  2. 添加 c:\Go\bin 至你的PATH环境变量中。

环境变量可以通过系统控制面板中的环境变量按钮中的高级标签页来设置。某些版本的Windows通过System控制面板里面的高级系统Settings选项提供该控制面板。

打开一个命令行窗口,输入go version,你会看到类似这样的输出go version go1.3.3 windows/amd64

引言

每当我学习一门新语言的时候总是爱恨交加。一方面,语言是如此的重要,以至于一点小的变化对我们产生的不可估量的影响。在你的程序和可以重新定义你对其他语言的期望的时候,你会有一个持久的效果。同时,语言的设计是增量的。学习新的关键字、类型体系、编码方式以及新类库、通讯和范式需要很多工作,但又很难评估。相对学习其他必学的东西,学习新语言让我们常常感觉是对时间的投入很大。

也就是说,我们要进步。我们必须愿意采用渐进的方式,又一次因为,语言是我们的基础。虽然变化是增量的,但它们往往范围很广,它们影响效率,可读性、性能、可测试性、依赖管理、错误处理、文档、分析(监控?)、通讯、标准库等等。除了说千刀万剐我们还能说什么?

这留给我们一个重要的问题:为什么选Go?对我来说,有两个令人信服的理由。首先它是一门相对简单的语言,还自带相对简单的标准库。在很多方面,Go的增量本质简化了我们已经看到的在过去几十年引入的语言的复杂性。另一个原因对于很多开发者来说,它会完善你的军火库。

Go被构建为一个系统语言(比如操作系统、设备驱动)并且面向C和C++开发人员。纵观Go的社群,我非常确信,应用开发者,而非系统开发者已经成为Go的主要使用者。为什么?我不能代表系统开发者,但是我们建设的网站、服务、桌面应用等等,这些面向新兴需求可归结为一类介于低层次的系统应用程序和更高级别的应用程序之间的系统。

也许它是一个消息,缓存,大数据分析,命令行接口,日志或监控。我不知如何标记它,但是在我的职业身涯中,由于系统的复杂性不断和频繁并发数以万计的增长,定制的基础设施类系统成为一个不断增长的需求。你可以用Ruby或Python或别的东西(确实很多人这么做)来构建这样的系统,如果使用Go,这些系统可以有一个更严格的类型系统和更高的性能优势。同样,您可以使用Go建立网站(确实很多人这样做),但,为了更大的回旋余地,我还是喜欢使用Node或Ruby的来构建这类系统。

还有一些Go的长处。比如,运行Go程序时没有依赖。你不需要担心你的用户是否已经安装了Ruby或者JVM和它们的版本。因为这个原因,Go作为命令行程序或者需要分发的其他类型的实用程序(比如日志收集器)开发语言,越来越流行了。

简单地说,学习Go是一种有效利用你的时间。你将不必花费很长时间来学习甚至掌握go,你可以通过一些实践来达成。

关于作者

我曾犹豫写这本小册子有几个原因。首先Go有自已的文档,特别是高效Go,它很实在(实用?)。

另一个原因是我写一本关于语言的书时的不适。当我写MongoDB小册子的时候,你可以假设很多读者理解基本的关系型数据库和模型。写Redis小册子的时候,你可以假设从一个熟悉的键值存储开始。

当想到摆在面前的段落和章节的时候,我知道我不能做这样的假设。你要花多少时间来讲解接口,因为对于一些人来说这是一个新概念,而另个一些人已经不需用再了解了。最终,我会感到欣慰如果你让我知道有些部分是太浅或过于详细。考虑一下这本书的价格。