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,
|