go语言包-01
包的基本概念
Go语言的包借助了目录树的组织形式,一般包的名称就是其源文件所在目录的名称,虽然Go语言没有强制要求包名必须和其所在的目录名同名,但还是建议包名和所在目录同名,这样结构更清晰。
包可以定义在很深的目录中,包名的定义是不包括目录路径的,但是包在引用时一般使用全路径引用。比如在GOPATH/src/a/b/ 下定义一个包 c。在包 c 的源码中只需声明为package c,而不是声明为package a/b/c,但是在导入 c 包时,需要带上路径,例如import "a/b/c"。
- 包的习惯用法:
- 包名一般是小写的,使用一个简短且有意义的名称
- 包名一般要和所在的目录同名,也可以不同,包名中不能包含-等特殊符号
- 包名一般使用域名作为目录名称,这样能保证包名的唯一性,比如github项目的包一般会放在GOPATH/src/github.com/userName/projectName目录下,
- 包名为main的包为应用程序的入口包,编译不包含main包的源码文件时不会得到可执行文件
- 一个文件夹下的所有源码文件只能属于同一个包,同样属于同一个包的源码不能放在多个文件夹下
- 包的导入:
(1)全路径导入:
import "lab/test"
import "database/sql/driver"
import "database/sql"
上面代码的含义如下:
test 包是自定义的包,其源码位于GOPATH/src/lab/test 目录下;
driver 包的源码位于GOROOT/src/database/sql/driver 目录下;
sql 包的源码位于GOROOT/src/database/sql 目录下。
(2)相对路径导入
相对路径只能用于导入GOPATH下的包,标准包的导入只能使用全路径导入
例如包 a 的所在路径是GOPATH/src/lab/a,包 b 的所在路径为GOPATH/src/lab/b,如果在包 b 中导入包 a ,则可以使用相对路径导入方式。示例如下:
// 相对路径导入
import "../a"
当然了,也可以使用上面的全路径导入,如下所示:
// 全路径导入
import "lab/a"
- 包的引用格式
(1)标准引用格式
import fmt
此时可以用fmt.作为前缀来使用 fmt 包中的方法,这是常用的一种方式。
(2)自定义别名引用格式
在导入包的时候,我们还可以为导入的包设置别名:
import F fmt
其中 F 就是 fmt 包的别名,使用时我们可以使用F.来代替标准引用格式的fmt.来作为前缀使用 fmt 包中的方法。
(3)省略引用格式
import . fmt
这种格式相当于把fmt包直接合并到当前程序中,在使用fmt包中的方法时不需要使用前缀,可以直接引用。
(4)匿名引用格式
在引用某个包时,如果只是希望执行包初始化的init函数,而不使用包内部的数据时,可以使用匿名引用格式,如下所示:
import _ "fmt"
匿名导入的包与其它方式导入的包一样,都会被编译到可执行文件中,
使用标准格式引用包,但是代码中却没有使用包,编译器会报错。如果包中有 init 初始化函数,则通过import _ “包的路径” 这种方式引用包,仅执行包的初始化函数,即使包没有init初始化函数,也不会引发编译器报错。
注意:
- 一个包中可以有多个init函数,包加载时会执行全部的init函数,但 并不能保证执行顺序,所以不建议在一个包中放入多个init函数,将需要初始化的逻辑放入到一个init函数里面。
- 包不能出现环形引用的情况,比如包 a 引用了包 b,包 b 引用了包 c,如果包 c 又引用了包 a,则编译不能通过。
- 包的重复引用是允许的,比如包a引用了包b和包c,包b和包c引用了包d,这种场景相当于重复引用了d,这样情况是允许的,并且go编译器保证包d中的init函数只会执行一次。
- 在执行main包的main函数之前,go引导程序会先对整个程序的包进行初始化,整个执行流程如图所示:
go包的初始化
go语言包的初始化有如下特点: - 包初始化程序从main函数引用的包开始,逐级查找包的引用,直到找到没有引用其它包的包,最终生成一个包引用的有向无环图
- go编译器将会将有向无环图转换为一棵树,然后从树的叶子节点逐层向上对包进行初始化
- 单个包的初始化过程如上图所示,先初始化常量,然后是全局变量,最后执行包的init函数
go语言封装简介及实现细节
在go语言中的封装就是把抽象出来的字段和对字段的操作封装在一起,数据被保护在内部,程序的其它包只能通过被授权的方法,才能对字段进行操作。
封装的好处:
- 隐藏实现细节
- 可以对数据进行验证,保证数据安全合理
如何提现封装: - 对结构体中的属性进行封装
- 通过方法、包实现封装
封装的实现步骤: - 将结构体、字段的首字母小写
- 给结构体所在的包提供一个工厂模式的函数,首字母大写,类似于构造函数
- 提供一个首字母大写的Set方法(类似于其它语言的public),用于对属性判断并赋值
- 提供一个首字母大写的Get方法(类似于其它语言的public),用于获取属性的值
go语言常用内置包
- fmt
fmt包实现了格式化的标准输入输出,格式化短语派生于C语言,一些短语%-序列,这样使用
%v: 默认格式的值,当打印结构体时,增加%+v会增加字段名
%#:go样式的值表达式
%T: 带有类型的go样式的表达式
案例1:%+v打印结构体
func main() {
fmt.Printf("%+v", struct {
Name string
age int
}{
Name: "马艳娜",
age: 15,
})
}
# 打印结果
{Name:马艳娜 age:15}
案例2:%#v打印结构体
func main() {
fmt.Printf("%#v", struct {
Name string
age int
}{
Name: "马艳娜",
age: 15,
})
}
# 打印结果
struct { Name string; age int }{Name:"马艳娜", age:15}
案例3:%T打印结构体
func main() {
fmt.Printf("%T", struct {
Name string
age int
}{
Name: "马艳娜",
age: 15,
})
}
# 打印结果
struct { Name string; age int }
-
io
这个包提供了原始的 I/O 操作界面。它主要的任务是对 os 包这样的原始的 I/O 进行封装,增加一些其他相关,使其具有抽象功能用在公共的接口上。 -
bufio
bufio 包通过对 io 包的封装,提供了数据缓冲功能,能够一定程度减少大块数据读写带来的开销。
在 bufio 各个组件内部都维护了一个缓冲区,数据读写操作都直接通过缓存区进行。当发起一次读写操作时,会首先尝试从缓冲区获取数据,只有当缓冲区没有数据时,才会从数据源获取数据更新缓冲。
-
sort
sort 包提供了用于对切片和用户定义的集合进行排序的功能。 -
strconv
strconv 包提供了将字符串转换成基本数据类型,或者从基本数据类型转换为字符串的功能。 -
os
os 包提供了不依赖平台的操作系统函数接口,设计像 Unix 风格,但错误处理是 go 风格,当 os 包使用时,如果失败后返回错误类型而不是错误数量。 -
sync
sync 包实现多线程中锁机制以及其他同步互斥机制。 -
flag
flag 包提供命令行参数的规则定义和传入参数解析的功能。绝大部分的命令行程序都需要用到这个包。 -
encoding/json
JSON 目前广泛用做网络程序中的通信格式。encoding/json 包提供了对 JSON 的基本支持,比如从一个对象序列化为 JSON 字符串,或者从 JSON 字符串反序列化出一个具体的对象等。 -
html/template
主要实现了 web 开发中生成 html 的 template 的一些函数。 -
net/http
net/http 包提供 HTTP 相关服务,主要包括 http 请求、响应和 URL 的解析,以及基本的 http 客户端和扩展的 http 服务。
通过 net/http 包,只需要数行代码,即可实现一个爬虫或者一个 Web 服务器,这在传统语言中是无法想象的。
-
reflect
reflect 包实现了运行时反射,允许程序通过抽象类型操作对象。通常用于处理静态类型 interface{} 的值,并且通过 Typeof 解析出其动态类型信息,通常会返回一个有接口类型 Type 的对象。 -
os/exec
os/exec 包提供了执行自定义 linux 命令的相关实现。 -
strings
strings 包主要是处理字符串的一些函数集合,包括合并、查找、分割、比较、后缀检查、索引、大小写处理等等。
strings 包与 bytes 包的函数接口功能基本一致。
-
bytes
bytes 包提供了对字节切片进行读写操作的一系列函数。字节切片处理的函数比较多,分为基本处理函数、比较函数、后缀检查函数、索引函数、分割函数、大小写处理函数和子切片处理函数等。 -
log
log 包主要用于在程序中输出日志。
log 包中提供了三类日志输出接口,Print、Fatal 和 Panic。
Print 是普通输出;
Fatal 是在执行完 Print 后,执行 os.Exit(1);
Panic 是在执行完 Print 后调用 panic() 方法。
go语言自定义包
包是go语言中代码组成和代码编译的主要方式。
在go语言里允许我们将同一个包的代码分割成多个独立的源码文件来单独保存,只需将这些文件放在同一个目录下即可。
对引用自定义包需要注意以下几点:
- 使用import语句导入包时,使用的是包所属文件夹的名字
- 包中的函数名第一个字母要大写,否则无法在外部调用
go语言package
包(package)是多个 Go 源码的集合,是一种高级的代码复用方案,像 fmt、os、io 等这样具有常用功能的内置包在 Go语言中有 150 个以上,它们被称为标准库,大部分(一些底层的除外)内置于 Go 本身。
包的特性如下:
- 一个目录下的同级文件归属一个包
- 包名可以与其目录不同名
- 包名为main的包为应用程序的入口包,编译源码没有main包时,将无法编译输出可执行文件。
任何包系统设计的目的都是为了简化大型程序的设计和维护工作,通过将一组相关的特性放进一个独立的单元以便于理解和更新,在每个单元更新的同时保持和程序中其它单元的相对独立性。这种模块化的特性允许每个包可以被其它的不同项目共享和重用,在项目范围内、甚至全球范围统一的分发和复用。
每个包一般都定义了一个不同的名字空间用于它内部的每个标识符的访问。每个名字空间关联到一个特定的包,让我们给类型、函数等选择简短明了的名字,这样可以避免在我们使用它们的时候减少和其它部分名字的冲突。
每个包还通过控制包内名字的可见性和是否导出来实现封装特性。通过限制包成员的可见性并隐藏包 API 的具体实现,将允许包的维护者在不影响外部包用户的前提下调整包的内部实现。通过限制包内变量的可见性,还可以强制用户通过某些特定函数来访问和更新内部变量,这样可以保证内部变量的一致性和并发时的互斥约束。
当我们修改了一个源文件,我们必须重新编译该源文件对应的包和所有依赖该包的其它包,即使是从头构建,Go语言编译器的编译速度也明显快于其它编译语言。Go语言的闪电般的编译速度主要得益于三个语言特性。
- 所有导入的包必须在源文件的开头显示声明,这样的话编译器就没有必要读取和分析整个源文件来判断包的依赖关系
- 禁止包的环形依赖,因为没有循环依赖,包的依赖关系形成一个有向无环图,每个包都可以被独立编译,而且很可能是被并发编译
- 编译后的目标文件不仅仅记录包本身的导出信息,目标文件同时还记录了包的依赖关系,因此在编译一个包的时候,编译器只需要读取每个直接导入包的目标文件,而不需要遍历所有依赖的文件。
go语言导出包中的标识符
- 在 Go语言中,如果想在一个包里引用另外一个包里的标识符(如类型、变量、常量等)时,必须首先将被引用的标识符导出,将要导出的标识符的首字母大写就可以让引用者可以访问这些标识符了。
一个包要想访问另一个包中的标识符,那么该标识符首字母必须大写,否则只能在当前包内访问。
- 导出结构体及接口成员
type MyStruct struct {
// 包外可以访问的字段
ExportedField int
// 仅限包内访问的字段
privateField int
}
type MyInterface interface {
// 包外可以访问的方法
ExportedMethod()
// 仅限包内访问的方法
privateMethod()
}
go语言import导入包
导入包后自定义引用的包名
import (
"crypto/rand"
mrand "math/rand" // 将名称替换为mrand避免冲突
)
导入包的重命名只影响当前的源文件。其它的源文件如果导入了相同的包,可以用导入包原本默认的名字或重命名为另一个完全不同的名字。
导入包重命名是一个有用的特性,它不仅仅只是为了解决名字冲突。如果导入的一个包名很笨重,特别是在一些自动生成的代码中,这时候用一个简短名称会更方便。选择用简短名称重命名导入包时候最好统一,以避免包名混乱。选择另一个包名称还可以帮助避免和本地普通变量名产生冲突。例如,如果文件中已经有了一个名为 path 的变量,那么我们可以将”path”标准包重命名为 pathpkg。
每个导入声明语句都明确指定了当前包和被导入包之间的依赖关系。如果遇到包循环导入的情况,Go语言的构建工具将报告错误。 匿名导入包——只导入包但不使用包内类型和数值 如果只希望导入包,而不使用任何包内的结构和类型,也不调用包内的任何函数时,可以使用匿名导入包,格式如下:
import (
_ "path/to/package" // 匿名导入包
)
匿名导入包和其它导入包的方式一样可以让导入包编译到可执行文件当中,同时导入包,也会触发init函数调用。
包在程序启动前的初始化入口:init
在某些需求的设计上需要在程序启动时统一调用程序引用到的所有包的初始化函数,如果需要通过开发者手动调用这些初始化函数,那么这个过程可能会发生错误或者遗漏。我们希望在被引用的包内部,由包的编写者获得代码启动的通知,在程序启动时做一些自己包内代码的初始化工作。
init函数的特性如下:
- 每个源码文件可以使用一个init函数
- init函数会在程序执行前(main()函数执行前)被自动调用
- 调用顺序为main中引用的包,以深度优先顺序进行初始化
例如,假设有这样包的引用关系,main->A->B->C, 那么这些包的init函数调用顺序为:C.init--B.init--A.init--main.init
说明:
- 同一个包中的多个init函数调用顺序不可预期
- init函数不能被其它函数调用
理解包导入后的init函数初始化顺序:
go语言会从main包开始检查其引用的所有包,每个包也可能包含其它的包,go编译器由此构建出一个树状的包引用关系,在根据引用顺序决定编译顺序,一次编译这些包的代码。
在运行时,最后被导入的包会最先初始化并调用init函数
go语言sync包与锁
Go语言中 sync 包里提供了互斥锁 Mutex 和读写锁 RWMutex 用于处理并发过程中可能出现同时两个或多个协程(或线程)读或写同一个变量的情况。
为什么需要锁?
锁是 sync 包中的核心,它主要有两个方法,分别是加锁(Lock)和解锁(Unlock)。
在并发的情况下,多个线程或协程同时其修改一个变量,使用锁能保证在某一时间内,只有一个协程或线程修改这一变量。
不使用锁时,在并发的情况下可能无法得到想要的结果,如下所示:
func main() {
var wg sync.WaitGroup
var a = 0
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
a++
}()
}
wg.Wait()
fmt.Println(a) // 输出结果并不是1000,因为并发不安全导致的
}
协程的执行顺序大概如下所示:
- 从寄存器读取a的值
- 然后做加法运算
- 最后写到寄存器
按照上面的顺序,假如有一个协程取得 a 的值为 3,然后执行加法运算,此时又有一个协程对 a 进行取值,得到的值同样是 3,最终两个协程的返回结果是相同的。
而锁的概念就是,当一个协程正在处理 a 时将 a 锁定,其它协程需要等待该协程处理完成并将 a 解锁后才能再进行操作,也就是说同时处理 a 的协程只能有一个,从而避免上面示例中的情况出现。
互斥锁:
上面出现的问题加一个互斥锁就解决了,修改代码如下
func main() {
var wg sync.WaitGroup
var mu sync.Mutex
var a = 0
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
mu.Lock()
a++
mu.Unlock()
}()
}
wg.Wait()
fmt.Println(a) // 输出结果并不是1000,因为并发不安全导致的
}
读写锁:
读写锁有如下四个方法:
写操作的锁定和解锁分别是func (RWMutex) Lock和func (RWMutex) Unlock;
读操作的锁定和解锁分别是func (RWMutex) Rlock和func (RWMutex) RUnlock。
读写锁的区别在于:
- 当一个goroutine获得写锁定,其它无论是读锁定还是写锁定都将阻塞直到写解锁
- 当一个goroutine获得读锁定,其它读锁定仍然可以继续
- 当有一个或者任意多个读锁定,写锁定等待所有读锁定全部解锁之后才能够进行写锁定
所以说这里的读锁定(RLock)目的其实是告诉写锁定,有很多协程或者进程正在读取数据,写操作需要等它们读(读解锁)完才能进行写(写锁定)。
我们可以将其总结为如下三条:
- 同时只能有一个goroutine获得写锁定
- 同时可以有多个goroutine获得读锁定
- 同时只能存在读锁定或写锁定(读和写互斥)
多个读操作同时读取一个变量时,虽然加了锁,但是读操作是不受影响的(读和写是互斥的,读和读不互斥)
var mu *sync.RWMutex
var wg *sync.WaitGroup
func main() {
mu = new(sync.RWMutex)
wg = new(sync.WaitGroup)
wg.Add(2)
go read(1)
go read(2)
wg.Wait()
}
func read(i int) {
defer wg.Done()
fmt.Println(i, "starting")
mu.RLock()
fmt.Println(i, "reading...")
time.Sleep(time.Second * 3)
mu.RUnlock()
fmt.Println(i, "over")
}
由于读写互斥,所以写操作开始的时候,读操作必须要等写操作完成才能继续,不然读操作只能继续等待:
var mu *sync.RWMutex
var wg *sync.WaitGroup
func main() {
mu = new(sync.RWMutex)
wg = new(sync.WaitGroup)
wg.Add(2)
go read(1)
go write(2)
wg.Wait()
}
func read(i int) {
defer wg.Done()
fmt.Println(i, "read starting")
mu.RLock()
fmt.Println(i, "reading...")
time.Sleep(time.Second * 3)
mu.RUnlock()
fmt.Println(i, "read over")
}
func write(i int) {
defer wg.Done()
fmt.Println(i, "write staring")
mu.Lock()
fmt.Println(i, "writing...")
time.Sleep(time.Second*3)
mu.Unlock()
fmt.Println(i, "write over")
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)