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:
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:
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:
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.
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).
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.
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.
If we wanted to, we could write an add function with the following signature:
如果我们愿意,我们可以通过下面声明方式写一个add函数:
1 2 3
funcadd(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) { caseint: fmt.Printf("a is now an int and equals %d\n", a) casebool, 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 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:
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.
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:
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
funcmain() { fmt.Println(process(func(a int, b int)int{ return a + b })) }
funcprocess(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.
第四章 - 代码组织和接口(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/.
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.
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.
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.
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.
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.
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:
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.
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:
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:
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.
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.
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.
Go ahead and change the name of the various functions, types and fields from the shopping code. For example, if you rename the Item'sPrice field to price, you should get an error.
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.
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:
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.
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.
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.
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:
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.
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:
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.
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.
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).
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.
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.
第三章 - 字典 ,数组和切片(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:
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.
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:
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:
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:
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 lenare overloaded. Go is a language that, to the frustration of some, makes use of features which aren’t exposed for developers to use.)
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:
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:
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.
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.
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:
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?
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:
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:
funcremoveAtIndex(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:
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?
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.
// 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:
We can iterate over a map using a for loop combined with the range keyword:
我们可以通过for和range关键字来迭代映射:
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?
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.
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.
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.
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.
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.
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.)
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:
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):
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?
funcmain() { goku := Saiyan{"Goku", 9000} Super(goku) fmt.Println(goku.Power) }
funcSuper(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:
funcmain() { goku := &Saiyan{"Goku", 9000} Super(goku) fmt.Println(goku.Power) }
funcSuper(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.
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.
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
funcmain() { goku := &Saiyan{"Goku", 9000} Super(goku) fmt.Println(goku.Power) }
funcSuper(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.
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?
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.
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:
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{}:
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:
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.
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:
// 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:
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.
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方法:
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:
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.
在这种情况下,拷贝结构体的开销可以通过偏移量来直接访问X和Y,而不是间接访问。 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 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#).
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.
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).
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.
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-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:
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.
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.
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.
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:
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:
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.
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.
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.
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.
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.
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.)
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.
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" )
funcmain() { }
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.
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.
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.
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" )
funcmain() { 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:
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
funcmain() { power := getPower() }
funcgetPower()int { return9001 }
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.
funcmain() { 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
funcmain() { 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
funcmain() { 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.
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
funcmain() { 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.
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.
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
funcadd(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.
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.
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.