Go语言基础
入门
go run filename.go
可以将一个或者多个以.go为后缀的源文件进行编译、链接,然后运行生成的可执行文件。
go build filename.go
一般用于不是一次性的实验,编译输出成一个可复用的程序。
:=
用于短变量声明,这个宏语句声明一个或多个变量,并且更具初始化的值给予合适的类型
i++
等价于i += 1,又等价于i = i + 1,对应的递减语句i--同理。这些是语句,不同于C族语句一样是表达式,j = i++是不合法的,并且只支持后缀。
for
Go里面唯一的循环语句
_
空标识符,可以用在任何语法需要变量名但是程序逻辑不需要的地方
程序结构
名称
- 名称的开头是一个字母或下划线、后面可以跟任意数量的字符、数字和下划线并区分大小写。
- 风格上采用“驼峰式”。
25个关键字
break:退出循环
default:选择结构默认项(switch、select)
func:定义函数
interface:定义接口
select:channel
case:选择结构标签
chan:定义 channel
const:常量
continue:跳过本次循环
defer:延迟执行内容(收尾工作)
go:并发执行
map:map 类型
struct:定义结构体
else:选择结构
goto:跳转语句
package:包
switch:选择结构
fallthrough:流程控制
if:选择结构
range:从 slice、map 等结构中取元素
type:定义类型
for:循环
import:导入包
return:返回
var:定义变量
预声明常量、类型和函数
常量:true false iota nil
类型:int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr
float32 float64 complex128 complex64
bool byte rune string error
函数: make len cap new append copy close delete
complex real imag
panic recover
声明
4个主要的声明:变量(var),常量(const),类型(type)和函数(func)。
变量
var声明创建一个具体类型的变量。每一个声明都有一个通用的形式:
var name type = expression
类型和表达式部分可以省略一个,但是不能都省略,Go里面不存在未初始化变量。
可以声明一个变量列表,忽略类型允许声明多个不同类型的变量,如:
var i, j, k int //int, int, int
var b, f, s = true, 2.3, "four" //bool, float64, string
短变量声明
一种称作短变量声明的可选形式可以用来声明和初始化局部变量。它使用name := expression的形式,name的类型由expression的类型决定。
- 短变量声明不需要声明所有在左边的变量
在如下代码中,第一条语句声明了in和err。第二条语句仅声明了out,但向已有的err变量赋了值。
in, err := os.Open(infile)
// ...
out, err := os.Create(outfile)
- 短变量声明最少声明一个变量,否则,代码编译将无法通过。
f, err := os.Open(infile)
// ...
f, err := os.Create(outfile) // 编译错误:没有新的变量
指针
指针的值是一个变量的地址。使用指针,可以在无须知道变量名字的情况下,间接读取或更新变量的值。
x := 1
p := &x // p 是整型指针,指向 x
fmt.Println(*p) // "1"
*p = 2 // 等于 x = 2
fmt.Println(x) // 结果 "2"
- 指针是可比较的,两个指针当且仅当指向同一个变量或者两者都是
nil的情况才相等。
var x, y int
fmt.Println(&x == &x, &x == &y, &x == nil) // "true false false"
- 函数返回局部变量的地址是非常安全的。
例如,通过下面代码,调用函数f产生局部变量v即使在调用返回后依然存在,指针p依然引用它:
var p = f()
func f() *int {
v := 1
return &v
}
每次调用f都会返回一个不同的值:
fmt.Println(f() == f()) // "false"
new函数
使用内置的new函数也是创建变量的一种方式。表达式new(T)创建一个未命名的T类型变量,初始化为T类型的零值,并返回其地址(地址类型为*T)。
func newInt() *int {
return new(int)
}
p := new(int)
q := new(int)
fmt.Println(p == q) // "false"
new是一个预声明的函数,不是一个关键字,所以他可以重定义为另外的其他类型。
变量的生命周期
生命周期指在程序执行过程中变量存在的时间段。变量的生命周期是通过它是否可达来确定。
- 局部变量可在包含它的循环的一次迭代中之外继续存活,即使包含它的循环已经返回,它的存在还可能延续。
C++如果使用new操作申请的内存是分配在堆上的要自己利用delete进行回收,如果是声明的局部变量会在栈上分配内存,并且在函数退出后由系统自动回收。但是Golang在这方面与传统语言发生了非常大的区别,go语言编译器会做逃逸分析(escape analysis),分析局部变量的作用域是否逃出函数的作用域,要是没有,那么就放在栈上;要是变量的作用域超出了函数的作用域,那么就自动放在堆上。所以不用担心会不会memory leak,因为go语言有强大的垃圾回收机制。这样可以释放程序员的内存使用限制,让程序员关注程序逻辑本身。go语言也允许利用new来分配内存,但是new分配的内存也不是一定就放在堆上,而是根据其是否超出了函数作用域来判断是否放在堆上还是栈上。这点和C/C++很不一样。
赋值
赋值语句用来更新变量所指的值,它最简单的形式由赋值符=,以及符号左边的变量和右边的表达式组成。
多重赋值
在实际更新变量前,右边所有表达式被推演,当变量同时出现在赋值符两侧的时候这种形式特别有用。
- 交换两个变量
x, y = y, x
a[i], a[j] = a[j], a[i]
- 使一个普通的赋值序列变得紧凑
i, j, k = 2, 3, 5
可赋值性
可赋值性根据类型不同有着不同的规则,类型必须精确匹配,nil可以被赋给任何接口变量或引用类型。
类型声明
type声明定义一个新的命名类型,它和某个已有类型使用相同的底层类型。
- 命名类型提供了一种方式来区分底层类型的不同或者不兼容使用,这样它们就不会在无意中混用。
基本数据
计算机底层全是位,而实际操作则是基于大小固定的单元中的数值,称为字(word)。
- Golang的二元操作符按优先级的降序排列如下。
* / % << >> & &^(AND NOT)
+ - | ^
== != < > >=
&&
||
整数
有符号整数分四种大小:8位、16位、32位、64位,用int8,int16,int32,int64表示,对应的无符号整数是uint8,uint16,uint32,uint64。此外还有int和uint。
rune类型是int32类型的同义词,常常用于指明一个值是Unicode码点。同样,byte类型是uint8类型的同义词,强调一个值是原始数据,而非量值。- 无符号整数
uintptr其大小并不明确,但足以完整存放指针。 - 若表示算术运算结果所需的位超出该类型的范围,就称为
溢出。 - 比较表达式本身类型是布尔型。
- 如果将整数以位模式处理,须使用无符号整型。
- 无符号整数往往只用于位运算和特定算术运算符,如实现位集时,解析二进制格式的文件,或散列和加密。一般而言,无符号整数极少用于表示非负值。
fmt小技巧
- 通常
Printf的格式化字符串含有多个%谓词,这要求提供相同数目的操作数,而%后的副词[1]告知Printf重复使用第一个操作数。 %o,%x,或%X之前的副词#告知Printf输出相应的前缀0、0x、0X。Printf用谓词%b以二进制形式输出数值,副词08能在这个输出结果前补0,补够8位。- 用
%c输出文字符号,如果希望输出带有单引号则用%q。
浮点数
Go具有两种大小的浮点数float32和float64。常量math.MaxFloat32是float32的最大值,大约为3.4e38,而math.MaxFloat64则大约为1.8e308。相应地,最小的正浮点值大约位1.4e-45和4.9e-324。
fmt小技巧
%g会自动保持足够的精度,并选择最简洁的表达方式,但是对于数据表,%e(有指数)或%f(无指数)的形式可能更合适。
复数
Go具备两种大小的复数complex64和complex128,二者分别由float32和float64构成。内置的complex函数根据给定的实部和虚部创建复数,而内置的real函数和imag函数则分别提取
- 源码中,如果在浮点数或十进制整数后面紧接着写字母
i,如3.1415926i或2i,它就变成了一个虚数,表示一个实部为0的复数。
布尔值
bool型的值或布尔值(boolean)只有两种可能:真(true)和假(false)。
字符串
字符串是不可变的字节序列,它可以包含任意数据,包括0值字节,但主要是人类可读的文本。
- 不可变一位置两个字符串能安全地共用同一段底层内存,使得复制任何长度字符串的开销都低廉。
字符串字面量
Go的源文件总是按UTF-8编码,并且习惯上Go的字符串会按UTF-8解读。
- 原生的字符串字面量的书写形式是
...,使用反引号而不是双引号。
Unicode
文字符号的序列表示成int32值序列,这种表示方式称作UTF-32或UCS-4,每个Unicode码点的编码长度相同,都是32位。
UTF-8
import "unicode/utf8"
UTF-8以字节为单位对Unicode码点做变长编码。
- 若最高位为
0,则标示着它是7为的ASCII码,其文字符号的编码仅占1字节;若最高几位是110,则文字符号的编码占用2个字节,第二个字节以10开始。更长的编码以此类推。
字符串和字节slice
4个标准包对字符串操作特别重要:bytes,strings,strconv和unicode。
- 字符串包含一个字节数组,创建后它就无法改变。相反地,字节
slice的元素允许随意修改。
strings包具备下面6个函数:
func Contains(s, substr string) bool
func Count(s, sep string) int
func Fields(s string) []string
func HasPrefix(s, prefix string) bool
func Index(s, sep string) int
func Join(a []string, sep string) string
bytes包里面的对应函数为:
func Contains(b, subslice []byte) bool
func Count(s, sep []byte) int
func Fields(s []byte) [][]byte
func HasPrefix(s, prefix []byte) bool
func Index(s, sep []byte) int
func Join(s [][]byte, sep []byte) []byte
bytes包为高效处理字节slice提供了Buffer类型。bytes.Buffer类型无须初始化,原因是零值本来就有效。
- 若要在
bytes.Buffer变量后面添加任意文字符号的UTF-8编码,最好使用bytes.Buffer的WriteRune方法,而追加ASCII字符,则使用WriteByte亦可。
字符串和数字的相互转换
- 要将整数转换成字符串,一种选择是使用
fmt.Sprintf,另一种做法是用函数strconv.Itoa("integer to ASCII")。 strconv包内的Atoi函数或ParseInt函数用于解释表示整数的字符串,而ParseUint用于无符号整数。
x, err := strconv.Atoi("123") // x 是整型
x, err := strconv.ParseInt("123", 10, 64) // 十进制,最长为64位
`ParseInt`的第三个参数指定结果必须匹配何种大小的整型
常量
常量是一种表达式,其可以保证在编译阶段就计算出表达式的值,并不需要等到运行时,从而使编译器得以知晓其值。
- 所有常量本质上都属于基本类型:布尔型、字符串或数字。
- 常量声明可以同时指定类型和值,如果没有显示指定类型,则类型根据右边的表达式推断。
- 若同时声明一组常量,除了第一项之外,其他项在等号右侧的表达式都可以省略,这一位置会复用前面一项的表达式及其类型。
常量生成器iota
常量声明中,iota从0开始取值,逐项加1。
type Weekday int
const (
Sunday Weekday = iota
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
)
无类型常量
从属类型待定的常量共有6种,分别是无类型布尔,无类型浮点数,无类型复数,无类型字符串。
- 类似地,
true和false是无类型布尔值,而字符串字面量则是无类型字符串。 - 只有常量才可以是无类型的。
复合数据类型
复合数据类型是由基本数据类型以各种方式组合而构成的。
数组
数组是具有固定长度且拥有零个或者多个相同数据类型元素的序列。
- 相比数组,
slice很多场合下使用得更多。 - 默认情况下,一个新数组中的元素初始值位元素类型的零值,同时可以用
数组字面量初始化。
var r [3]int = [3]int{1, 2} // 1, 2, 0
- 如果省略号
...出现在数组长度的位置,那么数组的长度由初始化数组的元素个数决定。
q := [...]int{1, 2, 3} // [3]int
- 数组的长度是数组类型的一部分。
- 数组的长度必须是常量表达式,也就是说,这个表达式的值在程序编译时就可以确定。
- 如果一个数组的元素类型是可比较的,那么这个数组也是可比较的。
- Go把数组和其他的类型都看成值传递,而别的语言中数组是隐式地使用引用传递。
- 使用数组指针是高效的,但由于数组长度不可变的特性,很少使用数组。
slice
slice通常写作[]T,其中元素的类型都是T,是一种轻量级的数据结构,可以用来访问数组的部分或者全部元素,这个数组称为slice的底层数组。
-
slice有三个属性:指针,长度和容量。 -
这个新的slice引用了序列s中从i到j-1索引位置的所有元素,这里的s既可以是数组或者指向数组的指针,也可以是slice。
- 如果
x是字符串,那么x[m:n]返回的是一个字符串;如果x是字节slice,那么返回的结果是字节slice。 slice包含了指向数组元素的指针,所以将一个slice传递给函数的时候,可以在函数内部修改底层数组的元素。slice字面量看上去和数组字面量很像,都是用逗号分隔并用花括号括起来的一个元素序列,但slice没有指定长度。
s := []int{0, 1, 2, 3, 4, 5}
- 和数组不同,
slice不能用=来测试两个slice是否拥有相同的元素。bytes.Equal()可以用来比较两个字节slice([]byte)。 slice唯一允许的比较操作是和nil作比较。值为nil的slice没有对应的底层数组。
var s []int // len(s) == 0, s == nil
s = nil // len(s) == 0, s == nil
s = []int(nil) // len(s) == 0, s == nil
s = []int{} // len(s) == 0, s != nil
- 检查一个
slice是否是空,使用len(s) == 0,而不是s == nil。
append函数
内置函数append用来将元素追加到slice的后面。
var runes []rune
for _, r := range "Hello, 世界"{
runes = append(runes, r)
}
- 调用
append函数的情况下需要更新slice变量。对于任何函数,只要有可能改变slice的长度或者容量,亦或者使得slice指向不同的底层数组,都需要更新slice变量。 slice底层数组的元素是间接引用的,但是slice的指针、长度和容量不是。- 可以同时给
slice添加多个元素,甚至添加另一个slice里的所有元素。
slice就地修改
rotate和reverse等可以就地修改slice元素的函数。
map
一个拥有键值对元素的无序集合。在golang中,map是散列表的引用,map的类型是map[K]V,其中k和v是字典的键和值对应的数据类型。
- 内置函数
make可以用来创建一个map:
mapVal := make(map[string]int)
- 可以使用内置函数
delete来从字典中根据键移除一个元素,即使键不在map中,操作也是安全的:
delete(mapVal, "value") // 移除元素mapVal["value"]
map元素不是一个变量,不可以获取它的地址。map顺序是随机的,如果需要按照某种顺序来遍历map中的元素,必须显示地给键排序。如果键是字符串类型,可以使用sort包中的Strings函数来进行键的排序。- 通过一种下标方式访问
map中的元素输出两个值,第二个值是一个布尔值,用来报告该元素是否存在。
if mapValue, ok := mapValues["val"]; !ok { /* ... */ }