导包和包管理

BigSun丶 / 2024-02-22 / 原文

目录
  • 一、什么是包
  • 二、main函数和main包
  • 三、包管理
    • 1. main包
    • 2. 创建自定义包
    • 3. 导包
  • 四、init 函数和包的初始化
    • 1. 实例-验证包的初始化顺序
      • (1)修改 rectprops.go
      • (2)修改geometry.go
      • (3)运行结果
      • (4)再修改 geometry.go
  • 五、实现导包而不用
    • 1. 错误导包
    • 2. 错误屏蔽器
    • 3. 空白标识符避错

一、什么是包

  • 和python类似,包就是一个个包含某种功能的代码文件,为了代码以后的可重用性与可读性更高,维护更方便,才引入的一个概念。
  • 与python不同的是,python中的包就是一个文件,导包就是导文件,而go中,包以文件夹为基础,导包为导文件夹
  • 没有包的概念,就会把所有代码、方法全写在一个文件中,这对于一个项目来说,想想是不是就很可怕
  • 属于某一个包的源文件都应该放置于一个单独命名的文件夹里。且按照 Go 的惯例,应该用包名命名这个文件夹,在这个目录下的所有文件都属于这个包

二、main函数和main包

  • 所有可执行的 Go 程序都必须包含一个 main 函数。这个函数是程序运行的入口。main 函数应该放置于 main 包中,main 包是go自带的包。而python是没有这种限制的

  • package packagename 这行代码指定了某一源文件属于一个包。它应该放在每一个源文件的第一行

三、包管理

以下总体的目录结构:

src
    geometry
        geometry.go
        rectangle
            rectprops.go
bin
	geometry.exe  
	
// 执行 go build geometry 命令来编译上述程序。该命令会在 geometry 文件夹内搜索拥有 main 函数的文件。在这里,它找到了 geometry.go。接下来,它编译并产生一个名为 geometry (在 windows 下是 geometry.exe)的二进制文件,该二进制文件放置于工作区的 bin 文件夹

1. main包

// 在 geometry.go 中
package main 

import "fmt"

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

2. 创建自定义包

  • 创建一个自定义包 rectangle,它有一个计算矩形的面积和对角线的函数。

    属于某一个包的源文件都应该放置于一个单独命名的文件夹里。按照 Go 的惯例,应该用包名命名该文件夹。

    因此,我们在 geometry 文件夹中,创建一个命名为 rectangle 的文件夹。在 rectangle 文件夹中,所有文件都会以 package rectangle 作为开头,因为它们都属于 rectangle 包

  • 注意:在 Go 中,任何以大写字母开头的变量或者函数才是能被导出的名字。其它包 只能 访问这些指定的被导出的函数和变量。在这里,我们需要在 main 包中访问 Area 和 Diagonal 函数,因此会将它们的首字母大写

  • 在创建的 rectangle 文件夹中,再创建一个名为 rectprops.go 的文件,内容如下

  • // rectprops.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
    }
    

3. 导包

  • 为了使用自定义包,我们必须要先导入它。导入自定义包的语法为 import path。我们必须指定自定义包相对于工作区内 src 文件夹的相对路径。我们目前的文件夹结构是:

  • 1. 目录结构
    src
        geometry
            geometry.go
            rectangle
                rectprops.go
    
    
    
    2. **********在geometry.go中导入这个自定义的包***********
    
    // geometry.go
    package main 
    
    import (  
        "fmt"
        "geometry/rectangle" // 导入自定义包,只导入目录
    )
    
    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))
    }
    

四、init 函数和包的初始化

  • 所有包都可以包含一个 init 函数。init 函数不应该有任何返回值类型和参数,在我们的代码中也不能显式地调用它。init 函数的形式如下

  • func init() {  
    }
    
  • init 函数可用于执行初始化任务,也可用于在开始执行之前验证程序的正确性。

    包的初始化顺序如下:

    1. 首先初始化包级别(Package Level)的变量
    2. 紧接着调用 init 函数。包可以有多个 init 函数(在一个文件或分布于多个文件中),它们按照编译器解析它们的顺序进行调用。
  • 如果一个包导入了另一个包,会先初始化被导入的包。

  • 尽管一个包可能会被导入多次,但是它只会被初始化一次

1. 实例-验证包的初始化顺序

(1)修改 rectprops.go

  • 首先在 rectprops.go 文件中添加了一个 init 函数
// rectprops.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
}

(2)修改geometry.go

  • 其次对 geometry.go 做了如下修改:
    1. 变量 rectLenrectWidth 从 main 函数级别移到了包级别。
    2. 添加了 init 函数。当 rectLen 或 rectWidth 小于 0 时,init 函数使用 log.Fatal 函数打印一条日志,并终止了程序。
// geometry.go
package main 

import (  
    "fmt"
    "geometry/rectangle" // 导入自定义包
    "log"
)

// 1. 包级别变量
var rectLen, rectWidth float64 = 6, 7 


// 2. init 函数会检查长和宽是否大于0

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))
}

(3)运行结果

  • 理论上main 包的初始化顺序为:

    1. 首先初始化被导入的包。因此,首先初始化了 rectangle 包
    2. 接着初始化了包级别的变量 rectLenrectWidth
    3. 调用 init 函数
    4. 最后调用 main 函数
  • // 打印结果与理论符合
    
    rectangle package initialized  
    main package initialized  
    Geometrical shape properties  
    area of rectangle 42.00  
    diagonal of the rectangle 9.22
    

(4)再修改 geometry.go

将 geometry.go 中的 var rectLen, rectWidth float64 = 6, 7 改为 var rectLen, rectWidth float64 = -6, 7。
我们把 rectLen 初始化为负数

// 运行结果

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

/*
会首先初始化 rectangle 包,然后是 main 包中的包级别的变量 rectLen 和 rectWidth。rectLen 为负数,因此当运行 init 函数时,程序在打印 length is less than zero 后终止
*/

五、实现导包而不用

  • 在go中,导入了包或变量,却不在代码中使用它们,这在 Go 中都是非法的。当这么做时,编译器是会报错的。其原因是为了避免导入过多未使用的包或定义了过多的变量,从而导致编译时间显著增加

  • 变量还好,但是再开发之前,我们都会习惯先将要用的包导好,再进行后续开发,但是这就与上面说的相矛盾,此时就可以使用错误屏蔽器来避免

1. 错误导包

// geometry.go
package main 

import (
    "geometry/rectangle" // 导入自定的包
)
func main() {

}

// 导了而不用,会报错 geometry.go:6: imported and not used: "geometry/rectangle"

2. 错误屏蔽器

// geometry.go
package main

import (  
    "geometry/rectangle" 
)

var _ = rectangle.Area // 错误屏蔽器

func main() {

}


/*
var _ = rectangle.Area 这一行屏蔽了错误。我们应该了解这些错误屏蔽器(Error Silencer)的动态,在程序开发结束时就移除它们,包括那些还没有使用过的包。由此建议在 import 语句下面的包级别范围中写上错误屏蔽器
*/

3. 空白标识符避错

  • 有时候我们导入一个包,只是为了确保它进行了初始化,而无需使用包中的任何函数或变量。例如,我们或许需要确保调用了 rectangle 包的 init 函数,而不需要在代码中使用它。这种情况也可以使用空白标识符

  • // geometry.go
    package main 
    
    import (
        _ "geometry/rectangle" 
    )
    func main() {
    
    }
    
    // 运行这个的程序,会输出 rectangle package initialized。尽管在所有代码里,我们都没有使用这个包,但还是成功初始化了它