传统方法的缺点分析

  1. 不方便, 我们需要在 main 函数中去调用,这样就需要去修改 main 函数,如果现在项目正在运 行,就可能去停止项目。

  2. 不利于管理,因为当我们测试多个函数或者多个模块时,都需要写在 main 函数,不利于我们管 理和清晰我们思路

  3. 引出单元测试。-> testing 测试框架 可以很好解决问题

单元测试-基本介绍

​ Go 语言中自带有一个轻量级的测试框架 testing 和自带的 go test 命令来实现单元测试和性能测试,testing 框架和其他语言中的测试框架类似,可以基于这个框架写针对相应函数的测试用例,也可以基 于该框架写相应的压力测试用例。通过单元测试,可以解决如下问题:

  1. 确保每个函数是可运行,并且运行结果是正确

  2. 确保写出来的代码性能是好的,

  3. 单元测试能及时的发现程序设计或实现的逻辑错误,使问题及早暴露,便于问题的定位解决, 而性能测试的重点在于发现程序设计上的一些问题,让程序能够在高并发的情况下还能保持稳定

单元测试-快速入门

使用 Go 的单元测试,对 addUpper 和 sub 函数进行测试。

1、cal.go

1
2
3
4
5
6
7
8
9
10
package main

//编写测试函数
func addupper(n int) int {
res := 0
for i := 1; i <= n; i++ {
res += i
}
return res
}

2、cal_test.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import (
"testing"
)

func TestAddUpper(t *testing.T) {
res := addupper(10)
if res != 55 {
t.Fatalf("err 期待值为%v 实际值为%v\n", 55, res)
}
t.Logf("sucess")
}

单元测试快速入门总结

  1. 测试用例文件名必须以 _test.go 结尾。 比如 cal_test.go , cal 不是固定的。

  2. 测试用例函数必须以 Test 开头,一般来说就是 Test+被测试的函数名,比如 TestAddUpper

  3. TestAddUpper(t *tesing.T) 的形参类型必须是 *testing.T 【看一下手册】

  4. 一个测试用例文件中,可以有多个测试用例函数,比如 TestAddUpper、TestSub

  5. 运行测试用例指令

​ (1) cmd>go test [如果运行正确,无日志,错误时,会输出日志]

​ (2) cmd>go test -v [运行正确或是错误,都输出日志]

  1. 当出现错误时,可以使用 t.Fatalf 来格式化输出错误信息,并退出程序

  2. t.Logf 方法可以输出相应的日志

  3. 测试用例函数,并没有放在 main 函数中,也执行了,这就是测试用例的方便之处[原理图].

  4. PASS 表示测试用例运行成功,FAIL 表示测试用例运行失败

  5. 测试单个文件,一定要带上被测试的原文件

1
go test -v cal_test.go cal.go
  1. 测试单个方法
1
go test -v -test.run TestAddUpper

单元测试-综合案例

1、编写一个Minster结构体,字段Name,Age,Skill

2、给Monster绑定方法store,可以将一个Monster变量,序列化后保存到文件中

3、给Monster绑定方法Restore,可以将一个序列化的Monster从文件中读取,并反序列化为Monster对象,检查反序列化

4、变成测试用例文件store_test.go,编写测试用例函数TestStore和TestRestore进行测试

monster.go

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
47
48
49
50
51
package main

import (
"encoding/json"
"fmt"
"io/ioutil"
)

type Monster struct {
Name string
Age int
Skill string
}

//方法名首字母大写,不然调取不到
func (m *Monster) Store() bool {
data, err := json.Marshal(m)
if err != nil {
fmt.Printf("err=%v\n", err)
return false
}

//保存到文件
filePath := "./monster.txt"
err = ioutil.WriteFile(filePath, data, 0666)
if err != nil {
fmt.Printf("err=%v\n", err)
return false
}
return true
}

//给 Monster 绑定方法 ReStore, 可以将一个序列化的 Monster,从文件中读取,
//并反序列化为 Monster 对象,检查反序列化,名字正确
func (m *Monster) Restore() bool {
//先从文件中读取
filePath := "./monster.txt"
data, err := ioutil.ReadFile(filePath)
if err != nil {
fmt.Printf("err=%v\n", err)
return false
}

//反序列化
err = json.Unmarshal(data, m)
if err != nil {
fmt.Printf("err=%v\n", err)
return false
}
return true
}

monstore_test.go

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
package main

import (
"testing"
)

func TestStore(t *testing.T) {
monster := &Monster{
Name: "hello",
Age: 12,
Skill: "hello~~~",
}

res := monster.Store()
//如果res为假则为失败
if !res {
t.Fatalf("期待值为%v 实际值为%v\n", true, res)
}
t.Logf("success")
}

func TestRestore(t *testing.T) {
//先创建一个 Monster 实例 , 不需要指定字段的值
monster := &Monster{}
res := monster.Restore()
//如果res为假则为失败
if !res {
t.Fatalf("期待值为%v 实际值为%v\n", true, res)
}

if monster.Name != "hello" {
t.Fatalf("期待值为%v 实际值为%v\n", "hello", monster.Name)
}
t.Logf("success")
}

简介

​ Json是一种轻量级的数据交换格式,目前已经成为最主流的数据格式。

​ Json易于机器解析和生成,并有效地提升网络传输效率,通常程序在网络传输时会先将数据(结构体、map、切片等)序列化成json字符串,到接收方得到json字符串时,在反序列化恢复成原来的数据类型。

json 的序列化

介绍

json 序列化是指,将有 key-value 结构的数据类型(比如结构体、map、切片)序列化成 json 字符串 的操作。

应用案例

这里我们介绍一下结构体、map 和切片的序列化,其它数据类型的序列化类似。

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
package main

import (
"encoding/json"
"fmt"
)

//定义一个结构体
type Monster struct {
Name string `json:"monster_name"` //反射机制,给字段tag
Age int
Adress string
}

func testStruct() {
monster := Monster{
Name: "tom",
Age: 12,
Adress: "杭州",
}

//将monster序列化
data, err := json.Marshal(&monster)
if err != nil {
fmt.Printf("err=%v\n", err)
}
fmt.Printf("序列化后为%v\n", string(data)) //需要转化为string
}

//将map进行序列化,map需要先进行make
func testMap() {
//定义一个map
var a map[string]interface{}
a = make(map[string]interface{})
a["name"] = "jack"
a["age"] = 12
a["adress"] = "杭州"

//将map进行序列化
data, err := json.Marshal(a)
if err != nil {
fmt.Printf("err=%v\n", err)
}
fmt.Printf("序列化后%v\n", string(data))
}

//将切片进行序列化
func testSlice() {
var slice []map[string]interface{}
var m map[string]interface{}
m = make(map[string]interface{})
m["name"] = "kali"
m["age"] = 12
m["adress"] = "成都"
slice = append(slice, m)

//将切片进行序列化
data, err := json.Marshal(m)
if err != nil {
fmt.Printf("err=%v\n", err)
}
fmt.Printf("序列化后%v\n", string(data))
}

func main() {
testStruct()
testMap()
testSlice()
}
  • 对基本数据类型进行序列化的意义不大
  • 对于结构体的序列化,如果我们希望序列化后的 key的名字,又我们自己重新制定,那么可以给struct 指定一个 tag 标签

json的反序列化

json 反序列化是指,将 json 字符串反序列化成对应的数据类型(比如结构体、map、切片)的操作

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
47
48
49
50
51
52
53
54
55
package main

import (
"encoding/json"
"fmt"
)

//定义一个结构体
type Monster struct {
Name string
Age int
Adress string
}

//反序列化struct
func unstruct() {
str := "{\"Name\":\"tom\",\"Age\":12,\"Adress\":\"杭州\"}"
var monster Monster
err := json.Unmarshal([]byte(str), &monster)
if err != nil {
fmt.Printf("err=%v\n", err)
}
fmt.Printf("反序列化后=%v\n", monster)
}

//反序列化map
func unmap() {
str := "{\"Name\":\"tom~\",\"Age\":11,\"Adress\":\"杭州~\"}"
var a map[string]interface{}
err := json.Unmarshal([]byte(str), &a)
if err != nil {
fmt.Printf("err=%v\n", err)
}
fmt.Printf("反序列化后=%v\n", a)
}

//将slice反序列化
func unSlice() {
str := "[{\"address\":\"北京\",\"age\":8,\"name\":\"tom\"}]"

//定义一个slice
var m []map[string]interface{}
//注意:反序列化map,不需要make,因为make操作被封装到Unmarshal函数
err := json.Unmarshal([]byte(str), &m)
if err != nil {
fmt.Printf("unmarshal err=%v\n", err)
}
fmt.Printf("反序列化后 slice=%v\n", m)
}

func main() {
unstruct()
unmap()
unSlice()
}

备注:

  1. 在反序列化一个json 字符串时,要确保反序列化后的数据类型和原来序列化前的数据类型一致。

  2. 如果 json 字符串是通过程序获取到的,则不需要再对 “ 转义处理。

channel 的基本介绍

  1. channle 本质就是一个数据结构-队列【示意图】

  2. 数据是先进先出【FIFO : first in first out】

  3. 线程安全,多 goroutine 访问时,不需要加锁,就是说 channel 本身就是线程安全的

  4. channel 有类型的,一个 string 的 channel 只能存放 string 类型数据。

  5. 示意图:

image-20210104155905223

定义/声明 channel

1
2
3
4
5
6
7
8
var 变量名 chan 数据类型

举例:
var intChan chan int (intChan 用于存放 int 数据)
var mapChan chan map[int]string (mapChan 用于存放 map[int]string 类型)
var perChan chan Person
var perChan2 chan *Person
...

说明

channel 是引用类型 ,channel 必须初始化才能写入数据, 即 make 后才能使用 ,管道是有类型的,intChan 只能写入 整数 int

案例演示:

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
package main

import (
"fmt"
)

func main() {

//创建一个能够存放3个int类型的管道
var intChan chan int
intChan = make(chan int, 3)

//查看intChan是什么
fmt.Printf("值是%v 地址是%v\n", intChan, &intChan)

//向管道写入数据
intChan <- 10
num := 222
intChan <- num //写入数据不能超过管道的容量

//查看管道的容cap和长度
fmt.Printf("len=%v cap=%v\n", len(intChan), cap(intChan))

//从管道中读取数据
num2 := <-intChan
num3 := <-intChan
fmt.Println(num2, num3)

}

channel 使用的注意事项

  1. channel 中只能存放指定的数据类型

  2. channle 的数据放满后,就不能再放入了

  3. 如果从 channel 取出数据后,可以继续放入

  4. 在没有使用协程的情况下,如果 channel 数据取完了,再取,就会报 dead lock

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
package main

import (
"fmt"
)

func main() {

//创建一个能够存放3个int类型的管道
var mapChan chan map[string]string
mapChan = make(chan map[string]string, 10)

//map类型需要先make
m1 := make(map[string]string, 4)
m1["hero1"] = "h"
m1["hero2"] = "j"

m2 := make(map[string]string, 10)
m2["city1"] = "beijing"
m2["city2"] = "hangzhou"

mapChan <- m1
mapChan <- m2

m3 := <-mapChan
m4 := <-mapChan
fmt.Printf("m3=%v m4=%v\n", m3, m4)
}

channel关闭

使用内置函数 close 可以关闭 channel, 当 channel 关闭后,就不能再向 channel 写数据了,但是仍然 可以从该 channel 读取数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
"fmt"
)

func main() {
intChan := make(chan int, 3)
intChan <- 100
intChan <- 101
intChan <- 102
//当管道关闭后,不能再写入数据,但能继续读取
close(intChan)
//intChan <- 103

num1 := <-intChan
num2 := <-intChan
fmt.Printf("num1=%v num2=%v\n", num1, num2)
}

channel 的遍历

channel 支持 for–range 的方式进行遍历,请注意两个细节

  1. 在遍历时,如果 channel 没有关闭,则回出现 deadlock 的错误

  2. 在遍历时,如果 channel 已经关闭,则会正常遍历数据,遍历完后,就会退出遍历。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
"fmt"
)

func main() {
intChan := make(chan int, 100)
//管道中放入100个数
for i := 1; i <= 100; i++ {
intChan <- i
}
close(intChan) //在遍历时,如果 channel 没有关闭,则回出现 deadlock 的错误

//遍历管道
for v := range intChan {
fmt.Printf("v=%v\n", v)
}
}

应用实例

要求:

  1. 开启一个writeData协程,向管道intChan中写入50个整数
  2. 开启一个readData协程,从管道intChan中读取writeData写入的数据
  3. writeData和readData操作的是同一个管道
  4. 主线程需要等待writeData和readData操作完成都才能退出
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
package main

import (
"fmt"
)

//编写writeData协程
func writeData(intChan chan int) {
for i := 1; i <= 50; i++ {
intChan <- i
fmt.Printf("i=%v\n", i)
}
close(intChan)
}

//编写readData协程
func readData(intChan chan int, exitChan chan bool) {
for v := range intChan {
fmt.Printf("v=%v\n", v)
}
exitChan <- true
close(exitChan)
}

func main() {
//创建两个管道
intChan := make(chan int, 50)
exitChan := make(chan bool, 1)

go writeData(intChan)
go readData(intChan, exitChan)

for v := range exitChan {
if v != true {
break
}
}
}

channel 使用细节和注意事项

  1. channel 可以声明为只读,或者只写性质
1
2
var  intChan <-chan int
var intChan chan<- int
  1. 使用 select 可以解决从管道取数据的阻塞问题
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
package main

import (
"fmt"
"time"
)

func main() {
intChan := make(chan int, 10)
for i := 1; i <= 10; i++ {
intChan <- i
}

stringChan := make(chan string, 100)
for i := 6; i <= 16; i++ {
stringChan <- "12345" + fmt.Sprintf("%d", i)
}

//传统方法遍历管道时会报错
// for v := range intChan {
// fmt.Printf("intchan=%v\n", v)
// }

// for v := range stringChan {
// fmt.Printf("stringchan=%v\n", v)
// }

//使用select能避免这个问题
for {
select {
case v := <-intChan:
fmt.Printf("intchan=%d\n", v)
time.Sleep(time.Second)
case v := <-stringChan:
fmt.Printf("stringchan=%s\n", v)
time.Sleep(time.Second)
default:
fmt.Println("读取完毕!!!")

return
}
}
}

一、goroutine简介

​ goroutine是go语言中最为NB的设计,也是其魅力所在,goroutine的本质是协程,是实现并行计算的核心。goroutine使用方式非常的简单,只需使用go关键字即可启动一个协程,并且它是处于异步方式运行,你不需要等它运行完成以后在执行以后的代码。

二、goroutine内部原理

概念介绍

在进行实现原理之前,了解下一些关键性术语的概念。

并发

​ 一个cpu上能同时执行多项任务,在很短时间内,cpu来回切换任务执行(在某段很短时间内执行程序a,然后又迅速得切换到程序b去执行),有时间上的重叠(宏观上是同时的,微观仍是顺序执行),这样看起来多个任务像是同时执行,这就是并发。

并行

当系统有多个CPU时,每个CPU同一时刻都运行任务,互不抢占自己所在的CPU资源,同时进行,称为并行。

进程

​ cpu在切换程序的时候,如果不保存上一个程序的状态(也就是我们常说的context–上下文),直接切换下一个程序,就会丢失上一个程序的一系列状态,于是引入了进程这个概念,用以划分好程序运行时所需要的资源。因此进程就是一个程序运行时候的所需要的基本资源单位(也可以说是程序运行的一个实体)。

线程

​ cpu切换多个进程的时候,会花费不少的时间,因为切换进程需要切换到内核态,而每次调度需要内核态都需要读取用户态的数据,进程一旦多起来,cpu调度会消耗一大堆资源,因此引入了线程的概念,线程本身几乎不占有资源,他们共享进程里的资源,内核调度起来不会那么像进程切换那么耗费资源。

协程

​ 协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此,协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。线程和进程的操作是由程序触发系统接口,最后的执行者是系统;协程的操作执行者则是用户自身程序,goroutine也是协程。

调度模型简介

groutine能拥有强大的并发实现是通过GPM调度模型实现,下面就来解释下goroutine的调度模型。

img

Go的调度器内部有四个重要的结构:M,P,S,Sched,如上图所示(Sched未给出)

  • M:M代表内核级线程,一个M就是一个线程,goroutine就是跑在M之上的;M是一个很大的结构,里面维护小对象内存cache(mcache)、当前执行的goroutine、随机数发生器等等非常多的信息
  • G:代表一个goroutine,它有自己的栈,instruction pointer和其他信息(正在等待的channel等等),用于调度。
  • P:P全称是Processor,处理器,它的主要用途就是用来执行goroutine的,所以它也维护了一个goroutine队列,里面存储了所有需要它来执行的goroutine

Sched:代表调度器,它维护有存储M和G的队列以及调度器的一些状态信息等。

调度实现

img

​ 从上图中看,有2个物理线程M,每一个M都拥有一个处理器P,每一个也都有一个正在运行的goroutine。P的数量可以通过GOMAXPROCS()来设置,它其实也就代表了真正的并发度,即有多少个goroutine可以同时运行。图中灰色的那些goroutine并没有运行,而是出于ready的就绪态,正在等待被调度。P维护着这个队列(称之为runqueue),Go语言里,启动一个goroutine很容易:go function 就行,所以每有一个go语句被执行,runqueue队列就在其末尾加入一个goroutine,在下一个调度点,就从runqueue中取出(如何决定取哪个goroutine?)一个goroutine执行。

当一个OS线程M0陷入阻塞时(如下图),P转而在运行M1,图中的M1可能是正被创建,或者从线程缓存中取出。

img

​ 当MO返回时,它必须尝试取得一个P来运行goroutine,一般情况下,它会从其他的OS线程那里拿一个P过来,如果没有拿到的话,它就把goroutine放在一个global runqueue里,然后自己睡眠(放入线程缓存里)。所有的P也会周期性的检查global runqueue并运行其中的goroutine,否则global runqueue上的goroutine永远无法执行。

​ 另一种情况是P所分配的任务G很快就执行完了(分配不均),这就导致了这个处理器P很忙,但是其他的P还有任务,此时如果global runqueue没有任务G了,那么P不得不从其他的P里拿一些G来执行。一般来说,如果P从其他的P那里要拿任务的话,一般就拿run queue的一半,这就确保了每个OS线程都能充分的使用,如下图:

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
"fmt"
"strconv"
"time"
)

func test() {
res := 0
for i := 1; i <= 10; i++ {
res += i
fmt.Printf("res=%v\n", res)
time.Sleep(time.Second) //每隔一秒
}
}

func main() {
go test() //开启一个协程
for i := 1; i <= 10; i++ {
fmt.Println("hello world" + strconv.Itoa(i))
time.Sleep(time.Second)
}
}

抽象

抽象的介绍

定义一个结构体时候,实际上就是把一类事物的共有的属性(字段)和行为(方法)提取出来,形成一个物理模型(结构体)。

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
47
48
49
50
51
52
53
54
55
56
57
58
59
package main

import (
"fmt"
)

//银行账号都会有账户名,密码,余额
//应有的功能:查询,取款,存款
type Account struct {
AccountNo string
Pwd string
Balance float64 //余额
}

//存款
func (account *Account) Deposite(money float64, pwd string) {
if money < 0 {
fmt.Println("金额有误")
return
}
if pwd != account.Pwd {
fmt.Println("密码输入有误")
return
}
account.Balance += money
fmt.Println("存款成功")
}

//取款
func (account *Account) WithDraw(money float64, pwd string) {
if money < 0 || money > account.Balance {
fmt.Println("金额有误")
return
}
if pwd != account.Pwd {
fmt.Println("密码输入有误")
return
}
account.Balance -= money
fmt.Printf("取款成功,余额为%v元\n", account.Balance)
}

//查询
func (account *Account) Query(pwd string) {
if pwd != account.Pwd {
fmt.Println("密码输入有误")
return
}
fmt.Println("余额为=", account.Balance)
}

func main() {
account := Account{
AccountNo: "icbc",
Pwd: "123456",
Balance: 110,
}
account.WithDraw(33, "123456")
}

封装

封装介绍

封装(encapsulation)就是把抽象出的字段和对字段的操作封装在一起,数据被保护在内部,程序的其它包只有通过被授权的操作(方法),才能对字段进行操作

封装的理解和好处

  1. 隐藏实现细节

  2. 提可以对数据进行验证,保证安全合理(Age)

如何体现封装

  1. 对结构体中的属性进行封装

  2. 通过方法,包 实现封装

封装的实现步骤

  1. 将结构体、字段(属性)的首字母小写(不能导出了,其它包不能使用,类似 private)

  2. 给结构体所在包提供一个工厂模式的函数,首字母大写。类似一个构造函数

  3. 提供一个首字母大写的 Set 方法(类似其它语言的 public),用于对属性判断并赋值

1
2
3
4
func (var 结构体类型名) SetXxx(参数列表) (返回值列表) {
//加入数据验证的业务逻辑
var.字段 = 参数
}
  1. 提供一个首字母大写的 Get 方法(类似其它语言的 public),用于获取属性的值
1
2
3
func (var 结构体类型名) GetXxx() {
return var.age;
}

特别说明:在 Golang 开发中并没有特别强调封装,这点并不像 Java. 所以提醒学过 java 的朋友,不用总是用 java 的语法特性来看待 Golang, Golang 本身对面向对象的特性做了简化的

案例:一个程序(person.go),不能随便查看人的年龄,工资等隐私,并对输入的年龄进行合理的验证。设计: model 包(person.go) main 包(main.go 调用 Person 结构体)

一、

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
package person

import (
"fmt"
)

type person struct {
Name string
age int
sal float64 //工资
}

//写一个工厂模式函数
func NewPerson(name string) *person {
return &person{
Name: name,
}
}

//编写两个方法访问age和sal
func (p *person) SetAge(age int) {
if age > 0 && age < 150 {
p.age = age
} else {
fmt.Println("输入年龄有误")
}
}

func (p *person) GetAge() int {
return p.age
}

func (p *person) SetSal(sal float64) {
if sal > 3000 && sal < 20000 {
p.sal = sal
} else {
fmt.Println("薪资输入有误")
}
}

func (p *person) GetSal() float64 {
return p.sal
}

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import (
"fmt"
"hello2/person"
)

func main() {
p := person.NewPerson("tom")
p.SetAge(123)
p.SetSal(12342)
fmt.Println("name=", p.Name, "age=", p.GetAge(), "sal=", p.GetSal())
}

二、

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
package model

import (
"fmt"
)

//1) 创建程序,在 model 包中定义 Account 结构体:在 main 函数中体会 Golang 的封装性。
//2) Account 结构体要求具有字段:账号(长度在 6-10 之间)、余额(必须>20)、密码(必须是六位数)
//定义一个结构体
type account struct {
accountNo string
pwd string
balance float64 //余额
}

//工厂模式
func NewAccount(accountNo string, pwd string, balance float64) *account {
if len(accountNo) < 6 || len(accountNo) > 10 {
fmt.Println("账号输入有误")
return nil
}

if len(pwd) != 6 {
fmt.Println("密码输入有误")
return nil
}

if balance < 20 {
fmt.Println("金额输入有误")
return nil
}
return &account{
accountNo: accountNo,
pwd: pwd,
balance: balance,
}
}

//存款
func (account *account) Deposite(money float64, pwd string) {
if money < 0 {
fmt.Println("金额有误")
return
}
if pwd != account.pwd {
fmt.Println("密码输入有误")
return
}
account.balance += money
fmt.Println("存款成功")
}

//取款
func (account *account) WithDraw(money float64, pwd string) {
if money < 0 || money > account.balance {
fmt.Println("金额有误")
return
}
if pwd != account.pwd {
fmt.Println("密码输入有误")
return
}
account.balance -= money
fmt.Printf("取款成功,余额为%v元\n", account.balance)
}

//查询
func (account *account) Query(pwd string) {
if pwd != account.pwd {
fmt.Println("密码输入有误")
return
}
fmt.Println("余额为=", account.balance)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import (
"fmt"
"hello1/model"
)

func main() {
account := model.NewAccount("asadaf", "142356", 125)
if account != nil {
fmt.Println("创建成功")
} else {
fmt.Println("error")
}
}

继承

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package main

import (
"fmt"
)

//小学生
type Pupil struct {
Name string
Age int
Score int
}

func (p *Pupil) ShowInfo() {
fmt.Printf("name=%v age=%v acore=%v\n", p.Name, p.Age, p.Score)
}

func (p *Pupil) SetInfo(score int) {
p.Score = score
}

func (p *Pupil) testing() {
fmt.Println("小学生正在进行考试")
}

//大学生
type Graduate struct {
Name string
Age int
Score int
}

func (p *Graduate) ShowInfo() {
fmt.Printf("name=%v age=%v acore=%v\n", p.Name, p.Age, p.Score)
}

func (p *Graduate) SetInfo(score int) {
p.Score = score
}

func (p *Graduate) testing() {
fmt.Println("大学生正在进行考试")
}

func main() {
pupil := &Pupil{
Name: "tom",
Age: 12,
Score: 112,
}
pupil.testing()
pupil.SetInfo(100)
pupil.ShowInfo()

graduate := &Graduate{
Name: "kali",
Age: 110,
Score: 10,
}
graduate.testing()
graduate.SetInfo(10)
graduate.ShowInfo()
}

对上面代码的小结

  1. Pupil 和 Graduate 两个结构体的字段和方法几乎,但是我们却写了相同的代码, 代码复用性不强

  2. 出现代码冗余,而且代码不利于维护,同时也不利于功能的扩展。

  3. 解决方法-通过继承方式来解决

继承基本介绍

继承可以解决代码复用,让我们的编程更加靠近人类思维。

当多个结构体存在相同的属性(字段)和方法时,可以从这些结构体中抽象出结构体(比如刚才的Student),在该结构体中定义这些相同的属性和方法。

其它的结构体不需要重新定义这些属性(字段)和方法,只需嵌套一个 Student 匿名结构体即可。在 Golang 中,如果一个 struct 嵌套了另一个匿名结构体,那么这个结构体可以直接访问匿名结构体的字段和方法,从而实现了继承特性

嵌套匿名结构体的基本语法

1
2
3
4
5
6
7
8
type Goods struct {
Name string
Price int
}
type Book struct {
Goods //这里就是嵌套匿名结构体 Goods
Writer string
}

实例:对上面代码进行改进

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
47
48
49
50
51
52
53
package main

import (
"fmt"
)

type Student struct {
Name string
Age int
Score int
}

//将 Pupil 和 Graduate 共有的方法也绑定到 *Student
func (stu *Student) ShowInfo() {
fmt.Printf("name=%v age=%v acore=%v\n", stu.Name, stu.Age, stu.Score)
}

func (stu *Student) SetInfo(score int) {
stu.Score = score
}

type Pupil struct {
Student
}

func (p *Pupil) testing() {
fmt.Println("小学生正在进行考试")
}

//大学生
type Graduate struct {
Student
}

func (p *Graduate) testing() {
fmt.Println("大学生正在进行考试")
}

func main() {
pupil := &Pupil{}
pupil.Student.Name = "tom~"
pupil.Student.Age = 12
pupil.testing()
pupil.Student.SetInfo(10)
pupil.Student.ShowInfo()

graduate := &Graduate{}
graduate.Student.Name = "keli~"
graduate.Student.Age = 100
graduate.testing()
graduate.Student.SetInfo(1)
graduate.Student.ShowInfo()
}

继承给编程带来的便利

  1. 代码的复用性提高了

  2. 代码的扩展性和维护性提高了

继承的深入讨论

1、结构体可以使用嵌套匿名结构体所有的字段和方法,即:首字母大写或者小写的字段、方法,都可以使用。

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
package main

import (
"fmt"
)

type A struct {
Name string
age int
}

func (a *A) Sayok() {
fmt.Println("A Sayok", a.Name)
}

func (a *A) hello() {
fmt.Println("A hello", a.Name)
}

type B struct {
A
}

func main() {
var b B
b.A.Name = "tom!"
b.A.age = 11
b.A.Sayok()
b.A.hello()
}

2、匿名结构体字段访问可以简化

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
package main

import (
"fmt"
)

type A struct {
Name string
age int
}

func (a *A) Sayok() {
fmt.Println("A Sayok", a.Name)
}

func (a *A) hello() {
fmt.Println("A hello", a.Name)
}

type B struct {
A
}

func main() {
var b B
b.Name = "tom!"
b.age = 11
b.Sayok()
b.hello()
}

对上面的代码小结

(1) 当我们直接通过 b 访问字段或方法时,其执行流程如下比如 b.Name

(2) 编译器会先看 b 对应的类型有没有 Name, 如果有,则直接调用 B 类型的 Name 字段

(3) 如果没有就去看 B 中嵌入的匿名结构体 A 有没有声明 Name 字段,如果有就调用,如果没有继续查找..如果都找不到就报错

3、当结构体和匿名结构体有相同的字段或者方法时,编译器采用就近访问原则访问,如希望访问匿名结构体的字段和方法,可以通过匿名结构体名来区分

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
package main

import (
"fmt"
)

type A struct {
Name string
age int
}

func (a *A) Sayok() {
fmt.Println("A Sayok", a.Name)
}

func (a *A) hello() {
fmt.Println("A hello", a.Name)
}

type B struct {
A
Name string
}

func (b *B) Sayok() {N
fmt.Println("B Sayok", b.Name)
}

func (b *B) hello() {
fmt.Println("B hello", b.Name)
}

func main() {
var b B
b.Name = "hello"
//b.A.Name = "tom~"
b.age = 11
b.Sayok()
b.hello()
b.A.Sayok()
b.A.hello()
}

4、 结构体嵌入两个(或多个)匿名结构体,如两个匿名结构体有相同的字段和方法(同时结构体本身没有同名的字段和方法),在访问时,就必须明确指定匿名结构体名字,否则编译报错

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
package main

import (
"fmt"
)

type A struct {
Name string
age int
}

type B struct {
Score int
Name string
}

type C struct {
A
B
}

func main() {
var c C
c.A.Name = "tom"
c.B.Name = "hello"
fmt.Println(c)
}

5、 如果一个 struct 嵌套了一个有名结构体,这种模式就是组合,如果是组合关系,那么在访问组合的结构体的字段或方法时,必须带上结构体的名字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"fmt"
)

type A struct {
Name string
age int
}

type C struct {
a A
}

func main() {
var c C
c.a.Name = "tom"
fmt.Println(c)
}

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package main

import (
"fmt"
)

type Goods struct {
Name string
Price float64
}

type Brand struct {
Name string
Address string
}

type TV struct {
Goods
Brand
}

type TV2 struct {
*Goods
*Brand
}

func main() {
tv := TV{Goods{"电视机001", 3000.22}, Brand{"海尔", "上单"}}
tv2 := TV{
Goods{
Price: 3222.33,
Name: "电视机002",
},
Brand{
Name: "伤害",
Address: "黑背",
},
}
fmt.Println(tv, tv2)

tv3 := &TV{Goods{"电视机003", 300.22}, Brand{"尔", "上"}}
tv4 := &TV{
Goods{
Price: 322.33,
Name: "电视机004",
},
Brand{
Name: "伤",
Address: "黑",
},
}
fmt.Println(*tv3, *tv4)
}

结构体的匿名字段是基本数据类型

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
package main

import (
"fmt"
)

type Monster struct {
Name string
Age int
}

type E struct {
Monster
int
n int
}

func main() {
var e E
e.Name = "hhello"
e.Age = 200
e.int = 1
e.n = 2
fmt.Println(e)
}

说明

  1. 如果一个结构体有 int 类型的匿名字段,就不能第二个。

  2. 如果需要有多个 int 的字段,则必须给 int 字段指定名字

面向对象编程-多重继承

如一个 struct 嵌套了多个匿名结构体,那么该结构体可以直接访问嵌套的匿名结构体的字段和方法,从而实现了多重继承

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
package main

import (
"fmt"
)

type Goods struct {
Name string
Price float64
}

type Brand struct {
Name string
Address string
}

type TV struct {
Goods
Brand
}

func main() {
var tv TV
tv.Brand.Name = "hello1"
tv.Goods.Name = "hello2"
fmt.Println(tv)
}

多重继承细节说明

  1. 如嵌入的匿名结构体有相同的字段名或者方法名,则在访问时,需要通过匿名结构体类型名来区分。

  2. 为了保证代码的简洁性,建议大家尽量不使用多重继承

Go语言不是一种 “传统” 的面向对象编程语言:它里面没有类和继承的概念。

​ 但是Go语言里有非常灵活的接口概念,通过它可以实现很多面向对象的特性。很多面向对象的语言都有相似的接口概念,但Go语言中接口类型的独特之处在于它是满足隐式实现的。也就是说,我们没有必要对于给定的具体类型定义所有满足的接口类型;简单地拥有一些必需的方法就足够了。

​ 这种设计可以让你创建一个新的接口类型满足已经存在的具体类型却不会去改变这些类型的定义;当我们使用的类型来自于不受我们控制的包时这种设计尤其有用。

​ 接口类型是对其它类型行为的抽象和概括;因为接口类型不会和特定的实现细节绑定在一起,通过这种抽象的方式我们可以让我们的函数更加灵活和更具有适应能力。

​ 接口是双方约定的一种合作协议。接口实现者不需要关心接口会被怎样使用,调用者也不需要关心接口的实现细节。接口是一种类型,也是一种抽象结构,不会暴露所含数据的格式、类型及结构。

接口声明的格式

每个接口类型由数个方法组成。接口的形式代码如下:

1
2
3
4
5
type 接口类型名 interface{
方法名1( 参数列表1 ) 返回值列表1
方法名2( 参数列表2 ) 返回值列表2

}

对各个部分的说明:

  • 接口类型名:使用 type 将接口定义为自定义的类型名。Go语言的接口在命名时,一般会在单词后面添加 er,如有写操作的接口叫 Writer,有字符串功能的接口叫 Stringer,有关闭功能的接口叫 Closer 等。
  • 方法名:当方法名首字母是大写时,且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
  • 参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以被忽略,例如:
1
2
3
type writer interface{
Write([]byte) error
}

实例

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
47
48
49
50
51
52
package main

import (
"fmt"
)

//申明接口
type Usb interface {
Start()
Stop()
}

type Phone struct {
}

//让Phone实现Usb接口的方法
func (p Phone) Start() {
fmt.Println("手机开始工作")
}
func (p Phone) Stop() {
fmt.Println("手机停止工作")
}

type Camera struct {
}

func (c Camera) Start() {
fmt.Println("相机开始工作")
}
func (c Camera) Stop() {
fmt.Println("相机停止工作")
}

type Computer struct {
}

//编写一个working方法,接收一个Usb接口变量
//变量会根据传入的实参,来判断到底是Phone,还是Camera
func (c Computer) Working(usb Usb) {
usb.Start()
usb.Stop()
}

func main() {
//创建结构体变量
computer := Computer{}
phone := Phone{}
camera := Camera{}

computer.Working(phone)
computer.Working(camera)
}

小结说明:

  1. 接口里的所有方法都没有方法体,即接口的方法都是没有实现的方法。接口体现了程序设计的多态和高内聚低偶合的思想。

  2. Golang 中的接口,不需要显式的实现。只要一个变量,含有接口类型中的所有方法,那么这个变量就实现这个接口。因此,Golang 中没有 implement 这样的关键字

注意事项和细节

  1. 接口本身不能创建实例,但是可以指向一个实现了该接口的自定义类型的变量(实例)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
"fmt"
)

type AInterfacr interface {
Say()
}

type Stu struct {
Name string
}

func (s Stu) Say() {
fmt.Println("stu say")
}

func main() {
var stu Stu
var a AInterfacr = stu
a.Say()
}
  1. 接口中所有的方法都没有方法体,即都是没有实现的方法。

  2. 在 Golang 中,一个自定义类型需要将某个接口的所有方法都实现,我们说这个自定义类型实现了该接口。

  3. 只要是自定义数据类型,就可以实现接口,不仅仅是结构体类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
"fmt"
)

type AInterface interface {
Say()
}

type integer int

func (i integer) Say() {
fmt.Println("integer say i =", i)
}

func main() {

var i integer = 20
var b AInterface = i
b.Say()
}
  1. 一个自定义类型可以实现多个接口
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
package main

import (
"fmt"
)

type AInterface interface {
Say()
}

type BInterface interface {
Hello()
}

type Monster struct {
}

func (m Monster) Say() {
fmt.Println("Monster say~")
}

func (m Monster) Hello() {
fmt.Println("Monster hello~")
}

func main() {

var monster Monster
var n1 AInterface = monster
var n2 BInterface = monster
n1.Say()
n2.Hello()
}
  1. Golang 接口中不能有任何变量

  2. 一个接口(比如 A 接口)可以继承多个别的接口(比如 B,C 接口),这时如果要实现 A 接口,也必须将 B,C 接口的方法也全部实现。

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
package main

type BInterface interface {
test01()
}

type CInterface interface {
test02()
}

type AInterface interface {
BInterface
CInterface
test03()
}

type Stu struct {
}

func (s Stu) test01() {
}

func (s Stu) test02() {
}

func (s Stu) test03() {
}

func main() {
var stu Stu
var a AInterface = stu
a.test03()
}
  1. interface 类型默认是一个指针(引用类型),如果没有对 interface 初始化就使用,那么会输出 nil

  2. 空接口 interface{} 没有任何方法,所以所有类型都实现了空接口, 即我们可以把任何一个变量赋给空接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "fmt"

type T interface {
}

type Stu struct {
}

func main() {
var stu Stu
var t T = stu
fmt.Println(t)
var num1 float64 = 9.8
var t2 interface{} = stu
t2 = num1
t = num1
fmt.Println(t2, t)
}

最佳实践

实现对 Hero 结构体切片的排序: sort.Sort(data Interface)

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
47
48
49
50
51
52
53
54
55
56
57
58
59
package main

import (
"fmt"
"math/rand"
"sort"
)

//声明Hero结构体
type Hero struct {
Name string
Age int
}

//声明一个Hero结构体切片类型
type HeroSlice []Hero

//实现Interface接口
func (hs HeroSlice) Len() int {
return len(hs)
}

//Less方法就是决定使用什么标准进行排序
//i < j 从小到大,反之从大到小
func (hs HeroSlice) Less(i, j int) bool {
return hs[i].Age < hs[j].Age
}

func (hs HeroSlice) Swap(i, j int) {
hs[i], hs[j] = hs[j], hs[i]
}

func main() {
//先定义一个数组/切片
// var intSlice = []int{2, 44, 55, 66, 1, 3}
// //对intSlice切片进行排序
// sort.Ints(intSlice)
// fmt.Println(intSlice)

//对结构体切片进行排序
var heroes HeroSlice //变量heroes属于结构体切片类型
for i := 0; i < 8; i++ {
hero := Hero{
Name: fmt.Sprintf("hello~~~%d", rand.Intn(50)),
Age: rand.Intn(145), //返回一个取值范围在[0,n)的伪随机int值,如果n<=0会panic
}
heroes = append(heroes, hero)
}
fmt.Println("-----------排序前-----------")
for _, v := range heroes {
fmt.Println(v)
}
sort.Sort(heroes) //调用sort进行排序

fmt.Println("----------排序后------------")
for _, v := range heroes {
fmt.Println(v)
}
}

实现接口 vs 继承

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
package main

import (
"fmt"
)

//定义接口
type Bird interface {
Flying()
}

type Fish interface {
Swimming()
}

//Monkey结构体
type Monkey struct {
Name string
}

func (m *Monkey) slimbing() {
fmt.Println(m.Name, "天生会爬树")
}

type LittleMonkey struct {
Monkey
}

func (l *LittleMonkey) Flying() {
fmt.Println(l.Name, "通过学习会飞翔、、、")
}

func (l *LittleMonkey) Swimming() {
fmt.Println(l.Name, "通过学习会游泳")
}

func main() {
var monkey = LittleMonkey{Monkey{Name: "wukong"}}
var f Fish = &monkey
var b Bird = &monkey //LittleMonkey是指针类型,这里需要取地址
monkey.slimbing()
f.Swimming()
b.Flying()
}

对上面代码的小结

  1. 当 A 结构体继承了 B 结构体,那么 A 结构就自动的继承了 B 结构体的字段和方法,并且可以直接使用

  2. 当 A 结构体需要扩展功能,同时不希望去破坏继承关系,则可以去实现某个接口即可,因此我们可以认为:实现接口是对继承机制的补充.

接口和继承解决的解决的问题不同

1、继承的价值主要在于:解决代码的复用性和可维护性。

2、接口的价值主要在于:设计,设计好各种规范(方法),让其它自定义类型去实现这些方法。

3、接口比继承更加灵活

​ 接口比继承更加灵活,继承是满足 is - a 的关系,而接口只需满足 like - a 的关系。

4、接口在一定程度上实现代码解耦

接口体现多态

多态参数

在前面的 Usb 接口案例,Usb usb ,即可以接收手机变量,又可以接收相机变量,就体现了 Usb 接口多态。

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
47
48
49
50
51
52
package main

import (
"fmt"
)

//申明接口
type Usb interface {
Start()
Stop()
}

type Phone struct {
}

//让Phone实现Usb接口的方法
func (p Phone) Start() {
fmt.Println("手机开始工作")
}
func (p Phone) Stop() {
fmt.Println("手机停止工作")
}

type Camera struct {
}

func (c Camera) Start() {
fmt.Println("相机开始工作")
}
func (c Camera) Stop() {
fmt.Println("相机停止工作")
}

type Computer struct {
}

//编写一个working方法,接收一个Usb接口变量
//变量会根据传入的实参,来判断到底是Phone,还是Camera
func (c Computer) Working(usb Usb) {
usb.Start()
usb.Stop()
}

func main() {
//创建结构体变量
computer := Computer{}
phone := Phone{}
camera := Camera{}

computer.Working(phone)
computer.Working(camera)
}

多态数组

演示一个案例:给 Usb 数组中,存放 Phone 结构体 和 Camera 结构体变量

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

//申明接口
type Usb interface {
Start()
Stop()
}

type Phone struct {
name string
}

//让Phone实现Usb接口的方法
func (p Phone) Start() {
fmt.Println("手机开始工作")
}
func (p Phone) Stop() {
fmt.Println("手机停止工作")
}

type Camera struct {
name string
}

func (c Camera) Start() {
fmt.Println("相机开始工作")
}
func (c Camera) Stop() {
fmt.Println("相机停止工作")
}

func main() {
//定义一个 Usb 接口数组,可以存放 Phone 和 Camera 的结构体变量
//这里就体现出多态数组
var Arrusb [4]Usb
Arrusb[0] = Phone{"xiaomi"}
Arrusb[1] = Phone{"vivo"}
Arrusb[2] = Phone{"huawei"}
Arrusb[3] = Camera{"suoni"}

fmt.Println(Arrusb)
}

接口类型断言

基本介绍

类型断言,由于接口是一般类型,不知道具体类型,如果要转成具体类型,就需要使用类型断言

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import (
"fmt"
)

func main() {
var x interface{}
var b float64 = 2.333
x = b
y := x.(float64)
fmt.Printf("y的类型是%T 值为%v\n", y, y)
}

对上面代码的说明:

在进行类型断言时,如果类型不匹配,就会报 panic, 因此进行类型断言时,要确保原来的空接口指向的就是断言的类型.

如何在进行断言时,带上检测机制,如果成功就 ok,否则也不要报 panic

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import (
"fmt"
)

func main() {
var x interface{}
var b float64 = 2.333
x = b
if y, ok := x.(float32); ok {
fmt.Printf("y的类型是%T 值为%v\n", y, y)
} else {
fmt.Println("error")
}
fmt.Println("helloworld")
}

类型断言的最佳实践 1

在前面的 Usb 接口案例做改进:给 Phone 结构体增加一个特有的方法 call(), 当 Usb 接口接收的是 Phone 变量时,还需要调用 call方法

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
package main

import (
"fmt"
)

//申明接口
type Usb interface {
Start()
Stop()
}

type Phone struct {
name string
}

//让Phone实现Usb接口的方法
func (p Phone) Start() {
fmt.Println("手机开始工作")
}
func (p Phone) Stop() {
fmt.Println("手机停止工作")
}

func (p Phone) Call() {
fmt.Println("正在通话中。。。")
}

type Camera struct {
name string
}

func (c Camera) Start() {
fmt.Println("相机开始工作")
}
func (c Camera) Stop() {
fmt.Println("相机停止工作")
}

type Computer struct {
}

func (c Computer) Working(usb Usb) {
usb.Start()
usb.Stop()
//如果 usb 是指向 Phone 结构体变量,则还需要调用 Call 方法
//类型断言..
if a, ok := usb.(Phone); ok {
a.Call()
}
}

func main() {
//定义一个 Usb 接口数组,可以存放 Phone 和 Camera 的结构体变量
//这里就体现出多态数组
var usbArr [3]Usb
usbArr[0] = Phone{"vivo"}
usbArr[1] = Phone{"小米"}
usbArr[2] = Camera{"尼康"}
//遍历 usbArr
//Phone 还有一个特有的方法 call(),请遍历 Usb 数组,如果是 Phone 变量,
//除了调用 Usb 接口声明的方法外,还需要调用 Phone 特有方法 call. =》类型断言
var computer Computer
for _, v := range usbArr {
computer.Working(v)
fmt.Println()
}
fmt.Println(usbArr)
}

类型断言的最佳实践 2

写一函数,循环判断传入参数的类型

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
package main

import (
"fmt"
)

type Student struct {
}

//编写一个函数,判断输入的参数是什么类型
func typeJudge(items ...interface{}) {
for index, x := range items {
switch x.(type) {
case bool:
fmt.Printf("第%v个参数是bool类型,值是%v\n", index, x)
case float32:
fmt.Printf("第%v个参数是float32类型,值是%v\n", index, x)
case float64:
fmt.Printf("第%v个参数是float64类型,值是%v\n", index, x)
case int:
fmt.Printf("第%v个参数是int类型,值是%v\n", index, x)
case string:
fmt.Printf("第%v个参数是string类型,值是%v\n", index, x)
case Student:
fmt.Printf("第%v个参数是Student类型,值是%v\n", index, x)
case *Student:
fmt.Printf("第%v个参数是*Student类型,值是%v\n", index, &x)
default:
fmt.Printf("第%v个参数类型不确定,值是%v\n", index, x)
}
}
}
func main() {
var n1 float32 = 3.33333
var n2 float64 = 4.23454556
var n3 int = 5
var n4 string = "helloworld"
n5 := "你好"
n6 := 400

n7 := Student{}
n8 := &Student{}
typeJudge(n1, n2, n3, n4, n5, n6, n7, n8)
fmt.Println(n1, n2, n3, n4, n5, n6, n7, &n8)
}

说明:Golang 的结构体没有构造函数,首字母是小写的,通常可以使用工厂模式来解决这个问题

1
2
3
4
5
6
package model

type Student struct {
Name string
Age int
}
1
2
3
4
5
6
7
8
9
10
11
package main

import (
"fmt"
"hello1/model"
)

func main() {
var stu = model.Student{"tom", 11}
fmt.Println(stu)
}

如果 model 包的 结构体变量首字母小写,引入后,不能直接使用, 可以工厂模式解决

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package model

//定义一个结构体
type student struct {
Name string
Age int
}

func NewStudent(n string, a int) *student {
return &student{
Name: n,
Age: a,
}
}
1
2
3
4
5
6
7
8
9
10
11
12
package main

import (
"fmt"
"hello1/model"
)

func main() {
var stu = model.NewStudent("hello~", 98)

fmt.Println(*stu)
}

model 包的 student 的结构体的字段 Age 改成 age

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package model

//定义一个结构体
type student struct {
Name string
age int
}

func NewStudent(n string, a int) *student {
return &student{
Name: n,
age: a,
}
}

func (a *student) GetAge() int {
return a.age
}
1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import (
"fmt"
"hello1/model"
)

func main() {
var stu = model.NewStudent("hello~", 98)

fmt.Println(*stu)
fmt.Println(stu.Name, stu.GetAge())
}

基本介绍

​ 在某些情况下,我们要需要声明(定义)方法。比如 Person 结构体:除了有一些字段外( 年龄,姓名..),Person 结构体还有一些行为比如:可以说话、跑步..,通过学习,还可以做算术题。这时就要用方法才能完成。

​ Golang 中的方法是作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型,都可以有方法,而不仅仅是 struct。

方法的声明和调用

1
2
3
4
5
6
7
type A struct {
Num int
}

func (a A) test() {
fmt.Println(a.Num)
}

对上面的语法的说明

  1. func (a A) test() {} 表示 A 结构体有一方法,方法名为 test

  2. (a A) 体现 test 方法是和 A 类型绑定的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"fmt"
)

type Monster struct {
Name string
Age int
}

func (M Monster) test1() {
fmt.Println(M.Age)
}

func main() {
var N Monster
N.Age = 12
N.test1()
}

说明:

1、test1方法是和Monster结构体绑定的

2、test1方法只能通过Monster的变量M来调用,而不能直接调用,也不能使用其他类型变量调用

3、func (M Monster) test1()其中的M表示被哪个Monster调用,这个M就是Monster的副本

4、M为自行定义,不为固定值

方法快速入门

  1. 给 Monster结构体添加 speak 方法,输出 xxx 是一个好人
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
"fmt"
)

type Monster struct {
Name string
}

func (S Monster) speak() {
fmt.Println(S.Name, "is a good man")
}

func main() {
var N Monster
N.Name = "tom"
N.speak()
}

2)给 Monster结构体添加 jisuan 方法,可以计算从 1+..+1000 的结果, 说明方法体内可以函数一样进行各种运算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
"fmt"
)

type Monster struct {
Name string
}

func (m Monster) jisun() {
sum := 0
for i := 1; i <= 1000; i++ {
sum += i
}
fmt.Println(m.Name, "结果是=", sum)
}
func main() {
var N Monster
N.Name = "hello"
N.jisun()
}
  1. 给 Monster结构体 jisuan方法,该方法可以接收一个数 n,计算从 1+..+n 的结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
"fmt"
)

type Monster struct {
Name string
}

func (m Monster) jisun(n int) {
sum := 0
for i := 1; i <= n; i++ {
sum += i
}
fmt.Println(m.Name, "结果是=", sum)
}
func main() {
var N Monster
N.Name = "hello"
N.jisun(100)
}
  1. 给 Monster结构体添加 getSum 方法,可以计算两个数的和,并返回结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
"fmt"
)

type Monster struct {
Name string
}

func (m Monster) getSum(n1 int, n2 int) (n int) {
return n1 + n2
}
func main() {
var N Monster
N.Name = "hello"
sum := N.getSum(1, 2)
fmt.Println(sum)
}

方法的调用和传参机制原理

说明:方法的调用和传参机制和函数基本一样,不一样的地方是方法调用时,会将调用方法的变量,当做实参也传递给方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
"fmt"
)

// 1) 声明一个结构体 Circle, 字段为 radius
// 2) 声明一个方法 area 和 Circle 绑定,可以返回面积。
// 3) 提示:画出 area 执行过程+说明
type Circle struct {
redius float64
}

func (S Circle) area() float64 {
return 3.14 * S.redius * S.redius
}

func main() {
var M Circle
M.redius = 3.0
sum := M.area()
fmt.Println("面积是=", sum)
}
  1. 在通过一个变量去调用方法时,其调用机制和函数一样

  2. 不一样的地方时,变量调用方法时,该变量本身也会作为一个参数传递到方法(如果变量是值类型,则进行值拷贝,如果变量是引用类型,则进行地质拷贝)

方法的声明

1
2
3
4
func (recevier type) methodName(参数列表) (返回值列表){
方法体
return 返回值
}
  1. 参数列表:表示方法输入

  2. recevier type : 表示这个方法和 type 这个类型进行绑定,或者说该方法作用于 type 类型

  3. receiver type : type 可以是结构体,也可以其它的自定义类型

  4. receiver : 就是 type 类型的一个变量(实例),比如 :Person 结构体 的一个变量(实例)

  5. 返回值列表:表示返回的值,可以多个

  6. 方法主体:表示为了实现某一功能代码块

  7. return 语句不是必须的。

方法的注意事项和细节

  1. 结构体类型是值类型,在方法调用中,遵守值类型的传递机制,是值拷贝传递方式

  2. 如程序员希望在方法中,修改结构体变量的值,可以通过结构体指针的方式来处理

  3. Golang 中的方法作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型,都可以有方法,而不仅仅是 struct, 比如 int , float32 等都可以有方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
"fmt"
)

type test1 int

func (n test1) print1() {
fmt.Println(n)
}

func (m *test1) print2() {
*m = *m + 1
}

func main() {
var n1 test1 = 1
n1.print1()
n1.print2()
fmt.Println(n1)
}
  1. 方法的访问范围控制的规则,和函数一样。方法名首字母小写,只能在本包访问,方法首字母大写,可以在本包和其它包访问。

  2. 如果一个类型实现了 String()这个方法,那么 fmt.Println 默认会调用这个变量的 String()进行输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
"fmt"
)

type Student struct {
Name string
Age int
}

func (test2 *Student) test3() string {
return fmt.Sprintf("Name=[%v] Age=[%v]", test2.Name, test2.Age)

}

func main() {
test1 := Student{
Name: "tom",
Age: 11,
}
fmt.Println((&test1).test3())
}

方法和函数区别

  1. 调用方式不一样
  • 函数的调用方式: 函数名(实参列表)

  • 方法的调用方式: 变量.方法名(实参列表)

  1. 对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,反之亦然

  2. 对于方法(如 struct 的方法),接收者为值类型时,可以直接用指针类型的变量调用方法,反过来同样也可以

总结

  1. 不管调用形式如何,真正决定是值拷贝还是地址拷贝,看这个方法是和哪个类型绑定.

  2. 如果是和值类型,比如 (p Person) , 则是值拷贝, 如果和指针类型,比如是 (p *Person) 则是地址拷贝。

实例练习

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
package main

import (
"fmt"
)

//编写结构体(MethodUtils),编程一个方法,方法不需要参数,在方法中打印一个 10*8 的矩形,
//在 main 方法中调用该方法

type MethodUtils struct {
}

func (test1 MethodUtils) test2() {
for i := 1; i <= 10; i++ {
for j := 1; j <= 8; j++ {
fmt.Print("*")
}
fmt.Println()
}
}

func main() {
var test3 MethodUtils
test3.test2()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
"fmt"
)

//编写一个方法,提供 m 和 n 两个参数,方法中打印一个 m*n 的矩形

type MethodUtils struct {
}

func (test1 MethodUtils) test2(m int, n int) {
for i := 1; i <= m; i++ {
for j := 1; j <= n; j++ {
fmt.Print("*")
}
fmt.Println()
}
}

func main() {
var test3 MethodUtils
test3.test2(20, 10)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
"fmt"
)

//编写一个方法算该矩形的面积(可以接收长 len,和宽 width), 将其作为方法返回值。在 main
//方法中调用该方法,接收返回的面积值并打印

type MethodUtils struct {
len float64
width float64
}

func (test1 MethodUtils) test2(len float64, width float64) float64 {
return len * width

}

func main() {
var test3 MethodUtils
sum := test3.test2(2, 10)
fmt.Println(sum)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
"fmt"
)

//编写方法:判断一个数是奇数还是偶数

type totoal struct {
}

func (test1 *totoal) test2(num int) {
if num%2 == 0 {
fmt.Println("num是偶数")
} else {
fmt.Println("num是奇数")
}
}

func main() {
var test3 totoal
test3.test2(55)
}
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
package main

import (
"fmt"
)

/*1) 编写一个 Student 结构体,包含 name、gender、age、id、score 字段,分别为 string、string、int、
int、float64 类型。
2) 结构体中声明一个 say 方法,返回 string 类型,方法返回信息中包含所有字段值。
3) 在 main 方法中,创建 Student 结构体实例(变量),并访问 say 方法,并将调用结果打印输出。
*/

type Student struct {
name string
gender string
age int
id int
score float64
}

func (stu *Student) say() string {
strinfo := fmt.Sprintf("name=%v gender=%v age=%v id=%v score=%v",
stu.name, stu.gender, stu.age, stu.id, stu.score)
return strinfo
}

func main() {
var stu = Student{
name: "hello",
gender: "man",
age: 12,
id: 111,
score: 33.33,
}
fmt.Println(stu.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
29
30
31
package main

import (
"fmt"
)

/*
1) 编程创建一个 Box 结构体,在其中声明三个字段表示一个立方体的长、宽和高,长宽高要从终
端获取
2) 声明一个方法获取立方体的体积。
3) 创建一个 Box 结构体变量,打印给定尺寸的立方体的体积
*/

type Box struct {
l float64
h float64
w float64
}

func (b *Box) sum() float64 {
return b.l * b.w * b.h
}

func main() {
var test Box
test.h = 1.1
test.l = 1.1
test.w = 1.1
totol := test.sum()
fmt.Printf("体积=%.2f", totol)
}
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
package main

import (
"fmt"
)

/*
1) 一个景区根据游人的年龄收取不同价格的门票,比如年龄大于 18,收费 20 元,其它情况门票免费.
2) 请编写 Visitor 结构体,根据年龄段决定能够购买的门票价格并输出
*/
type Visitor struct {
name string
age int
}

func (vi *Visitor) test1() {
if vi.age > 18 {
fmt.Println("收费20元")
} else {
fmt.Println("免费")
}
}

func main() {
var pipo Visitor

for {
fmt.Println("请输入姓名")
fmt.Scanln(&pipo.name)
if pipo.name == "n" {
fmt.Println("ERROR")
break
} else {
fmt.Println("请输入年龄。。。")
fmt.Scanln(&pipo.age)
pipo.test1()
}
}
}

创建结构体变量时指定字段值

说明:Golang 在创建结构体实例(变量)时,可以直接指定字段的值

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
package main

import (
"fmt"
)

type Stu struct {
Name string
Age int
}

func main() {
//方式一
var stu1 = Stu{"hello1", 11}
stu2 := Stu{"hello2", 12}

var stu3 = Stu{
Name: "hello3",
Age: 13,
}
stu4 := Stu{
Name: "hello4",
Age: 14,
}
fmt.Println(stu1, stu2, stu3, stu4)

//方式二
var stu5 = &Stu{"hello5", 15}
stu6 := &Stu{"hello6", 16}

var stu7 = &Stu{
Name: "hello7",
Age: 17,
}
stu8 := &Stu{
Name: "hello8",
Age: 18,
}
fmt.Println(*stu5, *stu6, *stu7, *stu8)
}

面向对象编程说明

  1. Golang 也支持面向对象编程(OOP),但是和传统的面向对象编程有区别,并不是纯粹的面向对象语言。所以我们说 Golang 支持面向对象编程特性是比较准确的。

  2. Golang 没有类(class),Go 语言的结构体(struct)和其它编程语言的类(class)有同等的地位,你可以理解 Golang 是基于 struct 来实现 OOP 特性的。

  3. Golang 面向对象编程非常简洁,去掉了传统 OOP 语言的继承、方法重载、构造函数和析构函数、隐藏的 this 指针等等

  4. Golang 仍然有面向对象编程的 继承,封装和多态的特性,只是实现的方式和其它 OOP 语言不一样,比如继承 :Golang 没有 extends 关键字,继承是通过匿名字段来实现。

  5. Golang 面向对象(OOP)很优雅,OOP 本身就是语言类型系统(type system)的一部分,通过接口(interface)关联,耦合性低,也非常灵活。也就是说在 Golang 中面向接口编程是非常重要的特性。

结构体和结构体变量(实例)的区别和联系

  1. 结构体是自定义的数据类型,代表一类事物.

  2. 结构体变量(实例)是具体的,实际的,代表一个具体变量

如何声明结构体

基本语法

1
2
3
4
type 结构体名称 struct {
field1 type
field2 type
}

举例:

1
2
3
4
5
type Student struct {
Name string //字段
Age int //字段
Score float32
}

字段/属性

基本介绍

  1. 从概念或叫法上看: 结构体字段 = 属性 = field

  2. 字段是结构体的一个组成部分,一般是 基本数据类型、 数组,也可是 引用类型。

注意事项和细节说明

  1. 字段声明语法同变量,示例:字段名 字段类型

  2. 字段的类型可以为:基本类型、数组或引用类型

  3. 在创建一个结构体变量后,如果没有给字段赋值,都对应一个零值(默认值),规则同前面讲的一样:

1
2
3
4
5
布尔类型是 false ,数值是 0 ,字符串是 ""。

数组类型的默认值和它的元素类型相关,比如 score [3]int 则为[0, 0, 0]

指针,slice ,和 map 是 的零值都是 nil ,即还没有分配空间
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
package main

import (
"fmt"
)

type Cat struct {
Name string
Age int
Address string
Ptr *int //指针
slice []int //切片
map1 map[string]string //map
}

func main() {
var cat1 Cat
fmt.Println("cat1=", cat1)

cat1.slice = make([]int, 10)
cat1.slice[2] = 122

cat1.map1 = make(map[string]string)
cat1.map1["num1"] = "beijing"

fmt.Println("cat1=", cat1)
}
  1. 不同结构体变量的字段是独立,互不影响,一个结构体变量字段的更改,不影响另外一个, 结构体是值类型。

创建结构体变量和访问结构体字段

方式 1-直接声明

1
案例演示: var person Person

方式 2-{}

1
案例演示: var person Person = Person{}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import (
"fmt"
)

type Cat struct {
Name string
Age int
}

func main() {
cat1 := Cat{"hello", 20}
fmt.Println("cat1=", cat1)
}

方式 3-&

1
案例: var person *Person = new (Person)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
"fmt"
)

type Cat struct {
Name string
Age int
}

func main() {
var cat1 *Cat = new(Cat)
//底层会对cat1.Name = "hello-1"进行处理
//会自动给cat1加上取值运算(*cat1).Name = "hello-1"
(*cat1).Name = "hello"
cat1.Name = "hello-1"

(*cat1).Age = 12
cat1.Age = 13

fmt.Println(*cat1)
}

方式 4-{}

1
案例: var person *Person = &Person{}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
"fmt"
)

type Cat struct {
Name string
Age int
}

func main() {
var cat1 *Cat = &Cat{}
//底层会对cat1.Name = "hello-2"进行处理
//会自动给cat1加上取值运算(*cat1).Name = "hello-2"
(*cat1).Name = "hello"
cat1.Name = "hello-2"

(*cat1).Age = 12
cat1.Age = 14

fmt.Println(*cat1)
}

说明:

  1. 第 3 种和第 4 种方式返回的是 结构体指针。

  2. 结构体指针访问字段的标准方式应该是:(*结构体指针).字段名 ,比如 (*person).Name = “tom”

  3. 但 go 做了一个简化,持 也支持 结构体指针. 字段名, 比如 person.Name = “tom”。更加符合程序员使用的习惯,go 层 编译器底层 对 对 person.Name 化 做了转化 (*person).Name

结构体使用注意事项和细节

  1. 结构体的所有字段在内存中是连续的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"fmt"
)

type Point struct {
x int
y int
}

type Rect struct {
leftUp, rightDown Point
}

func main() {
r1 := Rect{Point{1, 2}, Point{3, 4}}
fmt.Printf("r1.leftUp.x地址=%p r1.leftUp.y地址=%p r1.rightDown.x地址=%p r1.rightDown.y地址=%p",
&r1.leftUp.x, &r1.leftUp.y, &r1.rightDown.x, &r1.rightDown.y)
}
1
2
3
输出结果:
r1.leftUp.x地址=0xc000010540 r1.leftUp.y地址=0xc000010548
r1.rightDown.x地址=0xc000010550 r1.rightDown.y地址=0xc000010558
  1. 结构体是用户单独定义的类型,和其它类型进行转换时需要有完全相同的字段(名字、个数和类型)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"fmt"
)

type A struct {
Num int
}

type B struct {
Num int
}

func main() {
var a A
var b B
a = A(b) //强转
fmt.Println(a, b)
}
  1. 结构体进行 type 重新定义(相当于取别名),Golang 认为是新的数据类型,但是相互间可以强转

  2. struct 的每个字段上,可以写上一个 tag, 该 tag 可以通过反射机制获取,常见的使用场景就是序列化和反序列化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
"encoding/json"
"fmt"
)

type Monster struct {
Name string `json:"name"`
Age int `json:"age"`
Skill string `json:"skill"`
}

func main() {
monster := Monster{"牛魔王", 300, "牛魔鞭"}
jsonStr, err := json.Marshal(monster)
if err != nil {
fmt.Println("json 处理错误", err)
}
fmt.Println("jsonstr", string(jsonStr))
}

map 的基本介绍

map 是 key-value 数据结构,又称为字段或者关联数组。类似其它编程语言的集合

map 的声明

基本语法

1
var map 变量名 map[keytype]valuetype

key 类型

golang 中的 map,的 key 可以是很多种类型,比如 bool, 数字,string, 指针, channel , 还可以是只包含前面几个类型的 接口, 结构体, 数组

通常 key 为 为 int 、string

注意: slice, map 还有 function 不可以,因为这几个没法用 == 来判断

valuetype 类型

valuetype 的类型和 key 基本一样

通常为: 数字(整数,浮点数),string,map,struct

map 声明的举例

1
2
3
4
5
6
map 声明的举例:
var a map[string]string
var a map[string]int
var a map[int]string
var a map[string]map[string]string
注意:声明是不会分配内存的,初始化需要 make ,分配内存后才能赋值和使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import (
"fmt"
)

func main() {

var a map[string]string
//在使用map前,需要先make,make的作用就是给map分配可用空间
a = make(map[string]string)
a["num1"] = "h"
a["num2"] = "e"
a["num1"] = "k"
fmt.Println(a)
}

对上面代码的说明

  1. map 在使用前一定要 make

  2. map 的 key 是不能重复,如果重复了,则以最后这个 key-value 为准

  3. map 的 value 是可以相同的.

  4. map 的 key-value 是无序

  5. make 内置函数数目

1
2
3
4
5
6
7
8
9
10
func make
func make(Type, size IntegerType) Type
make 内建函数分配并初始化一个类型为切片、映射、或(仅仅为)信道的对象。 与 new 相同的是,其第一个实参为类型,而非值。不同的是,make 的返回类型 与其参数相同,而非指向它的指针。其具体结果取决于具体的类型:

切片:size 指定了其长度。该切片的容量等于其长度。第二个整数实参可用来指定
不同的容量;它必须不小于其长度,因此 make([]int, 0, 10) 会分配一个长度为0,
容量为10的切片。
映射:初始分配的创建取决于 size,但产生的映射长度为0。size 可以省略,这种情况下
就会分配一个小的起始大小。
信道:信道的缓存根据指定的缓存容量初始化。若 size 为零或被省略,该信道即为无缓存的

map的使用

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
package main

import (
"fmt"
)

func main() {

//第一种
var a map[string]string
//在使用map前,需要先make,make的作用就是给map分配可用空间
a = make(map[string]string)
a["num1"] = "h"
a["num2"] = "e"
a["num1"] = "k"
fmt.Println(a)

//第二种
cities := make(map[string]string)
cities["num1"] = "成都"
cities["num2"] = "hangzhou"
fmt.Println(cities)

//第三种
heros := map[string]string{
"num1": "剑魂",
"num2": "奶爸",
}
fmt.Println(heros)
}

map 使用的实例

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
package main

import (
"fmt"
)

func main() {

//比如:我们要存放 3 个学生信息, 每个学生有 name 和 sex 信息
//思路: map[string]map[string]string
students := make(map[string]map[string]string)

students["stu1"] = make(map[string]string, 3)
students["stu1"]["name"] = "hello1"
students["stu1"]["age"] = "11"
students["stu1"]["sex"] = "男"

students["stu2"] = make(map[string]string, 3)
students["stu2"]["name"] = "hello2"
students["stu2"]["age"] = "12"
students["stu2"]["sex"] = "男"

students["stu3"] = make(map[string]string, 3)
students["stu3"]["name"] = "hello3"
students["stu3"]["age"] = "13"
students["stu3"]["sex"] = "女"

fmt.Println(students)
}

map 的增删改查操作

map 增加和更新

map[“key”] = value // 如果 key 还没有,就是增加,如果 key 存在就是修改

map 删除

说明:

delete(map,”key”) ,delete 是一个内置函数,如果 key 存在,就删除该 key-value,如果 key 不存在,不操作,但是也不会报错

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
package main

import (
"fmt"
)

func main() {

cities := make(map[string]string)
cities["num1"] = "重庆"
cities["num2"] = "浙江"
fmt.Println(cities)

//增加
cities["num3"] = "杭州"
fmt.Println(cities)

//修改
cities["num1"] = "四川"
fmt.Println(cities)

//删除
delete(cities, "num1")
fmt.Println(cities)

//全部删除
cities = make(map[string]string)
fmt.Println(cities)

//查找
val, ok := cities["num2"]
if ok {
fmt.Printf("找到了cities[num2]值为%v", val)
} else {
fmt.Println("没找到")
}
}

细节说明

如果我们要删除 map 的所有 key ,没有一个专门的方法一次删除,可以遍历一下 key, 逐个删除或者 map = make(…),make 一个新的,让原来的成为垃圾,被 gc 回收

查找

如果 cities这个 map 中存在 “num2” , 那么 findRes 就会返回 true,否则返回 false

map遍历

说明:map 的遍历使用 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
31
32
package main

import (
"fmt"
)

func main() {

cities := make(map[string]string)
cities["num1"] = "重庆"
cities["num2"] = "浙江"

for i, v := range cities {
fmt.Printf("i=%v v=%v\n", i, v)
}

stu := make(map[string]map[string]string)
stu["num1"] = make(map[string]string)
stu["num1"]["name"] = "金"
stu["num1"]["age"] = "11"

stu["num2"] = make(map[string]string)
stu["num2"]["name"] = "刚"
stu["num2"]["age"] = "12"

for i, v := range stu {
fmt.Printf("i=%v\n", i)
for i1, v1 := range v {
fmt.Printf("stu=%v i1=%v v2=%v\n", stu, i1, v1)
}
}
}

map长度

func len

1
func len(v Type) int

len 内建函数返回 v 的长度,这取决于具体类型:

1
2
3
4
5
6
7
8
数组:v 中元素的数量。
数组指针:*v 中元素的数量(即使 v 为 nil)。
切片或映射:v 中元素的数量;若 v 为 nil,len(v) 即为零。
字符串:v 中字节的数量。
信道:信道缓存中队列(未读取)元素的数量;若 v 为 nil,len(v) 即为零。

例如:
fmt.Println("map长度=", len(stu))

map 切片

基本介绍

切片的数据类型如果是 map,则我们称为 slice of map,map 切片,这样使用则 map 个数就可以动态变化了。

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
package main

import (
"fmt"
)

func main() {

//使用一个 map 来记录 monster 的信息 name 和 age, 也就是说一个 monster 对应一个 map,并
//且妖怪的个数可以动态的增加=>map 切片

//声明一个切片
var monster []map[string]string
monster = make([]map[string]string, 2)
if monster[0] == nil {
monster[0] = make(map[string]string)
monster[0]["name"] = "牛魔王"
monster[0]["age"] = "12"
}
if monster[1] == nil {
monster[1] = make(map[string]string)
monster[1]["name"] = "猴子"
monster[1]["age"] = "13"
}
fmt.Println(monster)

//使用append函数动态增加
newmonster := make(map[string]string)
newmonster["name"] = "猪八戒"
newmonster["age"] = "11"

monster = append(monster, newmonster)
fmt.Println(monster)
}

map 排序

基本介绍 【新版本已经可以自动排序】

  1. golang 中没有一个专门的方法针对 map 的 key 进行排序

  2. golang 中的 map 默认是无序的,注意也不是按照添加的顺序存放的,你每次遍历,得到的输出可能不一样.

  3. golang 中 map 的排序,是先将 key 进行排序,然后根据 key 值遍历输出即可

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
package main

import (
"fmt"
"sort"
)

func main() {

num := make(map[int]int)
num[41] = 1
num[22] = 2
num[33] = 3
fmt.Println(num)

//先将map的key放到切片中
//对切片进行排序
//遍历切片,然后按照排序来输出map值
var keys []int
for k, _ := range num {
keys = append(keys, k)
}
sort.Ints(keys)
fmt.Println(keys)

for _, k := range keys {
fmt.Printf("num[%v]=%v\n", k, num[k])
}
}

map 使用细节

  1. map 是引用类型,遵守引用类型传递的机制,在一个函数接收 map,修改后,会直接修改原来的 map
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
"fmt"
)

func test(num map[int]int) {
num[22] = 100
}

func main() {

num := make(map[int]int)
num[41] = 1
num[22] = 2
num[33] = 3
test(num)
fmt.Println(num)
}
  1. map 的容量达到后,再想 map 增加元素,会自动扩容,并不会发生 panic,也就是说 map 能动态的增长 键值对(key-value)

  2. map 的 value 也经常使用 struct 类型,更适合管理复杂的数据(比前面 value 是一个 map 更好)

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
package main

import (
"fmt"
)

type Stu struct {
Name string
age int
Address string
}

func main() {

students := make(map[string]Stu, 10)
stu1 := Stu{"tom", 11, "beijing"}
stu2 := Stu{"jack", 12, "shanghai"}

students["num1"] = stu1
students["num2"] = stu2
fmt.Println(students)

//遍历所有学生
for i, v := range students {
fmt.Printf("学生编号是%v \n", i)
fmt.Printf("学生年龄=%v \n", v.age)
fmt.Printf("学生年龄=%v \n", v.Name)
}
}

应用实例

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
package main

import (
"fmt"
)

/*
1)使用 map[string]map[string]sting 的 map 类型
2)key: 表示用户名,是唯一的,不可以重复
3)如果某个用户名存在,就将其密码修改"888888",如果不存在就增加这个用户信息,
(包括昵称 nickname 和 密码 pwd)。
4)编写一个函数 modifyUser(users map[string]map[string]sting, name string) 完成上述功能
*/

func modifyUser(users map[string]map[string]string, name string) {
//判断是否有这个用户
if users[name] != nil {
users[name]["pwd"] = "88888888"
} else {
users[name] = make(map[string]string)
users[name]["pwd"] = "9999999"
users[name]["nickname"] = "彭大将军"
}
}

func main() {
users := make(map[string]map[string]string)
users["tom"] = make(map[string]string)
users["tom"]["pwd"] = "88888888"
users["tom"]["age"] = "111"

modifyUser(users, "kack")
fmt.Println(users)
}