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
}
细节部分
- else与Java中else区别不大
- 语句执行覆盖
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中指针保存了值的内存地址。
&操作符会生成一个指向其操作数的指针。
做一个测试,验证猜想。
步骤:
- 声明变量i
- 生成一个指向i的指针p
- 打印指针,观察输出
猜测:打印指针,也就是内存地址值。观察输出验证一下
完整代码
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.总结
本篇有些知识点只是做了大概的梳理,并没有很详细深入,考虑到一个循序渐进的过程,先知全貌,再去扣细节,可能理解的会更深,后面会持续的更新与补充。
\