目录

Go语言圣经2.6

【Go语言圣经2.6】

目标

概念

  1. GOPATH模型
  • GOPATH:GOPATH 是一个环境变量,指明 Go 代码的工作区路径。
  • 工作区通常包含三个目录:
  • src :存放源代码,按照导入路径组织。例如,包 gopl.io/ch2/tempconv 应存放在 $GOPATH/src/gopl.io/ch2/tempconv 中。
  • pkg :编译后生成的包文件(中间产物)。
  • bin :可执行文件。
  • 在 GOPATH 模型中,包的导入路径直接对应于 src 目录下的子目录结构
  • 例如,import "gopl.io/ch2/tempconv" 表示编译器将在 $GOPATH/src/gopl.io/ch2/tempconv 下寻找该包的源代码。
  1. Go module 模型(现代依赖管理方式)
  • Go module
  • Go module 是从 Go 1.11 开始引入的,不再强制要求代码必须放在 GOPATH 内。
  • 每个模块有一个 go.mod 文件,其中定义了模块路径(作为导入路径的前缀)和依赖项及其版本。
  • 模块根目录可以放在任意位置,go.mod 中指定的模块路径决定了包的导入路径前缀。
  • 例如,如果 go.mod 声明模块为 gopl.io/ch2/tempconv,则该模块中的包可以直接用该路径导入,无需放在 GOPATH 内。
  • 优势:
  • 自动管理依赖版本,支持版本控制;
  • 使得项目结构更灵活,不受 GOPATH 限制;
  • 编译工具会根据 go.mod 自动解析和下载依赖。
  1. 构建工具如何根据不同模型处理依赖和编译项目
  • go build 命令
  • 在 GOPATH 模型下,go build 根据 GOPATH/src 中的目录结构找到并编译包;
  • 在 module 模型下,go build 会读取当前目录或上级目录中的 go.mod 文件来确定模块范围,并自动处理依赖。
  • 导入包时的区别
  • 在 GOPATH 模型下,你的代码必须位于 GOPATH/src 中;
  • 在 module 模型下,你可以在任何地方创建项目,依赖管理由 go.mod 文件控制。
  1. 包的作用和意义
  • 模块化与封装 Go 语言中的包类似于其他语言中的库或模块,其目的是将相关代码组织在一起,实现模块化编程。
  • 封装 :包内部的实现细节可以隐藏,仅公开需要被外部使用的部分。
  • 单独编译和重用 :每个包可以单独编译,也能在不同程序中复用,提高代码可维护性和协作效率。
  1. 命名空间 每个包都有自己独立的命名空间。当不同包中存在同名的函数或类型,外部引用时加上包前缀,这避免了名称冲突
  2. 导出规则
  • 包中的标识符(如变量、常量、函数、类型等)只有首字母大写时才是导出的,也就是对外可见的;否则只在包内部可见。这为包内部实现细节的隐藏提供了简单而有效的机制。
  1. 文件组织与包结构
  • 一个包通常由一个或多个以 .go 为后缀的源文件组成。这些文件必须以相同的包声明开始。例如,一个包可能存放在 $GOPATH/src/gopl.io/ch2/tempconv 目录中,其导入路径就是 gopl.io/ch2/tempconv
  1. 多个源文件协同工作
  • 包级别的声明(类型、变量、常量、函数)在同一包内的所有源文件中都是共享的,就像所有代码都写在一个文件中一样。
  • 可以将不同功能或逻辑拆分到多个文件中,提高代码组织和可维护性。例如:
  • tempconv.go :放置包级的常量、类型、以及为这些类型定义的方法(如 String())。
  • conv.go :专门放置温度转换函数,如 CToFFToC
  1. 导入包
  • 导入路径与包名
  • 每个包都有一个全局唯一的导入路径,如 "gopl.io/ch2/tempconv"。这个路径由构建工具解析,通常对应一个目录。
  • 包的名字通常在包声明处指定,惯例上包名和导入路径的最后一个字段相同(例如 tempconv
  1. 包注释
  • 在每个包的源文件开头紧跟着的注释称为包注释,它应该简明扼要地说明包的功能。
  • 通常只需在一个文件中包含完整的包注释,如果包比较复杂,也可以单独放在 doc.go 文件中。
  1. 开发工具支持 goimports 和 gofmt
  • 这些工具可以自动添加或删除导入语句,并格式化代码,保持代码风格一致,有助于日常开发。

要点

导入语句的写法与使用

  1. 在源文件中通过 import 语句导入包 import ( “fmt” “gopl.io/ch2/tempconv” )
  2. 导入后,包内导出的标识符(首字母大写的)可以通过“包名.标识符”访问,例如: tempconv.CToF(tempconv.BoilingC)
  • 如果导入后不使用该包,编译器会报错。这鼓励程序员只导入真正需要的包,保持依赖清晰。
  1. 如果有命名冲突或为了简洁,可以将导入的包绑定到另一个名字**(重命名导入)** import tconv “gopl.io/ch2/tempconv” 然后用 tconv.CToF 访问包中的内容。

包的初始化

  1. 初始化顺序规则
  • 包中的全局变量(包级变量)的初始化遵循先依赖后顺序:
  • 变量的初始化顺序是按照它们在源代码中出现的顺序进行。
  • 当一个包被导入时,所有包级变量会在 main() 函数执行前完成初始化。 var a = b + c // a 是第三个初始化的变量 var b = f() // b 是第二个初始化的变量(依赖 c) var c = 1 // c 是第一个初始化的变量 func f() int { return c + 1 }
  • 在这个例子中,初始化时会确保 c 已经赋值,这样 b 才能正确调用 f()
  • 包初始化顺序与依赖
  • 当一个包 A 导入包 B 时,B 包会先于 A 包初始化。
  • 这种自下而上的初始化方式确保 main 包执行前,所有依赖包都已完成初始化。
  1. init函数
  • init 函数的作用
  • init 函数用于在包初始化时执行额外的初始化逻辑。
  • 每个源文件可以包含多个 init 函数,且它们会在包初始化时按照声明顺序自动调用。
  • init 函数不能被显式调用或引用,仅用于初始化工作。
  • 示例
  • 构建辅助数据表或进行复杂的初始化运算。例如在 popcount 包中,用 init 函数预生成一个查表数组: var pc [256]byte func init() { for i := range pc { pc[i] = pc[i/2] + byte(i&1) } }
  • 如果初始化过程较复杂,可以采用匿名函数直接在变量声明中完成初始化 var pc [256]byte = func()(pc [256]byte) { for i := range pc{ pc[i] = pc[i/2] + byte(i&1) } return }

语言特性

习题

  1. 重写PopCount函数,用一个循环代替单一的表达式。 // 假设已经定义查数数组pc[256] func PopCountLoop(x uint64) int{ var sum int for i:=0; i<8; i++{ sum += int(pc[byte(x)]) // 取最低8位 x » 8 } }
  2. 用移位算法重写PopCount函数,每次测试最右边的1bit,然后统计总数。 func PopCountShift(x uint64) int { var sum int for i := 0; i < 64; i++ { sum += int(x & 1) x »= 1 } return sum }
  3. 表达式x&(x-1)用于将x的最低的一个非零的bit位清零。使用这个算法重写PopCount函数
  • 二进制,x-1 会把 x 中最右边的那个“1”变成“0”,并把后面所有的 0 变成 1。(当你给 x 减 1 时,从最右边开始,所有连续的 0 都借1减1,直到碰到第一个 1,这个 1 就变成 0。)
  • 当你把 x 和 x-1 做与操作时,只有当两个对应位置都是 1 时,结果才为 1。由于 x-1 在原来最低1的位置已经变成了 0,所以 x&(x-1) 在那个位置肯定是 0,并且之前为0的低位不会改变——这就把x的最低的一个非零的bit位清零 func PopCountClear(x uint64) int { var count int for x != 0 { x &= x - 1 // 清除最低位的1 count++ } return count }

总结与补充

  1. popcount算法解读 func PopCount(x uint64) int { return int(pc[byte(x»(08))] + pc[byte(x»(18))] + pc[byte(x»(28))] + pc[byte(x»(38))] + pc[byte(x»(48))] + pc[byte(x»(58))] + pc[byte(x»(68))] + pc[byte(x»(78))]) } 想象一下你有一本“数字图鉴”,里面记着0到255这256个数字,每个数字旁边都写着它的“1的个数”。这个图鉴就是那个预先计算好的表格(pc数组)。
  • 预先计算图鉴(init函数)
  • 做法: 对于0到255中的每个数字,我们算一算它的二进制写法里有几个1,然后把这个数字和它的1的个数存进图鉴里。
  • 秘诀: 计算一个二进制数字的1的个数时,我们可以把它分解为“除以2后的数字”里的1的个数,再加上“最后一位是否为1”。比如说,如果数字6(二进制110),我们先看6/2等于3(二进制11),再加上6最后一位(0),结果就是2个1。
  • 你把一个数字除以2(也就是右移一位),其实就是把最右边那一位扔掉了。那么,这个数字中1的总数就等于“扔掉最后一位后剩下的数字中的1的个数”加上“刚刚扔掉的那一位是否是1”。
  • 用图鉴快速数1的个数(PopCount函数)
  • 大数字切小块: 当我们有一个很大的64位数字时,不用检查64个数字,而是把它分成8个8位的小数字。
  • 表达式 x >> (k*8) 的意思是把数字 x 向右移动 k*8 位,这样原本在第 k 个8位区域的数字就会移动到最右边。
  • 使用 byte() 把移动后的结果截取成一个8位的数字(一个字节)。
  • 查表加和: 对每个8位的小数字,直接在图鉴里查出它有几个1,然后把8个结果加起来,就知道整个64位数字里有多少个1。