【golang学习笔记】包(package)的使用

Go语言的包和文件夹一一对应,源码复用建立在包基础之上。Go语言的入口main()函数所在的包叫main,如果main包想使用别的包也必须进行引用。
go语言给我们提供了很多的内置包,如fmt,os,io等。
任何源代码文件必须属于某个包,同时源码文件的第一行有效代码必须是 package pacakgeName 语句,声明自己所在的包。

包的基本概念

包借助了目录树的组织形式,一般包名就是目录名(可以不同)。
包的定义不包括目录路径,在引用时一般会使用全路径引用。比如:package c,引入包时 import "a/b/c"

习惯用法:

  • 包名一般小写,使用一个简短且有意义的名称
  • 包名一般要和所在目录同名,包名中不能包含-等特殊符号。
  • 包名一般使用域名作为目录名称,这样能保证包名的唯一性。
  • main包为应用程序的入口包,编译不包含main包的源码文件时不会得到可执行文件
  • 一个文件夹下的所有源码文件应该属于同一个包,同一个包下的源码文件不能放在多个文件夹下

包的导入

import "包的路径"

注意事项:

  • import导入语句通常放在源码文件开头包声明语句的下面
  • 包名需要用双引号括起来
  • 是从GOPATH/src/后开始计算的,使用/进行路径分隔

单行导入

import "包1的路径"
import "包2的路径"

例如:

import "fmt"
import "os"

多行导入

   "包1的名称"
   "包2的名称"

例如:

import (
   "fmt"
   "os"
)

包的导入路径

分为两种,全路径导入和相对路径导入

全路径导入

绝对路径就是GOTROOT/src/GOPATH/src后面包的存放地址

import "database/sql"
import "database/sql/driver"

这两个包都位于GOROOT/src

相对路径导入

只能导入GOPATH下的包,标准包的导入只能使用全路径导入。
比如b和a在同一个目录下

//相对路径导入
import "../a"

包的引用格式

有以下四种:

  • 标准引用格式
  • 自定义别名引用格式
  • 省略引用格式
  • 匿名引用格式

标准引用格式

import "fmt"
使用fmt.作为前缀来引用包中的方法
例如:

package main

import "fmt"

func main() {
    
    
   fmt.Println("hello")
}

自定义别名引用格式

可以为包设置别名
import F "fmt"
其中F就是包fmt的别名,使用时可以用别名替代

例如:

package main

import F "fmt"

func main() {
    
    
   F.Println("hello")
}

省略引用格式

import . "fmt"
相当于把fmt包直接合并到当前程序中。

例如:

package main

import . "fmt"

func main() {
    
    
   Println("hello")
}

匿名引用格式

import _ "fmt"
只是执行包初始化的init函数,而不是用包内部的数据

使用标准格式引用包,但是代码中却没有使用包,编译器会报错。如果包中有 init 初始化函数,则通过
import _ "包的路径 这种方式引用包,仅执行包的初始化函数,即使包没有 init 初始化函数,也不会引发编译器报错

package main

import _ "fmt"

func main() {
    
    
   
}

此处编译器没有报错

注意:

  • 一个包可以有多个init函数,包加载时会执行全部的init函数,但并不能保证执行顺序,所以不建议在一个包中放入多个inti函数,将需要初始化的逻辑放在一个init函数里。
  • 包不能出现循环引用
  • 包可以重复引用,并且Go编译器保证包d的init函数只会执行一次

包加载

在这里插入图片描述

  • 包初始化程序从 main 函数引用的包开始,逐级查找包的引用,直到找到没有引用其他包的包,最终生成一个包引用的有向无环图。
  • Go 编译器会将有向无环图转换为一棵树,然后从树的叶子节点开始逐层向上对包进行初始化。
  • 单个包的初始化过程如上图所示,先初始化常量,然后是全局变量,最后执行包的 init 函数。

Go语言封装及实现细节

封装就是将抽象出来的字段和对字段的操作保护在内部

封装的好处:

  • 隐藏实现细节
  • 可以对数据进行校验,保证数据安全合法

如何实现:

  • 对结构体中的属性进行封装
  • 通过方法,包,实现封装

步骤:

  • 将结构体、字段的首字母小写
  • 给结构体所在的包提供一个工厂模式的函数,首字母大写,类似于一个构造函数;
  • 提供一个首字母大写的Set方法,用于对属性判断并赋值;
  • 提供一个首字母大写的Get方法,用于获取属性的值;

工作目录(GOPATH)

GOPATH是Go语言中使用的一个环境变量,它使用绝对路径提供项目的工作目录。是程序开发的相对目录。
GOPATH适合处理大量的Go语言源码、多个包组合而成的复杂源码。

使用命令行查看GOPATH信息

go env

在这里插入图片描述

在Go 1.8之后,GOPATH会赋予一个默认的目录。

使用GOPATH的工程结构

代码包存在 $ GOPATH/src目录下。
(通过go build,go install或go get)二进制可执行文件放在 $GOPATH/bin目录下,生成的中间缓存文件会被保存在 $ GOPATH/pkg下。

设置和使用GOPATH

  1. 设置当前目录为GOPATH
    在目录中的命令行执行``export GOPATH=pwd
    pwd表示输出当前目录,使用反引号将pwd括起来,则进行替换。

  2. 建立GOPATH中源码目录mkdor -p src/hello
    使用-p可以连续创建一个路径

  3. 添加main.go源码文件
    使用vim即可,并将文件保存在$GOPATH/src/hello下

  4. 编译源码并运行
    编译源码:go install hello (此处为hello 目录)
    编译完成的可执行文件会保存在bin目录下,./hello执行(linux下)

Go语言导出包中的标识符——让外部访问包的类型和值

在 Go语言中,如果想在一个包里引用另外一个包里的标识符(如类型、变量、常量等)时,必须首先将被引用的标识符导出,将要导出的标识符的首字母大写就可以让引用者可以访问这些标识符了。## 导出包内标识符

下面代码中包含一系列未导出标识符,它们的首字母都为小写,这些标识符可以在包内自由使用,但是包外无法访问它们,代码如下:


package mypkg

var myVar = 100

const myConst = "hello"

type myStruct struct {
    
    
}

将 myStruct 和 myConst 首字母大写,导出这些标识符,修改后代码如下:


package mypkg

var myVar = 100

const MyConst = "hello"

type MyStruct struct {
    
    
}

此时,MyConst 和 MyStruct 可以被外部访问,而 myVar 由于首字母是小写,因此只能在 mypkg 包内使用,不能被外部包引用。## 导出结构体及接口成员

在被导出的结构体或接口中,如果它们的字段或方法首字母是大写,外部可以访问这些字段和方法,代码如下:

type MyStruct struct {
    
    

    // 包外可以访问的字段
    ExportedField int

    // 仅限包内访问的字段
    privateField int
}

type MyInterface interface {
    
    

    // 包外可以访问的方法
    ExportedMethod()

    // 仅限包内访问的方法
    privateMethod()
}

在代码中,MyStruct 的 ExportedField 和 MyInterface 的 ExportedMethod() 可以被包外访问。

Go语言import导入包——在代码中使用其他的代码

可以在一个 Go语言源文件包声明语句之后,其它非导入声明语句之前,包含零到多个导入包声明语句。每个导入声明可以单独指定一个导入路径,也可以通过圆括号同时导入多个导入路径。要引用其他包的标识符,可以使用 import 关键字,导入的包名使用双引号包围,包名是从 GOPATH 开始计算的路径,使用 / 进行路径分隔。

默认导入的写法

导入有两种基本格式,即单行导入和多行导入,两种导入方法的导入代码效果是一致的。

  1. 单行导入

单行导入格式如下:

import "包1"
import "包2"
  1. 多行导入

当多行导入时,包名在 import 中的顺序不影响导入效果,格式如下:

import(
    "包1"
    "包2")

参考代码 8-1 的例子来理解 import 的机制。

代码 8-1 的目录层次如下:.

└── src
    └── chapter08
        └── importadd
            ├── main.go
            └── mylib
                └── add.go

代码8-1 加函数(具体文件:…/chapter08/importadd/mylib/add.go)

package mylib

func Add(a, b int) int {
    
    
    return a + b
}

第 3 行中的 Add() 函数以大写 A 开头,表示将 Add() 函数导出供包外使用。当首字母小写时,为包内使用,包外无法引用到。

add.go 在 mylib 文件夹下,习惯上将文件夹的命名与包名一致,命名为 mylib 包。

代码8-2 导入包(具体文件:…/chapter08/importadd/main.go)


package main

import (
    "chapter08/importadd/mylib"
    "fmt"
)

func main() {
    
    
    fmt.Println(mylib.Add(1, 2))
}

代码说明如下:

  • 第 4 行,导入 chapter08/importadd/mylib 包。
  • 第 9 行,使用 mylib 作为包名,并引用 Add() 函数调用。

在命令行中运行下面代码:

export GOPATH=/home/davy/golangbook/code
go install chapter08/importadd
$GOPATH/bin/importadd

命令说明如下:

  • 第 1 行,根据你的 GOPATH 不同,设置 GOPATH。
  • 第 2 行,使用 go install 指令编译并安装 chapter08/code8-1 到 GOPATH 的 bin 目录下。
  • 第 3 行,执行 GOPATH 的 bin 目录下的可执行文件 code8-1。

运行代码,输出结果如下:3
导入的包之间可以通过添加空行来分组;通常将来自不同组织的包独自分组。包的导入顺序无关紧要,但是在每个分组中一般会根据字符串顺序排列。(gofmt 和 goimports 工具都可以将不同分组导入的包独立排序。)


import (
    "fmt"
    "html/template"
    "os"

    "golang.org/x/net/html"
    "golang.org/x/net/ipv4"
)

导入包后自定义引用的包名

如果我们想同时导入两个有着名字相同的包,例如 math/rand 包和 crypto/rand 包,那么导入声明必须至少为一个同名包指定一个新的包名以避免冲突。这叫做导入包的重命名。


import (
    "crypto/rand"
    mrand "math/rand" // 将名称替换为mrand避免冲突
)

导入包的重命名只影响当前的源文件。其它的源文件如果导入了相同的包,可以用导入包原本默认的名字或重命名为另一个完全不同的名字。

导入包重命名是一个有用的特性,它不仅仅只是为了解决名字冲突。如果导入的一个包名很笨重,特别是在一些自动生成的代码中,这时候用一个简短名称会更方便。选择用简短名称重命名导入包时候最好统一,以避免包名混乱。选择另一个包名称还可以帮助避免和本地普通变量名产生冲突。例如,如果文件中已经有了一个名为 path 的变量,那么我们可以将"path"标准包重命名为 pathpkg。

每个导入声明语句都明确指定了当前包和被导入包之间的依赖关系。如果遇到包循环导入的情况,Go语言的构建工具将报告错误。

匿名导入包——只导入包但不使用包内类型和数值

如果只希望导入包,而不使用任何包内的结构和类型,也不调用包内的任何函数时,可以使用匿名导入包,格式如下:


import (
    _ "path/to/package"
)

其中,path/to/package 表示要导入的包名,下画线 _ 表示匿名导入包。

匿名导入的包与其他方式导入包一样会让导入包编译到可执行文件中,同时,导入包也会触发 init() 函数调用。## 包在程序启动前的初始化入口:init

在某些需求的设计上需要在程序启动时统一调用程序引用到的所有包的初始化函数,如果需要通过开发者手动调用这些初始化函数,那么这个过程可能会发生错误或者遗漏。我们希望在被引用的包内部,由包的编写者获得代码启动的通知,在程序启动时做一些自己包内代码的初始化工作。

例如,为了提高数学库计算三角函数的执行效率,可以在程序启动时,将三角函数的值提前在内存中建成索引表,外部程序通过查表的方式迅速获得三角函数的值。但是三角函数索引表的初始化函数的调用不希望由每一个外部使用三角函数的开发者调用,如果在三角函数的包内有一个机制可以告诉三角函数包程序何时启动,那么就可以解决初始化的问题。

Go 语言为以上问题提供了一个非常方便的特性:init() 函数。

init() 函数的特性如下:

  • 每个源码可以使用 1 个 init() 函数。
  • init() 函数会在程序执行前(main() 函数执行前)被自动调用。
  • 调用顺序为 main() 中引用的包,以深度优先顺序初始化。

例如,假设有这样的包引用关系:main→A→B→C,那么这些包的 init() 函数调用顺序为:C.init→B.init→A.init→main
说明:

  • 同一个包中的多个 init() 函数的调用顺序不可预期。
  • init() 函数不能被其他函数调用。

理解包导入后的init()函数初始化顺序

Go 语言包会从 main 包开始检查其引用的所有包,每个包也可能包含其他的包。Go 编译器由此构建出一个树状的包引用关系,再根据引用顺序决定编译顺序,依次编译这些包的代码。

在运行时,被最后导入的包会最先初始化并调用 init() 函数。

通过下面的代码理解包的初始化顺序。

代码8-3 包导入初始化顺序入口(…/chapter08/pkginit/main.go)


package main

import "chapter08/code8-2/pkg1"

func main() {
    
    

    pkg1.ExecPkg1()
}

代码说明如下:

  • 第 3 行,导入 pkg1 包。
  • 第 7 行,调用 pkg1 包的 ExecPkg1() 函数。

代码8-4 包导入初始化顺序pkg1(…/chapter08/pkginit/pkg1/pkg1.go)


package pkg1

import (
    "chapter08/code8-2/pkg2"
    "fmt"
)

func ExecPkg1() {
    
    

    fmt.Println("ExecPkg1")

    pkg2.ExecPkg2()
}

func init() {
    
    
    fmt.Println("pkg1 init")
}

代码说明如下:

  • 第 4 行,导入 pkg2 包。
  • 第 8 行,声明 ExecPkg1() 函数。
  • 第 12 行,调用 pkg2 包的 ExecPkg2() 函数。
  • 第 15 行,在 pkg1 包初始化时,打印 pkg1 init。

代码8-5 包导入初始化顺序pkg2(…/chapter08/pkginit/pkg2/pkg2.go)

package pkg2

import "fmt"

func ExecPkg2() {
    
    
    fmt.Println("ExecPkg2")
}

func init() {
    
    
    fmt.Println("pkg2 init")
}

代码说明如下:

  • 第 5 行,声明 ExecPkg2() 函数。
  • 第 10 行,在 pkg2 包初始化时,打印 pkg2 init。

执行代码,输出如下:pkg2 init

pkg1 init
ExecPkg1
ExecPkg2

猜你喜欢

转载自blog.csdn.net/qq_45795744/article/details/125913390