指针
什么是指针?
Go 拥有指针,一个指针变量指向了一个值的内存地址。
比如:类型 *T 是指向 T 类型值的指针。其零值为 nil。指针声明格式如下:
var var_name *var-type
var p *int
& 操作符
& 操作符会生成一个指向其操作数的指针。
var i int = 10
p = &i
* 操作符
- 操作符表示指针指向的底层值,作用是可以操作变量具体的值,也就是通常所说的“间接引用”或“重定向”。
// 通过指针 p 读取 i
fmt.Println(*p)
// 通过指针 p 设置 i
*p = 20
实例
package main
import "fmt"
func main() {
var i int = 10
// 指针
var p *int = &i
fmt.Println("指针p指向的值:", *p)
// 指针重新设值
*p = 20
fmt.Println("指针p重新指向的值:", *p)
// panic: runtime error: invalid memory address or nil pointer dereference
var pointer *string = nil
fmt.Println("指针p指向的值为nil:", *pointer)
}
结构体
什么是结构体?
结构体是由一系列具有相同类型或不同类型的数据构成的数据集合。
简单来说,一个结构体(struct)就是一组字段(field)。
比如保存图书馆的书籍记录,每本书有以下属性:
Title :标题
Author : 作者
Subject:学科
定义结构体的语法:
type struct_variable_type struct {
member definition
member definition
...
member definition
}
简单实例
package main
import "fmt"
type Book struct {
Title string
Author string
Subject string
}
func main() {
// 初始化
var b Book = Book{"Go程序设计语言", "Kernighan", "计算机"}
fmt.Println(b)
// 使用点号来访问,
fmt.Println("书的名称:", b.Title)
}
结构体指针
结构体字段可以通过结构体指针来访问。比如可以通过 (*p).X 来访问其字段 X。不过这么写太啰嗦了,所以语言也允许我们使用隐式间接引用,直接写 p.X 就可以。
package main
import "fmt"
type Book struct {
Title string
Author string
Subject string
}
func main() {
// 初始化
var b Book = Book{"Go程序设计语言", "Kernighan", "计算机"}
// 结构体指针
var p *Book = &b
// 通过 (*p).X 来访问其字段 X
fmt.Println((*p).Title)
// 更方便的隐式间接引用
fmt.Println((*p).Title)
}
结构体文法
结构体文法通过直接列出字段的值来新分配一个结构体。
使用 Name: 语法可以仅列出部分字段(字段名的顺序无关)
package main
import "fmt"
type Book struct {
Title string
Author string
Subject string
}
var (
b1 = Book{"Go程序设计语言", "Kernighan", "计算机"}
b2 = Book{Author: "Kernighan", Title: "Go程序设计语言"}
p = &Book{Title: "Go程序设计语言"}
)
func main() {
fmt.Println(b1)
fmt.Println(b2)
fmt.Println(p)
}
数组
Go 语言提供了数组类型的数据结构。
数组是具有相同唯一类型的一组已编号且长度固定的数据项序列,这种类型可以是任意的原始类型例如整型、字符串或者自定义类型。
数组表达式:类型 [n]T 表示拥有 n 个 T 类型的值的数组。语法格式:
var variable_name [SIZE] variable_type
var a [10]int
var s [2]string
package main
import "fmt"
func main() {
var s [2]string
s[0] = "hello"
s[1] = "go!"
fmt.Println(s)
}
切片
为什么使用切片?
Go 数组的长度不可改变,在特定场景中这样的集合就不太适用。
Go 中提供了一种灵活,功能强悍的内置类型切片(“动态数组”),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。
所以在实践中,切片比数组更常用。
切片定义
类型 []T 表示一个元素类型为 T 的切片。
第一:可以声明一个未指定大小的数组来定义切片:
var identifier []type
如果是用数组的片段来创建切片,则切片通过两个下标来界定,即一个上界和一个下界(左闭右开),二者以冒号分隔:
a[low : high]
package main
import "fmt"
func main() {
a := [5]int{1, 2, 3, 4, 5}
slice := a[1:3]
fmt.Println(slice)
// 打印输出:[2 3]
}
第二:使用 make() 函数来创建切片,这是创建动态数组的方式。
var slice1 []type = make([]type, len)
// 也可以简写为
slice1 := make([]type, len)
make 函数会分配一个元素为零值的数组并返回一个引用了它的切片:
// len(s)=5
s := make([]int, 5)
//如果要指定它的容量,需向 make 传入第三个参数
// len(ss)=0, cap(ss)=5
ss := make([]int, 0, 5)
package main
import "fmt"
func main() {
s := make([]string, 5)
fmt.Printf("%s 长度=%d 容量=%d %v\n",
s, len(s), cap(s), s)
ss := make([]string, 5, 10)
fmt.Printf("%s 长度=%d 容量=%d %v\n",
ss, len(ss), cap(ss), ss)
}
// 打印
[ ] 长度=5 容量=5 [ ]
[ ] 长度=5 容量=10 [ ]
切片可包含任何类型,甚至包括其它的切片。
package main
import (
"fmt"
"strings"
)
func main() {
// 创建一个井字板(经典游戏)
b := [][]string{
[]string{"_", "_"},
[]string{"_", "_"},
}
// 设置
b[0][0] = "X"
b[1][1] = "O"
// 遍历输出
for i := 0; i < len(b); i++ {
fmt.Printf("%s\n", strings.Join(b[i], " "))
}
}
向切片追加元素
为切片追加新的元素是种常用的操作,为此 Go 提供了内建的 append 函数。
append 的第一个参数 s 是一个元素类型为 T 的切片,其余类型为 T 的值将会追加到该切片的末尾。
package main
import "fmt"
func main() {
s := []string{}
s = append(s, "张三")
fmt.Printf("长度=%d 容量=%d %v\n", len(s), cap(s), s)
}
切片原理
切片和数组的引用一样,切片并不存储任何数据,它只是描述了底层数组中的一段。更改切片的元素会修改其底层数组中对应的元素。与它共享底层数组的切片都会观测到这些修改。
package main
import "fmt"
func main() {
persons := [3]string{"张三", "李四", "王五"}
fmt.Println(persons)
fmt.Println("----------")
// : 代表首尾全部元素
s1 := persons[:]
s2 := persons[1:2]
fmt.Println(s1)
fmt.Println(s2)
fmt.Println("----------")
// 改名验证
s1[1] = "改名"
fmt.Println(s1)
fmt.Println(s2)
fmt.Println(persons)
}
//打印结果:
[张三 李四 王五]
----------
[张三 李四 王五]
[李四]
----------
[张三 改名 王五]
[改名]
[张三 改名 王五]
切片遍历
Range 遍历
for 循环的 range 形式可遍历切片,每次迭代都会返回两个值。第一个值为当前元素的下标,第二个值为该下标所对应元素的一份副本。
package main
import "fmt"
func main() {
persons := [3]string{"张三", "李四", "王五"}
for i, v := range persons {
fmt.Printf("%v = %v\n", i, v)
}
}
注意:可以将下标或值赋予 _ 来忽略它。
package main
import "fmt"
func main() {
persons := [3]string{"张三", "李四", "王五"}
for _, v := range persons {
fmt.Printf("%v\n", v)
}
}
切片默认行为
切片下界的默认值为 0,上界则是该切片的长度。所以在进行切片时,你可以利用它的默认行为来忽略上下界。
以下切片是等价的:
var a [10]int
// 等价
slice1 = a[0:10]
slice2 = a[:10]
slice3 = a[0:]
slice4 = a[:]
长度与容量
切片拥有 长度 和 容量。
对于切片 s 来说:
切片的长度就是它目前所包含的元素个数,可以使用 len(s) 来获取。
切片的容量是从它的第一个元素开始数,到其底层数组元素末尾的个数。可以使用 cap(s) 来获取。
package main
import "fmt"
func main() {
s := []int{1, 4, 7, 3, 6, 9}
slice1 := s[:]
fmt.Printf("长度=%d 容量=%d %v\n", len(slice1), cap(slice1), slice1)
slice2 := s[2:]
fmt.Printf("长度=%d 容量=%d %v\n", len(slice2), cap(slice2), slice2)
}
nil 切片
切片的零值是 nil。nil 切片的长度和容量为 0 且没有底层数组。
package main
import "fmt"
func main() {
var s []string
fmt.Println(s, len(s), cap(s))
if s == nil {
fmt.Println("nil!")
}
}
映射
Map 是一种无序的键值对的集合。Map 最重要的一点是通过 key 来快速检索数据,key 类似于索引,指向数据的值。
定义 Map
可以使用内建函数 make 也可以使用 map 关键字来定义 Map:
/* 声明变量,默认 map 是 nil */
var map_variable map[key_data_type]value_data_type
/* 使用 make 函数 */
map_variable := make(map[key_data_type]value_data_type)
package main
import "fmt"
type Person struct {
Name string
Age int
}
func main() {
persons := make(map[string]Person)
persons["man"] = Person{"张三", 30}
persons["woman"] = Person{"小花", 30}
fmt.Println(persons)
}
映射的文法
package main
import "fmt"
type Person struct {
Name string
Age int
}
func main() {
// 结构体定义
persons := map[string]Person{
"man": Person{"张三", 30},
"woman": Person{"小花", 30},
}
fmt.Println(persons)
}
增删改查映射
插入或修改元素(在映射 persons 中):
persons[key] = elem
// 比如
persons["man"] = Person{"李四", 22}
获取元素:
elem = m[key]
// 比如
man = persons["man"]
还可以通过双赋值检测某个键是否存在。
如果 key 在 persons 中存在,ok 为 true ;否则 ok 为 false。
如果 key 不在映射中,那么 elem 是该映射元素类型的零值,ok 为 false。
elem, ok = m[key]
// 比如
man, ok = persons["man"]
package main
import "fmt"
type Person struct {
Name string
Age int
}
func main() {
// 结构体定义
persons := map[string]Person{
"man": Person{"张三", 30},
"woman": Person{"小花", 30},
}
fmt.Println(persons)
// 修改元素
persons["woman"] = Person{"小红", 18}
fmt.Println(persons)
// 获取元素
man, ok := persons["man"]
if ok {
fmt.Println("man 存在:", man)
}
// 删除元素
delete(persons, "man")
// 再次获取元素
m, isExist := persons["man"]
if isExist {
fmt.Println("man 元素存在:", m)
} else {
fmt.Println("man 元素不存在!")
}
fmt.Println(persons)
}
// 打印结果
map[man:{张三 30} woman:{小花 30}]
map[man:{张三 30} woman:{小红 18}]
man 存在: {张三 30}
man 元素不存在!
map[woman:{小红 18}]
函数值
在 GO 中函数也是值,它们可以像其它值一样传递。函数值可以用作函数的参数或返回值。
package main
import "fmt"
// f 作为函数值传递
func computer(f func(int, int) int, a int, b int) int {
return f(a, b)
}
func Add(x int, y int) int {
return x + y
}
func Sub(x int, y int) int {
return x - y
}
func main() {
fmt.Println("传递Add函数:", computer(Add, 10, 20))
fmt.Println("传递Sub函数:", computer(Add, 10, 20))
}
函数的闭包
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() {
// 调用adder获得一个闭包函数
pos := adder()
for i := 0; i < 10; i++ {
// 调用闭包
fmt.Println(pos(i))
}
}
练习:斐波纳契闭包
实现一个 fibonacci 函数,它返回一个函数(闭包),该闭包返回一个斐波纳契数列 (0, 1, 1, 2, 3, 5, ...)
。
package main
import "fmt"
// 返回一个“返回int的函数”
func fibonacci() func() int {
// 预设初始值,相当于绑定闭包
back1, back2 := 0, 1
return func() int {
// 关键代码
temp := back1
// 核心:交换两值
back1, back2 = back2, (back1 + back2)
return temp
}
}
func main() {
f := fibonacci()
for i := 0; i < 10; i++ {
fmt.Println(f())
}
}
请关注公众号【Java千练】,更多干货文章等你来看!