Go的基础部分三(更多的数据类型:struct、array、slice、map、函数作为值、闭包)
文章目录
一、指针
Go拥有指针。一个指针持有一个值的内存地址。
类似*T
是一个指向T值的指针。它的零值是nil
。
var p *int
&
运算符生成指向其操作数的指针。
i := 42
p = &i
*
运算符表示指针的基础值。
fmt.Println(*p) // 通过指针p读取i
*p = 21 // 通过指针p设置i
这被称为“取消引用”或“间接”。
不像C,Go没有指针算术。
package main
import "fmt"
func main() {
i, j := 42, 2701
p := &i // 指向i
fmt.Println(*p) // 通过指针读取i
*p = 21 // 通过指针设置i
fmt.Println(i) // 查看i的新值
p = &j // 指向j
*p = *p / 37 // 通过指针除j
fmt.Println(j) // 查看j的新值
}
二、struct(结构)
一个struct
是一个字段的集合。
package main
import "fmt"
type Vertex struct {
X int
Y int
}
func main() {
fmt.Println(Vertex{1, 2})
}
三、struct字段
struct的字段通过点读取。
package main
import "fmt"
type Vertex struct {
X int
Y int
}
func main() {
v := Vertex{1, 2}
v.X = 4
fmt.Println(v.X)
}
四、指向Struct
能够通过一个struct的指针读取struct的字段。
当我们有一个struct指针p的时候,为了读取一个struct的字段X,我们能够写(*p).X
。可是,这个符号是繁琐的,因此该语言允许我们替换仅仅写p.X
,不用带明确的引用。
package main
import "fmt"
type Vertex struct {
X int
Y int
}
func main() {
v := Vertex{1, 2}
p := &v
p.X = 1e9
fmt.Println(v)
}
五、Struct的字面量
一个struct的字面量表示一个新的已经分配的struct值,通过列出字段的值。
你能仅仅列出一个子字段,通过使用Name:
语法。(并且字段名称的顺序是无关紧要的)
特殊的前缀&
,返回一个指针指向这个struct的值。
package main
import "fmt"
type Vertex struct {
X int
Y int
}
var (
v1 = Vertex{1, 2} // 有一个类型Vertex
v2 = Vertex{X: 1} // Y 是0 是隐含的
v3 = Vertex{} // X 是0, Y是0
p = &Vertex{1, 2} // 有一个类型 *Vertex
)
func main() {
fmt.Println(v1, p, v2, v3)
}
六、数组
类型[n]T
是一个有n个值的类型为T
的数组。
表达式:
var a [10]int
声明了一个变量a,作为一个数组,带有10个整数。
数组的长度是类型的一部分,因此数组不能改变大小。这似乎是限制,但是不用担心。Go提供了一个方便的方法作用与数组。
package main
import "fmt"
func main() {
var a [2]string
a[0] = "hello"
a[1] = "world"
fmt.Println(a[0], a[1])
fmt.Println(a)
primes := [6]int{2, 3, 5, 7, 11, 13}
fmt.Println(primes)
}
七、Slices(切片)
一个数组有一个固定大小。另一方面,切片是数组元素的动态大小、灵活的视图。实践中,切片比数组更常见。
类型[]T
是一个带有T类型元素的切片。
通过指定两个索引形成切片,一个低的和高的边界,通过冒号分割。
a[low:high]
这将选择一个半开范围——包含第一个元素,不包含最后一个元素。
下面的表达式创建一个切片,它包含元素1到3:
a[1:4]
package main
import "fmt"
func main() {
primes := [6]int{2, 3, 5, 7, 11, 13}
var s []int = primes[1:4]
fmt.Println(s)
}
八、切片就像数组的引用
一个切片不能存储任何的数据,它仅仅描述了内部数组的一部分。
改变切片的元素,修改相应内部数组的元素。
其它共享相同底层数组的切片将看到改变。
package main
import "fmt"
func main() {
names := [4]string{
"aa",
"bb",
"cc",
"dd",
}
fmt.Println(names)
a := names[0:2]
b := names[1:3]
fmt.Println(a, b)
b[1] = "xxx"
fmt.Println(a, b)
fmt.Println(names)
}
九、切片的字面量
切片的字面量就像数组的字面量,不带长度。
这是数组的字面量:
[3]bool{true, false, false}
这个创建和上面相同的数组,然后构建一个切片并引用它:
[]bool{true, false, false}
package main
import "fmt"
func main() {
a := [3]int{2, 3, 5}
q := []int{2, 3, 5, 7, 11, 13}
fmt.Printf("%T, %v\n", q, q)
fmt.Printf("%T, %v\n", a, a)
r := []bool{true, false, true, true, false, false}
fmt.Println(r)
s := []struct {
i int
b bool
}{
{2, true},
{3, false},
{5, true},
{7, true},
{11, false},
{13, false},
}
fmt.Println(s)
}
十、切片的默认值
当切片的时候,你能忽略高或低边界,去使用默认值代替。
对于低边界默认是0,切片的长度是高的边界。
对于数组:
var a [10]int
下面的切片表达式是等价的:
a[0:10]
a[:10]
a[0:]
a[:]
package main
import "fmt"
func main() {
s := []int{2, 3, 5, 7, 11, 13}
s = s[1:4]
fmt.Println(s)
s = s[:2]
fmt.Println(s)
s = s[1:]
fmt.Println(s)
}
十一、切片的长度和容量
切片既有长度,也有容量。
切片的长度是它包含元素的数量。
切片的容量是内部数组元素的数量,从切片的第一个元素统计。
一个切片的长度和容量能够获取通过使用表达式len(s)
和cap(s)
。
你能扩展切片的长度,通过重新进行切片,只要它有充足的容量。
试着去改变下面程序中其中一个切片的操作,去扩展超过它的容量,并看看会发生什么。
package main
import "fmt"
func main() {
s := []int{2, 3, 5, 7, 11, 13}
printSlice(s)
// 切片这个切片给一个0长度
s = s[:0]
printSlice(s)
// 扩展它的长度
s = s[:6]
printSlice(s)
// 删除它前两个值
s = s[2:6]
printSlice(s)
// s = s[:6]
// printSlice(s)
}
func printSlice(s []int) {
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}
十二、零切片
切片的零值是nil
。
一个nil
切片有一个长度和容量为0,并且没有底层数组。
package main
import "fmt"
func main() {
var s []int
fmt.Println(s, len(s), cap(s))
if s == nil {
fmt.Println("nil!")
}
}
十三、使用make创建切片
切片能够被创建通过内建的make
函数。这就是用来创建动态大小数组的方法。
这个make
函数分配一个零数组,并且返回一个指向这个数组的切片:
a := make([]int, 5) // len(a) = 5
为了指定一个容量,传递第三个参数给make
:
b := make([]int, 0, 5) // len(b) = 0, cap(b) = 5
b = b[:cap(b)] // len(b) = 5, cap(b) = 5
b = b[1:] // len(b) = 4, cap(b) = 4
package main
import "fmt"
func main() {
a := make([]int, 5)
printSlice("a", a)
b := make([]int, 0, 5)
printSlice("b", b)
c := b[0:2]
printSlice("c", c)
d := c[2:5]
printSlice("d", d)
}
func printSlice(s string, x []int) {
fmt.Printf("%s len=%d, cap=%d %v\n", s, len(x), cap(x), x)
}
十四、切片的切片
切片能够包含任何类型,包含其他的切片。
package main
import (
"fmt"
"strings"
)
func main() {
// 创建一个 井字 板
board := [][]string{
[]string{"_", "_", "_"},
[]string{"_", "_", "_"},
[]string{"_", "_", "_"},
}
// 玩家轮流进行
board[0][0] = "X"
board[2][2] = "O"
board[1][2] = "X"
board[1][0] = "O"
board[0][2] = "X"
for i := 0; i < len(board); i++ {
fmt.Printf("%s\n", strings.Join(board[i], " "))
}
}
十五、向切片中追加
它是非常常见的向切片中追加元素,因此Go提供了一个内置的添加函数。在内建包的文档里描述了append
。
这个内置的append函数,添加元素到切片的最后。如果它有一个充足的容量,则重新划分以容纳新的元素。如果没有充足的容量,一个新的内部数组将被分配。添加返回更新的切片。因此有必要将 append 的结果存储在保存切片本身的变量中。
func append(s []T, vs ...T) []T
append
的第一个参数是类型为T的切片,剩余的是要添加到这个切片的T类型的值。
append
的结果是是一个切片,包含了初始切片的元素和所提供的值的所有的元素。
如果s
后面的数组太小,为了存放所有给定的值,一个更大的数组将被分配。返回的切片将指向新分配的数组。
package main
import "fmt"
func main() {
var s []int
printSlice(s)
// append 使用于 nil 切片
s = append(s, 0)
printSlice(s)
// 切片根据需要增长
s = append(s, 1)
printSlice(s)
// 一次能够添加多个元素
s = append(s, 2, 3, 4, 5)
printSlice(s)
}
func printSlice(s []int) {
fmt.Printf("len=%d, cap=%d, %v\n", len(s), len(s), s)
}
更多关于切片,看Go的切片:用法和内部结构。
十六、范围Range
range
在一个切片或map之上产生一个for
循环迭代。
当在一个切片上获取范围的时候,对于每次迭代将返回两个值。第一个值是索引,第二个值是对基于索引元素的拷贝。
package main
import "fmt"
var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}
func main() {
for i, v := range pow {
fmt.Printf("2**%d ==%d\n", i, v)
}
}
可以声明_
来跳过索引或值:
for i, _ := range pow
for _, v := range pow
如果你仅仅只要索引,你可以忽略第二个值:
for i := range pow
package main
import "fmt"
func main() {
pow := make([]int, 10)
for i := range pow {
pow[i] = 1 << uint(i) // == 2**i
}
for _, v := range pow {
fmt.Printf("%d\n", v)
}
}
十七、练习:切片
实现一个Pic
。它应该返回一个长度为dy
的切片,它的每一个元素是一个dx
无符号的整数。当你运行程序的时候,它将展示你的图片,将整数解释为灰度值。
图片的选择取决于你,有趣的函数包含(x+y)/2
、x*y
、和x^y
你需要使用一个循环分配每一个[]uint8
到[][]uint8
中。
使用uint8(intValue)
在两种类型进行转化。
package main
import "golang.org/x/tour/pic"
func Pic(dx, dy int) [][]uint8 {
res := make([][]uint8, dy)
for i := range res {
a := make([]uint8, dx)
for j := range a {
a[j] = uint8((i + j) / 2)
}
res[i] = a
}
return res
}
func main() {
pic.Show(Pic)
}
运行程序
$ go mod init go-function/examples
$ go mod tidy
$ go run exercise-slices.go
十八、Maps
一个map映射键到值。
map的零值是nil。一个nil
的map没有key,也不能添加key。
make
函数返回一个给定类型的map,被初始化并准备使用。
package main
import "fmt"
type Vertex struct {
Lat, Long float64
}
var m map[string]Vertex
func main() {
if m == nil {
fmt.Println(m)
fmt.Println("m is nil")
}
m = make(map[string]Vertex)
m["aa"] = Vertex{23.1, -12.1}
fmt.Println(m["aa"])
}
十九、Map的字面量
Map的字面量,像结构字面量,但是key是需要的。
package main
import "fmt"
type Vertex struct {
Lat, Long float64
}
var m = map[string]Vertex{
"aa": Vertex{
12.1, -9.1,
},
"bb": Vertex{
51.1, -20.1,
},
}
func main() {
fmt.Println(m)
}
如果顶级类型只是一个类型名称,您可以从字面量的元素中省略它。
package main
import "fmt"
type Vertex struct {
Lat, Long float64
}
var m = map[string]Vertex{
"aa": {51.2, -161.1},
"bb": {-91.3, 9.91},
}
func main() {
fmt.Println(m)
}
二十、更改map
插入或更新map中的一个元素:
m[key] = elem
取出一个元素
elem = m[key]
删除一个元素中
delete(m, key)
通过二值分配测试一个键是否存在:
elem, ok = m[key]
如果key在m中,ok
是true
。如果没有,ok
是false
。
如果key
没有在map中,那么elem
是零值,具体取决于map元素的类型。
注意,如果elem
和ok
还没有声明,你应该使用一个简短的声明格式:
elem, ok := m[key]
package main
import "fmt"
func main() {
m := make(map[string]int)
m["Answer"] = 42
fmt.Println("The value: ", m["Answer"])
m["Answer"] = 48
fmt.Println("The value: ", m["Answer"])
delete(m, "Answer")
fmt.Println("The value: ", m["Answer"])
v, ok := m["Answer"]
fmt.Println("The value: ", v, "Persent?", ok)
}
二十一、练习:map
实现WorldCount
。它应该返回在一个字符串s
中每个单词的数量的map。这个wc.Test
函数对提供的函数运行一个测试序列,并打印成功或失败。
可以在strings.Fields中获取帮助。
package main
import (
"strings"
"golang.org/x/tour/wc"
)
func WorldCount(s string) map[string]int {
sp := strings.Split(s, " ")
res := make(map[string]int)
for _, v := range sp {
c := res[v]
c += 1
res[v] = c
}
return res
}
func main() {
wc.Test(WorldCount)
}
二十二、函数值
函数也是值。它们能被传递就像其它值一样。
函数值能作为函数的参数使用,并返回值。
package main
import (
"fmt"
"math"
)
func compute(fn func(float64, float64) float64) float64 {
return fn(3, 4)
}
func main() {
hypot := func(x, y float64) float64 { return math.Sqrt(x*x + y*y) }
fmt.Println(hypot(5, 12))
fmt.Println(compute(hypot))
fmt.Println(compute(math.Pow))
}
二十三、函数的闭包
Go函数可能是闭包。闭包是一个函数值,引用来它方法体外面的变量。函数能够被读取并分配给应用变量。在这种意义上函数是绑定到变量上的。
例如,adder
函数 返回一个闭包。每个闭包,绑定了它自己的sum
变量。
package main
import "fmt"
func adder() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}
func main() {
pos, neg := adder(), adder()
for i := 0; i < 10; i++ {
fmt.Println(
pos(i),
neg(-2*i),
)
}
}
二十四、斐波那契闭包
让我们使用一些带有函数的函数。
实现一个fibonacci
函数,它返回一个函数(一个闭包),它能一次返回斐波那契数值(0, 1, 1, 2, 3, 5, …)
package main
import "fmt"
func fibonacci() func() int {
first, second := 0, 0
return func() int {
if first == 0 {
first = 1
second = 1
return 0
} else {
current := first
firstc := second
second = first + second
first = firstc
return current
}
}
}
func main() {
f := fibonacci()
for i := 0; i < 10; i++ {
fmt.Println(f())
}
}