Go语言系列——数组和切片、可变参数函数、Maps、字符串、指针、结构体、方法、接口(一)、接口(二)、自定义集合类型、并发入门

文章目录

11-数组和切片

title: “11-数组和切片”date: 2019-03-10 08:08tags:

  • Go

数组

数组是同一类型元素的集合。例如,整数集合 5,8,9,79,76 形成一个数组。Go 语言中不允许混合不同类型的元素,例如包含字符串和整数的数组。(当然,如果是 interface{} 类型数组,可以包含任意类型)

数组的声明

一个数组的表示形式为 [n]Tn 表示数组中元素的数量,T 代表每个元素的类型。元素的数量 n 也是该类型的一部分(稍后我们将详细讨论这一点)。
可以使用不同的方式来声明数组,让我们一个一个的来看。

package main

import (
    "fmt"
)

func main() {
    var a [3]int //int array with length 3
    fmt.Println(a)
}

var a[3]int 声明了一个长度为 3 的整型数组。数组中的所有元素都被自动赋值为数组类型的零值。 在这种情况下,a 是一个整型数组,因此 a 的所有元素都被赋值为 0,即 int 型的零值。运行上述程序将 输出 [0 0 0]
数组的索引从 0 开始到 length - 1 结束。让我们给上面的数组赋值。

package main

import (
    "fmt"
)

func main() {
    var a [3]int //int array with length 3
    a[0] = 12 // array index starts at 0
    a[1] = 78
    a[2] = 50
    fmt.Println(a)
}

a[0] 将值赋给数组的第一个元素。该程序将 输出 [12 78 50]
让我们使用 简略声明 来创建相同的数组。

package main

import (
    "fmt"
)

func main() {
    a := [3]int{12, 78, 50} // short hand declaration to create array
    fmt.Println(a)
}

上面的程序将会打印相同的 输出 [12 78 50]
在简略声明中,不需要将数组中所有的元素赋值。

package main

import (
    "fmt"
)

func main() {
    a := [3]int{12} 
    fmt.Println(a)
}

在上述程序中的第 8 行 a := [3]int{12} 声明一个长度为 3 的数组,但只提供了一个值 12,剩下的 2 个元素自动赋值为 0。这个程序将输出 [12 0 0]
你甚至可以忽略声明数组的长度,并用 ... 代替,让编译器为你自动计算长度,这在下面的程序中实现。

package main

import (
    "fmt"
)

func main() {
    a := [...]int{12, 78, 50} // ... makes the compiler determine the length
    fmt.Println(a)
}

数组的大小是类型的一部分。因此 [5]int[25]int 是不同类型。数组不能调整大小,不要担心这个限制,因为 slices 的存在能解决这个问题。

package main

func main() {
    a := [3]int{5, 78, 8}
    var b [5]int
    b = a // not possible since [3]int and [5]int are distinct types
}

在上述程序的第 6 行中, 我们试图将类型 [3]int 的变量赋给类型为 [5]int 的变量,这是不允许的,因此编译器将抛出错误 main.go:6: cannot use a (type [3]int) as type [5]int in assignment。

数组是值类型

Go 中的数组是值类型而不是引用类型。这意味着当数组赋值给一个新的变量时,该变量会得到一个原始数组的一个副本。如果对新变量进行更改,则不会影响原始数组。

package main

import "fmt"

func main() {
    a := [...]string{"USA", "China", "India", "Germany", "France"}
    b := a // a copy of a is assigned to b
    b[0] = "Singapore"
    fmt.Println("a is ", a)
    fmt.Println("b is ", b) 
}

在上述程序的第 7 行,a 的副本被赋给 b。在第 8 行中,b 的第一个元素改为 Singapore。这不会在原始数组 a 中反映出来。该程序将 输出,

a is [USA China India Germany France]  
b is [Singapore China India Germany France]

同样,当数组作为参数传递给函数时,它们是按值传递,而原始数组保持不变。

package main

import "fmt"

func changeLocal(num [5]int) {
    num[0] = 55
    fmt.Println("inside function ", num)
}
func main() {
    num := [...]int{5, 6, 7, 8, 8}
    fmt.Println("before passing to function ", num)
    changeLocal(num) //num is passed by value
    fmt.Println("after passing to function ", num)
}

在上述程序的 13 行中, 数组 num 实际上是通过值传递给函数 changeLocal,数组不会因为函数调用而改变。这个程序将输出,

before passing to function  [5 6 7 8 8]
inside function  [55 6 7 8 8]
after passing to function  [5 6 7 8 8]

数组的长度

通过将数组作为参数传递给 len 函数,可以得到数组的长度。

package main

import "fmt"

func main() {
    a := [...]float64{67.7, 89.8, 21, 78}
    fmt.Println("length of a is",len(a))
}

上面的程序输出为 length of a is 4

使用 range 迭代数组

for 循环可用于遍历数组中的元素。

package main

import "fmt"

func main() {
    a := [...]float64{67.7, 89.8, 21, 78}
    for i := 0; i < len(a); i++ { // looping from 0 to the length of the array
        fmt.Printf("%d th element of a is %.2f\n", i, a[i])
    }
}

上面的程序使用 for 循环遍历数组中的元素,从索引 0length of the array - 1。这个程序运行后打印出,

0 th element of a is 67.70  
1 th element of a is 89.80  
2 th element of a is 21.00  
3 th element of a is 78.00

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 {//range returns both the index and value
        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 { // ignores index  
}

上面的 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"}, // this comma is necessary. The compiler will complain if you omit this comma
    }
    printarray(a)
    var b [3][2]string
    b[0][0] = "apple"
    b[0][1] = "samsung"
    b[1][0] = "microsoft"
    b[1][1] = "google"
    b[2][0] = "AT&T"
    b[2][1] = "T-Mobile"
    fmt.Printf("\n")
    printarray(b)
}

在上述程序的第 17 行,用简略语法声明一个二维字符串数组 a 。20 行末尾的逗号是必需的。这是因为根据 Go 语言的规则自动插入分号。至于为什么这是必要的,如果你想了解更多,请阅读https://golang.org/doc/effective_go.html#semicolons
另外一个二维数组 b 在 23 行声明,字符串通过每个索引一个一个添加。这是另一种初始化二维数组的方法。
第 7 行的 printarray 函数使用两个 range 循环来打印二维数组的内容。上述程序的 输出是

lion tiger
cat dog
pigeon peacock

apple samsung
microsoft google
AT&T T-Mobile

这就是数组,尽管数组看上去似乎足够灵活,但是它们具有固定长度的限制,不可能增加数组的长度。这就要用到 切片 了。事实上,在 Go 中,切片比传统数组更常见。

切片

切片是由数组建立的一种方便、灵活且功能强大的包装(Wrapper)。切片本身不拥有任何数据。它们只是对现有数组的引用。

创建一个切片

带有 T 类型元素的切片由 []T 表示

package main

import (
    "fmt"
)

func main() {
    a := [5]int{76, 77, 78, 79, 80}
    var b []int = a[1:4] // creates a slice from 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} // creates and array and returns a slice reference
    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() {
    numa := [3]int{78, 79 ,80}
    nums1 := numa[:] // creates a slice which contains all elements of the array
    nums2 := numa[:]
    fmt.Println("array before change 1", numa)
    nums1[0] = 100
    fmt.Println("array after modification to slice nums1", numa)
    nums2[1] = 101
    fmt.Println("array after modification to slice nums2", numa)
}

在 9 行中,numa [:] 缺少开始和结束值。开始和结束的默认值分别为 0len (numa)。两个切片 nums1nums2 共享相同的数组。该程序的输出是

array before change 1 [78 79 80]  
array after modification to slice nums1 [100 79 80]  
array after modification to slice nums2 [100 101 80]

从输出中可以清楚地看出,当切片共享同一个数组时,每个所做的修改都会反映在数组中。

切片的长度和容量

切片的长度是切片中的元素数。切片的容量是从创建切片索引开始的底层数组中元素数。
让我们写一段代码来更好地理解这点。

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)) // length of is 2 and capacity is 6
}

在上面的程序中,fruitslice 是从 fruitarray 的索引 1 和 2 创建的。 因此,fruitlice 的长度为 2
fruitarray 的长度是 7。fruiteslice 是从 fruitarray 的索引 1 创建的。因此, fruitslice 的容量是从 fruitarray 索引为 1 开始,也就是说从 orange 开始,该值是 6。因此, fruitslice 的容量为 6。该[程序]输出切片的 长度为 2 容量为 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)] // re-slicing furitslice till its capacity
    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

使用 make 创建一个切片

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)) // capacity of cars is 3
    cars = append(cars, "Toyota")
    fmt.Println("cars:", cars, "has new length", len(cars), "and capacity", cap(cars)) // capacity of cars is doubled to 6
}

在上述程序中,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

切片类型的零值为 nil。一个 nil 切片的长度和容量为 0。可以使用 append 函数将值追加到 nil 切片。

package main

import (  
    "fmt"
)

func main() {  
    var names []string //zero value of a slice is nil
    if names == nil {
        fmt.Println("slice is nil going to append")
        names = append(names, "John", "Sebastian", "Vinay")
        fmt.Println("names contents:",names)
    }
}

在上面的程序 names 是 nil,我们已经添加 3 个字符串给 names。该程序的输出是

slice is nil going to append  
names contents: [John Sebastian Vinay]

也可以使用 ... 运算符将一个切片添加到另一个切片。 你可以在[可变参数函数]教程中了解有关此运算符的更多信息。

package main

import (
    "fmt"
)

func main() {
    veggies := []string{"potatoes", "tomatoes", "brinjal"}
    fruits := []string{"oranges", "apples"}
    food := append(veggies, fruits...)
    fmt.Println("food:",food)
}

在上述程序的第 10 行,food 是通过 append(veggies, fruits…) 创建。程序的输出为 food: [potatoes tomatoes brinjal oranges apples]

切片的函数传递

我们可以认为,切片在内部可由一个结构体类型表示。这是它的表现形式,

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)                               // function modifies the slice
    fmt.Println("slice after function call", nos) // modifications are visible outside
}

上述程序的行号 17 中,调用函数将切片中的每个元素递减 2。在函数调用后打印切片时,这些更改是可见的。如果你还记得,这是不同于数组的,对于函数中一个数组的变化在函数外是不可见的。上述[程序]的输出是,

array before function call [8 7 6]  
array after function call [6 5 4]

多维切片

类似于数组,切片可以有多个维度。

package main

import (
    "fmt"
)

func main() {  
     pls := [][]string {
            {"C", "C++"},
            {"JavaScript"},
            {"Go", "Rust"},
            }
    for _, v1 := range pls {
        for _, v2 := range v1 {
            fmt.Printf("%s ", v2)
        }
        fmt.Printf("\n")
    }
}

程序的输出为,

C C++  
JavaScript  
Go Rust

内存优化

切片持有对底层数组的引用。只要切片在内存中,数组就不能被垃圾回收。在内存管理方面,这是需要注意的。让我们假设我们有一个非常大的数组,我们只想处理它的一小部分。然后,我们由这个数组创建一个切片,并开始处理切片。这里需要重点注意的是,在切片引用时数组仍然存在内存中。
一种解决方法是使用 [copy] 函数 func copy(dst,src[]T)int 来生成一个切片的副本。这样我们可以使用新的切片,原始数组可以被垃圾回收。

package main

import (
    "fmt"
)

func countries() []string {
    countries := []string{"USA", "Singapore", "Germany", "India", "Australia"}
    neededCountries := countries[:len(countries)-2]
    countriesCpy := make([]string, len(neededCountries))
    copy(countriesCpy, neededCountries) //copies neededCountries to countriesCpy
    return countriesCpy
}
func main() {
    countriesNeeded := countries()
    fmt.Println(countriesNeeded)
}

在上述程序的第 9 行,neededCountries := countries[:len(countries)-2 创建一个去掉尾部 2 个元素的切片 countries,在上述程序的 11 行,将 neededCountries 复制到 countriesCpy 同时在函数的下一行返回 countriesCpy。现在 countries 数组可以被垃圾回收, 因为 neededCountries 不再被引用。

12-可变参数函数

什么是可变参数函数

可变参数函数是一种参数个数可变的函数。

语法

如果函数最后一个参数被记作 ...T ,这时函数可以接受任意个 T 类型参数作为最后一个参数。
请注意只有函数的最后一个参数才允许是可变的。

通过一些例子理解可变参数函数如何工作

你是否曾经想过 append 函数是如何将任意个参数值加入到切片中的。这样 append 函数可以接受不同数量的参数。

func append(slice []Type, elems ...Type) []Type

上面是 append 函数的定义。在定义中 elems 是可变参数。这样 append 函数可以接受可变化的参数。
让我们创建一个我们自己的可变参数函数。我们将写一段简单的程序,在输入的整数列表里查找某个整数是否存在。

package main

import (
    "fmt"
)

func find(num int, nums ...int) {
    fmt.Printf("type of nums is %T\n", nums)
    found := false
    for i, v := range nums {
        if v == num {
            fmt.Println(num, "found at index", i, "in", nums)
            found = true
        }
    }
    if !found {
        fmt.Println(num, "not found in ", nums)
    }
    fmt.Printf("\n")
}
func main() {
    find(89, 89, 90, 95)
    find(45, 56, 67, 45, 90, 109)
    find(78, 38, 56, 98)
    find(87)
}

在上面程序中 func find(num int, nums ...int) 中的 nums 可接受任意数量的参数。在 find 函数中,参数 nums 相当于一个整型切片。
可变参数函数的工作原理是把可变参数转换为一个新的切片。以上面程序中的第 22 行为例,find 函数中的可变参数是 89,90,95 。 find 函数接受一个 int 类型的可变参数。因此这三个参数被编译器转换为一个 int 类型切片 int []int{89, 90, 95} 然后被传入 find函数。
在第 10 行, for 循环遍历 nums 切片,如果 num 在切片中,则打印 num 的位置。如果 num 不在切片中,则打印提示未找到该数字。
上面代码的输出值如下,

type of nums is []int
89 found at index 0 in [89 90 95]

type of nums is []int
45 found at index 2 in [56 67 45 90 109]

type of nums is []int
78 not found in  [38 56 98]

type of nums is []int
87 not found in  []

在上面程序的第 25 行,find 函数仅有一个参数。我们没有给可变参数 nums ...int 传入任何参数。这也是合法的,在这种情况下 nums 是一个长度和容量为 0 的 nil 切片。

给可变参数函数传入切片

下面例子中,我们给可变参数函数传入一个切片,看看会发生什么。

package main

import (
    "fmt"
)

func find(num int, nums ...int) {
    fmt.Printf("type of nums is %T\n", nums)
    found := false
    for i, v := range nums {
        if v == num {
            fmt.Println(num, "found at index", i, "in", nums)
            found = true
        }
    }
    if !found {
        fmt.Println(num, "not found in ", nums)
    }
    fmt.Printf("\n")
}
func main() {
    nums := []int{89, 90, 95}
    find(89, nums)
}

在第 23 行中,我们将一个切片传给一个可变参数函数。
这种情况下无法通过编译,编译器报出错误 main.go:23: cannot use nums (type []int) as type int in argument to find
为什么无法工作呢?原因很直接,find 函数的说明如下,

func find(num int, nums ...int)

由可变参数函数的定义可知,nums ...int 意味它可以接受 int 类型的可变参数。
在上面程序的第 23 行,nums 作为可变参数传入 find 函数。前面我们知道,这些可变参数参数会被转换为 int 类型切片然后在传入 find 函数中。但是在这里 nums 已经是一个 int 类型切片,编译器试图在 nums 基础上再创建一个切片,像下面这样

find(89, []int{nums})

这里之所以会失败是因为 nums 是一个 []int类型 而不是 int类型。
那么有没有办法给可变参数函数传入切片参数呢?答案是肯定的。
有一个可以直接将切片传入可变参数函数的语法糖,你可以在在切片后加上 … 后缀。如果这样做,切片将直接传入函数,不再创建新的切片
在上面的程序中,如果你将第 23 行的 find(89, nums) 替换为 find(89, nums...) ,程序将成功编译并有如下输出

type of nums is []int
89 found at index 0 in [89 90 95]

下面是完整的程序供您参考。

package main

import (
    "fmt"
)

func find(num int, nums ...int) {
    fmt.Printf("type of nums is %T\n", nums)
    found := false
    for i, v := range nums {
        if v == num {
            fmt.Println(num, "found at index", i, "in", nums)
            found = true
        }
    }
    if !found {
        fmt.Println(num, "not found in ", nums)
    }
    fmt.Printf("\n")
}
func main() {
    nums := []int{89, 90, 95}
    find(89, nums...)
}

不直观的错误

当你修改可变参数函数中的切片时,请确保你知道你正在做什么。
下面让我们来看一个简单的例子。

package main

import (
    "fmt"
)

func change(s ...string) {  
    s[0] = "Go"
}

func main() {
    welcome := []string{"hello", "world"}
    change(welcome...)
    fmt.Println(welcome)
}

你认为这段代码将输出什么呢?如果你认为它输出 [Go world] 。恭喜你!你已经理解了可变参数函数和切片。如果你猜错了,那也不要紧,让我来解释下为什么会有这样的输出。
在第 13 行,我们使用了语法糖 ... 并且将切片作为可变参数传入 change 函数。
正如前面我们所讨论的,如果使用了 ...welcome 切片本身会作为参数直接传入,不需要再创建一个新的切片。这样参数 welcome 将作为参数传入 change 函数
change 函数中,切片的第一个元素被替换成 Go,这样程序产生了下面的输出值

[Go world]

这里还有一个例子来理解可变参数函数。

package main

import (
    "fmt"
)

func change(s ...string) {
    s[0] = "Go"
    s = append(s, "playground")
    fmt.Println(s)
}

func main() {
    welcome := []string{"hello", "world"}
    change(welcome...)
    fmt.Println(welcome)
}

13-Maps

什么是 map ?

map 是在 Go 中将值(value)与键(key)关联的内置类型。通过相应的键可以获取到值。

如何创建 map ?

通过向 make 函数传入键和值的类型,可以创建 map。make(map[type of key]type of value) 是创建 map 的语法。

personSalary := make(map[string]int)

上面的代码创建了一个名为 personSalary 的 map,其中键是 string 类型,而值是 int 类型。
map 的零值是 nil。如果你想添加元素到 nil map 中,会触发运行时 panic。因此 map 必须使用 make 函数初始化。

package main

import (
    "fmt"
)

func main() {  
    var personSalary map[string]int
    if personSalary == nil {
        fmt.Println("map is nil. Going to make one.")
        personSalary = make(map[string]int)
    }
}

上面的程序中,personSalary 是 nil,因此需要使用 make 方法初始化,程序将输出 map is nil. Going to make one.

给 map 添加元素

给 map 添加新元素的语法和数组相同。下面的程序给 personSalary map 添加了几个新元素。

package main

import (
    "fmt"
)

func main() {
    personSalary := make(map[string]int)
    personSalary["steve"] = 12000
    personSalary["jamie"] = 15000
    personSalary["mike"] = 9000
    fmt.Println("personSalary map contents:", personSalary)
}

上面的程序输出:personSalary map contents: map[steve:12000 jamie:15000 mike:9000]
你也可以在声明的时候初始化 map。

package main

import (  
    "fmt"
)

func main() {  
    personSalary := map[string]int {
        "steve": 12000,
        "jamie": 15000,
    }
    personSalary["mike"] = 9000
    fmt.Println("personSalary map contents:", personSalary)
}

上面的程序声明了 personSalary,并在声明的同时添加两个元素。之后又添加了键 mike。程序输出:

personSalary map contents: map[steve:12000 jamie:15000 mike:9000]

键不一定只能是 string 类型。所有可比较的类型,如 boolean,interger,float,complex,string 等,都可以作为键。关于可比较的类型,如果你想了解更多,请访问 http://golang.org/ref/spec#Comparison_operators

获取 map 中的元素

目前我们已经给 map 添加了几个元素,现在学习下如何获取它们。获取 map 元素的语法是 map[key]

package main

import (
    "fmt"
)

func main() {
    personSalary := map[string]int{
        "steve": 12000,
        "jamie": 15000,
    }
    personSalary["mike"] = 9000
    employee := "jamie"
    fmt.Println("Salary of", employee, "is", personSalary[employee])
}

上面的程序很简单。获取并打印员工 jamie 的薪资。程序输出 Salary of jamie is 15000
如果获取一个不存在的元素,会发生什么呢?map 会返回该元素类型的零值。在 personSalary 这个 map 里,如果我们获取一个不存在的元素,会返回 int 类型的零值 0

package main

import (  
    "fmt"
)

func main() {
    personSalary := map[string]int{
        "steve": 12000,
        "jamie": 15000,
    }
    personSalary["mike"] = 9000
    employee := "jamie"
    fmt.Println("Salary of", employee, "is", personSalary[employee])
    fmt.Println("Salary of joe is", personSalary["joe"])
}

上面程序输出:

Salary of jamie is 15000
Salary of joe is 0

上面程序返回 joe 的薪资是 0。personSalary 中不包含 joe 的情况下我们不会获取到任何运行时错误。
如果我们想知道 map 中到底是不是存在这个 key,该怎么做:

value, ok := map[key]

上面就是获取 map 中某个 key 是否存在的语法。如果 ok 是 true,表示 key 存在,key 对应的值就是 value ,反之表示 key 不存在。

package main

import (
    "fmt"
)

func main() {
    personSalary := map[string]int{
        "steve": 12000,
        "jamie": 15000,
    }
    personSalary["mike"] = 9000
    newEmp := "joe"
    value, ok := personSalary[newEmp]
    if ok == true {
        fmt.Println("Salary of", newEmp, "is", value)
    } else {
        fmt.Println(newEmp,"not found")
    }
}

上面的程序中,第 15 行,joe 不存在,所以 ok 是 false。程序将输出:

joe not found

遍历 map 中所有的元素需要用 for range 循环。

package main

import (
    "fmt"
)

func main() {
    personSalary := map[string]int{
        "steve": 12000,
        "jamie": 15000,
    }
    personSalary["mike"] = 9000
    fmt.Println("All items of a map")
    for key, value := range personSalary {
        fmt.Printf("personSalary[%s] = %d\n", key, value)
    }

}

上面程序输出:

All items of a map
personSalary[mike] = 9000
personSalary[steve] = 12000
personSalary[jamie] = 15000

有一点很重要,当使用 for range 遍历 map 时,不保证每次执行程序获取的元素顺序相同。

删除 map 中的元素

删除 mapkey 的语法是 [delete(map, key)]。这个函数没有返回值。

package main

import (  
    "fmt"
)

func main() {  
    personSalary := map[string]int{
        "steve": 12000,
        "jamie": 15000,
    }
    personSalary["mike"] = 9000
    fmt.Println("map before deletion", personSalary)
    delete(personSalary, "steve")
    fmt.Println("map after deletion", personSalary)

}

上述程序删除了键 “steve”,输出:

map before deletion map[steve:12000 jamie:15000 mike:9000]
map after deletion map[mike:9000 jamie:15000]

获取 map 的长度

获取 map 的长度使用 [len]函数。

package main

import (
    "fmt"
)

func main() {
    personSalary := map[string]int{
        "steve": 12000,
        "jamie": 15000,
    }
    personSalary["mike"] = 9000
    fmt.Println("length is", len(personSalary))

}

上述程序中的 len(personSalary) 函数获取了 map 的长度。程序输出 length is 3

Map 是引用类型

和 [slices]类似,map 也是引用类型。当 map 被赋值为一个新变量的时候,它们指向同一个内部数据结构。因此,改变其中一个变量,就会影响到另一变量。

package main

import (
    "fmt"
)

func main() {
    personSalary := map[string]int{
        "steve": 12000,
        "jamie": 15000,
    }
    personSalary["mike"] = 9000
    fmt.Println("Original person salary", personSalary)
    newPersonSalary := personSalary
    newPersonSalary["mike"] = 18000
    fmt.Println("Person salary changed", personSalary)

}

上面程序中的第 14 行,personSalary 被赋值给 newPersonSalary。下一行 ,newPersonSalarymike 的薪资变成了 18000personSalaryMike 的薪资也会变成 18000。程序输出:

Original person salary map[steve:12000 jamie:15000 mike:9000]
Person salary changed map[steve:12000 jamie:15000 mike:18000]

当 map 作为函数参数传递时也会发生同样的情况。函数中对 map 的任何修改,对于外部的调用都是可见的。

Map 的相等性

map 之间不能使用 == 操作符判断,== 只能用来检查 map 是否为 nil

package main

func main() {
    map1 := map[string]int{
        "one": 1,
        "two": 2,
    }

    map2 := map1

    if map1 == map2 {
    }
}

上面程序抛出编译错误 invalid operation: map1 == map2 (map can only be compared to nil)
判断两个 map 是否相等的方法是遍历比较两个 map 中的每个元素。我建议你写一段这样的程序实现这个功能

14-字符串

什么是字符串?

Go 语言中的字符串是一个字节切片。把内容放在双引号””之间,我们可以创建一个字符串。让我们来看一个创建并打印字符串的简单示例。

package main

import (
    "fmt"
)

func main() {
    name := "Hello World"
    fmt.Println(name)
}

上面的程序将会输出 Hello World
Go 中的字符串是兼容 Unicode 编码的,并且使用 UTF-8 进行编码。

单独获取字符串的每一个字节

由于字符串是一个字节切片,所以我们可以获取字符串的每一个字节。

package main

import (
    "fmt"
)

func printBytes(s string) {
    for i:= 0; i < len(s); i++ {
        fmt.Printf("%x ", s[i])
    }
}

func main() {
    name := "Hello World"
    printBytes(name)
}

上面程序的第 8 行,len(s) 返回字符串中字节的数量,然后我们用了一个 for 循环以 16 进制的形式打印这些字节。%x 格式限定符用于指定 16 进制编码。上面的程序输出 48 65 6c 6c 6f 20 57 6f 72 6c 64。这些打印出来的字符是 “Hello World” 以 [Unicode UTF-8 编码]的结果。为了更好的理解 go 中的字符串,需要对 Unicode 和 UTF-8 有基础的理解。我推荐阅读一下 https://naveenr.net/unicode-character-set-and-utf-8-utf-16-utf-32-encoding/ 来理解一下什么是 Unicode 和 UTF-8。
让我们稍微修改一下上面的程序,让它打印字符串的每一个字符。

package main

import (
    "fmt"
)

func printBytes(s string) {
    for i:= 0; i < len(s); i++ {
        fmt.Printf("%x ", s[i])
    }
}


func printChars(s string) {
    for i:= 0; i < len(s); i++ {
        fmt.Printf("%c ",s[i])
    }
}

func main() {
    name := "Hello World"
    printBytes(name)
    fmt.Printf("\n")
    printChars(name)
}

printChars 方法(第 16 行中)中,%c 格式限定符用于打印字符串的字符。这个程序输出结果是:

48 65 6c 6c 6f 20 57 6f 72 6c 64  
H e l l o   W o r l d

上面的程序获取字符串的每一个字符,虽然看起来是合法的,但却有一个严重的 bug。让我拆解这个代码来看看我们做错了什么。

package main

import (
    "fmt"
)

func printBytes(s string) {
    for i:= 0; i < len(s); i++ {
        fmt.Printf("%x ", s[i])
    }
}

func printChars(s string) {
    for i:= 0; i < len(s); i++ {
        fmt.Printf("%c ",s[i])
    }
}

func main() {
    name := "Hello World"
    printBytes(name)
    fmt.Printf("\n")
    printChars(name)
    fmt.Printf("\n")
    name = "Señor"
    printBytes(name)
    fmt.Printf("\n")
    printChars(name)
}

上面代码输出的结果是:

48 65 6c 6c 6f 20 57 6f 72 6c 64  
H e l l o   W o r l d  
53 65 c3 b1 6f 72  
S e à ± o r

在上面程序的第 28 行,我们尝试输出 Señor 的字符,但却输出了错误的 S e à ± o r。 为什么程序分割 Hello World 时表现完美,但分割 Señor 就出现了错误呢?这是因为 ñ 的 Unicode 代码点(Code Point)是 U+00F1。它的 [UTF-8 编码]占用了 c3 和 b1 两个字节。它的 UTF-8 编码占用了两个字节 c3 和 b1。而我们打印字符时,却假定每个字符的编码只会占用一个字节,这是错误的。在 UTF-8 编码中,一个代码点可能会占用超过一个字节的空间。那么我们该怎么办呢?rune 能帮我们解决这个难题。

rune

rune 是 Go 语言的内建类型,它也是 int32 的别称。在 Go 语言中,rune 表示一个代码点。代码点无论占用多少个字节,都可以用一个 rune 来表示。让我们修改一下上面的程序,用 rune 来打印字符。

package main

import (
    "fmt"
)

func printBytes(s string) {
    for i:= 0; i < len(s); i++ {
        fmt.Printf("%x ", s[i])
    }
}

func printChars(s string) {
    runes := []rune(s)
    for i:= 0; i < len(runes); i++ {
        fmt.Printf("%c ",runes[i])
    }
}

func main() {
    name := "Hello World"
    printBytes(name)
    fmt.Printf("\n")
    printChars(name)
    fmt.Printf("\n\n")
    name = "Señor"
    printBytes(name)
    fmt.Printf("\n")
    printChars(name)
}

在上面代码的第 14 行,字符串被转化为一个 rune 切片。然后我们循环打印字符。程序的输出结果是

48 65 6c 6c 6f 20 57 6f 72 6c 64  
H e l l o   W o r l d 

53 65 c3 b1 6f 72  
S e ñ o r

上面的输出结果非常完美,就是我们想要的结果:)。

字符串的 for range 循环

上面的程序是一种遍历字符串的好方法,但是 Go 给我们提供了一种更简单的方法来做到这一点:使用 for range 循环。

package main

import (
    "fmt"
)

func printCharsAndBytes(s string) {
    for index, rune := range s {
        fmt.Printf("%c starts at byte %d\n", rune, index)
    }
}

func main() {
    name := "Señor"
    printCharsAndBytes(name)
}

在上面程序中的第8行,使用 for range 循环遍历了字符串。循环返回的是是当前 rune 的字节位置。程序的输出结果为:

S starts at byte 0  
e starts at byte 1  
ñ starts at byte 2
o starts at byte 4  
r starts at byte 5

从上面的输出中可以清晰的看到 ñ 占了两个字节:)。

用字节切片构造字符串

package main

import (  
    "fmt"
)

func main() {  
    byteSlice := []byte{0x43, 0x61, 0x66, 0xC3, 0xA9}
    str := string(byteSlice)
    fmt.Println(str)
}

上面的程序中 byteSlice 包含字符串 Café 用 UTF-8 编码后的 16 进制字节。程序输出结果是 Café
如果我们把 16 进制换成对应的 10 进制值会怎么样呢?上面的程序还能工作吗?让我们来试一试:

package main

import (  
    "fmt"
)

func main() {  
    byteSlice := []byte{67, 97, 102, 195, 169}//decimal equivalent of {'\x43', '\x61', '\x66', '\xC3', '\xA9'}
    str := string(byteSlice)
    fmt.Println(str)
}

上面程序的输出结果也是Café

用 rune 切片构造字符串

package main

import (  
    "fmt"
)

func main() {  
    runeSlice := []rune{0x0053, 0x0065, 0x00f1, 0x006f, 0x0072}
    str := string(runeSlice)
    fmt.Println(str)
}

在上面的程序中 runeSlice 包含字符串 Señor的 16 进制的 Unicode 代码点。这个程序将会输出Señor

字符串的长度

[utf8 package] 包中的 func RuneCountInString(s string) (n int) 方法用来获取字符串的长度。这个方法传入一个字符串参数然后返回字符串中的 rune 的数量。

package main

import (  
    "fmt"
    "unicode/utf8"
)

func length(s string) {  
    fmt.Printf("length of %s is %d\n", s, utf8.RuneCountInString(s))
}
func main() { 
    word1 := "Señor" 
    length(word1)
    word2 := "Pets"
    length(word2)
}

上面程序的输出结果是:

length of Señor is 5  
length of Pets is 4

字符串是不可变的

Go 中的字符串是不可变的。一旦一个字符串被创建,那么它将无法被修改。

package main

import (  
    "fmt"
)

func mutate(s string)string {  
    s[0] = 'a'//any valid unicode character within single quote is a rune 
    return s
}
func main() {  
    h := "hello"
    fmt.Println(mutate(h))
}

在上面程序中的第 8 行,我们试图把这个字符串中的第一个字符修改为 ‘a’。由于字符串是不可变的,因此这个操作是非法的。所以程序抛出了一个错误 main.go:8: cannot assign to s[0]
为了修改字符串,可以把字符串转化为一个 rune 切片。然后这个切片可以进行任何想要的改变,然后再转化为一个字符串。

package main

import (  
    "fmt"
)

func mutate(s []rune) string {  
    s[0] = 'a' 
    return string(s)
}
func main() {  
    h := "hello"
    fmt.Println(mutate([]rune(h)))
}

在上面程序的第 7 行,mutate 函数接收一个 rune 切片参数,它将切片的第一个元素修改为 'a',然后将 rune 切片转化为字符串,并返回该字符串。程序的第 13 行调用了该函数。我们把 h 转化为一个 rune 切片,并传递给了 mutate。这个程序输出 aello

15-指针

什么是指针?

指针是一种存储变量内存地址(Memory Address)的变量。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cbOIbtYb-1647421138459)(/Users/liuqingzheng/Desktop/go%E7%B3%BB%E5%88%97%E6%95%99%E7%A8%8B/image-20190209154549871.png)]
如上图所示,变量 b 的值为 156,而 b 的内存地址为 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 的地址赋值给 *int 类型的 a。我们称 a 指向了 b。当我们打印 a 的值时,会打印出 b 的地址。程序将输出:

Type of a is *int  
address of b is 0x1040a124

由于 b 可能处于内存的任何位置,你应该会得到一个不同的地址。

指针的零值(Zero Value)

指针的零值是 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。程序会输出:

b is <nil>  
b after initialisation is 0x1040a124

指针的解引用

指针的解引用可以获取指针所指向的变量的值。将 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

向函数传递指针参数

package main

import (  
    "fmt"
)

func change(val *int) {  
    *val = 55
}
func main() {  
    a := 58
    fmt.Println("value of a before function call is",a)
    b := &a
    change(b)
    fmt.Println("value of a after function call is", a)
}

在上面程序中的第 14 行,我们向函数 change 传递了指针变量 b,而 b 存储了 a 的地址。程序的第 8 行在 change 函数内使用解引用,修改了 a 的值。该程序会输出:

value of a before function call is 58  
value of a after function call is 55

不要向函数传递数组的指针,而应该使用切片

假如我们想要在函数内修改一个数组,并希望调用函数的地方也能得到修改后的数组,一种解决方案是把一个指向数组的指针传递给这个函数。

package main

import (  
    "fmt"
)

func modify(arr *[3]int) {  
    (*arr)[0] = 90
}

func main() {  
    a := [3]int{89, 90, 91}
    modify(&a)
    fmt.Println(a)
}

在上面程序的第 13 行中,我们将数组的地址传递给了 modify 函数。在第 8 行,我们在 modify 函数里把 arr 解引用,并将 90 赋值给这个数组的第一个元素。程序会输出 [90 90 91]
a[x] 是 (*a)[x] 的简写形式,因此上面代码中的 (*arr)[0] 可以替换为 arr[0]。下面我们用简写形式重写以上代码。

package main

import (  
    "fmt"
)

func modify(arr *[3]int) {  
    arr[0] = 90
}

func main() {  
    a := [3]int{89, 90, 91}
    modify(&a)
    fmt.Println(a)
}

该程序也会输出 [90 90 91]
这种方式向函数传递一个数组指针参数,并在函数内修改数组。尽管它是有效的,但却不是 Go 语言惯用的实现方式。我们最好使用切片来处理。
接下来我们用[切片]来重写之前的代码。

package main

import (  
    "fmt"
)

func modify(sls []int) {  
    sls[0] = 90
}

func main() {  
    a := [3]int{89, 90, 91}
    modify(a[:])
    fmt.Println(a)
}

在上面程序的第 13 行,我们将一个切片传递给了 modify 函数。在 modify 函数中,我们把切片的第一个元素修改为 90。程序也会输出 [90 90 91]所以别再传递数组指针了,而是使用切片吧。上面的代码更加简洁,也更符合 Go 语言的习惯。

Go 不支持指针运算

Go 并不支持其他语言(例如 C)中的指针运算。

package main

func main() {  
    b := [...]int{109, 110, 111}
    p := &b
    p++
}

上面的程序会抛出编译错误:main.go:6: invalid operation: p++ (non-numeric type *[3]int)

16-结构体

什么是结构体?

结构体是用户定义的类型,表示若干个字段(Field)的集合。有时应该把数据整合在一起,而不是让这些数据没有联系。这种情况下可以使用结构体。
例如,一个职员有 firstNamelastNameage 三个属性,而把这些属性组合在一个结构体 employee 中就很合理。

结构体的声明

type Employee struct {
    firstName string
    lastName  string
    age       int
}

在上面的代码片段里,声明了一个结构体类型 Employee,它有 firstNamelastNameage 三个字段。通过把相同类型的字段声明在同一行,结构体可以变得更加紧凑。在上面的结构体中,firstNamelastName 属于相同的 string 类型,于是这个结构体可以重写为:

type Employee struct {
    firstName, lastName string
    age, salary         int
}

上面的结构体 Employee 称为 命名的结构体(Named Structure)。我们创建了名为 Employee 的新类型,而它可以用于创建 Employee 类型的结构体变量。
声明结构体时也可以不用声明一个新类型,这样的结构体类型称为 匿名结构体(Anonymous Structure)

var employee struct {
    firstName, lastName string
    age int
}

上述代码片段创建一个匿名结构体 employee

创建命名的结构体

通过下面代码,我们定义了一个命名的结构体 Employee

package main

import (  
    "fmt"
)

type Employee struct {  
    firstName, lastName string
    age, salary         int
}

func main() {

    //creating structure using field names
    emp1 := Employee{
        firstName: "Sam",
        age:       25,
        salary:    500,
        lastName:  "Anderson",
    }

    //creating structure without using field names
    emp2 := Employee{"Thomas", "Paul", 29, 800}

    fmt.Println("Employee 1", emp1)
    fmt.Println("Employee 2", emp2)
}

在上述程序的第 7 行,我们创建了一个命名的结构体 Employee。而在第 15 行,通过指定每个字段名的值,我们定义了结构体变量 emp1。字段名的顺序不一定要与声明结构体类型时的顺序相同。在这里,我们改变了 lastName 的位置,将其移到了末尾。这样做也不会有任何的问题。
在上面程序的第 23 行,定义 emp2 时我们省略了字段名。在这种情况下,就需要保证字段名的顺序与声明结构体时的顺序相同。
该程序将输出:

Employee 1 {Sam Anderson 25 500}
Employee 2 {Thomas Paul 29 800}

创建匿名结构体

package main

import (
    "fmt"
)

func main() {
    emp3 := struct {
        firstName, lastName string
        age, salary         int
    }{
        firstName: "Andreah",
        lastName:  "Nikola",
        age:       31,
        salary:    5000,
    }

    fmt.Println("Employee 3", emp3)
}

在上述程序的第 3 行,我们定义了一个匿名结构体变量 emp3。上面我们已经提到,之所以称这种结构体是匿名的,是因为它只是创建一个新的结构体变量 em3,而没有定义任何结构体类型。
该程序会输出:

Employee 3 {Andreah Nikola 31 5000}

结构体的零值(Zero Value)

当定义好的结构体并没有被显式地初始化时,该结构体的字段将默认赋为零值。

package main

import (  
    "fmt"
)

type Employee struct {  
    firstName, lastName string
    age, salary         int
}

func main() {  
    var emp4 Employee //zero valued structure
    fmt.Println("Employee 4", emp4)
}

该程序定义了 emp4,却没有初始化任何值。因此 firstNamelastName 赋值为 string 的零值("")。而 agesalary 赋值为 int 的零值(0)。该程序会输出:

Employee 4 { 0 0}

当然还可以为某些字段指定初始值,而忽略其他字段。这样,忽略的字段名会赋值为零值。

package main

import (  
    "fmt"
)

type Employee struct {  
    firstName, lastName string
    age, salary         int
}

func main() {  
    emp5 := Employee{
        firstName: "John",
        lastName:  "Paul",
    }
    fmt.Println("Employee 5", emp5)
}

在上面程序中的第 14 行和第 15 行,我们初始化了 firstNamelastName,而 agesalary 没有进行初始化。因此 agesalary 赋值为零值。该程序会输出:

Employee 5 {John Paul 0 0}

访问结构体的字段

点号操作符 . 用于访问结构体的字段。

package main

import (  
    "fmt"
)

type Employee struct {  
    firstName, lastName string
    age, salary         int
}

func main() {  
    emp6 := Employee{"Sam", "Anderson", 55, 6000}
    fmt.Println("First Name:", emp6.firstName)
    fmt.Println("Last Name:", emp6.lastName)
    fmt.Println("Age:", emp6.age)
    fmt.Printf("Salary: $%d", emp6.salary)
}

上面程序中的 emp6.firstName 访问了结构体 emp6 的字段 firstName。该程序输出:

First Name: Sam  
Last Name: Anderson  
Age: 55  
Salary: $6000

还可以创建零值的 struct,以后再给各个字段赋值。

package main

import (
    "fmt"
)

type Employee struct {  
    firstName, lastName string
    age, salary         int
}

func main() {  
    var emp7 Employee
    emp7.firstName = "Jack"
    emp7.lastName = "Adams"
    fmt.Println("Employee 7:", emp7)
}

在上面程序中,我们定义了 emp7,接着给 firstNamelastName 赋值。该程序会输出:

Employee 7: {Jack Adams 0 0}

结构体的指针

还可以创建指向结构体的指针。

package main

import (  
    "fmt"
)

type Employee struct {  
    firstName, lastName string
    age, salary         int
}

func main() {  
    emp8 := &Employee{"Sam", "Anderson", 55, 6000}
    fmt.Println("First Name:", (*emp8).firstName)
    fmt.Println("Age:", (*emp8).age)
}

在上面程序中,emp8 是一个指向结构体 Employee 的指针。(*emp8).firstName 表示访问结构体 emp8firstName 字段。该程序会输出:

First Name: Sam
Age: 55

*Go 语言允许我们在访问 firstName 字段时,可以使用 emp8.firstName 来代替显式的解引用 (emp8).firstName

package main

import (  
    "fmt"
)

type Employee struct {  
    firstName, lastName string
    age, salary         int
}

func main() {  
    emp8 := &Employee{"Sam", "Anderson", 55, 6000}
    fmt.Println("First Name:", emp8.firstName)
    fmt.Println("Age:", emp8.age)
}

在上面的程序中,我们使用 emp8.firstName 来访问 firstName 字段,该程序会输出:

First Name: Sam
Age: 55

匿名字段

当我们创建结构体时,字段可以只有类型,而没有字段名。这样的字段称为匿名字段(Anonymous Field)。
以下代码创建一个 Person 结构体,它含有两个匿名字段 stringint

type Person struct {  
    string
    int
}

我们接下来使用匿名字段来编写一个程序。

package main

import (  
    "fmt"
)

type Person struct {  
    string
    int
}

func main() {  
    p := Person{"Naveen", 50}
    fmt.Println(p)
}

在上面的程序中,结构体 Person 有两个匿名字段。p := Person{"Naveen", 50} 定义了一个 Person 类型的变量。该程序输出 {Naveen 50}
虽然匿名字段没有名称,但其实匿名字段的名称就默认为它的类型。比如在上面的 Person 结构体里,虽说字段是匿名的,但 Go 默认这些字段名是它们各自的类型。所以 Person 结构体有两个名为 stringint 的字段。

package main

import (  
    "fmt"
)

type Person struct {  
    string
    int
}

func main() {  
    var p1 Person
    p1.string = "naveen"
    p1.int = 50
    fmt.Println(p1)
}

在上面程序的第 14 行和第 15 行,我们访问了 Person 结构体的匿名字段,我们把字段类型作为字段名,分别为 “string” 和 “int”。上面程序的输出如下:

{naveen 50}

嵌套结构体(Nested Structs)

结构体的字段有可能也是一个结构体。这样的结构体称为嵌套结构体。

package main

import (  
    "fmt"
)

type Address struct {  
    city, state string
}
type Person struct {  
    name string
    age int
    address Address
}

func main() {  
    var p Person
    p.name = "Naveen"
    p.age = 50
    p.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

提升字段(Promoted Fields)

如果是结构体中有匿名的结构体类型字段,则该匿名结构体里的字段就称为提升字段。这是因为提升字段就像是属于外部结构体一样,可以用外部结构体直接访问。我知道这种定义很复杂,所以我们直接研究下代码来理解吧。

type Address struct {  
    city, state string
}
type Person struct {  
    name string
    age  int
    Address
}

在上面的代码片段中,Person 结构体有一个匿名字段 Address,而 Address 是一个结构体。现在结构体 Addresscitystate 两个字段,访问这两个字段就像在 Person 里直接声明的一样,因此我们称之为提升字段。

package main

import (
    "fmt"
)

type Address struct {
    city, state string
}
type Person struct {
    name string
    age  int
    Address
}

func main() {  
    var p Person
    p.name = "Naveen"
    p.age = 50
    p.Address = Address{
        city:  "Chicago",
        state: "Illinois",
    }
    fmt.Println("Name:", p.name)
    fmt.Println("Age:", p.age)
    fmt.Println("City:", p.city) //city is promoted field
    fmt.Println("State:", p.state) //state is promoted field
}

在上面代码中的第 26 行和第 27 行,我们使用了语法 p.cityp.state,访问提升字段 citystate 就像它们是在结构体 p 中声明的一样。该程序会输出:

Name: Naveen  
Age: 50  
City: Chicago  
State: Illinois

导出结构体和字段

如果结构体名称以大写字母开头,则它是其他包可以访问的导出类型(Exported Type)。同样,如果结构体里的字段首字母大写,它也能被其他包访问到。
让我们使用自定义包,编写一个程序来更好地去理解它。
在你的 Go 工作区的 src 目录中,创建一个名为 structs 的文件夹。另外在 structs 中再创建一个目录 computer
computer 目录中,在名为 spec.go 的文件中保存下面的程序。

package computer

type Spec struct { //exported struct  
    Maker string //exported field
    model string //unexported field
    Price int //exported field
}

上面的代码片段中,创建了一个 computer 包,里面有一个导出结构体类型 SpecSpec 有两个导出字段 MakerPrice,和一个未导出的字段 model。接下来我们会在 main 包中导入这个包,并使用 Spec 结构体。

package main

import "structs/computer"  
import "fmt"

func main() {  
    var spec computer.Spec
    spec.Maker = "apple"
    spec.Price = 50000
    fmt.Println("Spec:", spec)
}

包结构如下所示:

src  
   structs
        computer
            spec.go
        main.go

在上述程序的第 3 行,我们导入了 computer 包。在第 8 行和第 9 行,我们访问了结构体 Spec 的两个导出字段 MakerPrice。执行命令 go install structsworkspacepath/bin/structs,运行该程序。
如果我们试图访问未导出的字段 model,编译器会报错。将 main.go 的内容替换为下面的代码。

package main

import "structs/computer"  
import "fmt"

func main() {  
    var spec computer.Spec
    spec.Maker = "apple"
    spec.Price = 50000
    spec.model = "Mac Mini"
    fmt.Println("Spec:", spec)
}

在上面程序的第 10 行,我们试图访问未导出的字段 model。如果运行这个程序,编译器会产生错误:spec.model undefined (cannot refer to unexported field or method model)

结构体相等性(Structs Equality)

结构体是值类型。如果它的每一个字段都是可比较的,则该结构体也是可比较的。如果两个结构体变量的对应字段相等,则这两个变量也是相等的

package main

import (  
    "fmt"
)

type name struct {  
    firstName string
    lastName string
}


func main() {  
    name1 := name{"Steve", "Jobs"}
    name2 := name{"Steve", "Jobs"}
    if name1 == name2 {
        fmt.Println("name1 and name2 are equal")
    } else {
        fmt.Println("name1 and name2 are not equal")
    }

    name3 := name{firstName:"Steve", lastName:"Jobs"}
    name4 := name{}
    name4.firstName = "Steve"
    if name3 == name4 {
        fmt.Println("name3 and name4 are equal")
    } else {
        fmt.Println("name3 and name4 are not equal")
    }
}

在上面的代码中,结构体类型 name 包含两个 string 类型。由于字符串是可比较的,因此可以比较两个 name 类型的结构体变量。
上面代码中 name1name2 相等,而 name3name4 不相等。该程序会输出:

name1 and name2 are equal  
name3 and name4 are not equal

如果结构体包含不可比较的字段,则结构体变量也不可比较。

package main

import (  
    "fmt"
)

type image struct {  
    data map[int]int
}

func main() {  
    image1 := image{data: map[int]int{
        0: 155,
    }}
    image2 := image{data: map[int]int{
        0: 155,
    }}
    if image1 == image2 {
        fmt.Println("image1 and image2 are equal")
    }
}

在上面代码中,结构体类型 image 包含一个 map 类型的字段。由于 map 类型是不可比较的,因此 image1image2 也不可比较。如果运行该程序,编译器会报错:main.go:18: invalid operation: image1 == image2 (struct containing map[int]int cannot be compared)

17-方法

什么是方法?

方法其实就是一个函数,在 func 这个关键字和方法名中间加入了一个特殊的接收器类型。接收器可以是结构体类型或者是非结构体类型。接收器是可以在方法的内部访问的。
下面就是创建一个方法的语法。

func (t Type) methodName(parameter list) {
}

上面的代码片段创建了一个接收器类型为 Type 的方法 methodName

方法示例

让我们来编写一个简单的小程序,它会在结构体类型上创建一个方法并调用它。

package main

import (
    "fmt"
)

type Employee struct {
    name     string
    salary   int
    currency string
}

/*
  displaySalary() 方法将 Employee 做为接收器类型
*/
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() // 调用 Employee 类型的 displaySalary() 方法
}

在上面程序的第 16 行,我们在 Employee 结构体类型上创建了一个 displaySalary 方法。displaySalary()方法在方法的内部访问了接收器 e Employee。在第 17 行,我们使用接收器 e,并打印 employee 的 name、currency 和 salary 这 3 个字段。
在第 26 行,我们调用了方法 emp1.displaySalary()
程序输出:Salary of Sam Adolf is $5000

为什么我们已经有函数了还需要方法呢?

上面的程序已经被重写为只使用函数,没有方法。

package main

import (
    "fmt"
)

type Employee struct {
    name     string
    salary   int
    currency string
}

/*
displaySalary()方法被转化为一个函数,把 Employee 当做参数传入。
*/
func displaySalary(e Employee) {
    fmt.Printf("Salary of %s is %s%d", e.name, e.currency, e.salary)
}

func main() {
    emp1 := Employee{
        name:     "Sam Adolf",
        salary:   5000,
        currency: "$",
    }
    displaySalary(emp1)
}

在上面的程序中,displaySalary 方法被转化为一个函数,Employee 结构体被当做参数传递给它。这个程序也产生完全相同的输出:Salary of Sam Adolf is $5000
既然我们可以使用函数写出相同的程序,那么为什么我们需要方法?这有着几个原因,让我们一个个的看看。

  • Go 不是纯粹的面向对象编程语言,而且Go不支持类。因此,基于类型的方法是一种实现和类相似行为的途径。
  • 相同的名字的方法可以定义在不同的类型上,而相同名字的函数是不被允许的。假设我们有一个 SquareCircle 结构体。可以在 SquareCircle 上分别定义一个 Area 方法。见下面的程序。
package main

import (
    "fmt"
    "math"
)

type Rectangle struct {
    length int
    width  int
}

type Circle struct {
    radius float64
}

func (r Rectangle) Area() int {
    return r.length * r.width
}

func (c Circle) Area() float64 {
    return math.Pi * c.radius * c.radius
}

func main() {
    r := Rectangle{
        length: 10,
        width:  5,
    }
    fmt.Printf("Area of rectangle %d\n", r.Area())
    c := Circle{
        radius: 12,
    }
    fmt.Printf("Area of circle %f", c.Area())
}

该程序输出:

Area of rectangle 50
Area of circle 452.389342

上面方法的属性被使用在接口中。我们将在接下来的教程中讨论这个问题。

指针接收器与值接收器

到目前为止,我们只看到了使用值接收器的方法。还可以创建使用指针接收器的方法。值接收器和指针接收器之间的区别在于,在指针接收器的方法内部的改变对于调用者是可见的,然而值接收器的情况不是这样的。让我们用下面的程序来帮助理解这一点。

package main

import (
    "fmt"
)

type Employee struct {
    name string
    age  int
}

/*
使用值接收器的方法。
*/
func (e Employee) changeName(newName string) {
    e.name = newName
}

/*
使用指针接收器的方法。
*/
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.changeName("Michael Andrew") 这个方法的前后打印出相同的名字。由于 changeAge 方法是使用指针 (e *Employee) 接收器的,所以在调用 (&e).changeAge(51) 方法对 age 字段做出的改变对调用者将是可见的。该程序输出如下:

Employee name before change: Mark Andrew
Employee name after change: Mark Andrew

Employee age before change: 50
Employee age after change: 51

在上面程序的第 36 行,我们使用 (&e).changeAge(51) 来调用 changeAge 方法。由于 changeAge 方法有一个指针接收器,所以我们使用 (&e) 来调用这个方法。其实没有这个必要,Go语言让我们可以直接使用 e.changeAge(51)e.changeAge(51) 会自动被Go语言解释为 (&e).changeAge(51)
下面的程序重写了,使用 e.changeAge(51) 来代替 (&e).changeAge(51),它输出相同的结果。

package main

import (
    "fmt"
)

type Employee struct {
    name string
    age  int
}

/*
使用值接收器的方法。
*/
func (e Employee) changeName(newName string) {
    e.name = newName
}

/*
使用指针接收器的方法。
*/
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)
}

那么什么时候使用指针接收器,什么时候使用值接收器?

一般来说,指针接收器可以使用在:对方法内部的接收器所做的改变应该对调用者可见时。
指针接收器也可以被使用在如下场景:当拷贝一个结构体的代价过于昂贵时。考虑下一个结构体有很多的字段。在方法内使用这个结构体做为值接收器需要拷贝整个结构体,这是很昂贵的。在这种情况下使用指针接收器,结构体不会被拷贝,只会传递一个指针到方法内部使用。
在其他的所有情况,值接收器都可以被使用。

匿名字段的方法

属于结构体的匿名字段的方法可以被直接调用,就好像这些方法是属于定义了匿名字段的结构体一样。

package main

import (
    "fmt"
)

type address struct {
    city  string
    state string
}

func (a address) fullAddress() {
    fmt.Printf("Full address: %s, %s", a.city, a.state)
}

type person struct {
    firstName string
    lastName  string
    address
}

func main() {
    p := person{
        firstName: "Elon",
        lastName:  "Musk",
        address: address {
            city:  "Los Angeles",
            state: "California",
        },
    }

    p.fullAddress() //访问 address 结构体的 fullAddress 方法
}

在上面程序的第 32 行,我们通过使用 p.fullAddress() 来访问 address 结构体的 fullAddress() 方法。明确的调用 p.address.fullAddress() 是没有必要的。该程序输出:

Full address: Los Angeles, California

在方法中使用值接收器 与 在函数中使用值参数

这个话题很多Go语言新手都弄不明白。我会尽量讲清楚。
当一个函数有一个值参数,它只能接受一个值参数。
当一个方法有一个值接收器,它可以接受值接收器和指针接收器。
让我们通过一个例子来理解这一点。

package main

import (
    "fmt"
)

type rectangle struct {
    length int
    width  int
}

func area(r rectangle) {
    fmt.Printf("Area Function result: %d\n", (r.length * r.width))
}

func (r rectangle) area() {
    fmt.Printf("Area Method result: %d\n", (r.length * r.width))
}

func main() {
    r := rectangle{
        length: 10,
        width:  5,
    }
    area(r)
    r.area()

    p := &r
    /*
       compilation error, cannot use p (type *rectangle) as type rectangle
       in argument to area
    */
    //area(p)

    p.area()//通过指针调用值接收器
}

第 12 行的函数 func area(r rectangle) 接受一个值参数,方法 func (r rectangle) area() 接受一个值接收器。
在第 25 行,我们通过值参数 area(r) 来调用 area 这个函数,这是合法的。同样,我们使用值接收器来调用 area 方法 r.area(),这也是合法的。
在第 28 行,我们创建了一个指向 r 的指针 p。如果我们试图把这个指针传递到只能接受一个值参数的函数 area,编译器将会报错。所以我把代码的第 33 行注释了。如果你把这行的代码注释去掉,编译器将会抛出错误 compilation error, cannot use p (type *rectangle) as type rectangle in argument to area.。这将会按预期抛出错误。
现在到了棘手的部分了,在第35行的代码 p.area() 使用指针接收器 p 调用了只接受一个值接收器的方法 area。这是完全有效的。原因是当 area 有一个值接收器时,为了方便Go语言把 p.area() 解释为 (*p).area()
该程序将会输出:

Area Function result: 50
Area Method result: 50
Area Method result: 50

在方法中使用指针接收器 与 在函数中使用指针参数

和值参数相类似,函数使用指针参数只接受指针,而使用指针接收器的方法可以使用值接收器和指针接收器。

package main

import (
    "fmt"
)

type rectangle struct {
    length int
    width  int
}

func perimeter(r *rectangle) {
    fmt.Println("perimeter function output:", 2*(r.length+r.width))

}

func (r *rectangle) perimeter() {
    fmt.Println("perimeter method output:", 2*(r.length+r.width))
}

func main() {
    r := rectangle{
        length: 10,
        width:  5,
    }
    p := &r //pointer to r
    perimeter(p)
    p.perimeter()

    /*
        cannot use r (type rectangle) as type *rectangle in argument to perimeter
    */
    //perimeter(r)

    r.perimeter()//使用值来调用指针接收器
}

在上面程序的第 12 行,定义了一个接受指针参数的函数 perimeter。第 17 行定义了一个有一个指针接收器的方法。
在第 27 行,我们调用 perimeter 函数时传入了一个指针参数。在第 28 行,我们通过指针接收器调用了 perimeter 方法。所有一切看起来都这么完美。
在被注释掉的第 33 行,我们尝试通过传入值参数 r 调用函数 perimeter。这是不被允许的,因为函数的指针参数不接受值参数。如果你把这行的代码注释去掉并把程序运行起来,编译器将会抛出错误 main.go:33: cannot use r (type rectangle) as type *rectangle in argument to perimeter.
在第 35 行,我们通过值接收器 r 来调用有指针接收器的方法 perimeter。这是被允许的,为了方便Go语言把代码 r.perimeter() 解释为 (&r).perimeter()。该程序输出:

perimeter function output: 30
perimeter method output: 30
perimeter method output: 30

在非结构体上的方法

到目前为止,我们只在结构体类型上定义方法。也可以在非结构体类型上定义方法,但是有一个问题。为了在一个类型上定义一个方法,方法的接收器类型定义和方法的定义应该在同一个包中。到目前为止,我们定义的所有结构体和结构体上的方法都是在同一个 main 包中,因此它们是可以运行的。

package main

func (a int) add(b int) {
}

func main() {

}

在上面程序的第 3 行,我们尝试把一个 add 方法添加到内置的类型 int。这是不允许的,因为 add 方法的定义和 int 类型的定义不在同一个包中。该程序会抛出编译错误 cannot define new methods on non-local type int
让该程序工作的方法是为内置类型 int 创建一个类型别名,然后创建一个以该类型别名为接收器的方法。

package main

import "fmt"

type myInt int

func (a myInt) add(b myInt) myInt {
    return a + b
}

func main() {
    num1 := myInt(5)
    num2 := myInt(10)
    sum := num1.add(num2)
    fmt.Println("Sum is", sum)
}

在上面程序的第5行,我们为 int 创建了一个类型别名 myInt。在第7行,我们定义了一个以 myInt 为接收器的的方法 add
该程序将会打印出 Sum is 15
我已经创建了一个程序,包含了我们迄今为止所讨论的所有概念

18-接口(一)

什么是接口?

在面向对象的领域里,接口一般这样定义:接口定义一个对象的行为。接口只指定了对象应该做什么,至于如何实现这个行为(即实现细节),则由对象本身去确定。
在 Go 语言中,接口就是方法签名(Method Signature)的集合。当一个类型定义了接口中的所有方法,我们称它实现了该接口。这与面向对象编程(OOP)的说法很类似。接口指定了一个类型应该具有的方法,并由该类型决定如何实现这些方法
例如,WashingMachine 是一个含有 Cleaning()Drying() 两个方法的接口。任何定义了 Cleaning()Drying() 的类型,都称它实现了 WashingMachine 接口。

接口的声明与实现

让我们编写代码,创建一个接口并且实现它。

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 类型。
在第 15 行,我们给接受者类型(Receiver Type) MyString 添加了方法 FindVowels() []rune。现在,我们称 MyString 实现了 VowelsFinder 接口。这就和其他语言(如 Java)很不同,其他一些语言要求一个类使用 implement 关键字,来显式地声明该类实现了接口。而在 Go 中,并不需要这样。如果一个类型包含了接口中声明的所有方法,那么它就隐式地实现了 Go 接口
在第 28 行,v 的类型为 VowelsFindername 的类型为 MyString,我们把 name 赋值给了 v。由于 MyString 实现了 VowelFinder,因此这是合法的。在下一行,v.FindVowels() 调用了 MyString 类型的 FindVowels 方法,打印字符串 Sam Anderson 里所有的元音。该程序输出 Vowels are [a e o]
祝贺!你已经创建并实现了你的第一个接口。

接口的实际用途

前面的例子教我们创建并实现了接口,但还没有告诉我们接口的实际用途。在上面的程序里,如果我们使用 name.FindVowels(),而不是 v.FindVowels(),程序依然能够照常运行,但接口并没有体现出实际价值。
因此,我们现在讨论一下接口的实际应用场景。
我们编写一个简单程序,根据公司员工的个人薪资,计算公司的总支出。为了简单起见,我们假定支出的单位都是美元。

package main

import (  
    "fmt"
)

type SalaryCalculator interface {  
    CalculateSalary() int
}

type Permanent struct {  
    empId    int
    basicpay int
    pf       int
}

type Contract struct {  
    empId  int
    basicpay int
}

//salary of permanent employee is sum of basic pay and pf
func (p Permanent) CalculateSalary() int {  
    return p.basicpay + p.pf
}

//salary of contract employee is the basic pay alone
func (c Contract) CalculateSalary() int {  
    return c.basicpay
}

/*
total expense is calculated by iterating though the SalaryCalculator slice and summing  
the salaries of the individual employees  
*/
func totalExpense(s []SalaryCalculator) {  
    expense := 0
    for _, v := range s {
        expense = expense + v.CalculateSalary()
    }
    fmt.Printf("Total Expense Per Month $%d", expense)
}

func main() {  
    pemp1 := Permanent{1, 5000, 20}
    pemp2 := Permanent{2, 6000, 30}
    cemp1 := Contract{3, 3000}
    employees := []SalaryCalculator{pemp1, pemp2, cemp1}
    totalExpense(employees)

}

上面程序的第 7 行声明了一个 SalaryCalculator 接口类型,它只有一个方法 CalculateSalary() int
在公司里,我们有两类员工,即第 11 行和第 17 行定义的结构体:PermanentContract。长期员工(Permanent)的薪资是 basicpaypf 相加之和,而合同员工(Contract)只有基本工资 basicpay。在第 23 行和第 28 行中,方法 CalculateSalary 分别实现了以上关系。由于 PermanentContract 都声明了该方法,因此它们都实现了 SalaryCalculator 接口。
第 36 行声明的 totalExpense 方法体现出了接口的妙用。该方法接收一个 SalaryCalculator 接口的切片([]SalaryCalculator)作为参数。在第 49 行,我们向 totalExpense 方法传递了一个包含 PermanentContact 类型的切片。在第 39 行中,通过调用不同类型对应的 CalculateSalary 方法,totalExpense 可以计算得到支出。
这样做最大的优点是:totalExpense 可以扩展新的员工类型,而不需要修改任何代码。假如公司增加了一种新的员工类型 Freelancer,它有着不同的薪资结构。Freelancer只需传递到 totalExpense 的切片参数中,无需 totalExpense 方法本身进行修改。只要 Freelancer 也实现了 SalaryCalculator 接口,totalExpense 就能够实现其功能。
该程序输出 Total Expense Per Month $14050

接口的内部表示

我们可以把接口看作内部的一个元组 (type, value)type 是接口底层的具体类型(Concrete Type),而 value 是具体类型的值。
我们编写一个程序来更好地理解它。

package main

import (  
    "fmt"
)

type Test interface {  
    Tester()
}

type MyFloat float64

func (m MyFloat) Tester() {  
    fmt.Println(m)
}

func describe(t Test) {  
    fmt.Printf("Interface type %T value %v\n", t, t)
}

func main() {  
    var t Test
    f := MyFloat(89.7)
    t = f
    describe(t)
    t.Tester()
}

Test 接口只有一个方法 Tester(),而 MyFloat 类型实现了该接口。在第 24 行,我们把变量 fMyFloat 类型)赋值给了 tTest 类型)。现在 t 的具体类型为 MyFloat,而 t 的值为 89.7。第 17 行的 describe 函数打印出了接口的具体类型和值。该程序输出:

Interface type main.MyFloat value 89.7  
89.7

空接口

没有包含方法的接口称为空接口。空接口表示为 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{}) 函数接收空接口作为参数,因此,可以给这个函数传递任何类型。
在第 13 行、第 15 行和第 21 行,我们分别给 describe 函数传递了 stringintstruct。该程序打印:

Type = string, value = Hello World  
Type = int, value = 55  
Type = struct { name string }, value = {Naveen R}

类型断言

类型断言用于提取接口的底层值(Underlying Value)。
在语法 i.(T) 中,接口 i 的具体类型是 T,该语法用于获得接口的底层值。
一段代码胜过千言。下面编写个关于类型断言的程序。

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)
}

在上面程序中,我们把具体类型为 strings 传递给了 assert 函数,试图从它提取出 int 值。该程序会报错:panic: interface conversion: interface {} is string, not int.
要解决该问题,我们可以使用以下语法:

v, ok := i.(T)

如果 i 的具体类型是 T,那么 v 赋值为 i 的底层值,而 ok 赋值为 true
如果 i 的具体类型不是 T,那么 ok 赋值为 falsev 赋值为 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)
}

当给 assert 函数传递 Steven Paul 时,由于 i 的具体类型不是 intok 赋值为 false,而 v 赋值为 0(int 的零值)。该程序打印:

56 true  
0 false

类型选择(Type Switch)

类型选择用于将接口的具体类型与很多 case 语句所指定的类型进行比较。它与一般的 switch 语句类似。唯一的区别在于类型选择指定的是类型,而一般的 switch 指定的是值。
类型选择的语法类似于类型断言。类型断言的语法是 i.(T),而对于类型选择,类型 T 由关键字 type 代替。下面看看程序是如何工作的。

package main

import (  
    "fmt"
)

func findType(i interface{}) {  
    switch i.(type) {
    case string:
        fmt.Printf("I am a string and my value is %s\n", i.(string))
    case int:
        fmt.Printf("I am an int and my value is %d\n", i.(int))
    default:
        fmt.Printf("Unknown type\n")
    }
}
func main() {  
    findType("Naveen")
    findType(77)
    findType(89.98)
}

在上述程序的第 8 行,switch i.(type) 表示一个类型选择。每个 case 语句都把 i 的具体类型和一个指定类型进行了比较。如果 case 匹配成功,会打印出相应的语句。该程序输出:

I am a string and my value is Naveen  
I am an int and my value is 77  
Unknown type

第 20 行中的 89.98 的类型是 float64,没有在 case 上匹配成功,因此最后一行打印了 Unknown type
还可以将一个类型和接口相比较。如果一个类型实现了接口,那么该类型与其实现的接口就可以互相比较
为了阐明这一点,下面写一个程序。

package main

import "fmt"

type Describer interface {  
    Describe()
}
type Person struct {  
    name string
    age  int
}

func (p Person) Describe() {  
    fmt.Printf("%s is %d years old", p.name, p.age)
}

func findType(i interface{}) {  
    switch v := i.(type) {
    case Describer:
        v.Describe()
    default:
        fmt.Printf("unknown type\n")
    }
}

func main() {  
    findType("Naveen")
    p := Person{
        name: "Naveen R",
        age:  25,
    }
    findType(p)
}

在上面程序中,结构体 Person 实现了 Describer 接口。在第 19 行的 case 语句中,v 与接口类型 Describer 进行了比较。p 实现了 Describer,因此满足了该 case 语句,于是当程序运行到第 32 行的 findType(p) 时,程序调用了 Describe() 方法。
该程序输出:

unknown type  
Naveen R is 25 years old

接口(一)的内容到此结束。在接口(二)中我们还会继续讨论接口

19-接口(二)

实现接口:指针接受者与值接受者

在接口(一)上的所有示例中,我们都是使用值接受者(Value Receiver)来实现接口的。我们同样可以使用指针接受者(Pointer Receiver)来实现接口。只不过在用指针接受者实现接口时,还有一些细节需要注意。我们通过下面的代码来理解吧。

package main

import "fmt"

type Describer interface {  
    Describe()
}
type Person struct {  
    name string
    age  int
}

func (p Person) Describe() { // 使用值接受者实现  
    fmt.Printf("%s is %d years old\n", p.name, p.age)
}

type Address struct {
    state   string
    country string
}

func (a *Address) Describe() { // 使用指针接受者实现
    fmt.Printf("State %s Country %s", a.state, a.country)
}

func main() {  
    var d1 Describer
    p1 := Person{"Sam", 25}
    d1 = p1
    d1.Describe()
    p2 := Person{"James", 32}
    d1 = &p2
    d1.Describe()

    var d2 Describer
    a := Address{"Washington", "USA"}

    /* 如果下面一行取消注释会导致编译错误:
       cannot use a (type Address) as type Describer
       in assignment: Address does not implement
       Describer (Describe method has pointer
       receiver)
    */
    //d2 = a

    d2 = &a // 这是合法的
    // 因为在第 22 行,Address 类型的指针实现了 Describer 接口
    d2.Describe()

}

在上面程序中的第 13 行,结构体 Person 使用值接受者,实现了 Describer 接口。
我们在讨论方法的时候就已经提到过,使用值接受者声明的方法,既可以用值来调用,也能用指针调用。不管是一个值,还是一个可以解引用的指针,调用这样的方法都是合法的
p1 的类型是 Person,在第 29 行,p1 赋值给了 d1。由于 Person 实现了接口变量 d1,因此在第 30 行,会打印 Sam is 25 years old
接下来在第 32 行,d1 又赋值为 &p2,在第 33 行同样打印输出了 James is 32 years old。棒棒哒。😃
在 22 行,结构体 Address 使用指针接受者实现了 Describer 接口。
在上面程序里,如果去掉第 45 行的注释,我们会得到编译错误:main.go:42: cannot use a (type Address) as type Describer in assignment: Address does not implement Describer (Describe method has pointer receiver)。这是因为在第 22 行,我们使用 Address 类型的指针接受者实现了接口 Describer,而接下来我们试图用 a 来赋值 d2。然而 a 属于值类型,它并没有实现 Describer 接口。你应该会很惊讶,因为我们曾经学习过,使用指针接受者的方法,无论指针还是值都可以调用它。那么为什么第 45 行的代码就不管用呢?
其原因是:对于使用指针接受者的方法,用一个指针或者一个可取得地址的值来调用都是合法的。但接口中存储的具体值(Concrete Value)并不能取到地址,因此在第 45 行,对于编译器无法自动获取 a 的地址,于是程序报错
第 47 行就可以成功运行,因为我们将 a 的地址 &a 赋值给了 d2
程序的其他部分不言而喻。该程序会打印:

Sam is 25 years old  
James is 32 years old  
State Washington Country USA

实现多个接口

类型可以实现多个接口。我们看看下面程序是如何做到的。

package main

import (  
    "fmt"
)

type SalaryCalculator interface {  
    DisplaySalary()
}

type LeaveCalculator interface {  
    CalculateLeavesLeft() int
}

type Employee struct {  
    firstName string
    lastName string
    basicPay int
    pf int
    totalLeaves int
    leavesTaken int
}

func (e Employee) DisplaySalary() {  
    fmt.Printf("%s %s has salary $%d", e.firstName, e.lastName, (e.basicPay + e.pf))
}

func (e Employee) CalculateLeavesLeft() int {  
    return e.totalLeaves - e.leavesTaken
}

func main() {  
    e := Employee {
        firstName: "Naveen",
        lastName: "Ramanathan",
        basicPay: 5000,
        pf: 200,
        totalLeaves: 30,
        leavesTaken: 5,
    }
    var s SalaryCalculator = e
    s.DisplaySalary()
    var l LeaveCalculator = e
    fmt.Println("\nLeaves left =", l.CalculateLeavesLeft())
}

上述程序在第 7 行和第 11 行分别声明了两个接口:SalaryCalculatorLeaveCalculator
第 15 行定义了结构体 Employee,它在第 24 行实现了 SalaryCalculator 接口的 DisplaySalary 方法,接着在第 28 行又实现了 LeaveCalculator 接口里的 CalculateLeavesLeft 方法。于是 Employee 就实现了 SalaryCalculatorLeaveCalculator 两个接口。
第 41 行,我们把 e 赋值给了 SalaryCalculator 类型的接口变量 ,而在 43 行,我们同样把 e 赋值给 LeaveCalculator 类型的接口变量 。由于 e 的类型 Employee 实现了 SalaryCalculatorLeaveCalculator 两个接口,因此这是合法的。
该程序会输出:

Naveen Ramanathan has salary $5200  
Leaves left = 25

接口的嵌套

尽管 Go 语言没有提供继承机制,但可以通过嵌套其他的接口,创建一个新接口。
我们来看看这如何实现。

package main

import (  
    "fmt"
)

type SalaryCalculator interface {  
    DisplaySalary()
}

type LeaveCalculator interface {  
    CalculateLeavesLeft() int
}

type EmployeeOperations interface {  
    SalaryCalculator
    LeaveCalculator
}

type Employee struct {  
    firstName string
    lastName string
    basicPay int
    pf int
    totalLeaves int
    leavesTaken int
}

func (e Employee) DisplaySalary() {  
    fmt.Printf("%s %s has salary $%d", e.firstName, e.lastName, (e.basicPay + e.pf))
}

func (e Employee) CalculateLeavesLeft() int {  
    return e.totalLeaves - e.leavesTaken
}

func main() {  
    e := Employee {
        firstName: "Naveen",
        lastName: "Ramanathan",
        basicPay: 5000,
        pf: 200,
        totalLeaves: 30,
        leavesTaken: 5,
    }
    var empOp EmployeeOperations = e
    empOp.DisplaySalary()
    fmt.Println("\nLeaves left =", empOp.CalculateLeavesLeft())
}

在上述程序的第 15 行,我们创建了一个新的接口 EmployeeOperations,它嵌套了两个接口:SalaryCalculatorLeaveCalculator
如果一个类型定义了 SalaryCalculatorLeaveCalculator 接口里包含的方法,我们就称该类型实现了 EmployeeOperations 接口。
在第 29 行和第 33 行,由于 Employee 结构体定义了 DisplaySalaryCalculateLeavesLeft 方法,因此它实现了接口 EmployeeOperations
在 46 行,empOp 的类型是 EmployeeOperationse 的类型是 Employee,我们把 empOp 赋值为 e。接下来的两行,empOp 调用了 DisplaySalary()CalculateLeavesLeft() 方法。
该程序输出:

Naveen Ramanathan has salary $5200
Leaves left = 25

接口的零值

接口的零值是 nil。对于值为 nil 的接口,其底层值(Underlying Value)和具体类型(Concrete Type)都为 nil

package main

import "fmt"

type Describer interface {  
    Describe()
}

func main() {  
    var d1 Describer
    if d1 == nil {
        fmt.Printf("d1 is nil and has type %T value %v\n", d1, d1)
    }
}

上面程序里的 d1 等于 nil,程序会输出:

d1 is nil and has type <nil> value <nil>

对于值为 nil 的接口,由于没有底层值和具体类型,当我们试图调用它的方法时,程序会产生 panic 异常。

package main

type Describer interface {
    Describe()
}

func main() {  
    var d1 Describer
    d1.Describe()
}

在上述程序中,d1 等于 nil,程序产生运行时错误 panicpanic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0xffffffff addr=0x0 pc=0xc8527]

19-go语言自定义集合类型

一 集合介绍

集合是一个重要的数据结构,其中:集合中的元素不可重复;集合中的元素无序排列
由于go语言中没有集合元素,现在我们来通过map实现一个集合元素,使其具备以下功能:
增加元素:set
删除元素:delete
判断元素是否存在:isExist
查看集合长度:len

二 代码实现

package main

import "fmt"
//定义MySet类型
type MySet map[interface{}]bool
//判断元素是否存在
func (m MySet) isExist(a interface{}) bool {
	return m[a]
}
//返回set长度
func (m MySet) len() int {
	return len(m)
}
//设置值
func (m MySet) set(a interface{}) {
	m[a] = true
}
//删除值
func (m MySet) delete(a interface{}) {
	delete(m, a)
}
//测试代码
func main() {
	//创建一个set
	var a MySet = make(MySet)
	//打印set的长度
	fmt.Println(a.len())
	//放入一个值
	a.set(1)
	//放入一个相同值
	a.set(1)
	//打印长度,还是1
	fmt.Println(a.len())
	//判断1是否存在
	fmt.Println(a.isExist(1))
	//删除1
	a.delete(1)
	//判断1是否存在
	fmt.Println(a.isExist(1))
}

20-并发入门

Go 是并发式语言,而不是并行式语言。在讨论 Go 如何处理并发之前,我们必须理解何为并发,以及并发与并行的区别。

并发是什么?

并发是指立即处理多个任务的能力。一个例子就能很好地说明这一点。
我们可以想象一个人正在跑步。假如在他晨跑时,鞋带突然松了。于是他停下来,系一下鞋带,接下来继续跑。这个例子就是典型的并发。这个人能够一下搞定跑步和系鞋带两件事,即立即处理多个任务。

并行是什么?并行和并发有何区别?

并行是指同时处理多个任务。这听起来和并发差不多,但其实完全不同。
我们同样用这个跑步的例子来帮助理解。假如这个人在慢跑时,还在用他的 iPod 听着音乐。在这里,他是在跑步的同时听音乐,也就是同时处理多个任务。这称之为并行。

从技术上看并发和并行

通过现实中的例子,我们已经明白了什么是并发,以及并发与并行的区别。作为一名极客,我们接下来从技术的角度来考察并发和并行。😃
假如我们正在编写一个 web 浏览器。这个 web 浏览器有各种组件。其中两个分别是 web 页面的渲染区和从网上下载文件的下载器。假设我们已经构建好了浏览器代码,各个组件也都可以相互独立地运行(通过像 Java 里的线程,或者通过即将介绍的 Go 语言中的 Go 协程来实现)。当浏览器在单核处理器中运行时,处理器会在浏览器的两个组件间进行上下文切换。它可能在一段时间内下载文件,转而又对用户请求的 web 页面进行渲染。这就是并发。并发的进程从不同的时间点开始,分别交替运行。在这里,就是在不同的时间点开始进行下载和渲染,并相互交替运行的。
如果该浏览器在一个多核处理器上运行,此时下载文件的组件和渲染 HTML 的组件可能会在不同的核上同时运行。这称之为并行。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UehRonx9-1647421225464)(https://raw.githubusercontent.com/studygolang/gctt-images/master/golang-series/concurrency-parallelism-copy.png)]
并行不一定会加快运行速度,因为并行运行的组件之间可能需要相互通信。在我们浏览器的例子里,当文件下载完成后,应当对用户进行提醒,比如弹出一个窗口。于是,在负责下载的组件和负责渲染用户界面的组件之间,就产生了通信。在并发系统上,这种通信开销很小。但在多核的并行系统上,组件间的通信开销就很高了。所以,并行不一定会加快运行速度!

Go 对并发的支持

Go 编程语言原生支持并发。Go 使用 Go 协程(Goroutine) 和信道(Channel)来处理并发。在接下来的教程里,我们还会详细介绍它们。

posted @ 2024-04-30 13:47  码诅  阅读(35)  评论(0编辑  收藏  举报  来源