Go 结构体和流程控制

一、if

和 Go 函数一样,if 语句的分支代码块的左大括号与 if 关键字在同一行上,这也是 Go 代码风格的统一要求,gofmt 工具会帮助我们实现这一点;

if 语句的布尔表达式整体不需要用括号包裹,一定程度上减少了开发人员敲击键盘的次数。而且,if 关键字后面的条件判断表达式的求值结果必须是布尔类型,即要么是 true,要么是 false。

1.1 if 语句形式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package main

import "fmt"

//判断两个数字大小
func f1() {
a := 10
b := 15
if a > b {
fmt.Printf("a: %v\n", a)
} else {
fmt.Printf("b: %v\n", b)
}
}

//输入一个数,判断一个数是奇偶数
func f2() {
var num int

fmt.Println("请输入一个数字:")
fmt.Scan(&num)
if num%2 == 0 {
fmt.Printf("num: %v%s\n", num, "偶数")
} else {
fmt.Printf("num: %v%s\n", num, "奇数")
}
}

//输入年龄判断是否未成年
func f3() {
var age int
fmt.Println("请输入年龄:")
fmt.Scan(&age)
if age >= 18 {
fmt.Println("成年人")

} else if age < 18 {
fmt.Println("未成年人")
}
}

func main() {
f1()
f3()
f2()
}

1.2 if 自用变量

if 语句中声明自用变量是 Go 语言的一个惯用法,这种使用方式直观上可以让开发者有一种代码行数减少的感觉,提高可读性。

同时,由于这些变量是 if 语句自用变量,它的作用域仅限于 if 语句的各层隐式代码块中,if 语句外部无法访问和更改这些变量,这就让这些变量具有一定隔离性,这样你在阅读和理解 if 语句的代码时也可以更聚焦。

1
2
3
4
5
6
func main() {
if c, d, e := 5, 9, 3; c < d && (c > e || c > 3) {
fmt.Println(c)
}

}

逻辑表达式中可以含有变量变量或常量;
if句子中允许包含1个(仅1个分号),在分号初始化一些局部变量(即只在if块内可见);

二、switch

Go语言的 switch 要比C语言的更加通用,表达式不需要为常量,甚至不需要为整数,case 按照从上到下的顺序进行求值,直到找到匹配的项,如果 switch 没有表达式,则对 true 进行匹配,因此,可以将 if else-if else 改写成一个 switch。

相对于C语言和 Java 等其它语言来说,Go语言中的 switch 结构使用上更加灵活,语法设计尽量以使用方便为主。

2.1 基本用法

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
color := "black"
switch color {
case "green": //相当于 if color=="green"
println("green")
//当出现多个 case 要放在一起的时候,可以写成下面这样
case "red", "yellow": //相当于else if color == "red" || color == "yellow"
fmt.Println("red or yellow")
default: //相当于else
fmt.Printf("invalid traffic signal: %s\n", strings.ToUpper(color))
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func add(n int) int {
return n + 1
}

func main() {
var a int = 19
const b = 20
switch add(a) {
case 5:
fmt.Println("sum is 5")
case 7, 8, b:
fmt.Println("sum is 7 or 8 or b")
default:
fmt.Println("no")
}
}

2.2 空 switch

switch后带表达式时,switch-case只能模拟相等的情况;如果switch后不带表达式,case后就可以跟任意的条件表达式。

1
2
3
4
5
6
7
8
func main() {
switch {
case 3 > 7 && 5 < 8:
fmt.Println("right")
default:
fmt.Println("wrong")
}
}

2.3 fallthrough

switch中从上往下,只要找到成立的case,就不再执行后面的case了,所以为了提高性能,把大概率会匹配的case放在前面;
case里如果带了fallthrough,则执行完本case还会去判断下一个case是否满足;
在switch type语句的case子句中不能使用fallthrough。

1
2
3
4
5
6
7
8
9
10
func main() {
var s = "hello"
switch {
case s == "hello":
fmt.Println("hello")
fallthrough
case s != "world":
fmt.Println("world")
}
}

输出如下:

1
2
hello
world

2.4 switch type

switch 语句还可以被用于 type-switch 来判断某个 interface 变量中实际存储的变量类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
var num interface{} = 7.5
switch num.(type) {
case int:
fmt.Println("num is int")
case float64:
fmt.Println("num is float64")
case byte, string, bool:
fmt.Println("num is byte or string or bool")
default:
fmt.Println("num 是其它类型")
}
}

三、for 循环

在go中,只有for循环,没有其他循环关键字,没有while循环,也没有do while,通过for循环是可以实现 类似于while的功能

3.1 for 循环基本使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func main() {
// 初始化自语句;条件子语句;后置子语句
var res int
for i := 1; i <= 100; i++ {
res = res + i
}
fmt.Println(res)

// 初始化语句可以放在for前面,后置子语句可以放在for里面
m := 1
for m < 100 {
res = res + 1
m++
}

// 死循环
for true {
res = res + 1
}
for {
res = res + 1
}
for ; ; {
fmt.Println("hello world")
}

}

3.2 for range

  • 遍历数组或切片

    for i, ele := range arr

  • 遍历string

    for i, ele := range “Hello World”

  • 遍历map,go不保证遍历的顺序

    for key, value := range m

  • 遍历channel,遍历前一定要先close

    for ele := range ch

  • for range拿到的是数据的拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
func main() {
// 遍历数组
arr := []int{2, 1, 4, 3, 5}
for i, ele := range arr {
fmt.Printf("%d %d\n", i, ele)
}

// 遍历string
str := "Hello World"
for i, ele := range str {
fmt.Printf("%d %c\n", i, ele)
}

//遍历map
m := map[int]int{1: 1, 2: 2, 3: 3}
for k, v := range m {
fmt.Printf("%d=%d", k, v)
}

// 遍历channel
ch := make(chan int, 10)
for i := 0; i <= 9; i++ {
ch <- i
}
close(ch)
fmt.Println(ch)
for ele := range ch {
fmt.Println(ele)
}
}

冒泡排序

  • 排序就是把一组数据按照特定的顺序重新排列.可以是升序,降序等
  • 冒泡排序利用双重for循环把最大(小)的值移动到一侧,每次可以判断出一个数据,如果有n个数组,执行n-1次循环就可以完成排序
  • 排序代码(升序还是降序主要是看if判断是大于还是小于
1
2
3
4
5
6
7
8
9
10
11
12
13
// 冒泡排序
func main() {
arr01 := [...]int{54,123,11,22,875,124}
for i :=1;i<len(arr01);i++{
//fmt.Println(arr01[i])
for j:=0;j<len(arr01)-i;j++{
if arr01[j] > arr01[j+1]{
arr01[j],arr01[j+1] = arr01[j+1],arr01[j]
}
}
fmt.Println(arr01)
}
}

3.3 break 和 continue

break与continue用于控制for循环的代码流程,并且只针对最靠近自己的外层for循环;

break:跳出循环,break语句用于在结束其正常执行之前突然终止fro循环;

continue:跳出一次循环,continue语句用于跳过for循环的当前迭代,在continue语句后面的for循环中的所有代码将不会在当前迭代中执行,循环将继续到下一个迭代。

break

1
2
3
4
5
6
7
8
9
func main() {
for i := 1; i <= 10; i++ {
if i == 5 {
break
}
fmt.Println(i)
}
fmt.Println("main,over....")
}

continue

1
2
3
4
5
6
7
8
9
func for_continue() {
for i := 1; i <= 10; i++ {
if i == 5 {
continue
}
fmt.Println(i)
}
fmt.Println("main,over....")
}

四、goto 与 label

Go 语言中有 goto 这个功能,这个功能会影响代码的可读性, 会让代码结构看起来比较乱。

for、switch 或 select 语句都可以配合标签(label)形式的标识符使用,即某一行第一个以冒号(:)结尾的单词(gofmt 会将后续代码自动移至下一行)。

1
2
3
4
5
6
7
8
9
10
func main() {
fmt.Println("执行程序")
i := 6
if i == 6 {
goto Loop
}
fmt.Println("if下面输出")
Loop:
fmt.Println("loop")
}

Labal可以有多个,但是标签(Labal)定义了就必须使用

goto也可以用于跳出循环,执行指定标签位置代码

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
for i := 1; i < 5; i++ {
fmt.Println(i)
if i == 2 {
goto L1
}
}
fmt.Println("for循环执行结束")
L1:
fmt.Println("程序结束")

}

break和continue 也可以使用label

1
2
3
4
5
6
7
8
9
10
11
12
func main() {

LABEL1:
for i := 0; i <= 5; i++ {
for j := 0; j <= 5; j++ {
if j == 4 {
continue LABEL1
}
fmt.Printf("i is: %d, and j is: %d\n", i, j)
}
}
}

还可以使用 goto 语句和标签配合使用来模拟循环

1
2
3
4
5
6
7
8
9
10
11
func main() {
i := 0
HERE:
print(i)
i++
if i == 5 {
return
}
goto HERE

}

特别注意 使用标签和 goto 语句是不被鼓励的:它们会很快导致非常糟糕的程序设计,而且总有更加可读的替代方案来实现相同的需求。

五、struct

go语言可以通过自定义的方式形成新的类型,结构体就是这些类型中的一种复合类型,结构体是由一个或多个任意类型的值聚合成的实体,每个值都可以称为结构体的成员。
结构体的成员也可以称为字段,每个字段有如下属性:

  • 字段名必须唯一
  • 字段拥有自己的类型和值
  • 字段的类型也可以是结构体,甚至是字段所在结构体的类型

使用关键字 type 可以将各种基本类型定义为自定义类型,基本类型包括整型、字符串、布尔等。结构体是一种复合的基本类型,通过 type 定义为自定义类型后,使结构体更便于使用。
结构体的定义格式如下:

1
2
3
4
5
type 类型名 struct {
字段1 字段1类型
字段2 字段2类型

}

对各个部分的说明:

  • 类型名:标识自定义结构体的名称,在同一个包内不能重复。
  • struct{}:表示结构体类型,type 类型名 struct{}可以理解为将 struct{} 结构体定义为类型名的类型。
  • 字段1、字段2……:表示结构体字段名,结构体中的字段名必须唯一。
  • 字段1类型、字段2类型……:表示结构体各个字段的类型。

5.1 结构体创建、访问和修改

  • 结构体可以定义在函数内部或函数外部(与普通变量一样),定义位置影响到结构体的访问范围;
  • 如果结构体定义在函数外面,结构体名称首字母大写则结构体能跨包访问;
  • 如果结构体能跨包访问,并且属性首字母大写则属性也能跨包访问。

结构体创建、访问和修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//定义结构体
type user struct {
id int
score float32
enrollment time.Time
name, addr string
}

//声明结构体变量
var u user //声明,会用相应类型的默认值初始化struct里的每一个字段
u = user{} //相应类型的默认值初始化struct里的每一个字段
u = user{id: 3, name: "zhangshan"} //为id和name赋值
u = user{5, 100.0, time.Now(), "zhangshan", "beigin"} //为所有字段赋值时,可以不写字段名,但需要跟结构体定义里的字段顺序一致

//给结构体的成员变量赋值
u.enrollment = time.Now()

//访问结构体的成员变量
fmt.Printf("id=%d,enrollment=%v,name=%s\n", u.id, u.enrollment, u.name)

5.2 成员函数(方法)

1
2
3
4
5
6
7
8
9
10
11
12
13
//可以把user理解为hello函数的参数,即heool(u user, man string)
func (u user) hello(man string) string {
return "hi " + man + " my name is " + u.name
}
//等价于
func hello(man string, user user) string {
return "hi " + man + " my name is " + user.name
}

//函数里不需要访问user的成员,可以传匿名,甚至_也不传
func (user) think(man string) string {
return "hello " + man
}

调用函数方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type user struct {
id int
score float32
enrollment time.Time
name, addr string
}

func main() {
u := user{}
u = user{id: 3, name: "zhangshan"}
fmt.Println(u.hello("zhansan"))
fmt.Println(hello("zhansan", u))
fmt.Println(u.think("zhansan"))
}

输出结果:
hi zhansan my name is zhangshan
hi zhansan my name is zhangshan
hello zhansan

为任意类型添加方法

1
2
3
4
5
6
7
//自定义类型
type UserMap map[int]User

//可以给自定义类型添加任意方法
func (um UserMap) GetUser(id int) User{
return um[id]
}

5.3 匿名结构体

匿名结构体通常用于只使用一次的情况

1
2
3
4
5
6
7
8
//声明一个名为stu的匿名结构体
var stu struct {
Name string
Addr string
}

stu.Name = "a1"
stu.Addr = "b1"

5.4 结构体中含有匿名成员

1
2
3
4
5
6
7
8
9
10
11
type Stuednt struct {
Id int
string //匿名字段
float32 //直接使用数据类型作为字段名,所以匿名字段中不能出现重复的数据类型
}

func main() {
var stu = Stuednt{Id: 1, string: "zhangsan", float32: 65.1}
fmt.Printf("anonymous_member string member=%s float member=%f\n", stu.string, stu.float32)

}

5.5 结构体指针

例如:在成员函数中修改一个的结构体的属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
type Stuednt struct {
name string
}

func (s Stuednt) say(name string) {
s.name = name
}

func main() {
s := Stuednt{"xiaozhu"}
fmt.Println("原来的值:", s.name)

fmt.Println("使用方法1修改")
s.say("xiaomin")
fmt.Println("修改后的值:", s.name)

}

输出结果:
原来的值: xiaozhu
使用方法1修改
修改后的值: xiaozhu

我们创建的s想要调用say,实参是按照值传递的,所以say接收到的是实参的副本,也就是s的复制品。既然是副本,那么我们在say修改形参接收者的属性并不会影响到实参本身的属性

通过指针修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
type Stuednt struct {
name string
}

func (s Stuednt) say(name string) {
s.name = name
}

func main() {
s := Stuednt{"xiaozhu"}
fmt.Println("原来的值:", s.name)

fmt.Println("使用方法1修改")
s.say("xiaomin")
fmt.Println("修改后的值:", s.name)

fmt.Println("使用方法2修改")
s.say2("xiaowang")
// (&s).say2("xiaowang") //等价s.say2("xiaowang")
fmt.Println("修改后的值:", s.name)
}

输出结果:
原来的值: xiaozhu
使用方法1修改
修改后的值: xiaozhu
使用方法2修改
修改后的值: xiaowang

我们使用未修改前的语句时,实参接收者是Student类型的变量,形参接收者是*Student类型的变量,go的编译器会隐式的获取变量的地址, 因此我们可以修改成功

5.6 结构体嵌套

一个结构体中可以嵌套包含另一个结构体或结构体指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//Address 地址结构体
type Address struct {
Province string
City string
}

//User 用户结构体
type User struct {
Name string
Gender string
Address Address
}

func main() {

//直接实例化该嵌套结构体
user1 := User{
Name: "pprof",
Gender: "女",
Address: Address{
Province: "黑龙江",
City: "哈尔滨",
},
}

fmt.Printf("user1=%#v\n", user1)
}

输出结果:
user1=main.User{Name:"pprof", Gender:"女", Address:main.Address{Province:"黑龙江", City:"哈
尔滨"}}

嵌套匿名结构体

当访问结构体成员时会先在结构体中查找该字段,找不到再去匿名结构体中查找。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//Address 地址结构体
type Address struct {
Province string
City string
}

//User 用户结构体
type User struct {
Name string
Gender string
Address //匿名结构体
}

func main() {
var user2 User
user2.Name = "pprof"
user2.Gender = "女"
user2.Address.Province = "黑龙江" //通过匿名结构体.字段名访问
user2.City = "哈尔滨" //直接访问匿名结构体的字段名
fmt.Printf("user2=%#v\n", user2)
}

输出结果:
user2=main.User{Name:"pprof", Gender:"女", Address:main.Address{Province:"黑龙江", City:"哈尔滨"}}

嵌套结构体的字段名冲突

嵌套结构体内部可能存在相同的字段名。这个时候为了避免歧义需要指定具体的内嵌结构体的字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//Address 地址结构体
type Address struct {
Province string
City string
CreateTime string
}

//Email 邮箱结构体
type Email struct {
Account string
CreateTime string
}

//User 用户结构体
type User struct {
Name string
Gender string
Address
Email
}

func main() {
var user3 User
user3.Name = "pprof"
user3.Gender = "女"

//备注:这里便不能直接访问结构体中的字段了
//需要指明结构体才行
// user3.CreateTime = "2019" //ambiguous selector user3.CreateTime
user3.Address.CreateTime = "2000" //指定Address结构体中的CreateTime
user3.Email.CreateTime = "2000" //指定Email结构体中的CreateTime
}

六、深拷贝与浅拷贝

深拷贝(Deep Copy)

拷贝的是数据本身,创造一个样的新对象,新创建的对象与原对象不共享内存,新创建的对象在内存中开辟一个新的内存地址,新对象值修改时不会影响原对象值。既然内存地址不同,释放内存地址时,可分别释放。

值类型的数据,默认全部都是深复制,Array、Int、String、Struct、Float,Bool。

浅拷贝(Shallow Copy)

拷贝的是数据地址,只复制指向的对象的指针,此时新对象和老对象指向的内存地址是一样的,新对象值修改时老对象也会变化。释放内存地址时,同时释放内存地址。

引用类型的数据,默认全部都是浅复制,Slice,Map。

-------------本文结束感谢您的阅读-------------