Skip to content

Latest commit

 

History

History
942 lines (652 loc) · 35.9 KB

File metadata and controls

942 lines (652 loc) · 35.9 KB

六、Go 包和程序结构

第 5 章函数在 Go中涵盖了函数,这是代码组织的基本抽象层次,使代码具有可寻址性和可重用性。本章以 Go 包为中心展开讨论,继续进行抽象。正如本文将详细介绍的,包是存储在源代码文件中的语言元素的逻辑分组,可以共享和重用,如以下主题所述:

  • Go 包
  • 创建包
  • 建筑包
  • 包可见性
  • 导入包
  • 包初始化
  • 创建程序
  • 远程包

Go 包

与其他语言类似,Go 源代码文件被分为可编译和可共享单元,称为包。但是,所有 Go 源文件必须属于一个包(没有默认包的概念)。这种严格的方法允许 Go 通过支持约定而不是配置来保持其编译规则和包解析规则的简单性。让我们深入了解软件包的基本原理、创建、使用和推荐实践。

了解 Go 包

在我们深入研究包的创建和使用之前,从高层次上了解包的概念是至关重要的,这有助于指导以后的讨论。Go 包是用于封装可重用的相关概念的代码组织的物理和逻辑单元。按照惯例,存储在同一目录中的一组源文件被视为同一包的一部分。下面演示了一个简单的目录树,其中每个目录表示一个包含一些源代码的包:

 foo
 ├── blat.go
 └── bazz
 ├── quux.go
 └── qux.go 

戈朗。

虽然不是要求,但建议在每个源文件中设置包的名称,以匹配文件所在目录的名称。例如,源文件blat.go被声明为包foo的一部分,如下代码所示,因为它存储在名为foo的目录中:

package foo 

import ( 
   "fmt" 
   "foo/bar/bazz" 
) 

func fooIt() { 
   fmt.Println("Foo!") 
   bazz.Qux() 
} 

golang.fyi/ch06-foo/foo/blat.go

文件quux.goqux.go都是包bazz的一部分,因为它们位于具有该名称的目录中,如以下代码段所示:

|
package bazz
import "fmt"
func Qux() {
  fmt.Println("bazz.Qux")
}

golang.fyi/ch06-foo/foo/bazz/qux.go |

package bazz
import "fmt"
func Quux() {
  Qux()fmt.Println("gazz.Quux")
}

golang.fyi/ch06-foo/foo/bazz/qux.go |

工作空间

在讨论软件包时,另一个需要理解的重要概念是Go**工作区。工作空间只是一个任意目录,在某些任务(如编译)期间用作解析包的命名空间。按照惯例,Go 工具希望在工作区目录中有三个专门命名的子目录:srcpkgbin。这些子目录分别存储 Go 源文件和所有构建的包工件。

建立一个存放 Go 包的静态目录位置具有以下优点:

  • 接近零配置的简单设置
  • 通过将代码搜索减少到已知位置来快速编译
  • 工具可以轻松创建代码和包工件的源代码图
  • 从源代码自动推断和解析可传递依赖项
  • 项目设置可以进行移植,并且易于分发

以下是我笔记本电脑上 Go 工作区的部分(简化)树形布局,突出显示了三个子目录binpkgsrc

|
/home/vladimir/Go/   
├── bin   
│  ├── circ   
│  ├── golint...   
├── pkg   
│  └── linux_amd64    
│    ├── github.com   
│    │  ├── golang   
│    │  │  └── lint.a   
│    │  └── vladimirvivien   
│    │    └── learning-go   
│    │      └── ch06   
│    │        ├── current.a...       ...    
└── src   
  ├── github.com   
  │  ├── golang   
  │  │  └── lint   
  │  │    ├── golint   
  │  │    │  ├── golint.go...   ... ...   
  │  └── vladimirvivien   
  │    └── learning-go   
  │      ├── ch01...   
  │      ├── ch06   
  │      │  ├── current   
  │      │  │  ├── doc.go   
  │      │  │  └── lib.go   
  ...     ...      

|

示例工作区目录

  • bin:这是一个自动生成的目录,用于存储已编译的 Go 可执行工件(也称为程序或命令)。当 Go 工具编译和安装可执行程序包时,它们将被放置在此目录中。前面的示例工作区显示了两个列出的二进制文件circgolint。建议将此目录添加到操作系统的PATH环境变量中,以使您的命令在本地可用。
  • pkg:此目录也是自动生成的,用于存储生成的包工件。当 Go 工具构建和安装不可执行的软件包时,它们作为对象文件(带有[T1]后缀)存储在基于目标操作系统和体系结构的具有名称模式的子目录中。在示例工作区中,对象文件位于子目录linux_amd64下,这表示此目录中的对象文件是为运行在 64 位体系结构上的 Linux 操作系统编译的。
  • src:这是一个用户创建的目录,用于存储 Go 源代码文件。src下的每个子目录都映射到一个包。src是解析所有导入路径的根目录。Go 工具搜索该目录以解析编译期间代码中引用的包或其他依赖于源路径的活动。上图中的示例工作区显示了两个包:github.com/golang/lint/golint/github.com/vladimirvivien/learning-go/ch06/current

您可能想知道工作区示例中显示的包路径中的[T0]前缀。值得注意的是,软件包目录没有命名要求(参见命名软件包部分)。包可以有任意名称。但是,Go 推荐某些有助于全局命名空间解析和包组织的约定。

创建工作区

创建工作区非常简单,只需设置一个名为GOPATH的操作系统环境,并为其分配工作区目录位置的根路径。例如,在 Linux 机器上,工作区的根目录为/home/username/Go,工作区将设置为:

$> export GOPATH=/home/username/Go 

设置GOPATH环境变量时,可以指定多个存储包的位置。每个目录由一个依赖于操作系统的路径分隔符分隔(换句话说,Linux/Unix 为冒号,Windows 为分号),如下所示:

$> export GOPATH=/home/myaccount/Go;/home/myaccount/poc/Go

解析包名时,Go 工具将搜索GOPATH中列出的所有位置。然而,Go 编译器将只在分配给GOPATH的第一个目录位置存储编译后的工件,如对象和二进制文件。

通过简单地设置 OS 环境变量来配置工作区的能力具有巨大的优势。它使开发人员能够在编译时动态设置工作空间,以满足特定的工作流需求。例如,开发人员可能希望在合并未经验证的代码分支之前对其进行测试。他或她可能想建立一个临时工作区,按照如下方式构建代码(Linux):$> GOPATH=/temporary/go/workspace/path go build

导入路径

在继续讨论设置和使用包的细节之前,最后一个需要介绍的重要概念是导入路径的概念。工作区路径$GOPATH/src下的每个包的相对路径构成一个称为包的import path的全局标识符。这意味着在给定的工作区中,任何两个包都不能具有相同的导入路径值。

让我们回到前面的简化目录树。例如,如果我们将工作区设置为一些任意路径值,例如GOPATH=/home/username/Go

/home/username/Go
└── foo
 ├── ablt.go
 └── bazz
 ├── quux.go
 └── qux.go 

从上述示例工作区中,包的目录路径映射到各自的导入路径,如下表所示:

| **目录路径** | **导入路径** | | `/home/username/Go/foo` |
"foo"   

| | /home/username/Go/foo/bar |

"foo/bar"   

| | /home/username/Go/foo/bar/bazz |

"foo/bar/bazz"   

|

创建包

到目前为止,本章已经涵盖了 Go 包的基本概念;现在是时候深入研究一下包中包含的 Go 代码的创建了。Go 包的主要用途之一是将公共逻辑抽象出来并聚合为可共享的代码单元。本章前面提到,目录中的一组 Go 源文件被视为一个包。虽然这在技术上是正确的,但 Go 包的概念不仅仅是将一堆文件推送到一个目录中。

为了帮助说明我们第一个软件包的创建,我们将使用[T1]github.com/vladimirviven/learning-go/ch06[T2]中的示例源代码。该目录中的代码定义了一组函数,以帮助使用欧姆定律计算电气值。下面显示了构成示例包的目录的布局(假设它们保存在某个工作区目录$GOPATH/src中):

|
github.com/vladimirvivien/learning-go/ch06   
├── current   
│  ├── curr.go   
│  └── doc.go   
├── power   
│  ├── doc.go   
│  ├── ir   
│  │  └── power.go   
│  ├── powlib.go   
│  └── vr   
│    └── power.go   
├── resistor   
│  ├── doc.go   
│  ├── lib.go   
│  ├── res_equivalence.go   
│  ├── res.go   
│  └── res_power.go   
└── volt   
  ├── doc.go   
  └── volt.go   

|

欧姆定律示例的封装布局

上一个目录树中的每个目录都包含一个或多个定义和实现函数的 Go 源代码文件,以及其他源代码元素,它们将被安排到包中并可重用。下表总结了从前面的工作区布局中提取的导入路径和包信息:

| **导入路径** | **包装** | | “github.com/vladimirviven/learning-go/ch06/**current** | `current` | | “github.com/vladimirviven/learning-go/ch06/**power** | `power` | | “github.com/vladimirviven/learning-go/ch06/**power/ir**” | `ir` | | “github.com/vladimirviven/learning-go/ch06/**power/vr**” | `vr` | | “github.com/vladimirviven/learning-go/ch06/**电阻器** | `resistor` | | “github.com/vladimirviven/learning-go/ch06/**volt**” | `volt` |

虽然没有命名要求,但命名包目录以反映其各自的用途是明智的。从上表中可以看出,示例中的每个包都被命名为表示电气概念,例如电流、功率、电阻器和电压。命名包部分将进一步详细介绍包命名约定。

申报包裹

Go 源文件必须声明自己是包的一部分。这是使用package条款完成的,作为 Go 源文件中的第一个法律声明。声明的包由package关键字和名称标识符组成。下面显示来自volt包的源文件volt.go

package volt 

func V(i, r float64) float64 { 
   return i * r 
} 

func Vser(volts ...float64) (Vtotal float64) { 
   for _, v := range volts { 
         Vtotal = Vtotal + v 
   } 
   return 
} 

func Vpi(p, i float64) float64 { 
   return p / i 
} 

Golang.fyi/ch06/was/was.go

源文件中的包标识符可以设置为任意值。与 Java 不同,包的名称并不反映源文件所在的目录结构。虽然对包名没有任何要求,但公认的惯例是将包标识符命名为与文件所在目录相同的名称。在我们前面的源代码清单中,使用标识符volt声明包,因为文件存储在volt目录中。

多文件包

包的逻辑内容(源代码元素,如类型、函数、变量和常量)可以跨多个 Go 源文件进行物理扩展。包目录可以包含一个或多个 Go 源文件。例如,在下面的示例中,包resistor被不必要地分割到多个 Go 源文件中,以说明这一点:

|
package resistor   

func recip(val float64) float64 {   
   return 1 / val   
}   

golang.fyi/ch06/电阻器/lib.go | |

  package resistor   

func Rser(resists ...float64) (Rtotal float64) {   
   for _, r := range resists {   
         Rtotal = Rtotal + r   
   }   
   return   
}   

func Rpara(resists ...float64) (Rtotal float64) {   
   for _, r := range resists {   
         Rtotal = Rtotal + recip(r)   
   }   
   return   
}   

golang.fyi/ch06/res_equivalance.go | |

package resistor   

func R(v, i float64) float64 {   
   return v / i   
}   

golang.fyi/ch06/res.go 电阻器 | |

package resistor   

func Rvp(v, p float64) float64 {   
   return (v * v) / p   
}   

golang.fyi/ch06/res_power.go |

包中的每个文件必须具有具有相同名称标识符的包声明(在本例中为resistor。Go 编译器将把所有源文件中的所有元素缝合在一起,在单个范围内形成一个逻辑单元,可供其他包使用。

必须指出,如果给定目录中的所有源文件的包声明不一致,编译将失败。这是可以理解的,因为编译器希望目录中的所有文件都是同一个包的一部分。

命名包

如前所述,Go 希望工作区中的每个包都有一个唯一的完全限定的导入路径。您的程序可能有您想要的任意多个包,并且您的包结构可以在工作区中任意深度。然而,惯用的 Go 为包的命名和组织规定了一些规则,以简化包的创建和使用。

使用全局唯一的名称空间

首先,在全局上下文中完全限定包的导入路径是一个好主意,特别是如果您计划与其他人共享代码。考虑使用唯一标识您或您的组织的命名空间方案启动导入路径的名称。例如,公司*Acme,Inc.*可能会选择以acme.com/apps开头所有 Go 包名称。因此,包的完全限定导入路径为"acme.com/apps/foo/bar"

在本章后面,我们将看到在将 Go 与 GitHub 等源代码存储库服务集成时如何使用包导入路径。

将上下文添加到路径

接下来,在为包设计命名方案时,使用包的路径为包名称添加上下文。名称中的上下文应以泛型开头,并从左到右变得更具体。例如,让我们参考电源包的导入路径(来自前面的示例)。功率值的计算分为三个子包,如下所示:

  • github.com/vladimirvivien/learning-go/ch06/**power**
  • github.com/vladimirvivien/learning-go/ch06/**power/ir**
  • github.com/vladimirvivien/learning-go/ch06/**power/vr**

父路径power包含具有更广泛上下文的包成员。子包irvr包含的成员更具体,上下文范围更窄。此命名模式在 Go 中大量使用,包括以下内置包:

  • crypto/md5
  • net/http
  • net/http/httputil
  • reflect

请注意,包深度为 1 是一个完全合法的包名(请参见[T0]),只要它同时捕获了上下文和它所做工作的本质。再次强调,保持简单。避免在命名空间中嵌套超过三个深度的包。如果您是一个习惯于使用长嵌套包名的 Java 开发人员,这种诱惑会特别强烈。

使用短名称

在查看内置 Go 包的名称时,您会注意到,与其他语言相比,这些名称非常简洁。在 Go 中,包被认为是实现一组特定的密切相关功能的代码集合。因此,包的导入路径应该简洁,并反映它们的功能,而不会过长。我们的示例源代码通过使用短名称(如伏特、功率、电阻、电流)命名包目录来说明这一点。在各自的上下文中,每个目录名都确切地说明了包的作用。

短名称规则严格应用于 Go 的内置包中。例如,以下是 Go 内置包中的几个包名:loghttpxmlzip。每个名称都很容易识别包的用途。

短包名的优点是可以减少较大代码基中的击键次数。然而,使用简短的通用包名也有一个缺点,即容易发生导入路径冲突,大型项目中的开发人员(或开源库的开发人员)可能最终在其代码中使用相同的流行名称(换句话说,logutildb等等)。正如我们将在本章后面看到的,这可以使用named导入路径来处理。

建筑工程包

Go 工具通过应用某些约定和合理的默认值来降低编译代码的复杂性。虽然对 Go 的构建工具的全面讨论超出了本节(或本章)的范围,但了解buildinstall工具的用途和用途是很有用的。通常,构建和安装工具的使用如下所示:

$>去构建<包导入路径>

import path可以明确提供,也可以完全省略。build工具接受表示为完全限定路径或相对路径的import path。给定一个正确设置的工作区,以下是编译前面示例中的包volt的所有等效方法:

$> cd $GOPATH/src/github.com/vladimirvivien/learning-go
$> go build ./ch06/volt 
$> cd $GOPATH/src/github.com/vladimirvivien/learning-go/ch06
$> go build ./volt 
$> cd $GOPATH/src/github.com/vladimirvivien/learning-go/ch06/volt
$> go build . 
$> cd $GOPATH/src/ 
$> go build github.com/vladimirvivien/learning-go/ch06/current /volt

上面的go build命令将编译目录volt中找到的所有 Go 源文件及其依赖项。此外,还可以使用附加到导入路径的通配符参数在给定目录中构建所有包和子包,如下所示:

$> cd $GOPATH/src/github.com/vladimirvivien/learning-go/ch06
$> go build ./...

前一个将构建目录$GOPATH/src/github.com/vladimirvivien/learning-go/ch06中找到的所有包和子包。

安装包

默认情况下,build 命令将其结果输出到工具生成的临时目录中,该目录在生成过程完成后丢失。要真正生成可用的工件,必须使用install工具保存已编译对象文件的副本。

install工具与构建工具具有完全相同的语义:

$> cd $GOPATH/src/github.com/vladimirvivien/learning-go/ch06
$> go install ./volt

除编译代码外,还将结果保存并输出到工作区位置$GOPATH/pkg,如下所示:

$GOPATH/pkg/linux_amd64/github.com/vladimirvivien/learning-go/
└── ch06
 └── volt.a

生成的对象文件(具有.a扩展名)允许重用包,并与工作区中的其他包进行链接。在本章后面,我们将研究如何编译可执行程序。

包装可视性

无论声明为包的一部分的源文件数量如何,在包级别声明的所有源代码元素(类型、变量、常量和函数)都共享一个公共范围。因此,编译器不允许在整个包中多次重新声明元素标识符。假设两个源文件都是同一个包$GOPATH/src/foo的一部分,让我们使用以下代码片段来说明这一点:

|
package foo   

var (   
  bar int = 12   
)   

func qux () {   
  bar += bar   
}   

foo/file1.go |

package foo   

var bar struct{   
  x, y int   
}   

func quux() {   
  bar = bar * bar   
}   

foo/file2.go |

非法变量标识符重新声明

虽然它们位于两个单独的文件中,但在 Go 中,标识符为[T0]的变量声明是非法的。由于文件是同一个包的一部分,因此两个标识符具有相同的作用域,因此会发生冲突。

函数标识符也是如此。Go 不支持在同一范围内重载函数名。因此,无论函数的签名如何,函数标识符被多次使用都是非法的。如果我们假设以下代码出现在同一个包中的两个不同源文件中,则以下代码段将是非法的:

|
package foo   

var (   
  bar int = 12   
)   

func qux () {   
  bar += bar   
}   

foo/file1.go |

package foo   

var (   
  fooVal int = 12   
)   

func qux (inc int) int {   
  return fooVal += inc   
}   

foo/file1.go |

非法函数标识符重新声明

在前面的代码片段中,函数名标识符qux使用了两次。即使两个函数具有不同的签名,编译器也会使编译失败。解决此问题的唯一方法是更改名称。

包成员可见性

包的用处在于它能够将其源元素公开给其他包。控制包元素的可见性很简单,并遵循以下规则:大写标识符将自动导出。这意味着任何带有大写标识符的类型、变量、常量或函数都可以从声明它的包的外部自动看到。

参考前面描述的欧姆定律示例,以下说明了软件包resistor(位于github.com/vladimirviven/learning-go/ch06/resistor中)中的该功能:

| **代码** | **说明** | |
package resistor   

func R(v, i float64) float64 {   
   return v / i   
}   

| 功能R自动导出,可从其他软件包访问:resistor.R() | |

package resistor   

func recip(val float64) float64 {   
   return 1 / val   
}   

| 函数标识符recip全部为小写,因此不导出。虽然可以在其自身范围内访问该函数,但从其他包中看不到该函数。 |

值得重申的是,同一包中的成员始终彼此可见。在 Go 中,没有像在其他语言中一样复杂的 private、friend、default 等可见性结构。这使开发人员可以专注于正在实现的解决方案,而不是建模可见性层次结构。

进口包装

在这一点上,您应该很好地理解包是什么,它做什么,以及如何创建包。现在,让我们看看如何使用包导入和重用其成员。在其他几种语言中,关键字import用于从外部包导入源代码元素。它允许导入源访问导入包中的导出元素(请参阅本章前面的包范围和可见性部分)。进口条款的一般格式如下:

导入【包名标识符】<导入路径>

请注意,导入路径必须用双引号括起来。import语句还支持可选的包标识符,可用于显式命名导入的包(稍后讨论)。导入语句也可以作为导入块写入,如以下格式所示。当列出两个或多个导入包时,此选项非常有用:

进口(

【包名标识符】<导入路径>

*以下源代码片段显示了前面介绍的欧姆定律示例中的导入声明块:

import ( 
   "flag" 
   "fmt" 
   "os" 

   "github.com/vladimirvivien/learning-go/ch06/current" 
   "github.com/vladimirvivien/learning-go/ch06/power" 
   "github.com/vladimirvivien/learning-go/ch06/power/ir" 
   "github.com/vladimirvivien/learning-go/ch06/power/vr" 
      "github.com/vladimirvivien/learning-go/ch06/volt" 
) 

golang.fyi/ch06/main.go

通常,导入包的名称标识符会被省略,如上所述。Go 然后将导入路径的最后一个目录的名称应用为导入包的名称标识符,如下表中某些包的名称标识符所示:

| **导入路径** | **包装名称** | | `flag` | `flag` | | `github.com/vladimirvivien/learning-go/ch06/current` | `current` | | `github.com/vladimirvivien/learning-go/ch06/power/ir` | `ir` | | `github.com/vladimirvivien/learning-go/ch06/volt` | `volt` |

点符号用于访问导入包的导出成员。例如,在下面的源代码片段中,从导入的包"github.com/vladimirvivien/learning-go/ch06/volt"调用了方法volt.V()

... 
import "github.com/vladimirvivien/learning-go/ch06/volt" 
func main() { 
   ... 
   switch op { 
   case "V", "v": 
         val := volt.V(i, r) 
  ... 
} 

golang.fyi/ch06/main.go

指定包标识符

如前所述,import声明可以显式声明导入的名称标识符,如以下导入片段所示:

import res "github.com/vladimirvivien/learning-go/ch06/resistor"

按照前面描述的格式,名称标识符放置在导入路径之前,如前面的代码段所示。命名包可以用作缩短或自定义包名称的方法。例如,在大量使用某个包的大型源文件中,这是减少击键的一个受欢迎的特性。

为包指定名称也是避免给定源文件中包标识符冲突的一种方法。可以设想导入两个或多个具有不同导入路径的包,这些包解析为相同的包名。例如,您可能需要使用来自不同库的两个不同日志系统记录信息,如以下代码段所示:

package foo 
import ( 
   flog "github.com/woom/bat/logger" 
   hlog "foo/bar/util/logger" 
) 

func main() { 
   flog.Info("Programm started") 
   err := doSomething() 
   if err != nil { 
     hlog.SubmitError("Error - unable to do something") 
   } 
} 

如前一段所述,默认情况下,两个日志记录包将解析为相同的名称标识符"logger"。若要解决此问题,必须为至少一个导入的包分配名称标识符以解决名称冲突。在上一个示例中,两个导入路径都使用有意义的名称命名,以帮助理解代码。

点标识符

可以选择为包分配一个点(句点)作为其标识符。当import语句使用点标识符(.作为导入路径时,会导致导入包的成员在范围内与导入包的成员合并。因此,可以引用导入的成员,而无需附加限定符。因此,如果使用以下源代码片段中的点标识符导入包logger,则在从 logger 包访问导出的成员函数SubmitError时,将忽略包名:

package foo 

import ( 
   . "foo/bar/util/logger" 
) 

func main() { 
   err := doSomething() 
   if err != nil { 
     SubmitError("Error - unable to do something") 
   } 
} 

虽然此功能有助于减少重复击键,但不鼓励使用此功能。通过合并包的范围,它更有可能遇到标识符冲突。

空白标识符

导入包时,要求其一个成员在导入代码中至少引用一次。否则将导致编译错误。虽然此功能有助于简化包依赖项解析,但它可能会很麻烦,尤其是在开发代码的早期阶段。

使用空白标识符(类似于变量声明)会使编译器绕过这一要求。例如,下面的代码段导入内置包fmt;但是,它从不在后续源代码中使用它:

package foo 
import ( 
   _ "fmt" 
   "foo/bar/util/logger" 
) 

func main() { 
   err := doSomething() 
   if err != nil { 
     logger.Submit("Error - unable to do something") 
   } 
} 

空白标识符的常见习惯用法是加载包以产生副作用。这取决于导入包时包的初始化顺序(参见下面的包初始化部分)。使用空白标识符将导致导入的包被初始化,即使其成员中没有一个成员可以引用。这在需要代码以静默方式运行某些初始化序列的上下文中使用。

包初始化

导入包时,在其成员准备好使用之前,它会经历一系列初始化序列。包级变量是使用依赖于词法范围解析的依赖性分析初始化的,这意味着变量是根据它们的声明顺序和它们彼此解析的可传递引用初始化的。例如,在下面的代码段中,foo包中解析的变量声明顺序为aybx

package foo 
var x = a + b(a) 
var a = 2 
var b = func(i int) int {return y * i} 
var y = 3 

Go 还使用了一个名为init的特殊函数,该函数不接受任何参数,也不返回任何结果值。它用于封装导入包时调用的自定义初始化逻辑。例如,下面的源代码显示了在resistor包中用于初始化函数变量Rpiinit函数:

package resistor 

var Rpi func(float64, float64) float64 

func init() { 
   Rpi = func(p, i float64) float64 { 
         return p / (i * i) 
   } 
} 

func Rvp(v, p float64) float64 { 
   return (v * v) / p 
} 

golang.fyi/ch06/res_power.go

在前面的代码中,init函数是在初始化包级变量后调用的。因此,init函数中的代码可以安全地依赖于声明的变量值处于稳定状态。init功能在以下方面具有特殊性:

  • 一个包可以定义多个init函数
  • 您不能在运行时直接访问声明的init函数
  • 它们按照它们在每个源文件中出现的词法顺序执行
  • init函数是一种很好的方法,可以将逻辑注入到在任何其他函数或方法之前执行的包中。

创建程序

到目前为止,在本书中,您已经学习了如何创建 Go 代码并将其打包为可重用的包。但是,包不能作为独立程序执行。要创建程序(也称为命令),请获取一个包并定义一个执行入口点,如下所示:

  • 声明(至少一个)源文件是名为main的特殊包的一部分
  • 声明一个函数名main()作为程序的入口点

函数main不接受任何参数,也不返回任何值。下面显示了欧姆定律示例中使用的main包的缩写源代码(来自前面)。它使用 Go 标准库中的包flag解析格式化为flag的程序参数:

package main 
import ( 
   "flag" 
   "fmt" 
   "os" 

   "github.com/vladimirvivien/learning-go/ch06/current" 
   "github.com/vladimirvivien/learning-go/ch06/power" 
   "github.com/vladimirvivien/learning-go/ch06/power/ir" 
   "github.com/vladimirvivien/learning-go/ch06/power/vr" 
   res "github.com/vladimirvivien/learning-go/ch06/resistor" 
   "github.com/vladimirvivien/learning-go/ch06/volt" 
) 

var ( 
   op string 
   v float64 
   r float64 
   i float64 
   p float64 

   usage = "Usage: ./circ <command> [arguments]\n" + 
     "Valid command { V | Vpi | R | Rvp | I | Ivp |"+  
    "P | Pir | Pvr }" 
) 

func init() { 
   flag.Float64Var(&v, "v", 0.0, "Voltage value (volt)") 
   flag.Float64Var(&r, "r", 0.0, "Resistance value (ohms)") 
   flag.Float64Var(&i, "i", 0.0, "Current value (amp)") 
   flag.Float64Var(&p, "p", 0.0, "Electrical power (watt)") 
   flag.StringVar(&op, "op", "V", "Command - one of { V | Vpi |"+   
    " R | Rvp | I | Ivp | P | Pir | Pvr }") 
} 

func main() { 
   flag.Parse() 
   // execute operation 
   switch op { 
   case "V", "v": 
    val := volt.V(i, r) 
    fmt.Printf("V = %0.2f * %0.2f = %0.2f volts\n", i, r, val) 
   case "Vpi", "vpi": 
   val := volt.Vpi(p, i) 
    fmt.Printf("Vpi = %0.2f / %0.2f = %0.2f volts\n", p, i, val) 
   case "R", "r": 
   val := res.R(v, i)) 
    fmt.Printf("R = %0.2f / %0.2f = %0.2f Ohms\n", v, i, val) 
   case "I", "i": 
   val := current.I(v, r)) 
    fmt.Printf("I = %0.2f / %0.2f = %0.2f amps\n", v, r, val) 
   ... 
   default: 
         fmt.Println(usage) 
         os.Exit(1) 
   } 
} 

golang.fyi/ch06/main.go

前面的清单显示了main包的源代码以及程序运行时执行的函数main的实现。欧姆定律程序接受指定要执行的电气操作的命令行参数(请参见以下访问程序参数部分)。函数init用于初始化程序标志值的解析。函数 main 被设置为一个大的 switch 语句块,用于根据所选标志选择要执行的正确操作。

访问程序参数

执行程序时,Go 运行时通过包变量os.Args将所有命令行参数作为切片提供。例如,当执行以下程序时,它将打印传递给该程序的所有命令行参数:

package main 
import ( 
   "fmt" 
   "os" 
) 

func main() { 
   for _, arg := range os.Args { 
         fmt.Println(arg) 
   } 
} 

golang.fyi/ch06-args/hello.go

以下是使用所示参数调用程序时的输出:

$> go run hello.go hello world how are you?
/var/folders/.../exe/hello
hello
world
how
are
you?

请注意,命令行参数"hello world how are you?"位于程序名称之后,被拆分为空格分隔的字符串。切片os.Args中的位置 0 保存程序二进制路径的完全限定名。切片的其余部分分别将每个项存储在字符串中。

来自 Go 标准库的flag包在内部使用此机制来提供结构化命令行参数(称为标志)的处理。在前面列出的欧姆定律示例中,flag包用于解析多个标志,如以下源代码片段(从前面的完整列表中提取)所示:

var ( 
   op string 
   v float64 
   r float64 
   i float64 
   p float64 
) 

func init() { 
   flag.Float64Var(&v, "v", 0.0, "Voltage value (volt)") 
   flag.Float64Var(&r, "r", 0.0, "Resistance value (ohms)") 
   flag.Float64Var(&i, "i", 0.0, "Current value (amp)") 
   flag.Float64Var(&p, "p", 0.0, "Electrical power (watt)") 
   flag.StringVar(&op, "op", "V", "Command - one of { V | Vpi |"+   
    " R | Rvp | I | Ivp | P | Pir | Pvr }") 
} 
func main(){ 
  flag.Parse() 
  ... 
} 

该代码段显示了用于解析和初始化预期标志"v""i""p","op"(在运行时,每个标志都以减号作为前缀)的函数init。包flag中的初始化函数设置了预期的类型、默认值、标志描述以及标志解析值的存储位置。flag 包还支持特殊的标志“help”,用于提供有关每个标志的有用提示。

函数main中的flag.Parse()用于启动解析作为命令行提供的任何标志的过程。例如,为了计算 12 伏和 300 欧姆电路的电流,程序采用三个标志并产生所示输出:

$> go run main.go -op I -v 12 -r 300
I = 12.00 / 300.00 = 0.04 amps

建立和安装程序

构建和安装 Go 程序的过程与构建常规软件包的过程完全相同(正如前面在构建和安装软件包一节中所讨论的)。当您构建一个可执行 Go 程序的源文件时,编译器将通过传递链接main包中声明的所有十进制文件来生成一个可执行二进制文件。构建工具将命名输出二进制文件,默认情况下与 Go 程序源文件所在的目录同名。

例如,在欧姆定律示例中,位于目录github.com/vladimirvivien/learning-go/ch06中的文件main.go被声明为main包的一部分。可按如下所示构建程序:

$> cd $GOPATH/src/github.com/vladimirvivien/learning-go/ch06
$> go build .

当构建main.go源文件时,构建工具将生成一个名为ch06的二进制文件,因为程序的源代码位于具有该名称的目录中。您可以使用输出标志-o控制二进制文件的名称。在下面的示例中,构建工具创建了一个名为ohms的二进制文件。

$> cd $GOPATH/src/github.com/vladimirvivien/learning-go/ch06
$> go build -o ohms

最后,安装 Go 程序的方式与使用 Goinstall命令安装常规软件包的方式完全相同:

$> cd $GOPATH/src/github.com/vladimirvivien/learning-go/ch06
$> go install .

当使用 Go install 命令安装程序时,如有必要,将构建该程序,并将其生成的二进制文件保存在$GOPAHT/bin目录中。将 workspacebin目录添加到操作系统的$PATH环境变量将使您的 Go 程序可供执行。

Go 生成的程序是静态链接的二进制文件。它们不需要满足其他依赖项即可运行。但是,Go 编译的二进制文件包括 Go 运行时。这是一组处理垃圾收集、类型信息、反射、goroutines 调度和紧急管理等功能的操作。虽然一个可比的 C 程序要小几个数量级,但 Go 的运行时附带了使 Go 变得有趣的工具。

远程包

Go 附带的一个工具允许程序员直接从远程源代码存储库检索包。默认情况下,Go 随时支持与版本控制系统的集成,包括:

为了从远程存储库中获取包源代码,必须在操作系统的执行路径上安装该版本控制系统的客户端作为命令。在封面下,Go 启动客户端与源代码存储库服务器交互。

get命令行工具允许程序员使用完全限定的项目路径作为包的导入路径来检索远程包。下载包后,可以将其导入本地源文件中使用。例如,如果您希望包含前面代码段中欧姆定律示例中的一个包,则可以从命令行发出以下命令:

$> go get github.com/vladimirvivien/learning-go/ch06/volt

go get工具将下载指定的导入路径以及所有引用的依赖项。然后,该工具将在$GOPATH/pkg中构建并安装包工件。如果import路径恰好是一个程序,go-get 将生成$GOPATH/bin中的二进制文件以及$GOPATH/pkg中的任何引用包。

总结

本章详细介绍了源代码组织和包的概念。读者了解了 Go 工作区和导入路径。还向读者介绍了包的创建以及如何导入包以实现代码重用。本章介绍了导入成员的可见性和包初始化等机制。本章的最后一部分讨论了从打包代码创建可执行 Go 程序所需的步骤。

这是一个很长的章节,对于 Go 中的包创建和管理这样一个广泛的主题来说,这是理所当然的。下一章将返回到 Go 类型讨论,详细讨论复合类型,如 array、slice、struct 和 map。*