《GO语言圣经》读书笔记(一):程序结构
文章目录
变量和常量是编程中必不可少的部分,也是很好理解的一部分。
标识符与关键字
标识符
Go中的变量名、常量名、函数名都遵循一个命名规则:一个名字由字母数字和_
(下划线)组成,并且只能以字母和_
开头,区分大小写。 举几个例子:abc
, _
, _123
, a123
。
如果一个变量实在函数内部定义的,那么它的作用域就是在整个函数内部,如果在函数外部定义,那么他的作用域就是当前包的所有文件都可以访问,这个名字是个包级别的名字。
除此之外,要特别说的,如果一个名字的是大写字母开头的包级名,那么它是可以被外部的包访问的,如果是小写的他们只有内部包可以访问。举个例子,我们常用的Printf
函数是fmt
包中可以被外部包访问的函数。
推荐使用驼峰式命名。
关键字
关键字是指编程语言中预先定义好的具有特殊含义的标识符。 关键字和保留字都不建议用作变量名,关键字只能在特定语法结构中使用。
Go语言中有25个关键字:
break default func interface select
case defer go map struct
chan else goto package switch
const fallthrough if range type
continue for import return var
此外,Go语言中还有37个保留字。
Constants: true false iota nil
Types: int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr
float32 float64 complex128 complex64
bool byte rune string error
Functions: make len cap new append copy close delete
complex real imag
panic recover
变量
每个源文件以包的声明语句开始,说明该源文件是属于哪个包,之后是import导入依赖的其他包,然后是包一级的类型、变量、常量和函数的声明语句,包一级的各种类型的声明语句的顺序无关紧要。那么首先来看一下变量是如何声明的。
变量的来历
程序运行过程中的数据都是保存在内存中,我们想要在代码中操作某个数据时就需要去内存上找到这个变量,但是如果我们直接在代码中通过内存地址去操作变量的话,代码的可读性会非常差而且还容易出错,所以我们就利用变量将这个数据的内存地址保存起来,以后直接通过这个变量就能找到内存上对应的数据了。
变量类型
变量(Variable)的功能是存储数据。不同的变量保存的数据类型可能会不一样。经过半个多世纪的发展,编程语言已经基本形成了一套固定的类型,常见变量的数据类型有:整型、浮点型、布尔型等。
Go语言中的每一个变量都有自己的类型,并且变量必须经过声明才能开始使用。
变量声明
Go语言主要有四种类型的声明语句,分别是var/const/type/func
,他们对应了变量、常量、类型和函数实体对象的声明。现在,我们来看看如何声明变量。
Go语言中的变量需要声明后才能使用,同一作用域内不支持重复声明。 并且Go语言的变量声明后必须使用。
标准声明
Go语言的变量声明格式为:
var 变量名 变量类型
变量声明以关键字var
开头,变量类型放在变量的后面,行尾无需分号。 举个例子:
var name string
var age int
var isOk bool
批量声明
每声明一个变量就需要写var
关键字会比较繁琐,go语言中还支持批量变量声明:
var (
a string
b int
c bool
d float32
)
变量的初始化
Go语言在声明变量的时候,会自动对变量对应的内存区域进行初始化操作。每个变量会被初始化成其类型的默认值,例如: 整型和浮点型变量的默认值为0
。 字符串变量的默认值为空字符串
。 布尔型变量默认为false
。 切片、函数、指针变量的默认为nil
。数组或结构体等聚合类型的零值是每个元素/字段对应类型的零值。所以,Go中不存在未初始化的变量。
当然我们也可在声明变量的时候为其指定初始值。变量初始化的标准格式如下:
var 变量名 类型 = 表达式
举个例子:
var name string = "咕叽咕叽"
var age int = 18
或者一次初始化多个变量
var name, age = "咕叽咕叽", 20
类型推导
有时候我们会将变量的类型省略,这个时候编译器会根据等号右边的值来推导变量的类型完成初始化。
var name = "咕叽咕叽"
var age = 18
短变量声明
在函数内部,可以使用更简略的 :=
方式声明并初始化变量。
package main
import (
"fmt"
)
// 全局变量m
var m = 100
func main() {
n := 10
fmt.Println(m, n)
}
变量m
是在包一级的变量,变量n
是在main
内部声明的,m
可以在整个包对应的每个文件中被访问到,而不仅仅是当前文件中访问。包级别声明的变量m
会在main
入口函数执行前完成初始化,对于局部变量n
在声明语句快要被执行的时候完成初始化。
短变量声明这种声明方式,变量的类型会根据表达式来自动推导,和var
相比,var
比较适合需要显式指定变量类型的地方,或者是变量稍后会被重新赋值而初始值不太重要的地方。
请看下面的例子:
in,err:=os.Open(infile)
//do something
out,err:=os.Create(outfile)
第一句,我们通过短变量声明的方式声明了in
和err
两个变量,在第二个语句中,声明了几个变量呢?哈哈哈,只声明了out
一个变量噢,对于之前声明过的err
,只是做了赋值操作。
对比一下下面这个例子:
file,err:=os.Open(infile)
//do something
file,err:=os.Create(outfile) //compile error
你会发现,编译无法通过,咦,这是为什么呢?因为简短变量声明语句中要求必须、至少要声明一个新的变量,然鹅无论是file
还是err
我们在第一句代码中已经声明过了,所以会出现CE状况。解决方法就是改成多重赋值的语句:
file,err=os.Create(outfile)
这里来小结以下刚刚对于短变量声明的使用:
-
短变量声明不适用于声明包级别的变量,只适合在函数内部使用,对于声明包级别的变量还是使用
var
来完成吧 -
如果在相同的作用域中声明过该变量了,那么简短变量声明语句会对这些已经声明过的变量进行赋值操作,而不是声明噢。如果变量名虽然相同,但两个同名的变量不在同一个作用域,那么使用简短变量声明相当于在当前作用域重新声明一个新变量。显然下面的这个例子就证实了两个
x
不是同一个变量。package main import ( "fmt" ) var x int64 func main() { //这里在main函数内重新声明了一个新的变量x x := "hello" fmt.Println(x) //print:hello }
-
简短变量声明语句中必须至少要声明一个新的变量,否则编译不通过
匿名变量
在使用多重赋值时,如果想要忽略某个值,可以使用匿名变量(anonymous variable)
。 匿名变量用一个下划线_
表示,例如:
func foo() (int, string) {
return 10, "Q1mi"
}
func main() {
x, _ := foo()
_, y := foo()
fmt.Println("x=", x)
fmt.Println("y=", y)
}
匿名变量不占用命名空间,不会分配内存,所以匿名变量之间不存在重复声明。 (在Lua
等编程语言里,匿名变量也被叫做哑元变量。
注意事项:
- 函数外的每个语句都必须以关键字开始(var、const、func等)
-
:=
不能使用在函数外。 -
_
多用于占位,表示忽略值。
PS:一组变量也可以通过函数返回的多个返回值进行初始化,如下:
//该函数的返回值类型为file和error
var f,err=os.Open(name)
指针
一个指针的值是另一个变量的值,一个指针对应变量在内存中的存储位置。并不是每一个值都会有一个内存地址,但是对于每一个变量必然有对应的内存地址。通过指针,我们可以直接读或更新对应变量的值,而不需要知道该变量的名字。
通过短变量声明,x
变量的类型为int
,&x
表达式的意思是取x
变量的内存地址,这一操作会产生一个指向该整数变量的指针,指针的类型是*int
,所以p
是一个指向变量x
的指针,或者理解为p
指针保存了x
变量的内存地址。
接着,我们取出指针所指向变量的值,然后打印,*p
表达式对应p
指针指向的变量的值。
我们将新的值赋给了*p
,因为*p
对应一个变量,所以出现在赋值语句的左侧是合理的,这样一来,我们更新了p
指针所指向的变量的值,相当于x=2
。
x:=1
p:=&x //&x表示取x的内存地址,p是一个指向*int类型的指针,
fmt.Println(*p) //打印1
*p=2
fmt.Println(x) //打印2
对于结构体的每一个字段或者数组的每一个元素来说,都是可以被取地址的,因为他们可以看作是一个变量。
任何类型的指针的零值都是nil
,如果p!=nil
为真,那么说明p
指针指向某个有效变量。指针和指针之间,当他们指向同一个变量或者全部是nil
时才相等。
var x, y int
fmt.Println(&x == &x, &x == &y, &x == nil) // "true false false
&x
和&y
对应着不同的内存地址,所以&x==&y
结果为false
,x
指向一个有效变量(有内存地址,初始值为0),所以和nil
自然不相等。
其他:
- 我们对一个变量取地址,或者是赋值指针,其实都是为原变量创建了新的别名。比如说
p:=&x
,*p
其实就是变量x
的别名。有没有发现,通过这种方式,我们可以不使用名字而去访问x
变量。
new函数
另一个创建变量的方法就是调用new
函数。
//new(int)创建了一个int类型的匿名变量,初始化为0,返回的是地址,所以p是*int类型,指向匿名的int变量
p:=new(int)
每一次调用new
函数返回的都是不同变量的地址,p:=new(T)
这种方式等价于:
func newInt() *int {
return new(int)
}
func newInt() *int {
var dummy int
return &dummy
}
new
并不是一个关键字,我们可以将new
名字重新定义成别的类型。比如我们将它定义为int
类型的变量名,然后作为delta
函数的参数:
func delta(old,new int)int{
return new-old;
}
注意,这样的情况,我们在该函数内部是无法使用new
函数的。
变量的生命周期
之前我们提到了包一级声明的变量和局部变量,前者的声明周期和整个程序的运行周期是一致的,而局部变量的声明周期是动态的,从每次创建一个新的变量的声明语句开始,一直到该变量不再被引用位置,然后变量的存储空间可能被回收。
判断一个变量何时可以被回收的方法是可达性分析。从每个包级的变量和每个当前运行函数的每一个局部变量开始,通过指针或者引用的访问路径遍历,是否可以找到该变量。如果不存在这样的路径,说明不可达,那么该变量就要被清除掉了。
像函数的入参和返回值都是局部变量,它们在函数被调用时创建。
还要多说一点,编译器是自动选择在栈上或者堆上发呢配局部变量的内存,和如何声明变量的方式无关。下面看一个例子。
var global *int
//f函数中的变量x在堆上分配,因为在函数结束后依然可以通过包级别变量去访问到,尽管是在函数内部定义的
func f() {
var x int
x = 1
global = &x
}
//g函数中的变量*y在函数结束后将会是不可达的,编译器可以选择栈上或者堆上分配存储空间
func g() {
y := new(int)
*y = 1
}
上述代码中出现的f
函数中出现了x
变量逃逸的现象,因为在f
函数结束后,依然可以通过变量global
去找到x
,实际上局部变量x
属于短生命周期对象,但是在f
函数中,我们将它的指针保存到了global
对象(长生命周期的对象)中,这会阻止对短声明周期对象的垃圾回收。
作用域和声明周期不是一个概念!
-
声明语句的作用域对应的是一个源代码的文本区域,是一个编译时属性
-
一个变量的生命周期是指程序运行时变量存在的有效时间段,是一个运行时概念
类型
任何程序中都会存在一些变量有着相同的内部结构,但是却表示完全不同的概念。比如说,一个int
变量可以表示一个循环的迭代索引或者一个月份。
我们可以通过一个类型声明语句创建一个新的类型名称,和现有类型具有相同的底层结构。新命名的类型提供了一个方法,用来分隔不同概念的类型,这样即使他们底层类型相同也是不兼容的。
类型声明语句
类型声明语句出现在包一级,因此如果新创建的类型名字的首字符大写,那么包外也可以使用。
type 类型名字 底层类型
我们声明两个类型Celsius和Fahrenheit:
type Celsius float64 //摄氏温度
type Fahrenheit float64 //华式温度
这两个类型虽然底层类型都是float64
,但是属于不同的数据类型,所以是不可以相互比较或者混在一个表达式中进行运算的。因此,两个类型需要进行显式转型操作才可以进行运算。类型转化不会改变值本身,但是会使语义发生变化。
func CToF(c Celsius) Fahrenheit{
return Fahrenheit(c*9/5+32)
}
func FToC(f Fahrenheit) Celsius{
return Celsius((f-32)*5/9)
}
Celsius(t)和Fahrenheit(t)是类型转换操作,并不是函数噢。对于每一个类型T,都有一个对应的类型转换操作T(x)
,可以将x
转换为T
类型,如果T
是指针类型,可能会需要用小括号包装T
。
只有两个类型的底层基础类型相同的时候,才可以转型,或者两者都是指向相同底层结构的指针类型,这些转换只会改变类型而不会影响值本身。
命名类型可以为该类型的值定义新的行为。这些行为表示为一组关联到该类型的函数集合,我们成为类型的方法集。
包的初始化
包的初始化是解决包级别变量的依赖顺序,然后按照包级变量声明出现的顺序依次进行初始化。
var a = b + c // a 第三个初始化, 为 3
var b = f() // b 第二个初始化, 为 2, 通过调用 f (依赖c)
var c = 1 // c 第一个初始化, 为 1
func f() int { return c + 1 }
如果一个包中有多个.go
文件,那么会将.go
文件根据文件名排序,然后依次调用编译器编译。
对于包级别声明的变量,我们可以通过一个特殊的init
函数来简化初始化工作,一个文件中可以包含多个init
初始化函数。
这个函数很特殊,它不可以被调用和引用,程序开始执行时按照每个文件中的init
函数生命的顺序被自动调用。
每个包只会初始化一次,初始化的工作是自下而上的,main
包是最后被初始化的,这样我们可以确保main
函数之前,所有依赖的包都完成了初始化工作。
导入包的注意事项:
- 当包被导入的时候,包内的成员将通过类似
包名.变量名
的形式访问。 - 如果导入了一个包,但是又没有使用该包,者会被当作一个编译错误处理。
if else(分支结构)
if条件判断基本写法
Go语言中if
条件判断的格式如下:
if 表达式1 {
分支1
} else if 表达式2 {
分支2
} else{
分支3
}
当表达式1的结果为true
时,执行分支1,否则判断表达式2,如果满足则执行分支2,都不满足时,则执行分支3。 if判断中的else if
和else
都是可选的,可以根据实际需要进行选择。
Go语言规定与if
匹配的左括号{
必须与if和表达式
放在同一行,{
放在其他位置会触发编译错误。 同理,与else
匹配的{
也必须与else
写在同一行,else
也必须与上一个if
或else if
右边的大括号在同一行。
举个例子:
func ifDemo1() {
score := 65
if score >= 90 {
fmt.Println("A")
} else if score > 75 {
fmt.Println("B")
} else {
fmt.Println("C")
}
}
if条件判断特殊写法
if条件判断还有一种特殊的写法,可以在 if 表达式之前添加一个执行语句,再根据变量值进行判断,举个例子:
func ifDemo2() {
if score := 65; score >= 90 {
fmt.Println("A")
} else if score > 75 {
fmt.Println("B")
} else {
fmt.Println("C")
}
}
思考题: 上下两种写法的区别在哪里?
for(循环结构)
Go 语言中的所有循环类型均可以使用for
关键字来完成。
for循环的基本格式如下:
for 初始语句;条件表达式;结束语句{
循环体语句
}
条件表达式返回true
时循环体不停地进行循环,直到条件表达式返回false
时自动退出循环。
func forDemo() {
for i := 0; i < 10; i++ {
fmt.Println(i)
}
}
for循环的初始语句可以被忽略,但是初始语句后的分号必须要写,例如:
func forDemo2() {
i := 0
for ; i < 10; i++ {
fmt.Println(i)
}
}
for循环的初始语句和结束语句都可以省略,例如:
func forDemo3() {
i := 0
for i < 10 {
fmt.Println(i)
i++
}
}
这种写法类似于其他编程语言中的while
,在while
后添加一个条件表达式,满足条件表达式时持续循环,否则结束循环。
无限循环
for {
循环体语句
}
for循环可以通过break
、goto
、return
、panic
语句强制退出循环。
for range(键值循环)
Go语言中可以使用for range
遍历数组、切片、字符串、map 及通道(channel)。 通过for range
遍历的返回值有以下规律:
- 数组、切片、字符串返回索引和值。
- map返回键和值。
- 通道(channel)只返回通道内的值。
switch case
使用switch
语句可方便地对大量的值进行条件判断。
func switchDemo1() {
finger := 3
switch finger {
case 1:
fmt.Println("大拇指")
case 2:
fmt.Println("食指")
case 3:
fmt.Println("中指")
case 4:
fmt.Println("无名指")
case 5:
fmt.Println("小拇指")
default:
fmt.Println("无效的输入!")
}
}
Go语言规定每个switch
只能有一个default
分支。
一个分支可以有多个值,多个case值中间使用英文逗号分隔。
func testSwitch3() {
switch n := 7; n {
case 1, 3, 5, 7, 9:
fmt.Println("奇数")
case 2, 4, 6, 8:
fmt.Println("偶数")
default:
fmt.Println(n)
}
}
分支还可以使用表达式,这时候switch语句后面不需要再跟判断变量。例如:
func switchDemo4() {
age := 30
switch {
case age < 25:
fmt.Println("好好学习吧")
case age > 25 && age < 35:
fmt.Println("好好工作吧")
case age > 60:
fmt.Println("好好享受吧")
default:
fmt.Println("活着真好")
}
}
fallthrough
语法可以执行满足条件的case的下一个case,是为了兼容C语言中的case设计的。
func switchDemo5() {
s := "a"
switch {
case s == "a":
fmt.Println("a")
fallthrough
case s == "b":
fmt.Println("b")
case s == "c":
fmt.Println("c")
default:
fmt.Println("...")
}
}
输出:
a
b
goto(跳转到指定标签)
goto
语句通过标签进行代码间的无条件跳转。goto
语句可以在快速跳出循环、避免重复退出上有一定的帮助。Go语言中使用goto
语句能简化一些代码的实现过程。 例如双层嵌套的for循环要退出时:
func gotoDemo1() {
var breakFlag bool
for i := 0; i < 10; i++ {
for j := 0; j < 10; j++ {
if j == 2 {
// 设置退出标签
breakFlag = true
break
}
fmt.Printf("%v-%v\n", i, j)
}
// 外层for循环判断
if breakFlag {
break
}
}
}
使用goto
语句能简化代码:
func gotoDemo2() {
for i := 0; i < 10; i++ {
for j := 0; j < 10; j++ {
if j == 2 {
// 设置退出标签
goto breakTag
}
fmt.Printf("%v-%v\n", i, j)
}
}
return
// 标签
breakTag:
fmt.Println("结束for循环")
}
break(跳出循环)
break
语句可以结束for
、switch
和select
的代码块。
break
语句还可以在语句后面添加标签,表示退出某个标签对应的代码块,标签要求必须定义在对应的for
、switch
和 select
的代码块上。 举个例子:
func breakDemo1() {
BREAKDEMO1:
for i := 0; i < 10; i++ {
for j := 0; j < 10; j++ {
if j == 2 {
break BREAKDEMO1
}
fmt.Printf("%v-%v\n", i, j)
}
}
fmt.Println("...")
}
continue(继续下次循环)
continue
语句可以结束当前循环,开始下一次的循环迭代过程,仅限在for
循环内使用。
在 continue
语句后添加标签时,表示开始标签对应的循环。例如:
func continueDemo() {
forloop1:
for i := 0; i < 5; i++ {
// forloop2:
for j := 0; j < 5; j++ {
if i == 2 && j == 2 {
continue forloop1
}
fmt.Printf("%v-%v\n", i, j)
}
}
}
上一篇: 排序——直接插入排序