Go语言学习(二)
第2章 程序结构
2.1 名称
Go中函数、变量、常量、类型、语句标签和包的名称遵循一个简单的规则:名称的开头是一个字母(Unicode中的字符即可)或下划线,后面可跟任意数量的字符、数字和下划线,并区分大小写。如heapSort和Heapsort是不同的名称。
- Go有25个关键字,如下:
- 三十几个内置的预声明的常量、类型和函数:
这些名称不是预留的,可以在生命中使用他们。但是有冲突的风险
名称本身没有长度限制,但是习惯以及Go的编程风格倾向于使用短名称,特别是作用域较小的局部变量,我们更喜欢使用一个变量叫i而不是thisLoopIndex。通常,名称的作用域越大,就使用越长且更有意义的名称。
2.2 声明
四个主要的声明:变量(var)、常量(const)、类型(type)和函数(func)。
Go程序存储在一个或多个以.go为后缀的文件里。每个文件以package声明开头,表明文件属于哪个包。package声明后面是import声明,然后是包级别的类型、变量、常量、函数的声明,不区分顺序。如下:声明了一个常量、一个函数和一对变量:
package main
import "fmt"
const boilingF=212.0
func main(){
var f = boilingF
var c = (f - 32) * 5 / 9
fmt.Printf("boiling point = %g°F or %g°C\n", f, c)
//输出:
//boiling point = 212°F or 100°C
}
包级别的实体名字不仅对于包含其声明的源文件可见,而且对于同一个包里面的所有源文件都可见。
函数的声明包含一个名字、一个参数列表(由函数的调用者提供的变量)、一个可选的返回值列表,以及函数体(其中包含具体逻辑语句)。如果函数不返回任何内容,返回值列表可以省略。函数的执行从第一个语句开始,直到遇见一个返回语句,或者执行到无返回结果的函数的结尾。然后程序控制和返回值(如果有的话)都返回给调用者.
2.3 变量
变量声明通用格式:
var name type = expression
类型和表达式部分可以省略一个,但是不能都省略。如果类型省略,它的类型将有初始化表达式决定;如果表达式省略,其初始值对应于类型的零值。
对于数字是0,对于布尔值是false,对于字符串是"",对于接口和引用类型(slice、指针、map、通道、函数)是nil。对于一个像数组或是结构体这样的复合类型,零值是其所有元素或成员的零值。
2.3.1 短变量声明
在函数中,一种称作短变量声明
的可选形式可以用来声明和初始化局部变量。它使用name:=expression
的形式,name的类型有expression的类型决定。
i := 100 //一个int类型的变量
var boiling float64 = 100 //一个float64类型的变量
var names []string
var err error
var p Point
短变量声明最少声明一个新变量(:=
的左边最少有一个新的变量名)
PS:一个容易忽略的地方是:短变量声明不需要声明所有在左边的变量。如果一些变量在同一个词法块
中声明,那么对于那些变量,短声明的行为等同于赋值。
2.3.2 指针
指针的值是一个变量的地址。一个指针指示值保存的位置。不是所有的值都有地址,但是所有的变量都有。使用指针可以在无需知道变量名字的情况下,间接读取或更新变量的值。
每一个聚合类型变量的组成(结构体的成员或数组中的元素)都是变量,所以也有一个地址。变量有时候使用一个地址化的值。代表变量的表达式,是唯一可以应用取地址操作符&
的表达式。
函数返回局部变量的地址是非常安全的。例如在下面的代码中,通过调用f产生的局部变量v即使是在调用返回后依然存在,指针p依然引用它:
var p = f()
func f() *int {
v := 1
return &v
}
每次调用f都会返回一个不同的值:
fmt.Println(f()==f()) // "false"
因为一个指针包含变量的地址,所以传递一个指针参数给函数,能够让函数更新间接传递变量的值。
func incr(p *int) int {
*p++ //递增p所指向的值;p自身保持不变
return *p
}
v := 1
incr(&v) //v现在等于2
fmt.Println(incr(&v))//"3" v现在是3
每次使用变量的地址或者复制一个指针,我们就创建了新的别名或者方式来标记同一变量。例如,*p是v的别名。指针别名允许我们不用变量的名字来访问变量,这一点是非常有用的,但是,为了找到所有访问变量的语句,需要知道所有的别名。不仅指针产生别名,当赋值其他引用类型(像slice、map、通道,甚至包含这里引用类型的结构体、数组和接口)的值的时候,也会产生别名。
指针对于flag包很关键,它使用程序的命令行参数来设置整个程序内某些变量的值
在下面的这个例子中:
-n使echo忽略正常输出时结尾的换行符
-s sep使用sep替换默认参数输出时使用的空格分隔符。
package main
import (
"flag"
"fmt"
"strings"
)
var n = flag.Bool("n", false, "omit trailing newline")
var seq = flag.String("s", " ","separator")
func main(){
flag.Parse()
fmt.Print(strings.Join(flag.Args(), *sep))
if !*n{
fmt.Println()
}
}
- flag.Bool函数创建一个新的布尔表示变量。它有三个参数:标识的名字(“n”),变量的默认值(false),以及当用户提供非法标识、非法参数抑或-h或-help参数时输出的消息。同样地,flag.String也是用名字、默认值和消息来创建一个字符串变量。变量sep和n是指向标识变量的指针,他们必须通过sep和n来访问。
- 当程序运行时,在使用标识前,必须调用flag.Parse来更新标识变量的默认值。飞表示参数也可以从flag.Args()返回的字符串slice来访问。如果flag.Parse遇到错误,它输出一条帮助消息,然后调用os.Exit(2)来结束程序。
2.3.3 new函数
另外一种创建变量的方式是使用内置的new函数。表达式new(T)创建一个未命名的T类型变量,初始化的T类型的零值,并返回其地址(地址类型为*T)。
使用new创建的变量和取其地址的普通局部变量没有什么不同,只是不需要引入(和声明)一个虚拟的名字,通过new(T)就可以直接在表达式中使用。因此new只是语法上的便利,不是一个基础概念。
这个规则有一个例外:两个变量的类型不携带任何信息且是零值,例如struct{}或[0]int,当前的实现里面,它们有相同的地址。
因为最常见的未命名变量都是结构体类型,它的语法比较复杂,所以new函数使用得相对较少。
new是一个预声明的函数,不是一个关键字,所以它可以重定义为另外的其他类型,
func delta(old, new int) int { return new - old}
当然,在delta函数内,内置的new函数是不可用的。
2.3.4 变量的生命周期
2.4 赋值
2.4.1 多重赋值
另一种形式的赋值是多重赋值,它允许几个变量一次性被赋值。在实际更新变量前,右边所有的表达式被推演,当变量同时出现在赋值符两侧的时候这种形式特别有用。
x , y = y , x
a[i], a[j] = a[j], a[i]
通过函数使用额外的返回值来指示一些错误情况,例如通过os.Open返回的error类型,或者一个通常叫ok的bool类型变量。我们会在后面的章节中看到,这里有三个操作符也有类似的行为。如果map查询、类型断言或者通道接收动作出现在两个结果的赋值语句中,都会产生一个额外的布尔型结果:
v, ok = m[key] //map查询
v, ok = x.(T) //类型断言
v, ok = <-ch //通道接收
2.4.2 可赋值性
赋值语句是显示形式的赋值,但是程序中很多地方的赋值是隐式的:一个函数调用隐式地将参数的值赋给对应参数的变量;一个return语句隐式地将return操作数赋值给结果变量。复合类型的字面量表达式,例如slice:
medals := []string{"gold","silver","bronze"}
隐式地个每一个元素赋值。它可以写成下面这样:
medals[0] = "gold"
medals[1] = "silver"
medals[2] = "bronze"
可赋值性的规则就是类型必须精准匹配,nil可以被赋给任何接口变量或引用类型。常量有更灵活的可赋值性规则来规避显式的转换。
2.5 类型声明
type
声明定义一个新的命名类型,它和某个已有类型使用同样的底层类型。命名类型提供了一种方式来区分底层类型的不同活着不兼容使用,这样它们就不会在无意中混用。
type name underlying-type
类型的声明出现在包级别,这里命名的类型在整个包中可见,如果名字是导出的(开头是大写字母),其他的包也可以访问它。
在这里定义了两个类型——Celsius(摄氏温度)和Fahrenheit(华氏温度),它们分别对应两种温度计量单位。即使使用相同的底层类型float64,他们也不是相同的类型,所以他们不能使用算术表达式进行合并和比较。区分这些类型可以防止无意间合并不同计量单位的温度值;
-
对于每个类型T,都有一个对应的类型转换操作T(x)将值x转换为类型T。
-
如果两个类型具有相同的底层类型或二者都是指向相同底层类型变量的未命名指针类型,则二者可以互相转换。
-
类型转换不改变类型值的表达方式,仅改变类型。
-
如果x对于类型T是可赋值的,类型转换也是允许的,但是通常是不必要的。
-
命名类型的底层类型决定了它的结构和表达方式,以及它支持的内部操作集合,这些内部操作与直接使用底层类型的情况相同。
-
通过
==
和<
之类的比较操作符,命名类型的值可以与其相同类型的值或者底层类型相同的未命名类型的值相比较。但是不同命名类型的值不能直接比较
2.6 包和文件
- 每一个包给它的声明提供独立的命名空间。
- 在Go里,通过一条简单的规则来管理标识符是否对外可见:导出的标识符以大写字母开头。
- 习惯上,应该在开头用一句话对包进行总结性的描述。每一个包里只有一个文件应该包含该包的文档注释。拓展的文档注释通常放在一个文件中,按惯例名字叫做
doc.go
2.6.1 导入
- 如果导入一个没有被使用的包会报错。这个检查帮助消除代码演进过程中不再需要的依赖。
2.6.2 包初始化
- 报的初始化从初始化包级别的变量开始,这些变量按照声明顺序初始化,在依赖已解析完毕的情况下,根据依赖的顺序进行。
2.7 作用域
- 声明将名字和程序实体关联起来,如一个函数或一个变量。声明的作用域是指用到声明时所声明名字的源代码端。
一个程序可以包含多个同名的声明,前提是他们在不同词法块中。
当编译器遇到一个名字的引用时,将从最内层的封闭词法块到全局块中寻找其声明。如果在内层和外层块中都存在这个声明,内层的将先被找到,这种情况下,内层声明将覆盖外层声明,使他不可访问。
在包级别中,声明的顺序和他们的作用域没有关系,所以一个声明可以引用它自己或者跟在它后面的其他声明,使我们可以声明递归或相互递归的类型和函数。如果常量或变量声明引用它自己,则编译器会报错。