Go语言进阶:深入探索数据与集合类型

169 阅读3分钟

1.前言

上一篇主要学习了包、函数、变量以及类型等基本特性,本篇继续沿着基础语法学习。从流程控制语句、数组与切片、结构体与字典等方面进行进一步的学习

2.流程控制语句

流程控制是可以改变程序运行中的一些行为的,比如跳转、进入某个函数,再比如满足某个条件,进入某一段程序中,这样的可以改变运行状态的语句叫做流程控制语句,在一般的编程语言中都有类似的语句,可能在语法的细微处不同。在Go中主要有以下几个流程控制语句:

  • for
  • if..else
  • switch
  • defer

接下来会从这几个语句的用法中,简要的说明该语句的特性。

for

for语句一般用作循环,比如Java中分普通for循环和增强for循环,而在Go中for循环的表达式一般由三部分组成:

  • 初始化语句
  • 条件表达式
  • 后置语句

写法如:for i := 0; i < 10; i++ {

// 循环体

}

有关初始化语句、条件表达式、后置语句的执行时机,与其他编程语言差别不大。for语句后面的三个组成部分没有小括号,这点与Java不同。初始化语句通常是一个短变量声明,该声明仅在for作用域内中有效。

代码演示:

package main

import "fmt"

func main() {

   sum := 0
   for i := 0;i < 10;i++{
      sum += i
   }
   
   fmt.Println(sum)
}

for语句的初始化语句和后置语句可选,例如:

for ;sum < 100;{
      // 循环体
}

for语句只保留条件表达式类似于while。

for sum < 500{
      // 循环体
}

无限循环,也叫死循环,去掉条件表达式,类似于Java中的while(true)

for{
}

if..else

if语句,在很多编程语言中都用来作为条件判断,Go中if语句的小括号可以去掉,并且可以在表达式前执行一个简单的语句。

package main

import (
   "fmt"
   "math"
)

func main() {

   // 小功能 求两数平方根 -2 4

   fmt.Println(sqrt(-2),sqrt(4))

}

func sqrt(x float64) string {

   if x < 0 {
      return sqrt(-x) + "tcp"
   }

   return fmt.Sprint(math.Sqrt(x))
}

这段小程序中有趣的点特别多

  • 表达式没括号
  • 递归调用
  • sqrt与Sqrt
  • fmt.Sprint

if的简短语句

package main

import (
   "fmt"
   "math"
)

func main() {

   fmt.Println(
      pow(3,2,10),
      pow(3,3,10),
   )

}

func pow(x,n,lim float64) float64 {
   if v := math.Pow(x, n); v < lim {
      return v
   }
   return lim
}

这部分也有几个小细节

  • math.Pow(x,y) x的y次方
  • 打印时第二个参数后面还有逗号
  • 声明变量的作用域

else语句

在if中声明的变量也可以用在else中

部分代码

func pow(x,n,lim float64) float64 {
   if v := math.Pow(x, n); v < lim {
      return v
   } else {
      fmt.Println("执行else语句",v)
   }
   return lim
}

细节部分

  1. else与Java中else区别不大
  2. 语句执行覆盖

switch

总体来说,Go中的switch与Java中有几处不同,首先当case条件满足条件以后,执行该case以后就会结束执行,内置了break。而且case支持多种类型。

func main() {

   fmt.Print("Go runs on ")
   switch os := runtime.GOOS; os {
   case "Mac":
      fmt.Println("Mac X.")
   case "Linux":
      fmt.Println("Linux X.")
   default:
      fmt.Printf("%s.\n",os)
   }

}

细节部分

  • 只运行选定case,而后停止
  • 操作系统 darwin
  • case不需要常量,也不用整型
  • 取值顺序 从上向下

没有条件的switch语句

// 小需求 判断 当前时间所在范围段 12点之前 早上  17点之前 下午 17点之后 晚上

t := time.Now()
switch {
case t.Hour() < 12:
   fmt.Print("Good morning..")
case t.Hour() < 17:
   fmt.Println("Good afternoon")
default:
   fmt.Print("Good evening")
}

同switch true一样

defer

我在第一次看到defer语句时,并没有联想到Java中的一些概念。defer语句会将函数压入到栈中,如果有多个defer语句的话,会按照顺序压入到栈中,当前函数执行完毕后,被压入到栈中的这些defer语句定义的函数会按照后入先出的顺序执行。

测试效果

defer fmt.Println("world")
defer fmt.Println("gotodo")
defer fmt.Println("study")

fmt.Println("hello")

// 省略代码 

// 输出 
hello
study
gotodo
world

后来我想到了Java中的一个概念,就是finally。但是又有很多不同。比如finally一般配合try..catch使用,一般情况下执行完try..catch中代码后会执行finally,但是也有一些情况下不会执行。比如进入try语句之前程序以后,或者在try中执行类似System.exit(0)这样的函数,JVM进程都关闭了,finally肯定也不会再执行了。还有就是如果有多个finally的话也会按照顺序执行。

3.指针

Go中指针保存了值的内存地址。

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

做一个测试,验证猜想。

步骤:

  1. 声明变量i
  2. 生成一个指向i的指针p
  3. 打印指针,观察输出

猜测:打印指针,也就是内存地址值。观察输出验证一下

完整代码

package main

import "fmt"

func main() {

   i := 42
   p := &i
   fmt.Println(*p)

}

输出:

42

可以看到,利用*p打印输出的是变量i的值。

尝试直接打印p观察

fmt.Println(p)

输出:

0xc000018088

这次输出了指针p,也就是内存地址值,符合猜想。那么*p代表的又是什么意思呢?

原来,在上面的这个例子中,可以这样理解

  • 首先生成了一个指向变量i的指针p,保存的是变量i的内存地址
  • 然后通过*p的意思就是,通过指针p读取指针指向的某个变量(这里就是i)的值
  • 所以在打印*p时打印出了i的值

当然,也可以根据指针去设置变量的值。

比如:

package main

import "fmt"

func main() {

   i := 42
   p := &i
   fmt.Printf("第一次通过指针p读取的i值是 %v\n",*p)
   *p = 21
   fmt.Printf("经过指针p设置i的值后,读取到i的值是 %v",i)

}

输出:

第一次通过指针p读取的i值是 42
经过指针p设置i的值后,读取到i的值是 21

假设我此时再定义一个变量j,把指针p的引用指向j,再观察指针p的输出还与变量i有关系吗?

先猜测一下,没有关系了,因为引用已经发生改变(也就是指针指向),看代码

package main
import "fmt"

func main() {

   i := 42
   p := &i
   *p = 21
   j := 2701

   // 将p指针 指向j
   p = &j
   fmt.Println(*p)

}

输出:

2701

果然,通过指针p读取的值已经是j的了,这也是常说的间接引用或者重定向。

还有个问题是,此时变量i的值是多少?是42还是21?这个问题可以去验证一下,想想为什么

指针的定义:

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

var p * int

与C不同的是,Go没有指针运算

4.结构体

看到结构体我第一下就是想到了Java中的类,里面封装的一些属性,但是又有些不一样。Go中一个结构体就是一组字段。

package main
import "fmt"

type Vertex struct {
   X int
   Y int
}

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

比如我打印的时候,相当于构造器的赋值,但是省去了构造器的定义部分。

结构体字段用点号来标识 比如

v := Vertex{5,2}
v.X = 4
fmt.Println(v.X)

// 省略完整代码..

这个没什么好说的,基本都一样。

还可以通过指针访问

访问的几种方式

  • 通过点号
  • 通过指针
v := Vertex{5,2}
p := &v
p.X = 24
fmt.Println(v.X)

// 省略完整代码..

定义一个指向结构体的指针。

用法:

例如 (*p).X

精简 p.X 语言支持隐式间接引用

结构体文法

我理解类似于初始化一个结构体,如果对应字段的位置进行赋值就使用该值,如果没有赋值的话就使用默认值。

比如在下面的代码中

v1代表新建一个结构体并给X与Y赋值8和24 v2代表新建一个结构体给X赋值1,Y的话使用默认值0 v3代表新建一个结构体但是没有赋值,所以默认值为0,0 v4代表新建一个*Vertex类型的结构体指针 并赋值1,2

看代码

package main

import "fmt"

type Vertex struct {
   X,Y int
}

var (
   v1 = Vertex{8,24}
   v2 = Vertex{X: 1}
   v3 = Vertex{}
   p = &Vertex{1,2}
)

func main() {

   fmt.Println(v1,p,v2,v3)
}

输出:

{8 24} &{1 2} {1 0} {0 0}

5.数组与切片

数组

定义:数据结构中数组的定义,特性,可以加入到其中

在Go中如何定义一个数组呢?

表达式:

var arr [10]int 表示定义一个10个int类型值的数组

[n]T 拥有n个T类型值的数组

数组的长度是数组的一部分,不可变。

package main
​
import "fmt"func main() {
​
   var arr [2]string
   arr[0] = "hello"
   arr[1] = "world"
   fmt.Println(arr[0],arr[1])
   fmt.Println(arr)
}

输出:

hello world
[hello world]

可以看出,这种方式类似于Java中new int[n],然后给数组中每个位置赋值的方式。

伪代码

arr[0] = .. arr[1] = ..

还有另外一种方式

primes := [6]int{2, 3, 5, 7, 11, 13}
fmt.Println(primes)
​
// 省略完整代码..

类似于Java中new int[]{1,2,3}这种方式,在初始化数组时赋值,未赋值的部分赋予默认值。

切片

定义:切片就像在数组上切一段,可以是一个动态的大小,也更加的灵活。提供一种前闭后开的区间。

表达式 arr[1:4] 表示的就是取数组下标1-4的元素,包含1但不包含4

package main
​
import "fmt"func main() {
​
   arr := [6]int{2, 3, 5, 7, 11, 13}
​
   var s []int = arr[1:4]
   fmt.Println(s)
}

输出:

[3 5 7]

也正好对应了输出下标为1 2 3 的元素。

切片并不会存储数据,切片像数组的引用,在修改切片中的元素以后,与切片关联的底层数组的元素也会修改。

下面用一段代码尝试解释以上行为,观察打印

  • 定义原始数组
  • 定义切片a
  • 定义切片b
  • 输出原始数组 切片a 切片b的值
  • 修改切片中元素值 (注意在切片中索引位置与关联数组的索引位置不一定相同)
  • 输出修改后数组 切片a 切片b的值
package main
​
import "fmt"func main() {
​
   names := [4]string {
      "jack",
      "paul",
      "rose",
      "hake",
   }
​
   fmt.Printf("原始数组 %s\n", names) 
​
   a := names[0:2]
   b := names[1:3]
   fmt.Printf("切片a的值 %s\n",a)
   fmt.Printf("切片b的值 %s\n",b)
​
   fmt.Println("修改切片中元素的值")
   b[0] = "xxx"
   fmt.Printf("修改后切片a的值 %s\n",a)
   fmt.Printf("修改后切片b的值 %s\n",b)
   fmt.Printf("修改切片中元素后 数组值 %s\n", names)
}

输出:

原始数组 [jack paul rose hake]
--------------
切片a的值 [jack paul]
切片b的值 [paul rose]
--------------
修改切片中元素的值
修改后切片a的值 [jack xxx]
修改后切片b的值 [xxx rose]
修改切片中元素后 数组值 [jack xxx rose hake]

切片的默认行为

[:2] 表示从第1个到第二个元素 [1:] 表示从第1个开始(不包含第1个),到最后一个的前一个

还有一些各种各样的切法,我先搁置一下这块。

切片的长度和容量

切片的长度是指切片中元素的个数。

切片的容量是指从切片的第一个元素开始,到其底层数组最后一个元素的长度。

可以通过方法len(s)和cap(s)查看。

package main
​
import "fmt"func main() {
​
   fmt.Printf("测试数据 a=%d\n",0x12)
​
   s := []int{2,3,5,7,11,13}
   printSlice(s)
​
   s = s[:0]
   printSlice(s)
}
​
func printSlice(s []int) {
   fmt.Printf("len=%d cap=%d %v\n",len(s),cap(s),s)
}

输出:

测试数据 a=18
len=6 cap=6 [2 3 5 7 11 13]
len=0 cap=6 []

附加:

解释:对于占位符,用到了哪个解释一下。

%d: 整数占位符的一种,表示十进制表示,这里打印0x12的十进制的值。

%v: 普通占位符的一种,打印相应值的默认格式

切片为0 切片4 舍弃前两个

nil切片 切片的零值是nil。

make内建函数创建切片,也是你创建动态数组的方式。make函数会创建一个元素0值的数组并返回引用它的切片。

向切片追加元素,Go提供了内置的append()函数,新加入的元素加入到切面的末尾。返回的是原始切片的元素以及新追加的元素,当底层数组的大小不足以存储所定的值时,会分配一个更大的数组,我理解类似于动态扩容,返回的切片指向这个新的数组。

func append(s []T, vs ...T) []T

range遍历

利用for循环的range可遍历切片和字典

可以返回两个值,第一个值是数组下标,第二值是元素的副本

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

}

还有下标或者值可以用_忽略,也可以只返回一个值。

6.字典

Java中的Map,是一种键值对存储的形式。比如下面我定义一个Map<String,String>形式的map,在Go可以这样定义:

package main

import "fmt"

var m  map[string]string

func main() {

   m = make(map[string]string)
   m["jack"] = "addr"
   fmt.Println(m["jack"])
}

新增或修改:

m[key] = elem

删除

delete(map,key)

检查key是否存在并获取值

elem, ok = m[key]

存在 获取值 ok

不存在 类型零值 false

7.总结

本篇有些知识点只是做了大概的梳理,并没有很详细深入,考虑到一个循序渐进的过程,先知全貌,再去扣细节,可能理解的会更深,后面会持续的更新与补充。

\