本文的内容建立在已对 C 和 C++ 熟练掌握的基础之上,主要给出 Go 与 C 和 C++ 之间的不同之处。
复数类型支持两种赋值操作及各种运算操作,示例如下。
1 | c1 := complex(1, 2) |
运算操作符不支持 %
(取模),关系操作符仅支持 ==
及 !=
,不支持逻辑运算符及位运算符。
复数类型使用 real()
和 imag()
可以获取取实部与虚部,复数可以转换为浮点类型,示例如下。
1 | c := complex(1, 2) |
复数类型一旦创建无法直接进行修改,若想修改只能创建一个新的复数。
数组声明时必须指明大小,以下是几种数组的声明方法。
1 | var nums [10]int |
第一种是数组的最基本声明方式 var nums [size]type
,创建了一个长度为 10 的 int
型数组,其中的数值全部初始化为 0;第二种声明方式不明确指明变量类型,由 Go 进行推断,数组仅对前 5 个元素指明数值,其他元素初始化为 0;第三种为第二种的简明声明方式;第四种声明方式不指明数组长度,由 Go 根据数组元素数量进行推断,得到一个长度为 5 的数组;第五种根据索引对数组的某些特定位置进行赋值,将 nums[1]
和 nums[2]
分别赋值为 3 和 4,其他元素默认为 0。
多维数组的声明方式类似于一维数组,示例如下。
1 | var nums = [2][4]int { |
切片和数组很像,但是有着本质上的不同。数组在声明时需要明确指明大小,而切片是对数组的一个 引用,其实际为一个结构体,包含有切片的基址、长度和容量。长度为当前切片中实际存储的成员数量,容量则是最大可容纳的成员数量。下面是一个小实验,验证切片的结构。
1 | nums := [10]int{1, 2, 3, 4, 5} |
上面的实验可以发现,数组正常占用了 10 个元素的容量,但是切片仅占用了三个 int
型的容量,对应到了基址、长度及容量的结构体。
基于切片是对数组引用的思想,以下是切片的几种定义方式。
1 | var slice []int |
第一种是最基本的切片定义方式,定义了一个 nil
(空)切片;第二种和第三种指定了切片的长度和容量;第四种直接指明了切片引用的数组;第五种创建时引用 arr
数组中的元素 arr[start, end)
,并设定了底层引用最大位置为 max
;第六种创建了一个切片的子切片,其也是对切片的引用。
二维切片的创建方式类似于一维切片,示例如下。
1 | nums := [][]int{ |
切片可用的函数有 len()
(取长度),cap()
(取容量),append()
(追加),copy()
(复制),使用方式如下。
1 | slice1 := make([]int, 3, 10) |
特别注意,切片是对原数组的引用,那么对切片的任何修改都会反映到原数组上,以下是一个例子。
1 | arr := [5]int{1, 2, 3, 4, 5} |
这里的 cap(slice) = 4
原因是切片的最大容量是由底层引用的数组所限制,切片的最大容量是由切片起始点到引用的数组最大位置决定。
当 append()
操作使容量超出限制时,切片会重新初始化一个引用的底层数组,容量倍增,此时的切片和原数组不再存在关联。
字符串的底层实现实际为一个结构体,包含有字符串基址及字符串长度,基址中存放 byte
型数组。Go 中的字符串一经创建无法直接修改。
为了修改字符串,需要对字符串创建切片,然后对切片进行修改,示例如下。
1 | var str string = "Hello World" |
对于 UTF-8 字符串需要使用 []rune()
进行切片。仔细阅读上面的代码不难发现,就算是使用了切片修改字符串,最后还是需要将切片转换为字符串赋回原串,所以本质上是用一个新的字符串替代了原字符串。这里的原因是,字符串创建切片时用的是 str
的一个副本,而对于 []int
的切片使用的是对底层数组的引用。
字符串也可以使用 len()
函数,以下是示例。
1 | str1 := "Hello" |
不难发现,字符串的 len()
实际上求的是所占 byte
数量。
字符串支持 +
运算。
1 | str1 := "hello" |
多行字符串的定义方式如下。
1 | str := `the first line |
集合 map
是指无序的键值对集合,可以类比为 C 中的哈希表 unordered_map
。集合的定义方式如下。
1 | m := make(map[string]int, 10) |
第一种定义方法将 string
置为键,int
置为值,设定了一个大小为 10 的集合;第二种方法则是直接指定了键值对。
获取集合中的元素可以直接使用键获取,得到的返回值有两个,分别是值和是否存在的标志,如下。
1 | v, valid := m["apple"] |
键值对可以直接增加、修改或是使用 delete
删除,也可以使用 len()
获取长度,示例如下。
1 | var m map[string]int |
Go 中可以自定义类型,相当于创建了一个新的类型,示例如下。
1 | type myint int |
这里创建的新类型无法和 int
直接进行运算操作,他可以创建自己的方法(涉及到后面的函数),下面是一个示例。
1 | func (m *myint) add(y myint) { |
自定义结构体也是一种自定义类型,类似于 C,结构体的定义方式的示例如下。
1 | type Student struct { |
结构体的初始化有几种方式,如下。
1 | stu := Student{"1", "casey", 0} // 顺序初始化 |
若在结构体中存在 slice
、map
和channel
,则需要使用到 make
,示例如下。
1 | stu := Student{id: "1", name: "casey", age: 0, m: make([]int, 0, 5)} |
不同于 C 的是,访问结构体成员时,结构体变量和结构体指针都使用 .
来访问成员,示例如下。
1 | var stu1 Student; |
若要实现结构体在包外的访问,需要将结构体的首字母大写,使结构体成为可导出的。若要进一步实现结构体成员的可导出,需要将成员名也相应大写。
结构体可以是匿名的,示例如下。
1 | var t struct { |
结构体的字段可以是匿名的,但是由于匿名的原因,同种类型只能有一个,访问方式为直接访问类型,示例如下。
1 | type Student struct { |
Go 中的条件语句有两种,分别是 if
和 switch
。
if
语句if
条件语句的使用方式类似于 C,在语法上有细微的不同:判断条件不用添加 ()
,else
需要紧跟 }
,示例如下。
1 | if a >= 50 { // 判断条件无 () |
Go 中的 if
语句可以在条件中初始化变量,这种做法可以限制判断条件的作用范围,不污染命名空间。以前面的集合为例,如下。
1 | if val, ok := m["pear"]; ok { |
这种写法常见于类型断言和集合查找。
switch
语句switch
条件语句的使用方式也类似于 C,仅在语法上存在细微不同:变量不用添加 ()
,自动进行 break
操作,一个 case
可以有多个条件。示例如下。
1 | var x bool = true |
在 switch
中可以使用 fallthrough
来强制执行下一个 case
,并配合使用 break
实现提前终止,示例如下。
1 | var x bool = true |
Go 中使用 for
循环,与 C 类似,下面是一个示例。
1 | for i := 0; i < 10; i++ { |
对 for
循环稍加修改,可以得到 C 中的 while
循环相同的效果,示例如下。
1 | var i int = 0 |
类似于 C,Go 也有 break
和 continue
语句。另外 Go 也支持 goto
语句,示例如下。
1 | var i int = 0 |
使用 range 进行遍历。for range
循环可以遍历数组(array)、切片(slice)、通道(channel)或集合(map)的元素。字符串作为一种特殊的数组,也可以使用 range
遍历。示例如下。
1 | for i, val := arr { // arr, slice, str |
这里可以使用 _
来忽略 i
或是 val
,并且注意这里的 val
是个临时变量,对 val
操作并不会反映到原变量中。
...
有可变参数展开的作用,其会将变参展开为独立变量传入,示例如下。1 | s1 := []int{1, 2, 3} |
slice
和 chan
(通道) 有 cap()
函数,其他类型都只有 len()
函数,len()
返回实际元素个数。