第二章 程序结构
2.1 名称
- 名称区分大小写。
- 如果一个实体仅在函数中声明,那么其只在函数中有效。
- 实体名的首字母必须大写才能被包外的程序访问。
- 命名遵守驼峰命名法。
2.2 声明
声明用来创建函数实体,主要使用的声明有四个:
var : 用来声明变量。
const : 用来声明常量。
type : 用来声明类型。
fun : 用来声明函数。
go文件后缀名均为.go,文件中的内容按以下顺序排列。
- 文件中第一行以package开头,指定当前文件属于哪个包。
- 之后跟import,指明当前文件引用了哪些外部包。
- 接着定义包级别的常量、变量、类型、函数。
包级别的实体对于整个包内的所有源文件都可见,局部声明仅是在声明函数的内部可见。
文件的作用主要体现在文件内的函数上,函数的声明包含以下几个组件
- 函数名:一定要有,多个函数的函数名在一定条件下可以相同(重载)。
- 参数列表:调用函数时所传入的参数,不一定要有。
- 函数体:函数所执行的逻辑语句。
- 返回值:返回给函数调用者的处理结果,不一定有。
2.3 变量
变量通过var声明,声明时需要指定变量的变量名、类型、初始值,如果不显示指定变量的初始值,那么变量初始值将默认设置为该类型的零值(数字类型是0,字符串类型是空字符串······)。
go语言可以一次声明多个不同类型的变量,例如:
var a,b,c = 1,"hello",true
- go语言一次创建多个变量的机制也可以用来接收函数返回值,例如:
var x,err = os.Open(name) //os.Open 返回一个文件和一个错误2.3.1 短变量声明
- go语言有一种特殊的赋值语法(name := expression),他直接将experssion的值赋给实体name,name的类型和experssion相同。例如:
a:=1 //创建一个int类型的变量a,初始值为1
- 和变量创建一样,短变量也支持一次创建多个实体,例如:
a, b, c := 1, 0.2, "hello" //创建三个变量,类型依次为int,double,string,初始值依次为1,0.2,“hello”
- 与声明(:=)不同,(=)表示赋值
x, y = y, x //表示将y和x的值进行交换
- 短变量声明至少要声明一个新变量,否则编译无法通过
2.3.2 指针
变量是存储数据的地方,声明的变量具有不同的变量名,不同的变量使用名字进行区分。指针的值是指针对应变量的存放地址,所有的变量都有地址,只要知道变量对应的指针(变量地址),就可以在不知道变量名的情况下访问此变量。
假设我们声明了一个int类型的变量,变量名为x
var x int
- 对于变量x,表达式&x将获取一个指向x的指针,我们将这个值赋给p,此时我们就可以认为p指向x,或者p包含x的地址。通过表达式*p就可以获取到p指向的变量(x)的值。例如:
x := 1 p := &x //p是整形指针,指向x fmt.Println(*p) //输出x的值,也就是12.3.3 new函数
- 还有一种声明变量的方法是使用内置的new函数,表达式new(T)会创建一个未命名的T类型变量,初始值为T类型的零值。例如:
p := new(int) //创建了一个*int类型的p,指向未命名的int变量
使用new创建变量和先声明变量再取其地址是等价的,new只是语法上的便利。
每一次调用new会产生具有唯一地址的不同变量,例如:
p := new(int) q := new(int) fmt.Println(p==q) //输出flase,p和q的指向的地址是不同的2.3.4 变量的生命周期
- 变量的声明周期是变量在程序执行过程中存在的时间段。
- 包级别的变量生命周期是整个程序的执行时间。
- 函数级变量每次调用函数时创建,函数结束时变得不可再访问,此时被垃圾回收机制回收。
- go中的垃圾回收机制简单来说就是变量可以通过指针或其他方式访问到的时候是存在的。如果访问变量的路径以及不存在,那么就会判定此变量已经废弃,会被垃圾回收机制回收。
2.4 赋值
赋值语句用来更新变量所指的值,表达式x=y表示将y的值赋给x。
x = 1 //将1赋值给变量x *p = true //通过指针间接赋值2.4.1 多重赋值
和变量声明一样,赋值可以一次性对多个变量进行赋值。
a, b, c = 1, 2, 3 //分别为变量a、b、c赋值1、2、32.4.2 可赋值性
赋值语句是显示的赋值,也可以使用return等操作隐式的为变量赋值,前提是这个变量可赋值并且与被赋的值类型相同。
2.5 类型声明
每个实体都拥有对应的特性,例如自身的大小,进行的操作、拥有的功能。
不同的程序会使用一些相同的参数来表达不同的含义,例如一个int类型的变量a,赋值a=5。此时a可以根据自身程序的定义表示不同的含义,可以是5℃、5cm、5km/s、5分钟等。
2.6 包和文件
go语言中的包类似与其他语言中的库和模块类似,用于支持模块化、封装、编译隔离和重用。一个包以文件的格式存储,包中包含一个或多个.go结尾的文件。
go文件都是以package开头,指明当前文件是属于哪个包。
包中的变量或函数只有以大写文字开头才能被外部文件访问。
练习2.1 添加类型、常量和函数到tempconv包中,处理以开尔文为单位(K)的温度值,0K=-273.15℃,变化1K和变化1℃是等价的。
tempconv.go:tempconv包的基础属性
package tempconv import "fmt" //依次定义设施温度、华氏温度、开尔文温度 type Celsius float64 type Fahrenheit float64 type Kelvin float64 //定义三种温度下的绝对零度、水结冰温度、水沸点,这些参数可以以属性的格式被访问 const ( AbsoluteZeroC Celsius = -273.15 FreezingC Celsius = 0 BoilingC Celsius = 100 AbsoluteZeroK Kelvin = 0 FreezingK Kelvin = 273.15 BoilingK Kelvin = 373.15 AbsoluteZeroF Fahrenheit = -459.67 FreezingF Fahrenheit = 32 BoilingF Fahrenheit = 212 ) //定义每种类型的输出格式,这样之后使用这些类型时会自动添加对应的单位 func (c Celsius) String() string { return fmt.Sprintf("%g℃", c) } func (f Fahrenheit) String() string { return fmt.Sprintf("%g℉", f) } func (k Kelvin) String() string { return fmt.Sprintf("%gK", k) }conv.go
package tempconv // CToF 摄氏温度转换为华氏温度 func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) } // FToC 华氏温度转换为摄氏温度 func FToC(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) } // CToK 摄氏温度转换为开尔文温度 func CToK(c Celsius) Kelvin { return Kelvin(c + 273.15) } // KToC 开尔文温度转换为摄氏温度 func KToC(k Kelvin) Celsius { return Celsius(k - 273.15) } // FToK 华氏温度转换为开尔文温度 func FToK(f Fahrenheit) Kelvin { return Kelvin(CToK(FToC(f))) } // KToF 开尔文温度转换为华氏温度 func KToF(k Kelvin) Fahrenheit { return Fahrenheit(CToF(KToC(k))) }2.6.1 导入
- 在一个包中导入另一个包即可使用被导入包中的变量、函数等(前提是这些变量和函数命名以大写字母开头)。
- 在2018年以前,go进行包引用的方法主要是设置GOPATH,指定被导入包的存放路径或者网络地址就可以访问到对应的包,在main文件开头的import中指定需要引入的包名就可以访问对应的包。此处需要注意,go语句有非常严格的语法规范,所有创建的常量、变量以及引入的包都必须在程序中有使用,否则编译会报错。
- GOPATH使用起来较为方便,但其有一个严重问题就是版本管理不方便,当被导入的包有更新时,调用方也必须兼容这部分新的改动,这在项目管理中是很不方便的,因此在2018年引入了Go modules(简称go mod)来管理包之间的调用,这也是现在主流的go程序包管理方式。
以练习2.1中的温度转换包为例,假设我们要在一个其他的包中使用温度转换包的实体,需要进行以下配置:
设置GO111MODULE=on,GO111MODULE参数是Go modules模式的开关,有三个参数:off、auto、on,分别表示关闭、自动(包中有go.mod文件时开启Go modules)和开启。
如果包中没有go.mod文件的话需要执行“go mod init”指令来创建包对应的go.mod文件(现在在goland中创建文件时会自动创建此文件)。
创建好的go.mod文件是以下配置,第一行的module指明了当前包的包名(这里是起的保存路径),下面一行是go的版本,需要保证调用包和被调用包的版本相同。
module gopl.io/ch2/tempconv go 1.18
- 下面配置调用包中的go.mod文件
module variable //当前包的包名是variable go 1.18 //需要导入的包以及其版本,这里我是调用的本地包,所有可以随便指定。 //指定完包版本后还要注意加上-incompatible表示次包从本地引入,网络上导入的包不需要此参数 require ( gopl.io/ch2/tempconv v0.0.0-incompatible ) //replace表示需要从本地导入的包的存放路径,这里的路径是相对路径,我的variable包和gopl.io包 //在同一个文件夹中,../表示返回上一级目录,再依次访问到tempconv包 replace ( gopl.io/ch2/tempconv => ../gopl.io/ch2/tempconv )
- 在variable包中调用tempconv包
import ( "gopl.io/ch2/tempconv" )这样就可以在variable中以tempconv为前缀使用tempconv包中的属性和方法。
练习2.2 写一个类似于cf的通用单位转换程序,从命令行参数或标准输入(如果没有参数)获取数字然后将每一个数字转换为以摄氏温度和华氏温度表示的温度,以英寸和米表示的长度单位,以磅和千克表示的重量等等。
这里我选择写一个长度单位(米、厘米、毫米)的转换:
lengthconv.go:长度类型定义
package lengthconv import "fmt" //定义类型米、厘米、毫米 type Rice float64 type Centimeter float64 type Millimeter float64 //定义对应类型的输出格式 func (r Rice) String() string { return fmt.Sprintf("%gM", r) } func (c Centimeter) String() string { return fmt.Sprintf("%gcm", c) } func (m Millimeter) String() string { return fmt.Sprintf("%gmm", m) }conv.go:长度类型转换
package lengthconv // RToC 米转厘米 func RToC(r Rice) Centimeter { return Centimeter(r * 100) } // RToM 米转毫米 func RToM(r Rice) Millimeter { return Millimeter(r * 1000) } // CToR 厘米转米 func CToR(c Centimeter) Rice { return Rice(c / 100) } // CToM 厘米转毫米 func CToM(c Centimeter) Millimeter { return Millimeter(c * 10) } // MToR 毫米转米 func MToR(m Millimeter) Rice { return Rice(m / 1000) } // MToC 毫米转厘米 func MToC(m Millimeter) Centimeter { return Centimeter(m / 10) }还是使用variable来调用lengthconv
配置variable包中的go.mod
module variable go 1.18 require ( gopl.io/ch2/lengthconv v0.0.0-incompatiable ) replace ( gopl.io/ch2/lengthconv => ../gopl.io/ch2/lengthconv )在variable包中编写逻辑代码,完成题目中需求的输出操作
package main import ( "fmt" "gopl.io/ch2/lengthconv" ) //输出方法,参数为长度值与长度类型,根据传入的类型自动将其转换为其余两种函数类型进行输出 func conv(number float64, company float64) { switch { case company == 1: a := lengthconv.Rice(number) fmt.Printf("%v=%v\n", a, lengthconv.RToC(a)) fmt.Printf("%v=%v\n", a, lengthconv.RToM(a)) case company == 2: a := lengthconv.Centimeter(number) fmt.Printf("%v=%v\n", a, lengthconv.CToR(a)) fmt.Printf("%v=%v\n", a, lengthconv.CToM(a)) case company == 3: a := lengthconv.Millimeter(number) fmt.Printf("%v=%v\n", a, lengthconv.MToR(a)) fmt.Printf("%v=%v\n", a, lengthconv.MToC(a)) default: fmt.Println("暂无此长度单位,请重新输入!") } } func main() {t.Println(c) //控制台输入一个长度单位及其值,将其转换为其他两种单位的长度数据 //此处定义的for为无限循环,因为是做测试就简单写了,实际开发中还是要设置循环结束条件的 for { var number float64 var company float64 fmt.Println("请输入长度数值") fmt.Scan(&number) fmt.Println("请输入单位对应的数字:\n1.米\n2.厘米\n3.毫米") fmt.Scan(&company) //调用conv方法,将输入的长度值转换为其余两种长度值进行输出 conv(number, company) } }2.6.2 包初始化
包初始化是从包级别的变量开始,如果多个实体之间互相有依赖,则会先初始化被依赖的实体。
如果包是由多个.go文件组成的,编译会按照编译器收到文件的顺序进行,但go工具会在编译器前对多个.go文件进行排序,包含main函数的main包会被放在最后,保证编译main包时其所依赖的包都有被编译到。
这里书中有一个很精彩的例子。在popcount包中定义了一个PopCount函数,它用来返回一个数字中被置位的个数,即将这个数字转换为2进制后为值为1的个数,这被称为种群统计。
package popcount //定义一个byte格式的二进制类型pc,最大长度为256位 var pc [256]byte //init函数在main函数之前执行,他将256中byte对应的置位个数提前进行统计,放在pc[i]中 func init() { for i := range pc { //每种byte的置位个数等于自身除以2(右移一位的数)的置位个数再加自身最末尾是否为1 //i&1判断对应的i的最末尾是否为1,‘&’表示‘与’,相同为1,不同为0 pc[i] = pc[i/2] + byte(i&1) } } //PopCount,返回x的种群统计(置位的个数) //PopCount函数中的pc每次将x的值右移八位后取末尾的八位计算这八位的置位个数,并将其累加起来,结果就是x //的置位个数。 func PopCount(x uint64) int { return int(pc[byte(x>>(0*8))] + pc[byte(x>>(1*8))] + pc[byte(x>>(2*8))] + pc[byte(x>>(3*8))] + pc[byte(x>>(4*8))] + pc[byte(x>>(5*8))] + pc[byte(x>>(6*8))] + pc[byte(x>>(7*8))]) }练习2.3 使用循环重写PopCount。
func PopLoop(x uint64) int { a := 0 //每次计算八位的byte置位数,循环8次,累加所有的值 for i := 0; i < 8; i++ { a += int(pc[byte(x>>(i*8))]) } return a }练习2.4 写一个用于统计位的PopCount,它在其实际参数的64位上执行移位操作,每次判断最右边的位,进而实现统计功能
func PopDisPM(x uint64) int { a := 0 //循环64次,每次判断x的最后一位是否为1,并将x整体后移一位 for i := 0; i < 64; i++ { //a用来统计x的置位个数,若当前x的最后一位是置位,则a加一 if x&1 == 1 { a++ } //x整体后移一位 x = x >> 1 } return a }练习2.5 使用x&(x-1)可以清楚x最右边的非零位,利用该特点写一个PopCount,然后评价他的性能
func PopClear(x uint64) int { a := 0 //当x大于0时持续循环并记录循环次数,当x等于零时停止循环 for x > 0 { x = x & (x - 1) a++ } return a }算法的魅力,x减一会改变x的最后一个非0位,每次循环清除一个非零位,循环的次数就是x的置位个数,在PopDisPM中循环次数固定是64次,而在PopClear中最差的情况下(64位全部是1)循环次数才是64次,性能提升还是挺大的。
2.7 作用域
声明为程序实体赋予名称,如一个函数或一个变量。声明的作用域是指用到声明名字的源代码段。
作用域和生命周期不同。声名是编译时属性、声明周期是运行时属性。
当编译器遇到一个名字引用时,会从最内层的语法块逐渐向外层寻找其声明,如果语法块内外层都有这个声明,那么先查找到的内层声明会覆盖外层声明。
func f() {} varg = 'g' func main(){ f := 'f' fmt.Println(f) //输出'f',main函数内部的字符f声明覆盖了外部的f函数声明 }
- 代码块中定义的变量不可被外部实体访问,例如下面if函数中定义的f和err就无法被外部和函数使用。
if f, err := os.Open(fname); err != nil{ // 编译错误:未使用f return err } f.Stat() // 编译错误:未定义f f.Close() // 编译错误:未定义f
- 解决方法有两个,将f和err定义为包级变量或将Stat()和Close()放到if语法块中
//f和err定义为包级变量 f, err := os.Open(fname) if err != nil{ return err } f.Stat() f.Close() //将Stat()和Close()放到if语法块中 if f, err := os.Open(fname); err != nil{ return err } else { // f 与 err 在这里可见 f.Stat() f.Close() }
本文介绍了Go语言中的程序结构,包括名称规范、声明、变量管理、指针、类型声明、包和文件的组织。重点讲解了变量的声明与生命周期,包括使用var、const、type和fun进行声明,以及短变量声明和指针操作。同时,文章讨论了包的导入、生命周期以及初始化过程。此外,还介绍了作用域的概念,如何避免和解决变量覆盖问题。最后,通过几个练习展示了温度转换和长度单位转换的实现,以及位计数的不同方法。

被折叠的 条评论
为什么被折叠?



