# 包



#### Golang中的包

目前为止，我们实现的Golang程序都是单个含有`main`函数的.go文件。在真正的项目中这种简单粗暴的方式是根本行不通的，这种方式不具有任何的可服用性与可维护性。Golang中包(package)就是为了解决这个问题而存在的。

**Golang中包是为了更好的组织代码，使代码具有更好的可复用性、可维护性以及可读性。** 包用来将代码划分成不同的模块，以更加方便的来维护代码，其他编程语言也有类似的概念，比如**Node.JS**中的**npm包**。

比如我们准备用Golang来开发一个专门处理图片的项目，提供的功能包括图片裁剪、锐化、模糊化以及颜色增强等。一种比较好的组织代码的方式就是将跟特定功能点相关的代码放到一个包内。例如将图片裁剪拆成一个独立的包，图片锐化拆成一个独立的包，颜色增强也拆成一个独立的包。颜色增强部分有可能会用到图片锐化的功能，因此可以直接引入负责图片锐化的包。这样整个代码结构会更加容易维护。

下面我们创建一个计算矩形面积和对角线长度的应用，这中间我们会创建一个独立的包，来更好的学习下Golang中的包管理。

#### main函数和main包

所有可单独执行的Golang程序都需要有个`main`函数作为程序执行总入口。Golang要起`main`函数必须位于名也为`main`的包中。

**每个Golang代码文件头部的第一行代码都应该是**`package packagename`**，用来表示该文件属于哪个包。**

我们首先创建main包和main函数。在你的Golang工作目录的`src`目录下创建新目录`geomegry`，然后在该文件夹下创建文件`geomegry.go`。

将如下代码粘贴到`geomegry.go`文件中。

```go
package main

import "fmt"

func main() {
    fmt.Println("Geometrical shape properties")
}
```

第一行代码`package main`声明了该文件隶属于`main`包。语句`import "pacakgename"`用来导入其他包。这段代码中我们导入了`fmt`包，并且使用了该包中的`Println`方法。这里的`main`函数还没实现什么有用的逻辑，仅仅是打印出了`Geometrical shape properties`。

使用`go install geometry`命令来编译该程序。该命令会在`geometry`文件夹下面查找内含`main`函数的Golang文件，这里找到的是`geometry.go`文件，接着将该文件编译生成一个位于`bin`目录下的可执行文件`geometry`(windows系统下是`geometry.exe`文件)，现在的整体目录结构如下所示：

```
src
    geometry
        geometry.go
bin
    geometry
```

注意，这里的`src`和`bin`仍然是位于Golang的工作目录。 然后我们可以直接执行该二进制可执行文件：`Golang工作目录/bin/geometry`。执行结果如下：

```
Geometrical shape properties
```

#### 创建一个包

我们来创建一个名为`rectangle`的包，将跟矩形有关的计算矩形面积、对象线长度的逻辑放进去。

**所有属于某个包的代码应该也位于某个独立的文件夹内。Golang中一般会将该文件夹命名成跟包名相同。**

因此，我们在`geometry`目录下创建子文件夹`rectangle`。所有`rectangle`目录下面的代码文件都应该在文件头部声明`package rectangle`。

在我们刚刚创建的`rectangle`文件夹下创建文件`rectprops.go`，并输入如下代码：

```go
package rectangle

import "math"

func Area(len, wid float64) float64 {
    area := len * wid
    return area
}

func Diagonal(len, wid float64) float64 {
    diagonal := math.Sqrt(len * len + wid * wid)
    return diagonal
}
```

这里我们创建了两个函数`Area`和`Diagonal`，分别用来计算矩形面积和对角线长度。需要注意的是函数名称`Area`和`Diagonal`均以大写字母开始，后面马上会解释原因。

#### 使用自己创建的包

使用某个**自己创建**的包中的功能之前我们必须先导入这个包，语法如下：`import "path"`。这里的`path`相对于Golang工作目录的`src`目录。比如现在整体目录结构如下：

```
src
    geometry
        geometry.go
        rectangle
            rectprops.go
```

那么导入`rectangle`包的语句为：`import "geometry/rectangle"`。

在我们的主程序文件`geometry.go`中输入如下代码：

```go
//geometry.go
package main 

import (  
    "fmt"
    "geometry/rectangle" //importing custom package
)

func main() {  
    var rectLen, rectWidth float64 = 6, 7
    fmt.Println("Geometrical shape properties")
        /*Area function of rectangle package used
        */
    fmt.Printf("area of rectangle %.2f\n", rectangle.Area(rectLen, rectWidth))
        /*Diagonal function of rectangle package used
        */
    fmt.Printf("diagonal of the rectangle %.2f ",rectangle.Diagonal(rectLen, rectWidth))
}
```

这里导入了`rectangle`包，并且使用该包中的`Area`函数和`Diagonal`函数来计算矩形的面积和对角线长度。其中`%.2f`将浮点数打印结果格式化为保留两位小数。执行结果如下：

```
Geometrical shape properties
area of rectangle 42.00  
diagonal of the rectangle 9.22
```

#### 包的变量、函数导出

Golang支持导出类型、函数、常量和变量，但是要求导出的这些字段首字母大写。只有这些导出的字段才能被引用该包的程序访问。因此前面`rectangle`包的`Area`和`Diagnoal`函数均首字母大写。

如果我们将前面的`Area`、`Diagnoal`函数都改为首字母小写，主程序中引用时也使用小写形式，则会报错：`cannot refer to unexported name rectangle.area`。

因此，如果你希望将包中某个字段比如变量、函数暴露出去，则需要首字母大写。

#### 包初始化函数`init`

Golang中包支持添加初始化函数`init`，该函数无任何参数，无返回值，用来执行包的初始化逻辑。格式如下：

```go
func init() {
    // 初始化逻辑
}
```

一个包的初始化流程如下：

1. 引入的其他包初始化，类似递归
2. 包级别的变量初始化
3. 执行`init`函数(注意一个包可以包含多个`init`函数，多个`init`函数可以位于不同的文件，也可以位于相同的文件不同的位置)。

一个包即使被其他方引入了多次，也仅仅会初始化一次。我们修改下前面的代码，在`rectprops.go`文件中增加一个`init`函数：

```go
package rectangle

import "math"  
import "fmt"

/*
 * init function added
 */
func init() {  
    fmt.Println("rectangle package initialized")
}
func Area(len, wid float64) float64 {  
    area := len * wid
    return area
}

func Diagonal(len, wid float64) float64 {  
    diagonal := math.Sqrt((len * len) + (wid * wid))
    return diagonal
}
```

我们添加了一个简单的初始化函数，没干其他什么，仅仅是打印了`rectangle package initialised`。

然后我们修改下`main`包，将变量`rectLen`和`rectWidth`的声明提到包级别：

```go
package main 

import (  
    "fmt"
    "geometry/rectangle" //importing custom package
)

// package variables
var rectLen, rectWidth float64 = 6, 7 

func main() {  
    fmt.Println("Geometrical shape properties")
    fmt.Printf("area of rectangle %.2f\n", rectangle.Area(rectLen, rectWidth))
    fmt.Printf("diagonal of the rectangle %.2f ",rectangle.Diagonal(rectLen, rectWidth))
}
```

正常情况下矩形的长、宽都应该是非负数才有意义。我们可以利用`init`函数来做这个校验：

```go
package main 

import (  
    "fmt"
    "geometry/rectangle" //importing custom package
    "log"
)

// package variables
var rectLen, rectWidth float64 = 6, 7 

// init function to check if length and width are greater than zero
func init() {  
    println("main package initialized")
    if rectLen < 0 {
        log.Fatal("length is less than zero")
    }
    if rectWidth < 0 {
        log.Fatal("width is less than zero")
    }
}

func main() {  
    fmt.Println("Geometrical shape properties")
    fmt.Printf("area of rectangle %.2f\n", rectangle.Area(rectLen, rectWidth))
    fmt.Printf("diagonal of the rectangle %.2f ",rectangle.Diagonal(rectLen, rectWidth))
}
```

这里的`init`函数对矩形的长、宽做了校验，如果存在一个不符合条件，则会打印一条日志，然后终止程序的执行。

`main`包的初始化流程如下：

1. 初始化导入的外部包，因此`rectangle`包先初始化
2. 包级别变量(`rectLen`, `rectWidth`)的初始化
3. `init`函数执行
4. `main`函数执行

执行结果如下：

```
rectangle package initialized
main package initialized
Geometrical shape properties
area of rectangle 42.00
diagonal of the rectangle 9.22
```

如果我们把矩形长、宽中的一个变量初始化为负数，则执行结果如下：

```
rectangle package initialized
main package initialized
2017/04/04 00:28:20 length is less than zero
```
