Go语言学习——指针、切片、映射、函数

## 指针——值的内存地址
Go 拥有指针。指针保存了值的内存地址。

*类型 T 是指向 T 类型值的指针。其零值为 nil。

var p *int //p是一个可以指向int类型值的指针

& 操作符会生成一个指向其操作数的指针。

k := 42
p = &k //p这时候就是一个指向k的指针

*操作符表示指针指向的底层值。

fmt.Println(*p) // 通过指针 p 读取 k
*p = 21 // 通过指针 p 设置 k

package main

import "fmt"

func main() {
	i, j := 42, 2701

	p := &i         // 声明指针p指向 i
	fmt.Println(*p) // 通过指针读取 i 的值,其实就是根据p里面的地址访问i的值
	*p = 21         // 通过指针设置 i 的值
	fmt.Println(i)  // 查看 i 的值,此时已经是21了

	p = &j         // 指向 j
	*p = *p / 37   // 通过指针对 j 进行除法运算
	fmt.Println(j) // 查看 j 的值
}


结构体

一个结构体其实就是一组字段

package main

import "fmt"

type Vertex struct {
	X int
	Y int
}

func main() {
	fmt.Println(Vertex{1, 2})
}

// 输出——{1,2}

结构体字段

要访问或者修改结构体字段,通过.来访问

package main

import "fmt"

type Vertex struct {
	X int
	Y int
}

func main() {
	v := Vertex{1, 2}
	fmt.Println(v.X)//1
	v.X = 4
	fmt.Println(v.X)//4
}

结构体指针

结构体字段可以通过结构体指针来访问。

*如果我们有一个指向结构体的指针 p,那么可以通过 (p).X 来显式访问其字段 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)//{1000000000,2}
	(*p).X = 1e7
	fmt.Println(v)//{10000000,2}
}

结构体文法

结构体文法通过直接列出字段的值来新分配一个结构体。

使用 Name: 语法可以仅列出部分字段。(字段名的顺序无关。)

特殊的前缀 & 返回一个指向结构体的指针。

package main

import "fmt"

type Vertex struct {
	X, Y int
}

var (
	v1 = Vertex{1, 2}  // 创建一个 Vertex 类型的结构体
	v2 = Vertex{X: 1}  // Y:0 被隐式地赋予
	v4 = Vertex{Y: 2}  // X:0 隐式赋予,无关顺序在这里体现出来
	v3 = Vertex{}      // X:0 Y:0
	p  = &Vertex{1, 2} // 创建一个 *Vertex 类型的结构体(指针)
)

func main() {
	fmt.Println(v1, p, v2, v3)
}
// {1,2} &{1,2} {1,0} {0,0}

值得注意的是上面代码中的p指向的结构体也是{1,2},但是地址并不是v1的内存地址,所以通过p对结构体进行修改不会影响v1

数组

表达式

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}// 结构体的参数个数不能超出数组长度,如果少的会在数组末尾补0
	fmt.Println(primes)// [2,3,5,7,11,13]
}

切片

每个数组的大小都是固定的。而切片则为数组元素提供动态大小的、灵活的视角。在实践中,切片比数组更常用。切片因为比数组更灵活,所以在go里面广泛应用
切片不等同于数组!只是很类似,而且基于数组可以创建切片
类型 []T 表示一个元素类型为 T 的切片。
一般只要看创建的时候有没有指定长度就可以区分数组和切片。我
切片通过两个下标来界定,即一个上界和一个下界,二者以冒号分隔:

a[low : high]

它会选择一个半开区间,包括第一个元素,但排除最后一个元素。

以下表达式创建了一个切片,它包含 a 中下标从 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)// [3 5 7]
}


注意!!!
切片就像是对数组的引用,假设b是a的子切片,如果改变了b中的元素,那么a中的元素也会发生改变,并且其他引用了a切片的地方也会观测到这些修改

package main

import "fmt"

func main() {
	names := [4]string{
		"John",
		"Paul",
		"George",
		"Ringo",
	}
	fmt.Println(names)

	a := names[0:2]
	b := names[1:3]
	fmt.Println(a, b)

	b[0] = "XXX"// 此时b[0]对应的'Paul'变成了'XXX',导致a数组和names也同时发生改变
	fmt.Println(a, b)
	fmt.Println(names)
}

[John Paul George Ringo]
[John Paul] [Paul George]
[John XXX] [XXX George]
[John XXX George Ringo]

切片文法

切片文法类似于没有长度的数组文法。

这是一个数组文法:

[3]bool{true, true, false}

下面这样则会创建一个和上面相同的数组,然后构建一个引用了它的切片:

[]bool{true, true, false}

package main

import "fmt"

func main() {
	q := []int{2, 3, 5, 7, 11, 13}
	fmt.Println(q)

	r := []bool{true, false, true, true, false, true}
	fmt.Println(r)

	s := []struct {
		i int
		b bool
	}{
		{2, true},
		{3, false},
		{5, true},
		{7, true},
		{11, false},
		{13, true},
	}
	fmt.Println(s)
}

切片的默认行为

我们在对数组进行切片的时候,并不会改变底层数组(源数组的长度和容量)
长度:当前数组的元素个数
容量:当前数组的第一个元素到其底层数组的最后一个元素的距离
切片 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[:4]
	printSlice(s)

	// 舍弃前两个值
	s = s[2:]
	printSlice(s)
}

func printSlice(s []int) {
	fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}

len=6,cap=6,[2,3,5,7,11,13]
len=0,cap=6,[]
len=4,cap=6,[2,3,5,7]
len=2,cap=4,[5,7]

切片的零值是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!")
	}
}
/*
0 0 []
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

向切片中添加元素

为切片追加新的元素是种常用的操作,为此 Go 提供了内建的 append 函数。内建函数的文档对此函数有详细的介绍。

func append(s []T, vs …T) []T
append 的第一个参数 s 是一个元素类型为 T 的切片,其余类型为 T 的值将会追加到该切片的末尾。

append 的结果是一个包含原切片所有元素加上新添加元素的切片。

当 s 的底层数组太小,不足以容纳所有给定的值时,它就会分配一个更大的数组。返回的切片会指向这个新分配的数组。

package main

import "fmt"

func main() {
	var s []int
	printSlice(s)

	// 添加一个空切片
	s = append(s, 0)
	printSlice(s)

	// 这个切片会按需增长
	s = append(s, 1)
	printSlice(s)

	// 可以一次性添加多个元素
	s = append(s, 2, 3, 4)
	printSlice(s)
}

func printSlice(s []int) {
	fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}


使用append()添加元素时,如果此时切片长度超出容量的时候,会自动对切片扩容。
切片扩容机制:当原切片长度小于1024时,新切片的容量会直接翻倍。而当原切片的容量大于等于1024时,会反复地增加25%,直到新容量超过所需要的容量

注意点:像上述代码一次性添加多个元素的时候,切片容量本来是2,但是一次性添加3个元素后,翻倍也不够用,就会再加2,所以第三次添加的时候,cap就变成了2

Range

for 循环的 range 形式可遍历切片或映射。

当使用 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("[%d  %d]\n", i, v)
	}
}

上述循环每次输出的结果是[index,num]形式的

但有时候我们并不需要两个值,当我们想省略其中一个的时候,使用_(下划线)即可

package main
import "fmt"
func main() {
    //这是我们使用range去求一个slice的和。使用数组跟这个很类似
    nums := []int{2, 3, 4}
    sum := 0
    for _, num := range nums {
        sum += num
    }
    fmt.Println("sum:", sum)
    //在数组上使用range将传入index和值两个变量。上面那个例子我们不需要使用该元素的序号,所以我们使用空白符"_"省略了。有时侯我们确实需要知道它的索引。
    for i, num := range nums {
        if num == 3 {
            fmt.Println("index:", i)
        }
    }
    //range也可以用在map的键值对上。
    kvs := map[string]string{"a": "apple", "b": "banana"}
    for k, v := range kvs {
        fmt.Printf("%s -> %s\n", k, v)
    }
    //range也可以用来枚举Unicode字符串。第一个参数是字符的索引,第二个是字符(Unicode的值)本身。
    for i, c := range "go" {
        fmt.Println(i, c)
    }
}

sum: 9
index: 1
a -> apple
b -> banana
0 103
1 111

如果只是需要索引,我们甚至可以不使用空白符’_’

for i := range pow{
	fmt.Println("index=%d\n",i)// %d是占位符
}

映射——Map

映射将键映射到值。
(其实就是Java里的Map和python的字典,存储的元素是key-value对)
映射的零值为 nil 。nil 映射既没有键,也不能添加键。
make 函数会返回给定类型的映射,并将其初始化备用。所以make()不仅可以创建切片,还可以用来创建Map

package main

import "fmt"
// 初始化一个结构体,里面是两个浮点数类型元素
type Vertex struct {
	Lat, Long float64
}

var m map[string]Vertex

func main() {
	// string是key的类型,Vertex是value的类型
	m = make(map[string]Vertex)
	m["Bell Labs"] = Vertex{
		40.68433, -74.39967,
	}
	fmt.Println(m["Bell Labs"])// {40.68433 -74.39967}

}

映射的语法/文法

映射的文法与结构体相似,不过必须有键名。

package main

import "fmt"

type Vertex struct {
	Lat, Long float64
}

var m = map[string]Vertex{
	"Bell Labs": Vertex{40.68433, -74.39967},
	"Google":    Vertex{37.42202, -122.08408},
}
// Vertex可以省略
func main() {
	fmt.Println(m)
}

映射的修改操作

1.在映射m中插入或修改某个元素

m[key] = elem

2.获取某个元素

elem = m[key]

3.删除元素

delete(m,key)

4.通过双赋值验证m中是否存在某个元素

elem, ok = m[key]

如果key在m里面,ok为true,否则是false
如果key不在m中,elem会被赋予value数据类型的零值
所以我们在读取某个不存在映射中的数据时,得到的是value数据类型的零值

如果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, "Present?", ok)
}

The value: 42
The value: 48
The value: 0
The value: 0 Present? false

映射练习

实现 WordCount。它应当返回一个映射,其中包含字符串 s 中每个“单词”的个数。函数 wc.Test 会对此函数执行一系列测试用例,并输出成功还是失败。
**你会发现 strings.Fields 很有帮助。 **

package main

import (
	"golang.org/x/tour/wc"
	"strings"
)

func WordCount(s string) map[string]int {
	//1.先初始化一个映射
	var cnt = make(map[string]int)
	//2.声明elem用来保存value值,也就是每个单词出现的次数
	var elem = 0
	//3.strings.Fields(s)会返回一个将s字符串按空格分割的字符串列表,我们使用range进行一个遍历
	for i := range strings.Fields(s){
		//4.首先看这个单词之前出现了几次
		elem = cnt[strings.Fields(s)[i]]
		//5.然后再给这个单词出现的次数加1
		cnt[strings.Fields(s)[i]]=1+elem
	}
	return cnt
}

func main() {
	wc.Test(WordCount)
}

函数也可以当作值来传递

函数值可以用作函数的参数或返回值。

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

上述代码的hypot函数其实就是作为一个函数值在compute函数里面传递
输出1:hypot(5,12)返回13
输出2:compute(hypot)其实就是将3和4传入hypot中再返回结果5

Go 语言支持匿名函数,可作为闭包。匿名函数是一个"内联"语句或表达式。匿名函数的优越性在于可以直接使用函数内的变量,不必申明。
以下实例中,我们创建了函数 getSequence() ,返回另外一个函数。该函数的目的是在闭包中递增 i 变量,代码如下:

package main

import "fmt"

func getSequence() func() int {
   i:=0
   return func() int {
      i+=1
     return i  
   }
}

func main(){
   /* nextNumber 为一个函数,函数 i 为 0 */
   nextNumber := getSequence()  

   /* 调用 nextNumber 函数,i 变量自增 1 并返回 */
   fmt.Println(nextNumber())
   fmt.Println(nextNumber())
   fmt.Println(nextNumber())
   
   /* 创建新的函数 nextNumber1,并查看结果 */
   nextNumber1 := getSequence()  
   fmt.Println(nextNumber1())
   fmt.Println(nextNumber1())
}

以上代码执行结果为

1
2
3
1
2

练习——实现斐波那契数列

package main

import "fmt"

// 返回一个“返回int的函数”,其实底部调用的是闭包函数,每次都返回tmp
func fibonacci() func() int {
	num1,num2 := 0,1
	// 我们使用函数闭包,无需声明也可以直接使用
	return func() int{
		var tmp = num1
		num1,num2 = num2,num1+num2
		return tmp
	}
}

func main() {
	f := fibonacci()
	for i := 0; i < 10; i++ {
		fmt.Println(f())
	}
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值