入门08_教程编程时光

此文章和Go编程时光同一个作者,所以高度相似
零基础学 Go 语言:mp.weixin.qq.com/mp/appmsgalbum?__biz=MzU1NzU1MTM2NA==&action=getalbum&album_id=1337200768771866624&scene=173
Go编程时光:https://golang.iswbm.com/preface.html

01. 开发环境的搭建(Goland & VS Code)

02. 学习五种变量创建的方法

第五种:new 函数创建指针变量
而这里要说的 new 函数,是 Go 里的一个内建函数
使用表达式 new(Type) 将创建一个Type类型的匿名变量,初始化为Type类型的零值,然后返回变量地址,返回的指针类型为*Type。

如下两种写法,可以说是等价的

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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
// 使用 new  
func newInt() *int {
return new(int)
}

// 使用传统的方式
func newInt() *int {
var dummy int
return &dummy
}
```
变量/常量都只能声明一次,声明多次,编译就会报错。
但也有例外,这就要说到一个特殊变量:匿名变量,也称作占位符,或者空白标识符,用下划线表示。
匿名变量,优点有三:
不分配内存,不占用内存空间
不需要你为命名无用的变量名而纠结
多次声明不会有任何问题

通常我们用匿名接收必须接收,但是又不会用到的值


## 03. 详解数据类型:整形与浮点型

1. float32 和 float64 可以表示的数值很多
浮点数类型的取值范围可以从很微小到很巨大。浮点数取值范围的极限值可以在 math 包中找到:
常量 math.MaxFloat32 表示 float32 能取到的最大数值,大约是 3.4e38;
常量 math.MaxFloat64 表示 float64 能取到的最大数值,大约是 1.8e308;
float32 和 float64 能表示的最小值分别为 1.4e-45 和 4.9e-324。
2. 数值很大但精度有限
人家虽然能表示的数值很大,但精度位却没有那么大。
float32的精度只能提供大约6个十进制数(表示后科学计数法后,小数点后6位)的精度
float64的精度能提供大约15个十进制数(表示后科学计数法后,小数点后15位)的精度


## 04. 详解数据类型:byte、rune与string
在 Go 中**单引号与 双引号并不是等价**的。
单引号用来表示字符,在上面的例子里,如果你使用双引号,就意味着你要定义一个字符串

除了双引号之外 ,你还可以使用**反引号**。
大多情况下,二者并没有区别,但如果你的字符串中有转义字符\ ,这里就要注意了,它们是有区别的。
使用反引号包裹的字符串,相当于 Python 中的 raw 字符串,会忽略里面的转义。
比如我想表示 \r\n 这个 字符串,使用双引号是这样写的,这种叫解释型表示法
```
var mystr01 string = "\\r\\n"
```
而使用反引号,就方便多了,所见即所得,这种叫原生型表示法
```
var mystr02 string = `\r\n`
```
同**时反引号可以不写换行符**(因为没法写)来表示一个多行的字符串。

## 05. 详解数据类型:数组与切片
len 和 cap 的概念,可能不好理解 ,这里举个例子:
公司名,就是变量名。
公司里的所有工位,相当于已分配到的内存空间
公司里的员工,相当于元素。
cap 代表你这个公司最多可以容纳多少员工
len 代表你这个公司当前有多少个员工,所以cap>=len
由于 切片是引用类型,所以你不对它进行赋值的话,它的零值(默认值)是 nil
```
var myarr []int
fmt.Println(myarr == nil)
// true
```

## 06. 详解数据类型:字典与布尔类型
三种声明并初始化字典的方法

```
// 第一种方法
var scores map[string]int = map[string]int{"english": 80, "chinese": 85}

// 第二种方法
scores := map[string]int{"english": 80, "chinese": 85}

// 第三种方法
scores := make(map[string]int)
scores["english"] = 80
scores["chinese"] = 85
```

如何对字典进行循环
a获取 key 和 value
b只获取key,这里注意不用占用符。
c只获取 value,用一个占位符替代。

判断 key 是否存在
当key不存在,会返回value-type的零值 ,所以你不能通过返回的结果是否是零值来判断对应的 key 是否存在,因为 key 对应的 value 值可能恰好就是零值。
其实字典的下标读取可以返回两个值,使用第二个返回值都表示对应的 key 是否存在,若存在ok为true,若不存在,则ok为false
```
import "fmt"

func main() {
scores := map[string]int{"english": 80, "chinese": 85}
if math, ok := scores["math"]; ok {
fmt.Printf("math 的值是: %d", math)
} else {
fmt.Println("math 不存在")
}
}
```

## 07. 详解数据类型:指针

## 08. 面向对象编程:结构体与继承

## 09. 一篇文章理解 Go 里的函数

3. 函数实现可变参数
多个类型一致的参数
```
// 使用 ...类型,表示一个元素为int类型的切片
func sum(args ...int) int {
var sum int
for _, v := range args {
sum += v
}
return sum
}
func main() {
fmt.Println(sum(1, 2, 3))
}
```
多个类型不一致的参数
```
import "fmt"
func MyPrintf(args ...interface{}) {
for _, arg := range args {
switch arg.(type) {
case int:
fmt.Println(arg, "is an int value.")
case string:
fmt.Println(arg, "is a string value.")
case int64:
fmt.Println(arg, "is an int64 value.")
default:
fmt.Println(arg, "is an unknown type.")
}
}
}
```
4. 多个可变参数函数传递参数
上面提到了可以使用 ... 来接收多个参数,除此之外,它还有一个用法,就是用来解序列,将函数的可变参数(一个切片)一个一个取出来,传递给另一个可变参数的函数,而不是传递可变参数变量本身。
```
func Sum(args ...int) int {
// 利用 ... 来解序列
result := sum(args...)
return result
}
```


Go支持返回带有变量名的值
```
func double(a int) (b int) {
// 不能使用 := ,因为在返回值哪里已经声明了为int
b = a * 2
// 不需要指明写回哪个变量,在返回值类型那里已经指定了
return
}
func main() {
fmt.Println(double(2))
}
// output: 4
```

方法和函数有什么区别?
方法,是一种特殊的函数。当你一个函数和对象/结构体进行绑定的时候,我们就称这个函数是一个方法。


## 10. Go语言流程控制:if-else 条件语句

2. 一个 case 多个条件
4. switch 后可接函数(保证 case 后的值类型与函数的返回值 一致)
5. switch 可不接表达式
6. switch 的穿透能力
当 case 使用关键字 fallthrough 开启穿透能力的时候。
fallthrough 只能穿透一层,意思是它能让你无条件执行下一个case,此时不管你有没有匹配上,都要退出了。


## 11. Go语言流程控制:switch-case 选择语句

## 12. Go语言流程控制:for 循环语句

## 13. Go语言流程控制:goto 无条件跳转
goto语句与标签之间不能有变量声明,否则编译错误。
## 14. Go语言流程控制:defer 延迟调用

2. 即时求值的变量快照(对于引用类型也会变的,取值类型不变)
使用 defer 只是延时调用函数,此时传递给函数里的变量,不应该受到后续程序的影响。
3. 多个defer 反序调用
4. defer 与 return 孰先孰后

```
import "fmt"

var name string = "go"

func myfunc() string {
defer func() {
name = "python"
}()

fmt.Printf("myfunc 函数里的name:%s\n", name)
return name
}

func main() {
myname := myfunc()
fmt.Printf("main 函数里的name: %s\n", name)
fmt.Println("main 函数里的myname: ", myname)
}
```
输出如下
myfunc 函数里的name:go
main 函数里的name: python
main 函数里的myname: go
来一起理解一下这段代码,第一行很直观,name 此时还是全局变量,值还是go
第二行也不难理解,在 defer 里改变了这个全局变量,此时name的值已经变成了 python
重点在第三行,为什么输出的是 go ?
解释只有一个,那就是 defer 是return 后才调用的。所以在执行 defer 前,myname 已经被赋值成 go 了。


## 15. 面向对象编程:接口与多态

## 16. 关键字:make 和 new 的区别?
new:为所有的类型分配内存,并初始化为零值,返回指针。
make:只能为 slice,map,chan 分配内存,并初始化,返回的是类型。
另外,目前来看 new 函数并不常用,大家更喜欢使用短语句声明的方式。
```
a := new(int)
a = 1
// 等价于
a := 1
```
但是 make 就不一样了,它的地位无可替代,在使用slice、map以及channel的时候,还是要使用make进行初始化,然后才可以对他们进行操作。



## 17. 一篇文章理解 Go 里的语句块与作用域

## 18. 学习 Go 协程:goroutine
1. 协程的初步使用
main 的地位相当于主线程,当 main 函数执行完成后,这个线程也就终结了,其下的运行着的所有协程也不管代码是不是还在跑,也得乖乖退出。

## 19. 学习 Go 协程:详解信道/通道
信道的数据操作,无非就两种:发送数据与读取数据
```
// 往信道中发送数据
pipline<- 200

// 从信道中取出数据,并赋值给mydata
mydata := <-pipline
```
信道用完了,可以对其进行关闭,避免有人一直在等待。
```
close(pipline)
```

对一个已关闭的信道再关闭,是会报错的。所以我们还要学会,如何判断一个信道是否被关闭?
当从信道中读取数据时,可以有多个返回值,其中第二个可以表示 信道是否被关闭,如果已经被关闭,ok 为 false,若还没被关闭,ok 为true。


2. 信道的容量与长度
当容量为0时,说明信道中不能存放数据,在发送数据时,必须要求立马有人接收,否则会报错。此时的信道称之为无缓冲信道。
当容量为1时,说明信道只能缓存一个数据,若信道中已有一个数据,此时再往里发送数据,会造成程序阻塞。 利用这点可以利用信道来做锁。
当容量大于1时,信道中可以存放多个数据,可以用于多个协程之间的通信管道,共享资源。

4. 双向信道与单向信道
单向信道

单向信道,可以细分为 只读信道 和 只写信道。

定义只读信道
```
var pipline = make(chan int)
type Receiver = <-chan int // 关键代码:定义别名类型
var receiver Receiver = pipline
```
定义只写信道
```
var pipline = make(chan int)
type Sender = chan<- int // 关键代码:定义别名类型
var sender Sender = pipline
```
仔细观察,区别在于 <- 符号在关键字 chan 的左边还是右边。

<-chan 表示这个信道,只能从里发出数据,对于程序来说就是只读
chan<- 表示这个信道,只能从外面接收数据,对于程序来说就是只写
5. 遍历信道
遍历信道,可以使用 for 搭配 range关键字,在range时,要确保信道是处于关闭状态,否则循环会阻塞。
6. 用信道来做锁
当信道里的数据量已经达到设定的容量时,此时再往里发送数据会阻塞整个程序。
利用这个特性,可以用当他来当程序的锁。

几个注意事项
关闭一个未初始化的 channel 会产生 panic

重复关闭同一个 channel 会产生 panic

向一个已关闭的 channel 发送消息会产生 panic

从已关闭的 channel 读取消息不会产生 panic,且能读出 channel 中还未被读取的消息,若消息均已被读取,则会读取到该类型的零值。

从已关闭的 channel 读取消息永远不会阻塞,并且会返回一个为 false 的值,用以判断该 channel 是否已关闭(x,ok := <- ch)

关闭 channel 会产生一个广播机制,所有向 channel 读取消息的 goroutine 都会收到消息

channel 在 Golang 中是一等公民,它是线程安全的,面对并发问题,应首先想到 channel。


## 20. 几个信道死锁经典错误案例详解

## 21. 学习 Go 协程:WaitGroup
WaitGroup 你只要实例化了就能使用
var 实例名 sync.WaitGroup
实例化完成后,就可以使用它的几个方法:
Add:初始值为0,你传入的值会往计数器上加,这里直接传入你子协程的数量
Done:当某个子协程完成后,可调用此方法,会从计数器上减一,通常可以使用 defer 来调用。
Wait:阻塞当前协程,直到实例里的计数器归零。
## 22. 学习 Go 协程:互斥锁和读写锁

## 23. Go 里的异常处理:panic 和 recover
2. 捕获 panic
发生了异常,有时候就得捕获,就像 Python 中的except 一样,那 Golang 中是如何做到的呢?
这就不得不引出另外一个内建函数 -- recover,它可以让程序在发生宕机后起生回生。
但是 recover 的使用,有一个条件,就是它必须在 defer 函数中才能生效,其他作用域下,它是不工作的。
3. 无法跨协程
从上面的例子,可以看到,即使 panic 会导致整个程序退出,但在退出前,若有 defer 延迟函数,还是得执行完 defer 。
但是这个 defer 在多个协程之间是没有效果,在子协程里触发 panic,只能触发自己协程内的 defer,而不能调用 main 协程里的 defer 函数的。
4. 总结一下
Golang 异常的抛出与捕获,依赖两个内置函数:
panic:抛出异常,使程序崩溃
recover:捕获异常,恢复程序或做收尾工作
revocer 调用后,抛出的 panic 将会在此处终结,不会再外抛,但是 recover,并不能任意使用,它有强制要求,必须得在 defer 下才能发挥用途。

## 24. 超详细解读 Go Modules 前世今生及入门使用

## 25. Go 语言中关于包导入必学的 8 个知识点

6. 导入的是路径还是包?
当我们使用 import 导入 testmodule/foo 时,初学者,经常会问,这个 foo 到底是一个包呢,还是只是包所在目录名?
import "testmodule/foo"
为了得出这个结论,专门做了个试验(请看「第七点里的代码示例」),最后得出的结论是:
导入时,是按照目录导入。导入目录后,可以使用这个目录下的所有包。
出于习惯,包名和目录名通常会设置成一样,所以会让你有一种你导入的是包的错觉。

如果使用 go modules
你导入的包如果有域名,都会先在 $GOPATH/pkg/mod 下查找,找不到就连网去该网站上寻找,找不到或者找到的不是一个包,则报错。
而如果你导入的包没有域名(比如 "fmt"这种),就只会到 $GOROOT 里查找。
还有一点很重要,当你的项目下有 vendor 目录时,不管你的包有没有域名,都只会在 vendor 目录中想找。
通常vendor 目录是通过 go mod vendor 命令生成的,这个命令会将项目依赖全部打包到你的项目目录下的 verdor 文件夹中。


## 26. 如何开源自己写的模块给别人用?

## 27. 说说 Go 语言中的类型断言?

## 28. 这五点带你理解Go语言的select用法
跟 switch-case 相比,select-case 用法比较单一,它仅能用于 信道/通道 的相关操作。
```
select {
case 表达式1:
<code>
case 表达式2:
<code>
default:
<code>
}
```

3. select 随机性
之前学过 switch 的时候,知道了 switch 里的 case 是顺序执行的,但在 select 里却不是。
4. select 的超时(通过一定时间后向timeout临时信道填充数据实现)
当 case 里的信道始终没有接收到数据时,而且也没有 default 语句时,select 整体就会阻塞,但是有时我们并不希望 select 一直阻塞下去,这时候就可以手动设置一个超时时间。
5. 读取/写入都可以

select 与 switch 原理很相似,但它的使用场景更特殊,学习了本篇文章,你需要知道如下几点区别:
select 只能用于 channel 的操作(写入/读出),而 switch 则更通用一些;
select 的 case 是随机的,而 switch 里的 case 是顺序执行;
select 要注意避免出现死锁,同时也可以自行实现超时机制;
select 里没有类似 switch 里的 fallthrough 的用法;
select 不能像 switch 一样接函数或其他表达式。


## 29. 盘一盘 Go 语言中的编码规范
## 30. 非常详细的 Go 语言 GDB 调试指南
## 31. 说说 Go 语言里的空接口

## 32. 理解 Go 语言中的 Context
4. 根Context 是什么?
创建 Context 必须要指定一个 父 Context,
```
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
```
5. Context 的继承衍生
上面在定义我们自己的 Context 时,我们使用的是 WithCancel 这个方法。

除它之外,context 包还有其他几个 With 系列的函数
```
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key, val interface{}) Context
```
这四个函数有一个共同的特点,就是第一个参数,都是接收一个 父context。
例子 1:WithDeadline
例子 2:WithTimeout
例子 3:WithValue
6. Context 使用注意事项
通常 Context 都是做为函数的第一个参数进行传递(规范性做法),并且变量名建议统一叫 ctx
Context 是线程安全的,可以放心地在多个 goroutine 中使用。
当你把 Context 传递给多个 goroutine 使用时,只要执行一次 cancel 操作,所有的 goroutine 就可以收到 取消的信号
不要把原本可以由函数参数来传递的变量,交给 Context 的 Value 来传递。
当一个函数需要接收一个 Context 时,但是此时你还不知道要传递什么 Context 时,可以先用 context.TODO 来代替,而不要选择传递一个 nil。
当一个 Context 被 cancel 时,继承自该 Context 的所有 子 Context 都会被 cancel。




## 33. 如何手动实现一个协程池?


## 34 图解:静态类型与动态类型
## 35. 关于接口的三个"潜规则"
1. 对方法的调用限制
2. 调用函数时的隐式转换
Go 语言中的函数调用都是值传递的,变量会在方法调用前进行类型转换。
如何进行接口类型的显示转换
b := interface{}(a)
3. 类型断言中的隐式转换
而当类型断言完成后,会返回一个静态类型为你断言的类型的对象,也就是说,当我们使用了类型断言,Go 实际上又会默认为我们进行了一次隐式的类型转换。

## 36. 图解:通俗易懂解释反射三大定律
在反射的规则里
不是接收变量指针创建的反射对象,是不具备『可写性』的
是否具备『可写性』,可使用 CanSet() 来获取得知
对不具备『可写性』的对象进行修改,是没有意义的,也认为是不合法的,因此会报错。


## 37. 反引号的妙用:结构体里的 Tag 标签
## 38. os/exec 执行命令的五种姿势
第一种:只执行命令,不获取结果
```
cmd := exec.Command("ls", "-l", "/var/log/")
err := cmd.Run()
```
第二种:执行命令,并获取结果
```
cmd := exec.Command("ls", "-l", "/var/log/")
out, err := cmd.CombinedOutput()
```
CombinedOutput 函数,只返回 out,并不区分 stdout 和 stderr。
第三种:执行命令,并区分stdout 和 stderr
```
cmd := exec.Command("ls", "-l", "/var/log/*.log")
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout // 标准输出
cmd.Stderr = &stderr // 标准错误
err := cmd.Run()
outStr, errStr := string(stdout.Bytes()), string(stderr.Bytes())
```
第四种:多条命令组合,请使用管道
第五种:设置命令级别的环境变量
```
os.Setenv("NAME", "wangbm")
```

## 39. 超全整理! 使用 fmt.Printf 打印的 N 种姿势
## 如何使用 Go 中的函数类型 (function types)?
```
package main

import "fmt"

// Greeting function types
type Greeting func(name string) string

func (g Greeting) say(n string) { #注意:这里是函数,而非struct
fmt.Println(g(n)) # 注意其调用方式
}

func english(name string) string {
return "Hello, " + name
}

func main() {
greet := Greeting(english)
greet.say("World")
}
// output: Hello, World
```

## 超强总结:Go 语言四种类型转换方法

第一种:显式类型转换
```
s3 := string(s2)
```
第二种:隐式类型转换
函数调用时转换
函数返回时转换
第三种:类型断言
第四种:重新构造对象
```
age := (int)(25)
// 等价于 age := 25
```

## 2.11 面向对象:Go 语言中的空结构体
空结构体的表象特征,就是没有任何属性,而从更深层次的角度来说,空结构体是一个不占用空间的对象。
基于这个特性,在一些特殊的场合之下,可以用做占位符使用,合理的使用空结构体,会减小程序的内存占用空间。

比如在使用信道(channel)控制并发时,我们只是需要一个信号,但并不需要传递值,这个时候,也可以使用 struct{} 代替。


## 7.8 什么叫字面量和组合字面量?
值的字面量是代码中值的文字表示。一个值可能存在多种字面量表示。
表示基本类型值的文本称为基本字面量,基本字面量也被称为字面量常量或未命名常量。包括:

布尔字面量(非严格意义上的字面量)

整型字面量

浮点数字面量

虚数字面量

Rune 字面量

字符串字面量

等等

基本字面量,很好理解,那组合字面量呢?

不就是把多个基本字面量组合在一起的字面量嘛。

比如结构体,切片,数组,字典,他们都可以容纳多个基本字面量。
## 7.9 为什么常量、字符串和字典不可寻址?
什么叫不可寻址?它指的是,不能通过 & 来获得内存地址的行为。
常量
如果常量可以寻址的话,我们就可以通过指针修改常数的值,这无疑破坏了常数的定义。
字典
从两个角度来反向推导,假设字典的元素是可寻址的,会出现 什么问题?

如果字典的元素不存在,则返回零值,而零值是不可变对象,如果能寻址问题就大了。

而如果字典的元素存在,考虑到 Go 中 map 实现中元素的地址是变化的,这意味着寻址的结果也是无意义的。
字符串
字符串是不可变的,因此不可寻址,没有问题

## 7.12 slice 扩容后容量及内存如何计算?todo
https://golang.iswbm.com/c07/c07_12.html

## 7.14 说说 Go 中闭包的底层原理?
其中关于:5. 再度变题
解释:
```
如果你在返回值里写了变量名,那么该变量会存储 main 的栈空间里,而如果你不写,那 i 只能存储在 func1 的栈空间里,与此同时,return 的值,不会作用于原变量 i 上,而是会存储在该函数在另一块栈内存里。
```
有问题。
个人理解,写不写都是局部的,不会保存main中。否则的话(下文的)情况func01,应该是同一个地址,而非2个地址。


个人理解如下:
```
func func1() (i int) {
i = 10
defer func() {
i += 1
fmt.Println("func1 inner &:",&i)
}()
return
}

func func2() (i int) {
i = 10
defer func() {
i += 1
}()
return 5
}

func func3() (int) {
i := 10
defer func() {
i += 1
fmt.Println("func3 inner02 &:",&i)
}()
fmt.Println("func3 inner01 &:",&i)
return i
}


func main() {
closure := func1()
fmt.Println("func1",closure)
fmt.Println("func1 &",&closure)
closure = func2()
fmt.Println("func2",closure)
closure = func3()
fmt.Println("func3",closure)
fmt.Println("func3 &:",&closure)
fmt.Println(&closure)
}


```
结果:
```
func1 inner &: 0xc0000b6018
func1 11
func1 & 0xc0000b6010 #03,这里的地址和inner,也不一致。这么说来结果应该和func3类似呀。
func2 6
func3 inner01 &: 0xc0000b6030
func3 inner02 &: 0xc0000b6030 # 01,和上面inner01相同的,正常的
func3 10
func3 &: 0xc0000b6010 # 02,和inner01,inner02,不同。说明inner01的数据被复制出来了。然后再被inner02那里修改,等价于return那里产生2份数据,一份继续函数内add,一份拷贝到外部
```
func1和func3非常相似,结果却有差别,主要原因是return何时作用的,将数据进行分叉的。
对于func1,和func2,i定义在(i int)函数上部,return 的分叉都是defer执行后的的i。
func3,i没有函数上部,就是普通逻辑,return那里就(拷贝出来)分叉了。
## 7.15 defer 的变量快照什么情况会失效?
```
func func1() {
age := 0
defer fmt.Println(age) // output: 0

age = 18
fmt.Println(age) // output: 18
}


func main() {
func1()
}

vs:

func func1() {
age := 0
defer func() {
fmt.Println(age)
}()
age = 18
return
}

func main() {
func1() // output: 18
}
```
上面的两个例子的区别就在于,一个 defer 后接的是单个表达式,另一个 defer 后接的是一个函数,并且不是普通函数,而是一个匿名的闭包函数。

根据闭包的特性,实际上在闭包函数存的是 age 这个变量的指针(原因可以查看上一篇文章:Go 语言面试题 100 讲之 014篇:说说 Go 中闭包的底层原理?),因而,在 defer 后所修改的值会直接影响到 defer 中的 age 的值。

总结一下:

若 defer 后接的是单行表达式,那defer 中的 age 只是拷贝了 func1 函数栈中 defer 之前的 age 的值;

若 defer 后接的是闭包函数,那defer 中的 age 只是存储的是 func1 函数栈中 age 的指针。
## 8.2 Go 语言中边界检查



方法集,表达式:https://www.topgoer.com/方法/
1. 接口:https://www.topgoer.com/面向对象/接口.html


## 特别留意
1.12 流程控制:defer 延迟语句:https://golang.iswbm.com/c01/c01_12.html
章节:2. 即时求值的变量快照
作者说:非常抱歉,目前以我的水平,暂时还无法解释这种现象,我建议是单独记忆一下这种特殊场景。
实际不难理解,用切片作测试可以发现,并非:变量快照。(实际内容会受到后续修改影响)
而是:变量地址快照
defer 会将后面变量地址,
比如: name := "go"
中:字符串"go"的地址0x1234存下来。
后续 name = "python",修改了name变量指向,
原来地址:0x1234,保存的还是"go"。
所以,后续对name的修改,不会影响前面的defer。如果是数组,只要地址不变,内部内容变化, 会引起defer里信息的变化。

对于的闭包的坑,比如此文
Golang 中关于闭包的坑:https://www.cnblogs.com/sky1130/p/11011078.html
案例1,

func main() {
s := []string{“a”, “b”, “c”}
for _, v := range s {
go func() {
fmt.Println(v)
}()
}
select {} // 阻塞模式
}

// 嗯,结果应该是 a,b,c 吧
实际:c,c,c

1
2
3
原因就是:for里的这个v,是原地替换的,地址不变。  
比如第一次:v="a","a"地址0x1234,第二次v="b"时,地址还是,0x1234,而非新地址,
修改为

func main() {
s := []string{“a”, “b”, “c”}
for _, v := range s {
go func(v string) {
fmt.Println(v)
}(v) //每次将变量 v 的拷贝传进函数
}
select {}
}

之后,v是新的局部变量,地址自然每次循环都会变。  

所以不论这里的问题,**还是闭包的坑,本质都是返回的地址,指向信息是否变化了。指向的没变就不变,指向的变了就变了**。    


go入门系列  
[入门02_IDE安装](https://hexo.yuanjh.cn/hexo/a2b59ef5/)  
[入门03_工具链](https://hexo.yuanjh.cn/hexo/f17d3a44/)  
[入门04_入门demo和基本类型](https://hexo.yuanjh.cn/hexo/a2c6de1b/)  
[入门05_go升级版本](https://hexo.yuanjh.cn/hexo/809bd82a/)  
[入门06_教程biancheng](https://hexo.yuanjh.cn/hexo/e8158f17/)  
[入门08_教程编程时光](https://hexo.yuanjh.cn/hexo/d0d6289c/)  
[入门09_Go语言高级编程](https://hexo.yuanjh.cn/hexo/16919b98/)  
[入门10_包导入](https://hexo.yuanjh.cn/hexo/7ce88a01/)  
[入门11_方法接口和嵌入类型](https://hexo.yuanjh.cn/hexo/e0631d27/)  
[入门12_数组和切片](https://hexo.yuanjh.cn/hexo/2dfba3de/)  
#
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×