编译命令
go run
go run
命令直接编译和执行源码中的main
函数,但是并不会留下任何可执行文件(可执行文件被放在临时文件中执行,执行结束后将被自动删除)。go run
命令后可以添加参数。
来到HelloGo.go
文件的目录下,执行如下命令:
go run HelloGo.go
go build
go build
命令会将源码编译为可执行文件,默认将编译该目录下的所有的源码。也可以在命令后添加多个文件名,go build
命令将编译这些源码,输出可执行文件。
同样来到HelloGo.go
文件的目录下,执行如下命令,其中-o
选项用于指定生成的可执行文件的文件名:
go build -o HelloGo HelloGo.go
或者
go build HelloGo.go
都将在当前目录下生成一个HelloGo
的可执行文件。
基本语法
变量的声明与初始化
var
是Go语言中声明变量的关键字。Go语言在声明变量时,会自动把变量对应的内存区域 进行初始化操作,每个变量会被初始化为其类型的默认值。即变量一经声明,则被初始化为其类型的默认值。变量声明样式如下所示:
var name T
在Go语言中,每一个声明的变量都必须被使用,否则会编译不通过。即变量一经声明,则必须使用。
变量初始化样式:
var name T = 表达式
类型推导
Go语言提供了类型推导的语法糖,可精简变量初始化为以下样式:
name := 表达式
类型推导的语法糖省略了声明变量的关键字var
和类型属性T
除了类型推导的语法糖特性,Go语言还提供了多重赋值和匿名变量的语法糖特性。
多重赋值
多重赋值特性可以轻松实现变量变换任务,不需要借助第三方变量。如下所示:
a := 1 b := 2 b, a =a, b
匿名变量
通过在不需要的变量声明的地方使用’_'来代替变量名,我们就可以忽略部分不需要的左值。匿名变量不占用命名空间,不会分配内存。匿名变量与匿名变量之间也不会因为多次声明而无法使用。具体例子如下所示:
package main import "fmt" // 返回一个人的姓和名 func getName() (string, string){ return "王", "小二" // Go语言支持函数多返回值 } func main() { surname,_ := getName() // 使用匿名变量 _, personalName := getName() // 使用匿名变量 fmt.Printf("My surname is %v and my personal name is %v", surname, personalName) }
原生数据类型
基本数据类型:整型、浮点数、布尔型、字符串型等。
整型
整型中主要有两大类,分别是:
- 按照整型的长度划分:
int8
、int16
、int32
、int64
- 按照有无符号划分:
uint8
、uint16
、uint32
、uint64
除此之外,Go语言还提供了平台自匹配长度的int
类型和uint
类型。
整型类型之间可以相互转换,高长度类型向低长度类型转换时仅保留高长度类型的低位值。
浮点型
浮点型主要有两种:
float32
:单精度,存储占用4个字节,也即4*8=32位,其中1位用来符号,8位用来指数,剩下的23位表示尾数float64
:双精度,存储占用8个字节,也即8*8=64位,其中1位用来符号,11位用来指数,剩下的52位表示尾数
float32
和float64
之间可以进行类型转换,但需要注意精度损失。
布尔型
true
和false
。不能与整型进行强转,也无法参与数值运算。
字符串型
在Go语言中,字符串是基本类型,它基于UTF-8
编码实现。在遍历字符串型,我们需要区分byte
和rune
。
分别以byte
和rune
方式遍历字符串:
f := "Golang编程" fmt.Printf("byte len of f is %v\n", len(f)) fmt.Printf("rune len of f is %v\n", utf8.RuneCountInString(f))
上述例子的输出为:
byte len of f is 12 rune len of f is 8
第一种方式,统计的是字节的长度。由于中文字符在UTF-8
中占用了3个字节,所以使用len
方法获得的中文字符长度为6个字节。
第二种方式,统计的是字符的长度。
在本质上,byte
和rune
的底层类型分别为uint8
和int32
。由于int32
能够表达更多的值,可以更容易处理Unicode
字符,所以rune
能够处理一切的字符,而byte
仅仅局限与处理ASCII
字符。
指针
在C/C++语言中,指针直接操作内存的特性使得C/C++具备极高的性能,开发人员通过它直接操作和管理大块内存数据。但与此同时,指针偏移、运算和内存释放可能引发的错误也让指针编程饱受诟病。
Go语言限制了指针类型的偏移和运算能力,使得指针类型具备了指针高效访问的特性,但又不会发生指针偏移,避免了非法修改敏感数据的问题。同时Go语言中提供的自动垃圾回收机制,也减少了指针占用内存回收的复杂性。
在Go语言中,指针包含以下三个概念:
- 指针地址
- 指针类型
- 指针取值
在程序运行过程中,每一个变量的值都保存在内存中,变量对应的内存有其特定的地址。假设某一个变量的类型为T
,在Go语言中,我们可以通过取址符号&
获取该变量对应内存的地址,生成该变量对应的指针。此时**,指针的值即变量的内存地址**,指针类型即*T
,称为T
的指针类型,*
代表指针。
Go语言也提供根据指针获取变量值的取指操作*
,通过取值操作*
可以获取指针对应变量的值和对变量进行赋值操作。具体代码如下所示:
func main() { str := "Golang is Good!" // 获取 str 的指针 strPrt := &str fmt.Printf("str type is %T, value is %v, address is %p\n", str, str, &str) fmt.Printf("strPtr type is %T, and value is %v\n", strPrt, strPrt) // 获取指针对应变量的值 newStr := *strPrt fmt.Printf("newStr type is %T, value is %v, and address is %p\n", newStr, newStr, &newStr) // 通过指针对变量进行赋值 *strPrt = "Java is Good too!" fmt.Printf("newStr type is %T, value is %v, and address is %p\n", newStr, newStr, &newStr) fmt.Printf("str type is %T, value is %v, address is %p\n", str, str, &str) }
输出的结果为:
str type is string, value is Golang is Good!, address is 0xc00004a250 strPtr type is *string, and value is 0xc00004a250 newStr type is string, value is Golang is Good!, and address is 0xc00004a280 newStr type is string, value is Golang is Good!, and address is 0xc00004a280 str type is string, value is Java is Good too!, address is 0xc00004a250
在上述代码中,我们通过strPtr
指针获取str
的值赋予给newSt
r变量。
可以观察到str
和newSt
r是两个不同的变量,它们对应的内存不一样,赋值过程中发生了值拷贝。
值拷贝会创建新的内存空间,然后将原有变量的值复制到新的内存空间中,形成两个独立的变量。通过指针修改str
变量的值并不会影响到newStr
,因为这两个变量对应的内存地址不一样。
除了使用&
对变量进行取址操作创建指针,还可以使用new
函数直接分配内存,并返回指向内存的指针,此时内存中的值会被初始化为类型的默认值。如下例所示:
// 通过new函数创建一个*string指针 str := new(string) // 通过指针对变量进行赋值 *str = "Golang is Good!"
在Go语言的flag
包中,命令行参数一般以指返回。
常量
变量的值在运行时可变,而常量的值在声明之后不允许变化。通过const
关键字可以声明常量。如下例所示:
const name T = 表达式
类型推导
Go语言的类型推导省略常量声明时的类型T
和同时声明多个常量。如下例所示:
// 省略类型T const name = 表达式 // 同时声明多个常量 const ( name1 = 表达式1 name2 = 表达式2 )
类型别名
Go语言提供了类型别名的语法特性。类型别名本质上与原类型是属于同一个类型的,它相当于原类型T
的一个别称。定义一个类型别名的样式如下:
type name = T
类型定义
类型定义会创建一个新的类型,新建的类型将具备原类型T
的特性。类型定义的样式如下:
type name T
通过一个例子理解类型别名和类型定义之间的区别:
type aliasInt = int // 定义一个类型别名 type myInt int // 定义一个新的类型 func main() { var alias aliasInt fmt.Printf("alias value is %v, type is %T\n", alias, alias) var myint myInt fmt.Printf("myint value is %v, type is %T\n", myint, myint) }
输出结果为:
alias value is 0, type is int myint value is 0, type is main.myInt
从输出结果中,我们可以看到通过类型别名aliasInt
声明的alias
变量还是int
型,而重新定义的myInt
属于新的类型,但是通过它声明的变量myint
和alias
一样都为0。
分支与循环控制
Go语言的分支控制与其他语言相似,但是更为简略,简单的表达样式如下:
if expression1 { branch1 } else if expression2 { branch2 } else { branch3 }
Go语言中规定与if
匹配的{
必须与if
和表达式位于同一行,同样的,else
也必须与上一个分支的}
位于同一行,否则会发生编译错误。表达式两边可以省略()
。
Go语言还提供了switch
语句对大量的值和表达式进行判断。
- 为了避免人为错误,
switch
中的每一个case
都是独立的代码块,不需要通过break
跳出switch
选择体。 - 如果需要继续执行接下来的
case
判断,需要添加fallthrough
关键字对上下两个case
进行连接。 - 除了支持数值常量,Go语言的
switch
还能对字符串 、表达式等复杂情况进行处理。
一个简单的例子如下所示:
// 根据人名分配工作 name := "小红" switch name { case "小明": fmt.Println("扫地") case "小红": fmt.Println("擦黑板") case "小刚": fmt.Println("倒垃圾") default: fmt.Println("没人干活") }
在上面的例子中,每一个case
都是字符串样式,且无需通过break
控制跳出。
如果我们需要在case
中判断表达式,在这种情况下switch
后面不需要指定判断变量,这种形式就和if-else
类似。如下例所示:
// 根据分数判断成绩程度 score := 90 switch { case score < 100 && score >= 90: fmt.Println("优秀") case score < 90 && score >= 80: fmt.Println("良好") case score < 80 && score >= 60: fmt.Println("及格") case score < 60: fmt.Println("不及格") default: fmt.Println("分数错误") }
Go语言的循环体仅提供了for
关键字,没有其他语言中提供的while
或者do-while
形式。基本样式如下:
for init; condition; end { 循环体代码 }
这其中,初始语句、条件表达式、结束语句都可以不写。如果三者都缺省,这将变成一个无限循环语句,可以通过break
关键字跳出循环体,或者使用continue
关键字继续下一个循环。
Go中常用的容器
- 当我们在程序中操作大量同类型变量时,为了方便数据的存储和操作,我们需要借助容器的力量。
- Go语言中以标准库的方式提供了常用的容器实现,主要有固定大小的数组、可以动态扩容的切片、双向列表以及**
key-value
方式存储的字典**等。
数组
数组是一段存储固定类型固定长度的连续内存空间,它的大小在声明时就已经固定。数组的声明样式如下所示:
var name [size]T
size
必须在静态编译时就确定其大小,不能动态指定T
表示数组成员的类型,可为任意类型
在Go语言,可以在声明时使用初始化列表对数组进行初始化,也可以通过下标对数据成员进行访问和赋值。如下所示:
var classMates1 [3]string // 通过下标为数组成员赋值 classMates1[0] = "小明" classMates1[1] = "小红" classMates1[2] = "小李" fmt.Println(classMates1) // 通过下标访问数组成员 fmt.Println("The No.1 student is " + classMates1[0]) // 通过初始化列表声明并初始化数组 classMates2 := [...]string{"小明", "小红", "小李"} fmt.Println(classMates2)
输出结果为:
[小明 小红 小李] The No.1 student is 小明 [小明 小红 小李]
- 使用初始化列表初始化数组时,需要注意
[]
内的数组大小需要和{}
内的数组成员的数量一致。 - 上述例子中,我们使用了
...
让编译器为我们根据{}
内成员的数量确定数组的大小。
除此之外,我们还可以使用指针操作数组。如下例所示:
classMates3 := new([3]string) classMates3[0] = "小明" classMates3[1] = "小红" classMates3[2] = "小李" fmt.Println(*classMates3)
输出结果为:
[小明 小红 小李]
- 在上述代码中,我们通过
new
函数申请了[3]string
的内存空间并初始化,返回其对应的指针。 - 需要注意的是,该指针无法支持偏移和运算,这是Go语言对指针类型的限制。
- 我们可以通过指针直接操作数组,这与C语言中的指针功能无异。
new
和make
的区别:new
即分配内存又初始化,make
只分配内存不初始化。
切片
- 切片是对数组的一个连续片段的引用,它是一个容量可变的序列。
- 我们可以简单将切片理解为动态数组,它的内部结构包括底层数组指针、大小和容量,它通过指针引用底层数组,把对数据的读写操作限定在指定的区域内。
切片的结构体由三部分组成:
array
:指向底层存储数据数组的指针len
:指当前切片的长度,即成员数量cap
:指当前切片的容量,它总是大于等于len
从原生数组中生成切片
我们可以从原有数组中生成一个切片,生成的切片指针即指向原数组。生成的样式如下:
slice := source[begin:end]
source
表示生成切片的原有数组begin
表示切片的开始位置,end
表示切片的结束位置,- 不包含
end
索引指向的原有数组成员,即左闭右开。
具体例子如下所示:
sourceArray := [...]int{1,2,3} slice := sourceArray[0:1] fmt.Printf("slice value is %v\n", slice) fmt.Printf("slice len is %v\n", len(slice)) fmt.Printf("slice cap is %v\n", cap(slice)) slice[0] = 4 fmt.Printf("slice value is %v\n", slice) fmt.Printf("sourceArray value is %v\n", sourceArray)
输出的结果为:
slice value is [1] slice len is 1 slice cap is 3 slice value is [4] sourceArray value is [4 2 3]
- 因为切片作为指向原有数组的引用,对切片修改就是对原数组进行修改
动态创建切片
通过make
函数(只分配内存空间,并不初始化)动态创建切片,在创建过程中指定切片的长度和容量。样式如下所示:
make([]T, size, cap)
T
即切片中的成员类型size
为当前切片具备的长度cap
为当前切片预分配的长度,即切片的容量
例子如下所示:
slice = make([]int, 2, 4) fmt.Printf("slice value is %v\n", slice) fmt.Printf("slice len is %v\n", len(slice)) fmt.Printf("slice cap is %v\n", cap(slice))
输出的结果为:
slice value is [0 0] slice len is 2 slice cap is 4
从上述输出可以看出make
函数创建的新切片中的成员都被初始化为类型的初始值。
声明新的切片
直接声明新的切片类似于数组的初始化,但是不需要指定其大小,否则就变成了数组。样式如下所示:
var name []T
此时声明的切片并没有分配内存,我们可以在声明切片的同时对其进行初始化,如下例所示:
ex := []int{1, 2, 3} fmt.Printf("ex value is %v\n", ex) fmt.Printf("ex len is %v\n", len(ex)) fmt.Printf("ex cap is %v\n", cap(ex))
输出的结果为:
ex value is [1 2 3] ex len is 3 ex cap is 3
- 此时声明的切片大小和容量都为3
向切片添加元素
- Go语言中提供了
append
内建函数用于动态向切片添加元素,它将返回新的切片。 - 如果当前切片的容量可以容纳更多的元素,即
len
小于cap
,添加操作将在切片指向的原有数组上进行,这将会覆盖掉原有数组的值。 - 如果当前切片的容量不足以容纳更多的元素,那么切片将会进行扩容。
- 扩容的具体过程为:申请一个新的连续内存空间,空间大小一般为原有容量的两倍,然后将原来数组中的数据复制到新的数组中,同时将切片中的指针指向新的数组,最后将新的元素添加到新的数组中。
通过下例演示切片的动态扩容:
arr1 := [...]int{1,2,3,4} arr2 := [...]int{1,2,3,4} sli1 := arr1[0:2] // 长度为2,容量为4 sli2 := arr2[2:4] // 长度为2,容量为2 fmt.Printf("sli1 pointer is %p, len is %v, cap is %v, value is %v\n", &sli1, len(sli1), cap(sli1), sli1) fmt.Printf("sli2 pointer is %p, len is %v, cap is %v, value is %v\n", &sli2, len(sli2), cap(sli2), sli2) newSli1 := append(sli1, 5) fmt.Printf("newSli1 pointer is %p, len is %v, cap is %v, value is %v\n", &newSli1, len(newSli1), cap(newSli1), newSli1) fmt.Printf("source arr1 become %v\n", arr1) newSli2 := append(sli2, 5) fmt.Printf("newSli2 pointer is %p, len is %v, cap is %v, value is %v\n", &newSli2, len(newSli2), cap(newSli2), newSli2) fmt.Printf("source arr2 become %v\n", arr2)
上例的输出结果为:
sli1 pointer is 0xc000004078, len is 2, cap is 4, value is [1 2] sli2 pointer is 0xc000004090, len is 2, cap is 2, value is [3 4] newSli1 pointer is 0xc0000040d8, len is 3, cap is 4, value is [1 2 5] source arr1 become [1 2 5 4] newSli2 pointer is 0xc000004108, len is 3, cap is 4, value is [3 4 5] source arr2 become [1 2 3 4]
- 通过上面的例子,我们可以发现,容量足够的
sli1
直接将append
添加的新元素覆盖到原有数组arr1
中。而容量不够的sli2
进行了扩容操作,申请了新的底层数组,不在原数组的基础上进行操作。在实际使用的过程要记住这两种的区别。
如果原有数组可以添加新的元素,但切片自身的容量已经饱和,此时进行append
操作,同样会进行扩容,申请新的内存空间。如下例所示:
arr3 := [...]int{1,2,3,4} sli3 := arr3[0:2:2] // 长度为2,容量为2 fmt.Printf("sli3 pointer is %p, len is %v, cap is %v, value is %v\n", &sli3, len(sli3), cap(sli3), sli3) newSli3 := append(sli3,5) fmt.Printf("newSli3 pointer is %p, len is %v, cap is %v, value is %v\n", &newSli3, len(newSli3), cap(newSli3), newSli3) fmt.Printf("source arr3 become %v\n", arr3)
对应的输出结果为:
sli3 pointer is 0xc000004138, len is 2, cap is 2, value is [1 2] newSli3 pointer is 0xc000004168, len is 3, cap is 4, value is [1 2 5] source arr3 become [1 2 3 4]
- 在上述代码中,我们指定了创建切片的第三个参数
cap
。这里的cap
不是切片容量,切片容量是cap-begin
。
为了方便切片的数据快速复制到另一个切片中,Go语言提供了内建的copy
函数。它的使用样式如下:
copy(destSli, srcSli []T)
它的返回结果为实际发生复制的元素个数。
列表
Go语言中的列表即双向链表,它适合于存储需要经常进行元素插入和删除操作的元素集合。
列表的初始化样式如下所示:
// 方法一 var name list.List // 方法二 name := list.New()
- 方法一直接声明初始化列表,方法二使用
container/list
包中的New
函数初始化列表,返回列表对应的指针。 - 可以注意到,例表没有限制其内保存成员的类型,即任意类型的成员都可以同时存在列表中。
演示列表的插入、删除和遍历操作:
tmpList := list.New() // 尾插 for i := 1; i <= 10; i++ { tmpList.PushBack(i) } // 头插 first := tmpList.PushFront(0) // 删除 tmpList.Remove(first) // 遍历 for l := tmpList.Front(); l != nil; l = l.Next() { fmt.Print(l.Value, " ") }
字典
Go语言中的字典用于存储键值对,其中每一个键都会映射到一个值。其内部通过散列表的方式实现。定义的样式如下所示:
name := make(map[keyType]valueType)
通过一个简单的例子演示map
使用方式:
classMates1 := make(map[int]string) // 添加映射关系 classMates1[0] = "小明" classMates1[1] = "小红" classMates1[2] = "小张" fmt.Printf("id %v is %v\n", 1, classMates1[1]) // 在声明时初始化数据 classMates2 := map[int]string{ 0 : "小明", 1 : "小红", 2 : "小张", } fmt.Printf("id %v is %v\n", 3, classMates2[3])
- 如上代码所示,我们可以使用
make
函数为map
分配内存空间后,再为map
一一添加键值对映射关系。 - 也可以直接在声明时通过类
JSON
格式添加键值对映射关系 。 - 在
map
中可以通过键直接查询对应的值,如果不存在这样的键,将会返回值类型的默认值。
可以采用以下方式查询某个键是否存在于map
中:
mate,ok := classMates2[1]
如果键存在于map
中,布尔型ok
将会是true
。mate
为对应的值。
容器遍历
下例通过for-range
遍历数组、切片、字典
// 数组的遍历 nums := [...]int{1, 2, 3, 4, 5, 6, 7, 8} for k, v := range nums { // k为下标,v为对应的值 fmt.Println(k, v, " ") } fmt.Println() // 切片的遍历 slis := []int{1, 2, 3, 4, 5, 6, 7, 8} for k, v := range slis { // k为下标,v为对应的值 fmt.Println(k, v, " ") } fmt.Println() // 字典的遍历 tmpMap := map[int]string{ 0: "小明", 1: "小红", 2: "小张", } for k, v := range tmpMap { // k为键值,v为对应值 fmt.Println(k, v, " ") }
- 可以将不需要的键或值改为匿名变量
- 列表的遍历比较特殊,需要配合
Front
函数获取列表的头元素,再使用其Next
函数依次往下遍历,具体代码可见上面列表部分的举例。
特别注意:在for-range
遍历过程中,键和值是值拷贝,对它们修改不会影响容器内成员变化。