《Go语言圣经》学习笔记:2.程序结构(上)
2.1 命名
- 不要使用关键字:基本上市面上的语言都这么要求,并且go的关键字和c类的语言大同小异。还有大约30多个预定义的名字,比如int和true等,主要对应内建的常量、类型和函数。
- 命名时尽量短小精悍,除非这个变量作用域比较大。
- 使用驼峰式命名,也就是将单词用大小写字母区分开来。如QuoteRuneToASCII。但是一些缩略词就不要有大小写区分了,如htmlEscape、HTMLEscape或escapeHTML。
2.2 声明
- 声明语句定义了程序的各种实体对象以及部分或全部的属性
- Go语言主要有四种类型的声明语句:var、const、type和func,分别对应变量、常量、类型和函数实体对象的声明
- 包中声明的一级(全局)类型的顺序无关紧要。也就是说一级类型可以声明在后方。而函数内部的类型必须在其使用前声明。例子如下:
package main
import "fmt"
func main() {
x := "hello"
fmt.Println(i)
fmt.Println(x)
//x := "hello" // 函数里面不能在使用后进行声明,必须使用前进行声明
}
const i = 100 // 可以将常量(变量)定义在最后面
上面程序是可以正常运行并输出正确结果的。一级类型(全局变量,函数)都可以定义在后面。
2.3 变量
变量声明语法:
var 变量名字 类型 = 表达式
使用方式如下:
func main() {
var s1 string = "hello" // 完整表达
var s2 = "world" // 省略类型声明,交由编译器自行推导
var s3 string // 进行零初始化,为空字符串""
fmt.Println(s1) // hello
fmt.Println(s2) // world
fmt.Println(s3) // ""
}
当代码中的变量没有初始化的时候,编译器会自动进行零值初始化。零值初始化机制可以确保每个声明的变量总是有一个良好定义的值,因此在Go语言中不存在未初始化的变量。这个特性可以简化很多代码,而且可以在没有增加额外工作的前提下确保边界条件下的合理行为。
另外,数值类型(如int, float等等)变量对应的零值是0,布尔类型变量对应的零值是false,字符串类型对应的零值是空字符串,接口或引用类型(包括slice、指针、map、chan和函数)变量对应的零值是nil。
2.3.1 简短变量声明
- 以“名字 := 表达式”形式声明变量,变量的类型根据表达式来自动推导。
- 可以用来声明和初始化一组变量
- 可以用函数的返回值来声明和初始化变量
- 简短变量声明语句中必须至少要声明一个新的变量
i, j := 0, 1 // 声明并初始化一组变量,类型会自动推导为int
j, k := 2, 3 // 有一个新的变量k,可以使用简短变量声明
// j, k := 4, 5 // compile error,都是已被声明的变量,不能再次声明
f, err := os.Open(name) // 使用函数的返回值进行声明
2.3.2 指针
指针这块其实和C/C++使用方式大同小异。
不过在Go语言中,返回函数中局部变量的地址也是安全的。 如以下代码:
func f() *int {
i := 1
return &i
}
func main() {
p1 := f()
p2 := f()
fmt.Println(*p1) // 1
fmt.Println(*p1) // 1
fmt.Println(*p2) // 1
fmt.Println(*p2) // 1
}
这点和C++还是有差异的,C++在在函数中返回函数中声明的局部变量的地址是很危险的行为,而Go不会,原理在后面函数生命周期会讲到。
另外,鼓励在函数中多使用指针或者引用,特别是复合类型。这样可以节省资源并且减少拷贝时间。
如以下代码:
type Student struct {
Age int
Gender string
Name string
}
func printStudent(s *Student) { // 只需要传入结构体的地址便可
// 这里有一个小知识点就是获取地址必须是用Printf配合%p
fmt.Printf("%p\n",s) // 输出s的内容
fmt.Println("姓名:", s.Name)
fmt.Println("性别:", s.Gender)
fmt.Println("年龄:", s.Age)
}
func main() {
stu := Student{18, "Man", "Mike"}
fmt.Printf("%p\n",&stu) // stu的地址,输出结果和上面的s是一致的
printStudent(&stu)
}
2.3.3 new函数
new函数主要用于开辟新的内存空间。
个人感觉new函数并不如C++中使用的多,比如Go允许在函数中创建局部变量并返回局部变量的地址,并且函数外使用该变量是可以的。上面有段代码可以看出。
type Student struct {
Age int
Gender string
Name string
}
func main() {
var pi *int // 指针只是声明,初试值为nil,并没有分配内存
if pi == nil {
fmt.Println("p1 is nil")
}
pi = new(int) // 申请一块内存
*pi = 10 // 如果没有申请内存就进行取值或者赋值操作会报错
fmt.Println(*pi)
var pstu *Student // 也可以使用复合类型来创建指针
if pstu == nil {
fmt.Println("pstu is nil")
}
pstu = new(Student) // 申请一块内存
pstu.Name = "Mike"
pstu.Age = 18
pstu.Gender = "Man"
fmt.Println(pstu.Name, pstu.Gender, pstu.Age)
}
2.3.4 函数生命周期
- 一级声明的变量(全局变量)来说,它们的生命周期和整个程序的运行周期是一致的
- 局部变量的生命周期则是动态的。Go语言利用其自动垃圾收集器从每个包级的变量和每个当前运行函数的每一个局部变量开始,通过指针或引用的访问路径遍历,是否可以找到该变量。如果不存在这样的访问路径,那么说明该变量是不可达的,也就是说它是否存在并不会影响程序后续的计算结果。一旦确认变量不可达那么就进行垃圾回收。
- 编译器会自动选择在栈上还是在堆上分配局部变量的存储空间,但可能令人惊讶的是,这个选择并不是由用var还是new声明变量的方式决定的。这点和C++不一样
在堆还是在栈上面分配并非由var或new决定,由编译器自己判断。如以下代码:
var global *int
func f() {
var x int
x = 1
global = &x
}
func g() {
y := new(int)
*y = 1
}
编译器判断x变量从f()中逃逸出来,所以把它分配在堆中。而对于y,并不会从g()中逃逸出来,即便使用了new来申请了内存空间,所以编译器可以选择在栈上分配y的存储空间(也可以选择在堆上分配,然后由Go语言的GC回收这个变量的内存空间)。如果分配在栈上,g()函数运行结束时就对y进行回收。
本文主要参考:《Go语言圣经》
撩我?
我的公众号:Kyda