go 基础知识
1. 语法上的一些注意事项
- go语句末尾不需要
;,这里和Python类似,一行就是一条语句,当然也可以加上;但是没这个必要。 - go是一门编译型语言,不是面向对象,go没有对象概念。
- go语法中的所有
变量必须被使用,否则会报错,导入的包也必须被使用。
2. go 程序结构
2.1 命名
Go命名规则:一个名字必须以一个字母(Unicode字母)或下划线开头,后面可以跟任意数量的字母、数字或下划线。大小写敏感:heapSort和Heapsort是两个不同的名字。
2.1.1 go关键字
Go语言中类似if和switch的关键字有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
此外,还有大约30多个预定义的名字,比如int和true等,主要对应内建的常量、类型和函数。这些内部预先定义的名字并不是关键字,你可以再定义中重新使用它们。在一些特殊的场景中重新定义它们也是有意义的,但是也要注意避免过度而引起语义混乱。
内建常量: 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
2.2 声明
声明语句定义了程序的各种实体对象以及部分或全部的属性。Go语言主要有四种类型的声明语句:var、const、type和func,分别对应变量、常量、类型和函数实体对象的声明
// Boiling prints the boiling point of water.
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)
// Output:
// boiling point = 212°F or 100°C
}
2.3 变量
2.3.1 基础变量
变量声明的一般语法如下:var 变量名字 类型 = 表达式 -> var name string = "nizonglong"
也可以只声明:var 变量名 类型 -> var age int
也可以简写:变量名 := 值 -> name := "nizonglong"变量的类型根据表达式来自动推导,当然也可以同时作用于多个变量:a, b, c, d := 1, '2', 3.02, "4" ,这里使用不同类型按顺序进行赋值
批量声明:var a, b, c, d int,批量赋值:var a,b,c,d = 1, '2', 3.02, "4"
这里有一个比较微妙的地方:简短变量声明左边的变量可能并不是全部都是刚刚声明的
in, err := os.Open(infile)
out, err := os.Create(outfile)
// 简短变量声明语句中必须至少要声明一个新的变量,下面的代码将不能编译通过:
f, err := os.Open(infile)
f, err := os.Create(outfile) // compile error: no new variables
简短变量声明语句只有对已经在同级词法域声明过的变量才和赋值操作语句等价,如果变量是在外部词法域声明的,那么简短变量声明语句将会在当前词法域重新声明一个新的变量。
2.3.2 指针
一个指针的值是另一个变量的地址。一个指针对应变量在内存中的存储位置。并不是每一个值都会有一个内存地址,但是对于每一个变量必然有对应的内存地址。通过指针,我们可以直接读或更新对应变量的值,而不需要知道该变量的名字(如果变量有名字的话)。
如果用var x int声明语句声明一个x变量,那么&x表达式(取x变量的内存地址)将产生一个指向该整数变量的指针,指针对应的数据类型是*int ,指针被称之为指向int类型的指针。
如果指针名字为p,那么可以说p指针指向变量x,或者说p指针保存了x变量的内存地址。
同时 *p 表达式对应p指针指向的变量的值。一般 *p 表达式读取指针指向的变量的值,这里为int类型的值,同时因为*p 对应一个变量,所以该表达式也可以出现在赋值语句的左边,表示更新指针所指向的变量的值。
x := 1
p := &x // p, of type *int, points to x
fmt.Println(*p) // "1"
*p = 2 // equivalent to x = 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"
指针的迷惑操作
var num int = 3
var p = &num // 现在p是一个*int指针,指向num
fmt.Println("point=", point) // point = 0xc420014088,这个是num的地址
fmt.Println("&point=", &point) // &point = 0xc42000c030
fmt.Println("*point=", *point) // *point = 3,这个是num的值
2.3.3 new函数
另一个创建变量的方法是调用用内建的new函数。表达式new(T)将创建一个T类型的匿名变量,初始化为T类型的零值,然后返回变量地址,返回的指针类型为 *T 。
p := new(int) // p, *int 类型, 指向匿名的 int 变量
fmt.Println(*p) // "0"
*p = 2 // 设置 int 匿名变量的值为 2
fmt.Println(*p) // "2"
2.4 类型type
一个类型声明语句创建了一个新的类型名称,和现有类型具有相同的底层结构。新命名的类型提供了一个方法,用来分隔不同概念的类型,这样即使它们底层类型相同也是不兼容的。
type 类型名字 底层类型
类型声明语句一般出现在包一级,因此如果新创建的类型名字的首字符大写,则在外部包也可以使用。
package tempconv
import "fmt"
type Celsius float64 // 摄氏温度
type Fahrenheit float64 // 华氏温度
const (
AbsoluteZeroC Celsius = -273.15 // 绝对零度
FreezingC Celsius = 0 // 结冰点温度
BoilingC Celsius = 100 // 沸水温度
)
func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) }
func FToC(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) }
通常使用type来使得代码更具有阅读性
3. 数据类型
3.1 基础数据类型
3.1.1 for的使用
package main
import "fmt"
func main() {
s := "hello, world! 你好"
for i := 0; i < len(s); i++ { // byte
fmt.Printf("%c,", s[i])
}
fmt.Println()
for _, r := range s { // rune
fmt.Printf("%c,", r)
}
fmt.Println()
n := len(s) - 1
for n > 0 { // 类似while
fmt.Printf("%c", s[n])
n--
}
}
3.1.2 range
package main
import "fmt"
func main() {
s := "abc"
for i := range s { // 忽略 2nd value,支持 string/array/slice/map
fmt.Printf("%c", s[i])
}
fmt.Println()
for _, c := range s { // 忽略 index
fmt.Printf("%c", c)
}
fmt.Println()
for range s { // 忽略全部返回值,仅迭代
}
m := map[string]int{"a": 1, "b": 2}
for k, v := range m { // 返回 (key, value)
fmt.Println(k, v)
}
}
3.1.3 switch
分⽀支表达式可以是任意类型,不限于常量。可省略 break,默认⾃自动终⽌止。
如需要继续下⼀一分⽀支,可使⽤用 fallthrough,但不再判断条件
package main
import "fmt"
func main() {
x := []int{1, 2, 3}
i := 2
switch i {
case x[1]:
fmt.Println("a")
case 1, 3:
fmt.Println("b")
default:
}
fmt.Println("c")
// 如需要继续下⼀一分⽀支,可使⽤用 fallthrough,但不再判断条件
x2 := 10
switch x2 {
case 10:
fmt.Println("a")
fallthrough
case 0:
fmt.Println("b")
}
}
省略条件表达式,可当 if...else if...else 使⽤用
// 省略条件表达式,可当 if...else if...else 使⽤用
switch {
case x[1] > 0:
fmt.Println("a")
case x[1] < 0:
fmt.Println("b")
default:
fmt.Println("c")
}
switch i := x[2]; { // 带初始化语句
case i > 0:
fmt.Println("a")
case i < 0:
fmt.Println("b")
default:
fmt.Println("c")
}
3.2 复合数据类型
3.2.1 数组
数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。
数组下标从0开始,因此长度是n的数组下标最大为n-1
因为数组的长度是固定的,因此在Go语言中很少直接使用数组。和数组对应的类型是Slice(切片),它是可以增长和收缩动态序列,slice功能也更灵活,但是要理解slice工作原理的话需要先理解数组。
数组的基本形式
var 名称 [大小]类型 -> var name [5]string
var 名称 [大小]类型 = [大小]类型{值1,值2,……} -> var name [5]string = [5]string{"n1","n2"}
名称 := [大小]类型{值1,值2,……} -> name := [3]string{"n1","n2"}
var a [3]int // array of 3 integers
fmt.Println(a[0]) // print the first element
fmt.Println(a[len(a)-1]) // print the last element, a[2]
// Print the indices and elements.
for i, v := range a {
fmt.Printf("%d %d\n", i, v)
}
// Print the elements only.
for _, v := range a {
fmt.Printf("%d\n", v)
}
初始化数组。若数组没有被初始化值,那么对应的值是元素类型对应的零值。
var q [3]int = [3]int{1, 2, 3}
var r [3]int = [3]int{1, 2}
fmt.Println(r[2]) // "0"
数组的长度是数组类型的一个组成部分,因此[3]int和[4]int是两种不同的数组类型。数组的长度必须是常量表达式,因为数组的长度需要在编译阶段确定。
q := [3]int{1, 2, 3}
q = [4]int{1, 2, 3, 4} // compile error: cannot assign [4]int to [3]int
比较两个数组是否相等
如果两个数组类型相同(包括数组的长度,数组中元素的类型)的情况下,我们可以直接通过较运算符(==和!=)来判断两个数组是否相等,只有当两个数组的所有元素都是相等的时候数组才是相等的,不能比较两个类型不同的数组,否则程序将无法完成编译。
a := [2]int{1, 2}
b := [...]int{1, 2}
c := [2]int{1, 3}
fmt.Println(a == b, a == c, b == c) // "true false false"
d := [3]int{1, 2}
fmt.Println(a == d) // 编译错误:无法比较 [2]int == [3]int
遍历数组——访问每一个数组元素
遍历数组也和遍历切片类似,代码如下所示:
var team [3]string
team[0] = "hammer"
team[1] = "soldier"
team[2] = "mum"
for k, v := range team {
fmt.Println(k, v)
}
代码输出结果:
0 hammer
1 soldier
2 mum
代码说明如下:
- 第 6 行,使用 for 循环,遍历 team 数组,遍历出的键 k 为数组的索引,值 v 为数组的每个元素值。
- 第 7 行,将每个键值打印出来。
3.2.2 切片 Slice
Slice(切片)代表变长的序列,序列中每个元素都有相同的类型。一个slice类型一般写作[]T,其中T代表slice中元素的类型;slice的语法和数组很像,只是没有固定长度而已。
**一个slice由三个部分构成:指针、长度和容量。**指针指向第一个slice元素对应的底层数组元素的地址,要注意的是slice的第一个元素并不一定就是数组的第一个元素。长度对应slice中元素的数目;长度不能超过容量,容量一般是从slice的开始位置到底层数据的结尾位置。内置的len和cap函数分别返回slice的长度和容量。
months := [...]string{1: "January", /* ... */, 12: "December"}
slice的切片操作s[i:j],其中0 ≤ i≤ j≤ cap(s),用于创建一个新的slice,引用s的从第i个元素开
始到第j-1个元素的子序列。新的slice将只有j-i个元素。如果i位置的索引被省略的话将使用0代
替,如果j位置的索引被省略的话将使用len(s)代替
下面看张图来了解切片是如何切的吧
Q2 := months[4:7]
summer := months[6:9]
fmt.Println(Q2) // ["April" "May" "June"]
fmt.Println(summer) // ["June" "July" "August"]
make
内置的make函数创建一个指定元素类型、长度和容量的slice。容量部分可以省略,在这种情况下,容量将等于长度。
make([]T, len)
make([]T, len, cap) // same as make([]T, cap)[:len]
在底层,make创建了一个匿名的数组变量,然后返回一个slice;只有通过返回的slice才能引用底层匿名的数组变量。在第一种语句中,slice是整个数组的view。在第二个语句中,slice只引用了底层数组的前len个元素,但是容量将包含整个的数组。额外的元素是留给未来的增长用的。
3.2.2.1 append
内置的append函数用于向slice追加元素:
var runes []rune
for _, r := range "Hello, 世界" {
runes = append(runes, r)
}
fmt.Printf("%q\n", runes) // "['H' 'e' 'l' 'l' 'o' ',' ' ' '世' '界']"
不过需要注意的是,在使用 append() 函数为切片动态添加元素时,如果空间不足以容纳足够多的元素,切片就会进行“扩容”,此时新切片的长度会发生改变。
切片slice通常拥有type, len, cap [类型,长度,容量],当len增长cap会动态变化适应len增长,在一定范围内是2倍增长,后续是1.25倍。
因为 append 函数返回新切片的特性,所以切片也支持链式操作,我们可以将多个 append 操作组合起来,实现在切片中间插入元素:
var a []inta = append(a[:i], append([]int{x}, a[i:]...)...) // 在第i个位置插入xa = append(a[:i], append([]int{1,2,3}, a[i:]...)...) // 在第i个位置插入切片
3.2.3 map
内置的make函数可以创建一个map:
ages := make(map[string]int) // mapping from strings to ints
我们也可以用map字面值的语法创建map,同时还可以指定一些最初的key/value:
ages := map[string]int{
"alice": 31,
"charlie": 34,
}
这相当于
ages := make(map[string]int)
ages["alice"] = 31
ages["charlie"] = 34
使用内置的delete函数可以删除元素:
delete(ages, "alice") // remove element ages["alice"]
Map的迭代顺序是不确定的,并且不同的哈希函数实现可能导致不同的遍历顺序。在实践中,遍历的顺序是随机的,每一次遍历的顺序都不相同。
map遍历
map 的遍历过程使用 for range 循环完成
/**
map遍历
*/
scene := make(map[string]int)
scene["route"] = 66
scene["brazil"] = 4
scene["china"] = 960
for k, v := range scene {
fmt.Println(k, v)
}
// 只遍历值
for _, v := range scene {
fmt.Println(v)
}
// 只遍历 key
for k := range scene {
fmt.Println(k)
}
如果需要特定顺序的遍历结果,正确的做法是先排序,代码如下:
scene := make(map[string]int)
// 准备map数据
scene["route"] = 66
scene["brazil"] = 4
scene["china"] = 960
// 声明一个切片保存map数据
var sceneList []string
// 将map数据遍历复制到切片中
for k := range scene {
sceneList = append(sceneList, k)
}
// 对切片进行排序
sort.Strings(sceneList)
// 输出
fmt.Println(sceneList)
// [brazil china route]
sort.Strings 的作用是对传入的字符串切片进行字符串字符的升序排列