Go Little Book - 第四章 - 代码组织和接口

第四章 - 代码组织和接口(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的文件并添加如下代码:

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的文件。它的代码如下:

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的文件。它的内容如下:

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目录输入如下内容可以运行代码:

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应该看起来像这样:

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的代码为:

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。我们通过引入一个包含共享结构的另一个包来解决这个问题。你的目录结构看起来像这样:

$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

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文件中有这样的一个函数:

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/命令提示符,输入:

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包,我们需要像这样来导入它:

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:

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

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.

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

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

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

type Server struct {
  logger Logger
}

or a function parameter (or return value):

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

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的语言中,我们必须显示的申请明一个类实现了一个接口:

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来使用。这减少了使用接口时的繁索:

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之类的参数时,你会发现你自己感激作者的要求不是太苛刻。

转载于:https://my.oschina.net/airhead/blog/2980485

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值