目录
数组是具有相同 唯一类型 的一组以编号且长度固定的数据项序列。 例如,整数 5、8、9、79、76 的集合形成一个数组
数据的长度是固定的。我们在声明一个数组时需要指定它的长度,一旦指定了长度,那么它的长度值是不可以改变的。
一个数组的表示形式为 T[n]。n 表示数组中元素的数量,T 代表每个元素的类型。元素的数量n也是该类型的一部分(稍后我们将详细讨论这一点)。
可以使用不同的方式来声明数组,让我们一个一个的来看。
- package main
-
- import (
- "fmt"
- )
-
-
- func main() {
- var a [3]int // 定义长度为 3 的 int 类型数组
- fmt.Println(a)
- }
var a[3]int 声明了一个长度为 3 的整型数组。数组中的所有元素都被自动赋值为数组类型的零值。 在这种情况下,a 是一个整型数组,因此 a 的所有元素都被赋值为 0,即 int 型的零值。运行上述程序将输出 [0 0 0]
The index of an array starts from 0 and ends at length - 1. Let’s assign some values to the above array.
数组的索引从0开始到length - 1结束。让我们为上面的数组赋值。
- package main
-
- import (
- "fmt"
- )
-
-
- func main() {
- var a [3]int
- a[0] = 8
- a[1] = 18
- a[2] = 88
- fmt.Println(a)
- }
a[0] 将值分配给数组的第一个元素。该程序将打印[8 18 88]
也可以简写声明相同的数组
- package main
-
- import (
- "fmt"
- )
-
- func main() {
- a := [3]int{8, 18, 88} // 简写模式,在定义的同时给出了赋值
- fmt.Println(a)
- }
你甚至可以忽略声明数组的长度,并用 ... 代替,让编译器为你自动计算长度,这在下面的程序中实现。
- package main
-
- import (
- "fmt"
- )
-
- func main() {
- a := [...]int{8, 18, 88} // 也可以不显式定义数组长度,由编译器完成长度计算
- fmt.Println(a)
- }
Go 中的数组是值类型而不是引用类型。这意味着当数组赋值给一个新的变量时,该变量会得到一个原始数组的一个副本。如果对新变量进行更改,则不会影响原始数组。
- package main
-
- import "fmt"
-
- func main() {
- a := [...]string{"中国", "美国", "日本", "法国"}
- b := a // 将 a 数组赋值给数组 b
- b[1] = "俄罗斯"
- fmt.Println("a is ", a)
- fmt.Println("b is ", b)
- }
在上面程序的第 7 行,a的副本被赋给b。在第 8 行中,b 的第二个元素改为 俄罗斯。这不会在原始数组 a 中反映出来。该程序将输出:
- a is [中国 美国 日本 法国]
- b is [中国 俄罗斯 日本 法国]
for 循环可用于遍历数组中的元素。
- package main
-
- import "fmt"
-
- func main() {
- a := [...]float64{67.7, 89.8, 21, 78}
- for i := 0; i < len(a); i++ {
- fmt.Printf("%d th element of a is %.2f\n", i, a[i])
- }
- }
Go 提供了一种更好、更简洁的方法,通过使用 for 循环的 range 方法来遍历数组。range 返回索引和该索引处的值。让我们使用 range 重写上面的代码。还可以获取数组中所有元素的总和。
- package main
-
- import "fmt"
-
- func main() {
- a := [...]float64{67.7, 89.8, 21, 78}
- sum := float64(0)
- for i, v := range a {
- fmt.Printf("%d the element of a is %.2f\n", i, v)
- sum += v
- }
- fmt.Println("\nsum of all elements of a",sum)
- }
上面程序的第 8 行 for i, v := range a 利用的是 for 循环 range 方式。 它将返回索引和该索引处的值。 我们打印这些值,并计算数组 a 中所有元素的总和。 程序的输出如下
- 0 the element of a is 67.70
- 1 the element of a is 89.80
- 2 the element of a is 21.00
- 3 the element of a is 78.00
-
- sum of all elements of a 256.5
如果你只需要值并希望忽略索引,则可以通过用 _ 空白标识符替换索引来执行。
- for _, v := range a {
- }
上面的 for 循环忽略索引,同样值也可以被忽略。
到目前为止我们创建的数组都是一维的,Go 语言可以创建多维数组。
- package main
-
- import (
- "fmt"
- )
-
- func printarray(a [3][2]string) {
- for _, v1 := range a {
- for _, v2 := range v1 {
- fmt.Printf("%s ", v2)
- }
- fmt.Printf("\n")
- }
- }
-
- func main() {
- a := [3][2]string{
- {"lion", "tiger"},
- {"cat", "dog"},
- {"pigeon", "peacock"},
- }
- printarray(a)
- }
在上面程序的第 17 行,用简略语法声明一个二维字符串数组 a 。20 行末尾的逗号是必需的。这是因为根据 Go 语言的规则自动插入分号。至于为什么这是必要的,如果你想了解更多,请阅读 https://golang.org/doc/effective_go.html#semicolons。
上述程序的 输出是:
- lion tiger
- cat dog
- pigeon peacock
这就是数组,尽管数组看上去似乎足够灵活,但是它们具有固定长度的限制,不可能增加数组的长度。这就要用到 切片 了。事实上,在 Go 中,切片比传统数组更常见。
切片(slice)是对数组一个连续片段的引用(该数组我们称之为相关数组,通常是匿名的),所以切片是一个引用类型。
实际开发中我们很少使用数组,取而代之的是切片。切片是一个 长度可变的数组
具有 T 类型元素的切片表示为[]T
- package main
-
- import (
- "fmt"
- )
-
- func main() {
- a := [5]int{76, 77, 78, 79, 80}
- var b []int = a[1:4] //创建一个切片 a[1] to a[3]
- fmt.Println(b)
- }
使用语法 a[start:end] 创建一个从 a 数组索引 start 开始到 end - 1 结束的切片。因此,在上述程序的第 9 行中, a[1:4] 为从索引 1 到 3 创建了 a 数组的一个切片表示。因此, 切片 b的值为 [77 78 79]。
让我们看看另一种创建切片的方法。
- package main
-
- import (
- "fmt"
- )
-
- func main() {
- c := []int{6, 7, 8}
- fmt.Println(c)
- }
在上面程序的第 9 行,c:= [] int {6,7,8} 创建一个有 3 个整型元素的数组,并返回一个存储在 c中的切片引用。
切片自己不拥有任何数据。它只是底层数组的一种表示。对切片所做的任何修改都会反映在底层数组中。
- package main
-
- import (
- "fmt"
- )
-
- func main() {
- darr := [...]int{57, 89, 90, 82, 100, 78, 67, 69, 59}
- dslice := darr[2:5]
- fmt.Println("array before",darr)
- for i := range dslice {
- dslice[i]++
- }
- fmt.Println("array after",darr)
- }
在上面程序的第 9 行,我们根据数组索引 2,3,4 创建一个切片 dslice。for 循环将这些索引中的值逐个递增。当重新使用 for 循环打印数组时,可以看到对切片的更改反映到了数组中。该程序的输出为:
- array before [57 89 90 82 100 78 67 69 59]
- array after [57 89 91 83 101 78 67 69 59]
切片的长度是切片中的元素数。切片的容量是从创建切片索引开始的底层数组中元素数。
让我们写一段代码来更好地理解这点。
- package main
-
- import (
- "fmt"
- )
-
- func main() {
- fruitarray := [...]string{"apple", "orange", "grape", "mango", "water melon", "pine apple", "chikoo"}
- fruitslice := fruitarray[1:3]
- fmt.Printf("length of slice %d capacity %d", len(fruitslice), cap(fruitslice))
- }
在上面的程序中,从 fruitarray 的索引 1 和 2 创建fruitslice 。 因此,fruitlice 的长度为 2。
fruitarray 的长度是 7。fruiteslice 是从 fruitarray 的索引 1 开始创建的。因此, fruitslice的容量是从 fruitarray 索引为 1开始,也就是说从 orange 开始,该值为 6。因此, fruitslice的容量为 6。该程序输出length of slice 2 capacity 6 。
切片可以重置其容量。任何超出这一点将导致程序运行时抛出错误。
- package main
-
- import (
- "fmt"
- )
-
- func main() {
- fruitarray := [...]string{"apple", "orange", "grape", "mango", "water melon", "pine apple", "chikoo"}
- fruitslice := fruitarray[1:3]
- fmt.Printf("length of slice %d capacity %d\n", len(fruitslice), cap(fruitslice)) //length of is 2 and capacity is 6
- fruitslice = fruitslice[:cap(fruitslice)]
- fmt.Println("After re-slicing length is",len(fruitslice), "and capacity is",cap(fruitslice))
- }
-
在上面程序的第 11 行中,fruitslice 的容量是重置的。以上程序输出为:
`
- length of slice 2 capacity 6
- After re-slicing length is 6 and capacity is 6
func make([]T,len,cap)[]T 通过传递类型,长度和容量来创建切片。容量是可选参数, 默认值为切片长度。make 函数创建一个数组,并返回引用该数组的切片。
- package main
-
- import (
- "fmt"
- )
-
- func main() {
- i := make([]int, 5, 5)
- fmt.Println(i)
- }
使用 make 创建切片时默认情况下这些值为零。上面程序的输出为 [0 0 0 0 0]。
数组的长度是固定的,它的长度不能增加。切片是动态的,使用 append 可以将新元素追加到切片上。append 函数的定义是 func append(s[]T,x ... T)[]T。x ... T 在函数定义中表示该函数接受参数 x 的个数是可变的。这些类型的函数被称为可变参函数。
有一个问题可能会困扰你。如果切片由数组支持,并且数组本身的长度是固定的,那么切片如何具有动态长度。以及内部发生了什么,当新的元素被添加到切片时,会创建一个新的数组。现有数组的元素被复制到这个新数组中,并返回这个新数组的新切片引用。现在新切片的容量是旧切片的两倍。很酷吧:)。下面的程序会让你清晰理解。
- package main
-
- import (
- "fmt"
- )
-
- func main() {
- cars := []string{"Ferrari", "Honda", "Ford"}
- fmt.Println("cars:", cars, "has old length", len(cars), "and capacity", cap(cars))
- cars = append(cars, "Toyota")
- fmt.Println("cars:", cars, "has new length", len(cars), "and capacity", cap(cars))
- }
在上面程序中,cars的容量最初是 3。在第 10 行,我们给 cars 添加了一个新的元素,并把 append(cars, "Toyota") 返回的切片赋值给 cars。现在 cars 的容量翻了一番,变成了 6。上面程序的输出为:
- cars: [Ferrari Honda Ford] has old length 3 and capacity 3
- cars: [Ferrari Honda Ford Toyota] has new length 4 and capacity 6
可以认为切片在内部由结构类型表示。看起来是这样的:
- type slice struct {
- Length int
- Capacity int
- ZerothElement *byte
- }
切片包含长度、容量和指向数组第零元素的指针。当切片传递给函数时,即使它是按值传递的,指针变量也会引用同一个底层数组。因此,当切片作为参数传递给函数时,函数内部所做的更改在函数外部也可见。让我们编写一个程序来检查一下。
- package main
-
- import (
- "fmt"
- )
-
- func subtactOne(numbers []int) {
- for i := range numbers {
- numbers[i] -= 2
- }
-
- }
- func main() {
- nos := []int{8, 7, 6}
- fmt.Println("slice before function call", nos)
- subtactOne(nos)
- fmt.Println("slice after function call", nos)
- }
上面程序的行号 17 中,调用函数将切片中的每个元素递减 2。在函数调用后打印切片时,这些更改是可见的。如果你还记得,这是不同于数组的,对于函数中一个数组的变化在函数外是不可见的。上面程序的输出:
- slice before function call [8 7 6]
- slice after function call [6 5 4]
与数组类似,切片可以有多个维度。
- package main
-
- import (
- "fmt"
- )
-
-
- func main() {
- pls := [][]string {
- {"C", "C++"},
- {"Java"},
- {"Go", "Rust"},
- }
- for _, v1 := range pls {
- for _, v2 := range v1 {
- fmt.Printf("%s ", v2)
- }
- fmt.Printf("\n")
- }
- }
程序的输出如下:
- C C++
- Java
- Go Rust
注意:Go 使用 2x 算法来增加数组长度
map 一种无序的键值对, 它是数据结构 hash 表的一种实现方式。map工作方式就是:定义键和值,并且可以获取,设置和删除其中的值。
可以通过将键和值的类型make传递给函数来创建映射。以下是创建新 Map的语法。
make(map[type of key]type of value)
- package main
-
- import (
- "fmt"
- )
-
- func main() {
- // 使用关键字 map 来声明
- lookup := map[string]int{ "goku": 9001, "gohan": 2044, }
- // 使用make来声明
- cMap := make(map[string]int)
- cMap["北京"] = 1
- fmt.Println("lookup:",lookup)
- fmt.Println("cMap:",cMap)
- }
上面程序用两种方式创建了两个 map,运行结果如下:
- lookup: map[gohan:2044 goku:9001]
- cMap: map[北京:1]
将元素添加到 Map 的语法与数组相同。上面的程序就是两种添加 map 的方式。
键不一定只能是 string 类型。所有可比较的类型,如 boolean,interger,float,complex,string 等,都可以作为键。即使是用户定义的类型(例如结构)也可以是键。
Map的零值是nil。如果您尝试向 Map添加元素,则会发生nil运行时报错。因此,必须在添加元素之前初始化 Map。
- package main
-
- func main() {
- var employeeSalary map[string]int
- employeeSalary["steve"] = 12000
- }
在上面的程序中,employeeSalary为nil。而我们正在尝试向 Map添加一个新键。程序因错误而报错:panic: assignment to entry in nil map
现在我们已经向 Map添加了一些元素,让我们学习如何检索它们。检索 Map元素的语法为map[key]
- package main
-
- import (
- "fmt"
- )
-
- func main() {
- cities := map[string]int{
- "北京": 100000,
- "湖南": 430000,
- }
- city := "北京"
- postCode := city[employee]
- fmt.Println("城市:", city,"邮编:", postCode)
- }
上面的程序非常简单。检索城市邮编。程序打印城市: 北京 邮编: 100000
如果一个元素不存在会发生什么?该映射将返回该元素类型的零值。在cities map的情况下,如果我们尝试访问不存在的元素,将返回int类型的零值。
- package main
-
- import (
- "fmt"
- )
-
- func main() {
- cities := map[string]int{
- "北京": 100000,
- "湖南": 430000,
- }
-
- fmt.Println("城市:", cities["上海"])
- }
上述程序的输出是城市: 0 当我们尝试检索 Map中不存在的键的值时,不会出现运行时错误。
当键不存在时,将返回类型的零值。当我们想要找出键是否真的存在于 Map 中时,要怎么做?
例如,我们想知道 cities Map中是否存在某个键。
value, ok := map[key]
以上是找出特定键是否存在于映射中的语法。如果ok为真,则该键存在且其值存在于变量value中,否则该键不存在。
- package main
-
- import (
- "fmt"
- )
-
- func main() {
- cities := map[string]int{
- "北京": 100000,
- "湖南": 430000,
- }
- newEmp := "上海"
- value, ok := cities[newEmp]
- if ok == true {
- fmt.Println("邮编:", value)
- return
- }
- fmt.Println(newEmp, "邮编不存在")
-
- }
在上面的程序中,在第 13 行。ok将为 false,因为上海不存在。因此程序将打印,
上海 邮编不存在
我们可以用for循环的range形式用于迭代 Map的所有元素。
- package main
-
- import (
- "fmt"
- )
-
- func main() {
- cities := map[string]int{
- "北京": 100000,
- "湖南": 430000,
- }
-
- for key, value := range cities {
- fmt.Printf("cities[%s] = %d\n", key, value)
- }
-
- }
上述程序输出,
- cities[北京] = 100000
- cities[湖南] = 430000
值得注意的是,因为 map 是无序的,因此对于程序的每次执行,不能保证使用 for range 遍历 map 的顺序总是一致的,而且遍历的顺序也不完全与元素添加的顺序一致。可以试下多添加几个城市验证下。
delete(map, key) 用于删除 map 中的键。delete 函数没有返回值。
- package main
-
- import (
- "fmt"
- )
-
- func main() {
- cities := map[string]int{
- "北京": 100000,
- "湖南": 430000,
- }
-
- fmt.Println("map before deletion", cities)
- delete(cities, "北京")
- fmt.Println("map after deletion", cities)
-
- }
上面的程序删除以 北京 为键的元素。程序输出为:
- map before deletion map[北京:100000 湖南:430000]
- map after deletion map[湖南:430000]
如果我们尝试删除 Map中不存在的键,则不会出现运行时错误。
== 进行比较。字符串在 Go 中值得特别提及,因为与其他语言相比,string 的实现方式同其他语言略有不同。
字符串是 Go 中的字节切片。可以通过将一组字符括在双引号中来创建字符串" "。
让我们看个创建string并打印它的简单示例。
- package main
-
- import (
- "fmt"
- )
-
- func main() {
- web := "https:www.gotribe.cn"
- fmt.Println(web)
- }
上面的程序将打印https:www.gotribe.cn.
Go 中的字符串是兼容Unicode编码的,并且是UTF-8编码的。
由于字符串是字节切片,因此可以访问字符串的每个字节。
- package main
-
- import (
- "fmt"
- )
-
- func printBytes(s string) {
- fmt.Printf("Bytes: ")
- for i := 0; i < len(s); i++ {
- fmt.Printf("%x ", s[i])
- }
- }
-
- func main() {
- web := "https:www.gotribe.cn"
- fmt.Printf("String: %s\n", web)
- printBytes(web)
- }
%s 是打印字符串的格式说明符。在16行号,打印输入字符串。在上面程序的第9 行中,len(s) 返回字符串中的字节数,我们使用for 循环以十六进制格式符打印这些字节。%x是十六进制的格式说明符。上述程序输出
- String: https:www.gotribe.cn
- Bytes: 68 74 74 70 73 3a 77 77 77 2e 67 6f 74 72 69 62 65 2e 63 6e
我们稍微修改一下上面的程序来打印字符串的字符。
- package main
-
- import (
- "fmt"
- )
-
- func printBytes(s string) {
- fmt.Printf("Bytes: ")
- for i := 0; i < len(s); i++ {
- fmt.Printf("%x ", s[i])
- }
- }
-
- func printChars(s string) {
- fmt.Printf("Characters: ")
- for i := 0; i < len(s); i++ {
- fmt.Printf("%c ", s[i])
- }
- }
-
- func main() {
- web := "https:www.gotribe.cn"
- fmt.Printf("String: %s\n", web)
- printBytes(web)
- fmt.Printf("\n")
- printChars(web)
- }
-
在上面程序的第 17 行, %c 格式说明符用于在printChars方法中打印字符串的字符。程序打印:
- String: https:www.gotribe.cn
- Bytes: 68 74 74 70 73 3a 77 77 77 2e 67 6f 74 72 69 62 65 2e 63 6e
- Characters: h t t p s : w w w . g o t r i b e . c n
上面的程序看起来像是访问字符串中单个字符的合法方式,但它有一个严重的错误。让我们找出那个错误是什么。
- package main
-
- import (
- "fmt"
- )
-
- func printBytes(s string) {
- fmt.Printf("Bytes: ")
- for i := 0; i < len(s); i++ {
- fmt.Printf("%x ", s[i])
- }
- }
-
- func printChars(s string) {
- fmt.Printf("Characters: ")
- for i := 0; i < len(s); i++ {
- fmt.Printf("%c ", s[i])
- }
- }
-
- func main() {
- web := "https:www.gotribe.cn"
- fmt.Printf("String: %s\n", web)
- printBytes(web)
- fmt.Printf("\n")
- printChars(web)
-
- fmt.Printf("\n\n")
- web = "Señor"
- fmt.Printf("String: %s\n", web)
- printBytes(web)
- fmt.Printf("\n")
- printChars(web)
- }
-
上述程序的输出是
- String: https:www.gotribe.cn
- Bytes: 68 74 74 70 73 3a 77 77 77 2e 67 6f 74 72 69 62 65 2e 63 6e
- Characters: h t t p s : w w w . g o t r i b e . c n
-
- String: Señor
- Bytes: 53 65 c3 b1 6f 72
- Characters: S e à ± o r
上面程序的第 33 行,我们尝试将 “Señor” 中的每个字符打印出来,但是却得到了 “Señor”。为什么这个程序在 “https:www.gotribe.cn” 上运行正常,但是不适用于 “Señor” 呢?因为 ñ 的 Unicode 码点是 U+00F1,因而它的 UTF-8 编码占了两个字节:c3 和 b1。上面的程序假定每个码点只有一个字节长度,因此会发生错误。在 UTF-8 编码中,一个码点可能会占一个以上的字节。 在这种情况下,我们需要 rune 来帮助解决问题。
rune 是 Go 中的内置类型,它是 int32 的别名。Rune 表示 Go 中的 Unicode 代码点。代码点占用多少字节并不重要,它可以用一个符文来表示。让我们修改上面的程序以使用符文打印字符。
- package main
-
- import (
- "fmt"
- )
-
- func printBytes(s string) {
- fmt.Printf("Bytes: ")
- for i := 0; i < len(s); i++ {
- fmt.Printf("%x ", s[i])
- }
- }
-
- func printChars(s string) {
- fmt.Printf("Characters: ")
- runes := []rune(s)
- for i := 0; i < len(runes); i++ {
- fmt.Printf("%c ", runes[i])
- }
- }
- func main() {
- web := "https:www.gotribe.cn"
- fmt.Printf("String: %s\n", web)
- printBytes(web)
- fmt.Printf("\n")
- printChars(web)
-
- fmt.Printf("\n\n")
- web = "Señor"
- fmt.Printf("String: %s\n", web)
- printBytes(web)
- fmt.Printf("\n")
- printChars(web)
- }
-
在上面的程序的 第16行 中,字符串被转换为rune切片。然后我们循环它并显示字符。输出结果为:
- String: https:www.gotribe.cn
- Bytes: 68 74 74 70 73 3a 77 77 77 2e 67 6f 74 72 69 62 65 2e 63 6e
- Characters: h t t p s : w w w . g o t r i b e . c n
-
- String: Señor
- Bytes: 53 65 c3 b1 6f 72
- Characters: S e ñ o r
这时候结果就正确了。
在 Go 中有多种方法可以执行字符串连接。让我们来看看其中的几个。
执行字符串连接的最简单方法是使用+运算符。
- ackage main
-
- import (
- "fmt"
- )
-
- func main() {
- string1 := "Go"
- string2 := "is awesome"
- result := string1 + " " + string2
- fmt.Println(result)
- }
在上面的程序中,第 10 行。string1与中间的空格和string2连接。该程序打印,Go is awesome
连接字符串的第二种方法是使用 fmt 包的Sprintf函数。
该Sprintf函数根据输入格式说明符格式化字符串并返回结果字符串。让我们用Sprintf函数重写上面的程序。
- package main
-
- import (
- "fmt"
- )
-
- func main() {
- string1 := "Go"
- string2 := "is awesome"
- result := fmt.Sprintf("%s %s", string1, string2)
- fmt.Println(result)
- }
上面程序的 第10行%s %s格式是作为Sprintf参数输入两个字符串,它们之间有一个空格。这将连接两个字符串。结果字符串存储在result中. 该程序打印,Go is awesome
注意点:
RuneCountInString(s string) (n int)来获取字符串的长度。一些 Unicode 字符的代码点占据了超过 1 个字节。len用于找出这些字符串的长度将返回不正确的字符串长度。函数是执行特定任务的代码块。函数接受输入,对输入执行一些计算,然后生成输出。
在 go 中声明函数的语法是:
- func name(parameter) (result-list){
- //body
- }
函数声明以func关键字开头,后跟name(函数名). 在括号中指定参数,后面为函数返回值result-list
指定参数的语法是,参数名称后跟类型。可以指定任意数量的参数,例如(parameter1 type, parameter2 type)。而{,}内的代码为函数的主体内容。
参数和返回类型在函数中是可选的。因此,以下语法也是有效的函数声明。
- func name() {
- }
- package main
-
- import (
- "fmt"
- )
-
- func plus(a, b int) (res int){
- return a + b
- }
-
- func main() {
- a, b := 90, 6
- sumAll := plus(a, b)
- fmt.Println("sum", sumAll)
- }
上面程序,函数plus 接受两个 int 类型的值,并返回最终和。输出结果如下:
sum 96
一个函数可以返回多个值。
- package main
-
- import (
- "fmt"
- )
-
- func multi() (string, int) {
- return "小李", 18
- }
-
- func main() {
- name, age := multi()
- fmt.Println("name:", name, "age:", age)
- }
-
上述程序降会输出:
name: 小李 age: 18
可以从函数返回命名值。如果返回值被命名,则可以认为它在函数的第一行被声明为变量。
- // 被命名的返回参数的值为该类型的默认零值
- // 该例子中 name 默认初始化为空字符串,height 默认初始化为 0
- func namedReturnValue()(name string, height int){
- name = "xiaoming"
- height = 180
- return
- }
- package main
-
- import (
- "fmt"
- )
-
- func sum(nums ...int)int{
- fmt.Println("len of nums is : ", len(nums))
- res := 0
- for _, v := range nums{
- res += v
- }
- return res
- }
-
- func main(){
- fmt.Println(sum(1))
- fmt.Println(sum(1,2))
- fmt.Println(sum(1,2,3))
- }
- func main(){
- func(name string){
- fmt.Println(name)
- }("https://www.gotribe.cn")
- }
指针是存储另一个变量的内存地址的变量。
![[Pasted image 20220802230857.png]]
在上图中,变量 b 的值是 156,存储在地址为 0x1040a124的内存中。变量 a 存储了变量 b 的地址。现在可以说 a 指向 b。
指向类型 T 的指针用 *T 表示。
让我们编写一个声明指针的程序。
- package main
-
- import (
- "fmt"
- )
-
- func main() {
- b := 255
- var a *int = &b
- fmt.Printf("Type of a is %T\n", a)
- fmt.Println("address of b is", a)
- }
& 操作符用来获取一个变量的地址。在上面的程序中,第 9 行我们将 b 的地址赋给 a(a 的类型为 *int)。现在我们说 a指向了 b。当我们打印 a 的值时,b 的地址将会被打印出来。程序的输出为:
- Type of a is *int
- address of b is 0x1040a124
你可能得到的是一个不同的 b 的地址,因为 b 可以在内存中的任何地方。
指针的零值为nil。
- package main
-
- import (
- "fmt"
- )
-
- func main() {
- a := 25
- var b *int
- if b == nil {
- fmt.Println("b is", b)
- b = &a
- fmt.Println("b after initialization is", b)
- }
- }
b 在上述程序中最初为 nil,然后分配给 a 的地址。该程序输出
- b is <nil>
- b after initialisation is 0x1040a124
Go 还提供了一个方便的函数new来创建指针。该new函数将一个类型作为参数并返回一个指针,该指针指向作为参数传递的类型的新分配的空值。
下面的例子可以说明清楚。
- package main
-
- import (
- "fmt"
- )
-
- func main() {
- size := new(int)
- fmt.Printf("Size value is %d, type is %T, address is %v\n", *size, size, size)
- *size = 85
- fmt.Println("New size value is", *size)
- }
在上面的程序中,在第8行。我们使用new函数来创建类型的int指针。该函数将返回一个指向新分配的int类型的零值指针。int类型的零值为0. 因此 size 将是 *int类型并将指向0, *size将为0。
上面的程序将打印
- Size value is 0, type is *int, address is 0x414020
- New size value is 85
解引用指针的意思是通过指针访问被指向的值。指针 a 的解引用表示为:*a。
让我们看看这在程序中是如何工作的。
- package main
- import (
- "fmt"
- )
-
- func main() {
- b := 255
- a := &b
- fmt.Println("address of b is", a)
- fmt.Println("value of b is", *a)
- }
在上面程序的第 10 行,我们将 a 解引用,并打印了它的值。不出所料,它会打印出 b 的值。程序会输出:
- address of b is 0x1040a124
- value of b is 255
让我们再编写一个程序,用指针来修改 b 的值。
- package main
-
- import (
- "fmt"
- )
-
- func main() {
- b := 255
- a := &b
- fmt.Println("address of b is", a)
- fmt.Println("value of b is", *a)
- *a++
- fmt.Println("new value of b is", b)
- }
在上面程序的第 12 行中,我们把 a指向的值加 1,由于 a 指向了 b,因此 b 的值也发生了同样的改变。于是 b 的值变为 256。程序会输出:
- address of b is 0x1040a124
- value of b is 255
- new value of b is 256
函数返回局部变量的指针是完全合法的。Go 编译器足够智能,它会在堆上分配这个变量。
- package main
-
- import (
- "fmt"
- )
-
- func hello() *int {
- i := 5
- return &i
- }
- func main() {
- d := hello()
- fmt.Println("Value of d", *d)
- }
上面程序的第9行,我们从hello函数中返回局部变量的地址i。此代码的行为在 C 和 C++ 等编程语言中是未定义的,因为一旦函数返回,变量i就会超出范围。但是在 Go 的情况下,编译器会进行转义分析,并·在地址转义本地范围时在堆上进行分配。因此,该程序将起作用并打印:Value of d 5
Go 不支持其他语言(如 C 和 C++)中存在的指针算法。
- package main
-
- func main() {
- b := [...]int{109, 110, 111}
- p := &b
- p++
- }
上述程序会抛出编译错误main.go:6: invalid operation: p++ (non-numeric type *[3]int)
注意:
数组、切片和 Map 可以用来表示同一种数据类型的集合,但是当我们要表示不同数据类型的集合时就需要用到结构体。
结构体是由零个或多个任意类型的值聚合成的实体,它可以用于将数据分组为一个单元而不是将它们中的每一个作为单独的值。
Go 里面用关键字 type 和 struct 用来定义结构体,语法如下:
- type StructName struct{
- FieldName type
- }
我们来定义一个学生结构体:
- package main
-
- import "fmt"
-
- type Student struct {
- Age int
- Name string
- }
-
- func main() {
- stu := Student{
- Age: 18,
- Name: "name",
- }
- fmt.Println(stu)
-
- // 在赋值的时候,字段名可以忽略
- stu2 := Student{20, "new name"}
- fmt.Println(stu2)
-
- return
- }
上述代码 5 行声明了一个Student的结构体。而在第 11 行,通过指定每个字段名的值,我们定义了结构体变量 stu。字段名的顺序不一定要与声明结构体类型时的顺序相同。在这里,我们改变了 Age 的位置,将其移到了末尾。这样做也不会有任何的问题。
但是在第 18 行。定义 stu2 时我们省略了字段名。在这种情况下,就需要保证字段名的顺序与声明结构体时的顺序相同。请不要使用这种语法,因为它很难确定哪个值是哪个字段的。我们在这里指定这种格式只是为了理解这也是一种有效的语法:)
上述代码输出如下:
- {18 name}
- {20 new name}
可以在不创建新数据类型的情况下声明结构。这些类型的结构称为匿名结构。
- package main
-
- import (
- "fmt"
- )
-
- func main() {
- emp3 := struct {
- firstName string
- lastName string
- age int
- salary int
- }{
- firstName: "Andreah",
- lastName: "Nikola",
- age: 31,
- salary: 5000,
- }
-
- fmt.Println("Employee 3", emp3)
- }
在上述程序的第 8 行中,定义了一个匿名结构变量 emp3。正如我们已经提到的,这个结构被称为匿名的,因为它只创建一个新的结构变量 emp3,并没有像命名结构那样定义任何新的结构类型。
该程序输出:Employee 3 {Andreah Nikola 31 5000}
点.运算符用于访问结构的各个字段。
- package main
-
- import (
- "fmt"
- )
-
- type Employee struct {
- firstName string
- lastName string
- age int
- salary int
- }
-
- func main() {
- emp6 := Employee{
- firstName: "Sam",
- lastName: "Anderson",
- age: 55,
- salary: 6000,
- }
- fmt.Println("First Name:", emp6.firstName)
- fmt.Println("Last Name:", emp6.lastName)
- fmt.Println("Age:", emp6.age)
- fmt.Printf("Salary: $%d\n", emp6.salary)
- emp6.salary = 6500
- fmt.Printf("New Salary: $%d", emp6.salary)
- }
上面程序中的emp6.firstName用于获取结构体emp6的字段firstName的内容。在第25行号,我们修改员工的工资。该程序打印:
- First Name: Sam
- Last Name: Anderson
- Age: 55
- Salary: $6000
- New Salary: $6500
也可以创建指向结构的指针。
- package main
-
- import (
- "fmt"
- )
-
- type Employee struct {
- firstName string
- lastName string
- age int
- salary int
- }
-
- func main() {
- emp8 := &Employee{
- firstName: "Sam",
- lastName: "Anderson",
- age: 55,
- salary: 6000,
- }
- fmt.Println("First Name:", (*emp8).firstName)
- fmt.Println("Age:", (*emp8).age)
- }
在上面程序中,emp8是一个指向结构体 Employee 的指针。(*emp8).firstName 表示访问结构体 emp8 的 firstName 字段。该程序会输出:
- First Name: Sam
- Age: 55
Go 语言允许我们在访问 firstName 字段时,可以使用 emp8.firstName 来代替显式的解引用 (*emp8).firstName。
- package main
-
- import (
- "fmt"
- )
-
- type Employee struct {
- firstName string
- lastName string
- age int
- salary int
- }
-
- func main() {
- emp8 := &Employee{
- firstName: "Sam",
- lastName: "Anderson",
- age: 55,
- salary: 6000,
- }
- fmt.Println("First Name:", emp8.firstName)
- fmt.Println("Age:", emp8.age)
- }
在上面的程序中,我们使用emp8.firstName 来访问 firstName 字段,该程序会输出:
- First Name: Sam
- Age: 55
可以创建具有仅包含类型而没有字段名称的字段的结构。这些类型的字段称为匿名字段。
下面的代码片段创建了一个结构Person,它有两个匿名字段string和int
- type Person struct {
- string
- int
- }
即使匿名字段没有明确的名称,默认情况下匿名字段的名称就是其类型的名称。例如,在上面的 Person结构中,虽然字段是匿名的,但默认情况下它们采用字段类型的名称。所以Person结构体有 2 个字段,分别为string和int.
- package main
-
- import (
- "fmt"
- )
-
- type Person struct {
- string
- int
- }
-
- func main() {
- p1 := Person{
- string: "naveen",
- int: 50,
- }
- fmt.Println(p1.string)
- fmt.Println(p1.int)
- }
在上面程序的第 17 行和第 18 行,我们访问了 Person 结构体的匿名字段,我们把字段类型作为字段名,分别为 “string” 和 “int”。上面程序的输出如下:
- naveen
- 50
一个结构可能包含一个字段,而该字段又是一个结构。这些类型的结构称为嵌套结构。
- package main
-
- import (
- "fmt"
- )
-
- type Address struct {
- city string
- state string
- }
-
- type Person struct {
- name string
- age int
- address Address
- }
-
- func main() {
- p := Person{
- name: "Naveen",
- age: 50,
- address: Address{
- city: "Chicago",
- state: "Illinois",
- },
- }
-
- fmt.Println("Name:", p.name)
- fmt.Println("Age:", p.age)
- fmt.Println("City:", p.address.city)
- fmt.Println("State:", p.address.state)
- }
上述程序中Person的结构有一个字段address,该字段address又是一个结构。该程序打印:
- Name: Naveen
- Age: 50
- City: Chicago
- State: Illinois
注意:
方法主要源于 OOP 语言,在传统面向对象语言中 (例如 C++), 我们会用一个“类”来封装属于自己的数据和函数,这些类的函数就叫做方法。
虽然 Go 不是经典意义上的面向对象语言,但是我们可以在一些接收者(自定义类型,结构体)上定义函数,同理这些接收者的函数在 Go 里面也叫做方法。
方法(method)的声明和函数很相似, 只不过它必须指定接收者:
func (t T) F() {}
注意:
type 定义的类型,例如自定义类型,结构体。现在我们写一个简单的程序,在结构类型上创建一个方法并调用它。
- package main
-
- import (
- "fmt"
- )
-
- type Employee struct {
- name string
- salary int
- currency string
- }
-
- func (e Employee) displaySalary() {
- fmt.Printf("Salary of %s is %s%d", e.name, e.currency, e.salary)
- }
-
- func main() {
- emp1 := Employee {
- name: "Sam Adolf",
- salary: 5000,
- currency: "$",
- }
- emp1.displaySalary()
- }
在上面程序的第 13 行中,我们在 Employee 结构体类型上创建了一个方法 displaySalary。displaySalary() 方法可以访问其中的接收者 e。我们使用收款人 e,打印员工的 name、currency 和 salary。
我们在第23行使用该方法调用了 emp1.displaySalary()
上面程序的输出是 :Salary of Sam Adolf is $5000
前面都是带有值接收器的方法。可以使用指针接收器创建方法。值和指针接收器之间的区别在于,在具有指针接收器的方法内部所做的更改对调用者是可见的,而在值接收器中则不然。让我们在程序的帮助下理解这一点。
- package main
-
- import (
- "fmt"
- )
-
- type Employee struct {
- name string
- age int
- }
-
- /*
- Method with value receiver
- */
- func (e Employee) changeName(newName string) {
- e.name = newName
- }
-
- /*
- Method with pointer receiver
- */
- func (e *Employee) changeAge(newAge int) {
- e.age = newAge
- }
-
- func main() {
- e := Employee{
- name: "Mark Andrew",
- age: 50,
- }
- fmt.Printf("Employee name before change: %s", e.name)
- e.changeName("Michael Andrew")
- fmt.Printf("\nEmployee name after change: %s", e.name)
-
- fmt.Printf("\n\nEmployee age before change: %d", e.age)
- (&e).changeAge(51)
- fmt.Printf("\nEmployee age after change: %d", e.age)
- }
在上面的程序中,changeName 方法有一个值接收器 (e Employee),而 changeAge 方法有一个指针接收器 (e *Employee)。在 changeName 中对 Employee 结构体的 name 字段所做的更改对调用者是不可见的,因此程序会在方法 e 之前和之后打印相同的 name。因为 changeAge 方法有一个指针接收器 (e *Employee),所以在方法调用 (&e) 之后对 age 字段所做的更改 (51) 将对调用者可见。这个程序打印:
- Employee name before change: Mark Andrew
- Employee name after change: Mark Andrew
-
- Employee age before change: 50
- Employee age after change: 51
通常,当调用者应该可以看到方法内对接收器所做的更改时,可以使用指针接收器。
指针接收器也可用于复制数据结构成本高昂的地方。考虑一个具有许多字段的结构。将此结构用作方法中的值接收器将需要复制整个结构,这将是昂贵的。在这种情况下,如果使用指针接收器,则不会复制结构,而在方法中只会使用指向它的指针。
在所有其他情况下,可以使用值接收器。
a. 接收者定义的方法名不能重复, 例如:
- package main
-
- type T struct{}
-
- func (T) F() {}
- func (T) F(a string) {}
-
- func main() {
- t := T{}
- t.F()
- }
运行代码我们会得到 method redeclared: T.F 类似错误。
b. 结构体方法名不能和字段重复,例如:
- package main
-
- type T struct{
- F string
- }
-
- func (T) F(){}
-
- func main() {
- t := T{}
- t.F()
- }
运行代码我们会得到 : type T has both field and method named F 类似错误。
同一个接收者的方法名不能重复 (没有重载);如果是结构体,方法名不能和字段重复。
在 Go 语言中,方法的接收者可以同时为值或者指针,例如:
- package main
-
- type T struct{}
-
- func (T) F() {}
- func (*T) N() {}
-
- func main() {
- t := T{}
- t.F()
- t.N()
-
- t1 := &T{} // 指针类型
- t1.F()
- t1.N()
- }
可以看到无论值类型 T 还是指针类型 &T 都可以同时访问 F 和 N 方法。
同样我们先看一段代码:
- package main
-
- import "fmt"
-
- type T struct {
- value int
- }
-
- func (m T) StayTheSame() {
- m.value = 3
- }
-
- func (m *T) Update() {
- m.value = 3
- }
-
- func main() {
- m := T{0}
- fmt.Println(m) // {0}
-
- m.StayTheSame()
- fmt.Println(m) // {0}
-
- m.Update()
- fmt.Println(m) // {3}
- }
运行代码输出结果为:
- {0}
- {0}
- {3}
值作为接收者(T) 不会修改结构体值,而指针 *T 可以修改。
在 Go 中,接口类型是一种抽象类型,是方法的集合,其他类型实现了这些方法就是实现了这个接口。
在 Go 中接口的声明如下:
- /* 定义接口 */
- type interface_name interface {
- method_name1 [return_type]
- method_name2 [return_type]
- method_name3 [return_type]
- ...
- method_namen [return_type]
- }
现在我们通过一个简单的示例来看是如何创建接口并实现它:
- package main
-
- import (
- "fmt"
- )
-
- //interface definition
- type VowelsFinder interface {
- FindVowels() []rune
- }
-
- type MyString string
-
- //MyString implements VowelsFinder
- func (ms MyString) FindVowels() []rune {
- var vowels []rune
- for _, rune := range ms {
- if rune == 'a' || rune == 'e' || rune == 'i' || rune == 'o' || rune == 'u' {
- vowels = append(vowels, rune)
- }
- }
- return vowels
- }
-
- func main() {
- name := MyString("Sam Anderson")
- var v VowelsFinder
- v = name // possible since MyString implements VowelsFinder
- fmt.Printf("Vowels are %c", v.FindVowels())
-
- }
在上面程序第8行中,创建了一个名为 VowelsFinder 的接口类型,它有一个方法 FindVowels() []rune。
在下一行中创建一个类型 MyString 它只是 string 的包装类。
在第15行中,我们将方法 FindVowels()[]rune 添加到接收方类型 MyString 中。现在 MyString 被认为实现了 VowelsFinder 接口。这与 Java 等其他语言非常不同,在 Java 中,类必须使用 implements 关键字显式地声明它实现了接口。如果类型包含接口中声明的所有方法,则 go 和 go 接口将隐式实现。
在第28行,我们将 MyString 类型的 name 赋给 v 类型的 VowelsFinder。这是可能的,因为 MyString 实现了 VowelsFinder。下一行调用 MyString 类型上的 FindVowels 方法,并打印字符串中所有的元音 Sam Anderson。这个程序输出的 Vowels are [a e o]
这样已经创建并实现了第一个接口。
一个没有方法的接口称为空接口。它表示为 interface{}。由于空接口没有任何方法,所以所有类型都实现空接口。
- package main
-
- import (
- "fmt"
- )
-
- func describe(i interface{}) {
- fmt.Printf("Type = %T, value = %v\n", i, i)
- }
-
- func main() {
- s := "Hello World"
- describe(s)
- i := 55
- describe(i)
- strt := struct {
- name string
- }{
- name: "Naveen R",
- }
- describe(strt)
- }
在上面的程序中第 7 行,describe(i interface{}) 函数接受一个空接口作为参数,因此它可以传递任何类型。
我们将字符串、int 和 结构体分别传递给第 13、15 和 21 行中的 describe 函数。这个程序打印
类型断言用于提取接口的基础值。
i.(T)是用于获取具体类型为T的接口i的底层值的语法。
一个程序值一千字😀。让我们为类型断言写一个。
- package main
-
- import (
- "fmt"
- )
-
- func assert(i interface{}) {
- s := i.(int) //get the underlying int value from i
- fmt.Println(s)
- }
- func main() {
- var s interface{} = 56
- assert(s)
- }
第12行中的 s 的具体类型是 int。我们使用第8行中的 i.(int) 语法来获取 i 的底层 int 值。这个程序打印 56。
如果上面程序中的具体类型不是 int 会发生什么?好吧,让我们来了解一下。
- package main
-
- import (
- "fmt"
- )
-
- func assert(i interface{}) {
- s := i.(int)
- fmt.Println(s)
- }
- func main() {
- var s interface{} = "Steven Paul"
- assert(s)
- }
在上面的程序中,我们将具体类型string的s传递给assert函数,试图从中提取 int 值。这个程序会因为这个问题而报错panic: interface conversion: interface {} is string, not int。
为了解决上述问题,我们可以使用语法
v, ok := i.(T)
如果 i 的具体类型是 T,那么 v 的值就是 i, ok 为 true。
如果 i 的具体类型不是 T,那么 ok 将是 false,v 将是类型 T 的零值,程序不会报错。
- package main
-
- import (
- "fmt"
- )
-
- func assert(i interface{}) {
- v, ok := i.(int)
- fmt.Println(v, ok)
- }
- func main() {
- var s interface{} = 56
- assert(s)
- var i interface{} = "Steven Paul"
- assert(i)
- }
当Steven Paul被传递给assert函数时,ok 将为 false,因为i具体类型不是int,并且v为 0值。该程序将打印:
- 56 true
- 0 false
注意: