CrazyAirhead

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

0%

第五章 - 特点(珍品?)(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小册子的时候,你可以假设从一个熟悉的键值存储开始。

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

关于本书

许可

本书使用署名-非商业性使用-相同方式共享4.0许可。你不需要为本书付费。你可以自由的拷贝、发布、修改或展示本书。但是,我要求本书必须用我本人(Karl Seguin)的署名,同时不能作为经济用途。

你可在以下链接中查看到该许可的所有内容:

http://creativecommons.org/licenses/by-nc-sa/4.0/

最新版本

本书最新源码的放在:http://github.com/karlseguin/the-little-go-book

中文译本最新源码的放在:http://github.com/enderjo/the-little-go-book

为什么

自己用Hexo来做博客而用到了nodejs和npm。但npm生成的node-modules层次过深,时常打开文件夹时会很慢。

后来了解到了yarn,在yarn发布之前,所有Nodejs开发者用的都是npm包管理工具,而npm工具存在挺多难以忍受的诟病,包括安装速度慢、每次都要在线重新安装等问题,而yarn也是为了解决npm当前所存在的问题而出现的。

为了方便自己使用,整理了下安装和基本操作。

安装

  • 如果原先有npm工具的话,安装yarn很简单,只需要一行命令即可:
1
npm install -g yarn

更换安装源

安装yarn之后默认的包安装源是https://registry.yarnpkg.com,可用查看命令

1
yarn config get registry 

若想提高yarn安装的速度,可将包安装源修改为cnpm的安装源,执行以下命令即可

1
2
3
4

yarn config set registry https://registry.npm.taobao.org --global
yarn config set disturl https://npm.taobao.org/dist --global

操作及对比

操作 npm yarn
初始化项目 npm init yarn init
安装依赖操作 npm install/link yarn install/link
安装某个依赖,并且默认保存到package npm install xxx —save yarn add xxx
移除某个依赖项目 npm uninstall xxx —save yarn remove xxx
安装某个开发时依赖项目 npm install –save -dev xxx yarn add xxx —dev
更新某个依赖项目 npm update –save xxx yarn upgrade xxx
安装某个全局依赖项目 npm install -g xxx yarn global add xxx
发布/登录/登出,一系列NPM Registry操作 npm publish/login/logout yarn publish/login/logout
运行某个命令 npm run/test yarn run/test
查看帮助 npm -h yarn -h

注意事项

yarn global

yarn 的全局安装并不是加 -g 或者 –global 这样的参数,它使用 yarn global 命令。用 yarn global –help 可以看到子命令列表

这些子命令的用法和非 global 的同名命令用法差不多。

指定bin目录

对于一些带 CLI 的模块,通过 yarn global add 可能会出问题,yarn global bin查看目录,并把该目录添加到Path中。

指定registry但没有效果

  1. yarn.lock中包含的registry信息与设置的registry不同,删除yarn.lock重新生成即可。

参考

yarn —— Nodejs包新管理工具

使用 yarn global 代替 npm -g

“yarn config set registry” is not work · Issue #4862 · yarnpkg/yarn (github.com)

背景

最近刚好在做小程序的开发,需要用到本地调试,刚开始时使用Natapp的免费遂道,勉强能开发(域免),后面买了付费遂道,域名不变开发顺畅起来。但要真机调试时还是不行,微信不认三级域名,需要购买他的二级域名服务。转念一想还不如自己搭建一个呢,成本可控,也只要折腾一次就好,反正也能学东西。于是也就有了本文。

准备

  • 一台云主机,可在购买阿里云,开放对应端口。
  • 一个已经备案的域名(备案过程真是个痛苦的过程,各种资料,各种步骤,还可能审不过),并且做了二级域名泛解析。(本文假设你已经会域名解析配置)
  • Go http tunnel,可在这里下载(https://github.com/mmatczuk/go-http-tunnel/releases)
  • Let’s Encrypt工具。

配置

https证书

可参看这里这里
此处唯一要注意的就是有个域名解析的交互过程,这个在上述资料中也有说明。

记录下证书的生成位置如上文是/etc/letsencrypt/archive/newyingyong.cn,自动的生成路径可能不现。

http tunnel服务器

  • 服务器需要开放80,443,5223端口。
  • 根据自己的服务器系统下载对应版本。
  • 解压就可以了,进入目录。
  • 执行如下命令,以https证书参考资料证书地址为例。
    1
    tunneld -tlsCrt /etc/letsencrypt/archive/goldsyear.com/fullchain.pem -tlsKey /etc/letsencrypt/archive/goldsyear.com/privkey.pem

http tunnel客户端

  • 根据自己的服务器系统下载对应版本
  • 解压就可以了,进入目录。
  • 生成客户端证书openssl req -x509 -nodes -newkey rsa:2048 -sha256 -keyout client.key -out client.crt,并将证书放到解压目录
  • 创建tunnel.yml,默认读当前路径配置。
  • 参考样例:
    1
    2
    3
    4
    5
    6
    7
    server_addr: SERVER_IP:5223
    tunnels:
    webui:
    proto: http
    addr: localhost:8080
    auth: user:password
    host: goldsyear.com
    其中,server_addr为服务器地址,tunnels为开启的遂道列表,webui为实际的遂道名,正常可去掉auth配置,host中的域名与遂道名无直接关系。更多配置信息参看官网
  • 启动tunnel start-all

验证

在客户端启动最简单http服务,使用客户端配置的域名进行访问,如果正常访问,恭喜你。如果还有问题,请核查服务器或客户端配置。

推广

如果你觉得以上配置过于复杂。有以下两种方式供你选择,当然你可能有更好的选择。

  1. 可以使用Natapp,他提供免费的HTTPS的遂道,但提供的免费域名会一直变化。最好购买遂道和域名服务,付费时可以使用我的推广优惠码ADBE2C5C
  2. 如果你恰好刚起步,希望用于本地调试,可以加我微信号,也可提供相关服务支持。申请时请备注https tunnel技术支持微信