├── .gitignore ├── LICENSE ├── README.md └── src ├── chapter01 └── 01.0.md ├── chapter02 └── 01.0.md ├── chapter03 └── 01.0.md ├── chapter04 └── 01.0.md ├── chapter05 └── 01.0.md ├── chapter06 └── 01.0.md ├── chapter07 └── 01.0.md ├── chapter08 └── 01.0.md ├── chapter09 └── 01.0.md ├── chapter10 └── 01.0.md ├── chapter11 └── 01.0.md ├── chapter12 └── 01.0.md ├── chapter13 └── 01.0.md ├── chapter14 └── 01.0.md ├── chapter15 └── 01.0.md ├── chapter16 └── 01.0.md ├── chapter17 └── 01.0.md ├── chapter18 └── 01.0.md ├── chapter19 └── 01.0.md ├── chapter20 └── 01.0.md ├── example ├── EX.0.1.md └── EX.0.2.md ├── images ├── 1.gif ├── 1.jpg ├── 10.jpg ├── 11.jpg ├── 12.jpg ├── 13.jpg ├── 14.jpg ├── 15.jpg ├── 16.jpg ├── 17.jpg ├── 18.jpg ├── 19.jpg ├── 2.gif ├── 2.jpg ├── 20.jpg ├── 21.jpg ├── 22.jpg ├── 23.jpg ├── 24.jpg ├── 25.jpg ├── 26.jpg ├── 27.jpg ├── 28.jpg ├── 29.jpg ├── 3.gif ├── 3.jpg ├── 30.jpg ├── 31.jpg ├── 32.jpg ├── 33.jpg ├── 34.jpg ├── 35.jpg ├── 36.jpg ├── 37.jpg ├── 38.jpg ├── 39.jpg ├── 4.gif ├── 4.jpg ├── 40.jpg ├── 41.jpg ├── 5.gif ├── 5.jpg ├── 6.gif ├── 6.jpg ├── 7.gif ├── 7.jpg ├── 8.gif ├── 8.jpg ├── 9.gif └── 9.jpg └── spec ├── 01.0.md ├── 02.0.md └── 03.0.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | .idea 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### For-learning-Go-Tutorial 2 | 准备写一本Go的书针对初学者快速入门开发和使用go! 3 | 4 |

5 | 6 |

7 | 8 |

9 | 10 | 11 | 12 |

13 | 14 | 学习Go语言需要去了解Go的特性,然后在深入的去实践,如果你想使用Go语言写出Go味道的程序,那么你就需要付出努力去实践了! 15 | 先来了解下Go语言为何创造出来的历史吧,Go 语言是由谷歌公司在 2007 年开始开发的一门语言,目的是能在多核心时代高效编写网络应用程序。Go 语言的创始人 Robert Griesemer、Rob Pike 和 Ken Thompson 都是在计算机发展过程中作出过重要贡献的人。 16 | 自从 2009 年 11 月正式公开发布后,Go 语言迅速席卷了整个互联网后端开发领域,其社区里不断涌现出类似 vitess、Docker、etcd、Consul 等重量级的开源项目。 17 | 18 | Go是一种编译型语言,一种并发的、带垃圾回收的、快速编译的语言。它具有以下特点: 19 | 20 | * 它可以在一台计算机上用几秒钟的时间编译一个大型的Go程序。 21 | * Go为软件构造提供了一种模型,它使依赖分析更加容易,且避免了大部分C风格include文件与库的开头。 22 | * Go是静态类型的语言,它的类型系统没有层级。因此用户不需要在定义类型之间的关系上花费时间,这样感觉起来比典型的面向对象语言更轻量级。 23 | * Go完全是垃圾回收型的语言,并为并发执行与通信提供了基本的支持。 24 | 25 | 为了获得最佳的运行性能,GO语言被设计成一门静态编译型的语言,而不是动态解释型语言,这里我对静态编译型的语言和动态解释型语言做个简单的介绍,方便初学Go语言的新人理解。 26 | 27 | 1.静态编译型的语言 28 | 29 | 静态类型语言是指在编译时变量的数据类型即可确定的语言,多数静态类型语言要求在使用变量之前必须声明数据类型,某些具有类型推导能力的现代语言可能能够部分减轻这个要求. 并且通过编译器(compiler)将源代码编译成机器码,之后才能执行的语言。一般需经过编译(compile)、链接(linker)这两个步骤。编译是把源代码编译成机器码,链接是把各个模块的机器码和依赖库串连起来生成可执行文件。 30 | 31 | 代表语言:Java,Go,C 32 | 33 | 2.动态解释型语言 34 | 35 | 动态类型语言是在运行 时确定数据类型的语言。变量使用之前不需要类型声明,通常变量的类型是被赋值的那个值的类型。解释性语言的程序不需要编译,相比编译型语言省了道工序,解释性语言在运行程序的时候才逐行翻译。 36 | 37 | 代表语言:JavaScript、Python 38 | 39 | Go语言的官方编译器被称为gc,包括编译工具5g,6g.8g,链接工具51,61和81以及文档查看工具godoc. 40 | 41 | 学习一门语言最好的方式就是去实践,那么我们就从go的例子开始实践吧! 42 | 43 | * [例子入门](https://github.com/KeKe-Li/For-learning-Go-Tutorial/blob/master/src/example/EX.0.1.md) 44 | * [基本结构](https://github.com/KeKe-Li/For-learning-Go-Tutorial/blob/master/src/chapter01/01.0.md) 45 | * [基本数据类型](https://github.com/KeKe-Li/For-learning-Go-Tutorial/blob/master/src/chapter02/01.0.md) 46 | * [复合数据类型](https://github.com/KeKe-Li/For-learning-Go-Tutorial/blob/master/src/chapter03/01.0.md) 47 | * [函数](https://github.com/KeKe-Li/For-learning-Go-Tutorial/blob/master/src/chapter04/01.0.md) 48 | * [方法(method)](https://github.com/KeKe-Li/For-learning-Go-Tutorial/blob/master/src/chapter05/01.0.md) 49 | * [接口(Interfaces)](https://github.com/KeKe-Li/For-learning-Go-Tutorial/blob/master/src/chapter06/01.0.md) 50 | * [反射(reflection)](https://github.com/KeKe-Li/For-learning-Go-Tutorial/blob/master/src/chapter07/01.0.md) 51 | * [通信协议解析](https://github.com/KeKe-Li/For-learning-Go-Tutorial/blob/master/src/chapter08/01.0.md) 52 | * [Channel](https://github.com/KeKe-Li/For-learning-Go-Tutorial/blob/master/src/chapter09/01.0.md) 53 | * [Goroutine并发处理](https://github.com/KeKe-Li/For-learning-Go-Tutorial/blob/master/src/chapter10/01.0.md) 54 | * [Golang包详解](https://github.com/KeKe-Li/For-learning-Go-Tutorial/blob/master/src/chapter11/01.0.md) 55 | * [Grpc与Protobuf](https://github.com/KeKe-Li/For-learning-Go-Tutorial/blob/master/src/chapter12/01.0.md) 56 | * [Golang逃逸分析](https://github.com/KeKe-Li/For-learning-Go-Tutorial/blob/master/src/chapter13/01.0.md) 57 | * [Etcd的使用](https://github.com/KeKe-Li/For-learning-Go-Tutorial/blob/master/src/chapter14/01.0.md) 58 | * [TiDB的使用](https://github.com/KeKe-Li/For-learning-Go-Tutorial/blob/master/src/chapter15/01.0.md) 59 | * [Go排序算法及其性能比较](https://github.com/KeKe-Li/For-learning-Go-Tutorial/blob/master/src/chapter16/01.0.md) 60 | * [Go程序测试](https://github.com/KeKe-Li/For-learning-Go-Tutorial/blob/master/src/chapter17/01.0.md) 61 | * [Sync.Map解析](https://github.com/KeKe-Li/For-learning-Go-Tutorial/blob/master/src/chapter18/01.0.md) 62 | * [Sync.WaitGroup解析](https://github.com/KeKe-Li/For-learning-Go-Tutorial/blob/master/src/chapter19/01.0.md) 63 | * [Go异步抢占式调度](https://github.com/KeKe-Li/For-learning-Go-Tutorial/blob/master/src/chapter20/01.0.md) 64 | * [Go编程标准和规范](https://github.com/KeKe-Li/For-learning-Go-Tutorial/blob/master/src/spec/01.0.md) 65 | * [Golang垃圾回收](https://github.com/KeKe-Li/For-learning-Go-Tutorial/blob/master/src/spec/02.0.md) 66 | * [Go练习案例](https://github.com/KeKe-Li/For-learning-Go-Tutorial/blob/master/src/example/EX.0.2.md) 67 | * [Golang中处理slice的注意事项](https://github.com/KeKe-Li/For-learning-Go-Tutorial/blob/master/src/spec/03.0.md) 68 | 69 | 70 | ### golang编程 71 | 72 | 觉得此文章不错,支持我的话可以给我star ,:star:!如果有问题可以加我的微信,也可以加入我们的交流群一起交流golang技术! 73 | 74 | 75 | ### License 76 | This is free software distributed under the terms of the MIT license 77 | -------------------------------------------------------------------------------- /src/chapter01/01.0.md: -------------------------------------------------------------------------------- 1 | ### For-learning-Go-Tutorial 2 | 3 | Go语言是谷歌2009发布的第二款开源编程语言 4 | 5 | Go语言专门针对多处理器系统应用程序的编程进行了优化,使用Go编译的程序可以媲美C或C++代码的速度,而且更加安全、支持并行进程。 6 | 7 | 因而一直想的是自己可以根据自己学习和使用Go语言编程的心得,写一本Go的书可以帮助想要学习Go语言的初学者快速入门开发和使用! 8 | 9 | #### 基本结构 10 | 11 | 任何一个编程语言都有自己的基础架构,只有在了解了编程语言的基础架构后才能开始进行一些简单的编程的逻辑,然后组合简单结构变为复杂的数据结构,其实编程很多的时候都是一种艺术,写出的代码也是一种艺术,给人赏心悦目的感觉,这需要持续不断的努力和精进才能达到。 12 | 13 | 基本结构有: 14 | * [命名](#命名) 15 | * [常量](#常量) 16 | * [变量](#变量) 17 | * [赋值](#赋值) 18 | * [类型](#类型) 19 | * [包和文件](#包和文件) 20 | * [作用域](#作用域) 21 | 22 | 23 | 24 | #### 命名 25 | 26 | 命名在所有的编程语言中都是有规则可寻的,也是需要遵守的,只有我们有了好的命名习惯才可以写出好的代码,例如我们在生活中对建筑的命名也是希望可以表达这个建筑的含义和作用。在Go语言中也是一样的,Go语言的函数名,变量名,常量名,类型名和包的命名也是都遵循这一规则的:一个一个名字必须以一个字母(Unicode字母)或下划线开头,后面可以跟任意数量的字母、数字或下划线。大写字母和小写字母是不同的:Car和car是两个不同的名字。 27 | 28 | Go语言中也有类似java的关键字,且关键字不能用于自定义名字,只能在特定语法结构中使用. 29 | 30 | ```markdown 31 | break default func interface select 32 | case defer go map struct 33 | chan else goto package switch 34 | const fallthrough if range type 35 | continue for import return var 36 | ``` 37 | 除此之外Go语言中还有30多个预定义的名字,比如int和ture等 38 | 39 | ```markdown 40 | 内建常量: true false iota nil 41 | 42 | 内建类型: int int8 int16 int32 int64 uint uint8 uint16 uint32 uint64 uintptr 43 | float32 float64 complex128 complex64 bool byte rune string error 44 | 45 | 内建函数: make len cap new append copy close delete complex real imag panic recover 46 | ``` 47 | 通常我们在Go语言编程中推荐的命名方式是驼峰命名例如:ReadAll,不推荐下划线命名。 48 | 49 | 50 | #### 常量 51 | 52 | 在Go语言中,常量是指编译期间就已知且不可改变的值。常量可以是数值类型(包括整型、浮点型和复数类型)、布尔类型、字符串类型等。 53 | 下面我们使用const 关键字来定义常量: 54 | 55 | ```go 56 | package main 57 | 58 | import "fmt" 59 | import "math" 60 | 61 | // "const" 关键字用来定义常量 62 | const s string = "appropriate" 63 | 64 | func main() { 65 | fmt.Println(s) 66 | 67 | // "const"关键字可以出现在任何"var"关键字出现的地方 68 | // 区别是常量必须有初始值 69 | const n = 20 70 | 71 | // 常量表达式可以执行任意精度数学计算 72 | const d = 3e20 / n 73 | fmt.Println(d) 74 | 75 | // 数值型常量没有具体类型,除非指定一个类型 76 | // 比如显式类型转换 77 | fmt.Println(int64(d)) 78 | 79 | // 数值型常量可以在程序的逻辑上下文中获取类型 80 | // 比如变量赋值或者函数调用。 81 | // 例如,对于math包中的Sin函数,它需要一个float64类型的变量 82 | fmt.Println(math.Sin(n)) 83 | } 84 | ``` 85 | 输出的结果为: 86 | 87 | ```go 88 | appropriate 89 | 6e+11 90 | 600000000000 91 | -0.28470407323754404 92 | ``` 93 | 94 | #### 变量 95 | 通常用var声明语句可以创建一个特定类型的变量,然后给变量附加一个名字,并且设置变量的初始值。 96 | 97 | Go的基本类型有: 98 | ```markdown 99 | * bool 100 | * string 101 | * int int8 int16 int32 int64 102 | * uint uint8 uint16 uint32 uint64 uintptr 103 | * byte // uint8 的别名 104 | * rune // int32 的别名 代表一个Unicode码 105 | * float32 float64 106 | * complex64 complex128 107 | ``` 108 | 109 | 变量的声明的语法一般是: 110 | ```markdown 111 | var 变量名字 类型 = 表达式 112 | ``` 113 | 通常情况下“类型”或“= 表达式”两个部分可以省略其中的一个。如果省略的是类型信息,那么将根据初始化表达式来推导变量的类型信息。如果初始化表达式被省略,那么将用零值初始化该变量。数值类型变量对应的零值是0,布尔类型变量对应的零值是false,字符串类型对应的零值是空字符串,接口或引用类型(包括slice、map、chan和函数)变量对应的零值是nil。数组或结构体等聚合类型对应的零值是每个元素或字段都是对应该类型的零值。 114 | 115 | 零值初始化机制可以确保每个声明的变量总是有一个良好定义的值,因此在Go语言中不存在未初始化的变量。 116 | 117 | 通常我们在编程的过程中,也用简短声明变量。它以“名字 := 表达式”形式声明变量,变量的类型根据表达式来自动推导。 118 | 119 | ```go 120 | destination := 12 121 | result := rand.Float64() * 3.0 122 | ``` 123 | 124 | 因为简洁和灵活的特点,简短变量声明被广泛用于大部分的局部变量的声明和初始化。var形式的声明语 125 | 句往往是用于需要显式指定变量类型地方,或者因为变量稍后会被重新赋值而初始值无关紧要的地方。 126 | 127 | 变量的生命周期指的是在程序运行期间变量有效存在的时间间隔。对于在包一级声明的变量来说,它们 128 | 的生命周期和整个程序的运行周期是一致的。而相比之下,在局部变量的声明周期则是动态的:从每次 129 | 创建一个新变量的声明语句开始,直到该变量不再被引用为止,然后变量的存储空间可能被回收。函数 130 | 的参数变量和返回值变量都是局部变量。它们在函数每次被调用的时候创建。 131 | 132 | #### 赋值 133 | 134 | 使用赋值语句可以更新一个变量的值,最简单的赋值语句是将要被赋值的变量放在=的左边,新值的表达 135 | 式放在=的右边。 136 | 137 | ```go 138 | x = 1 // 命令变量的赋值 139 | *p = true // 通过指针间接赋值 140 | person.name = "keke" // 结构体字段赋值 141 | count[n] = count[n] * scale // 数组、slice或map的元素赋值 142 | ``` 143 | 数值变量也可以支持++递增和--递减语句 144 | 145 | ```go 146 | v := 1 147 | v++ // 等价方式 v = v + 1;v 变成 2 148 | v-- // 等价方式 v = v - 1;v 变成 1 149 | ``` 150 | 151 | #### 类型 152 | 153 | 变量或表达式的类型定义了对应存储值的属性特征,类型声明语句一般出现在包一级,因此如果新创建的类型名字的首字符大写,则在外部包也可以使用。 154 | 155 | ```go 156 | type 类型名字 底层类型 157 | 158 | type Precision float64 #精确度 159 | ``` 160 | 161 | #### 包和文件 162 | 163 | Go语言中的包和其他语言的库或模块的概念类似,目的都是为了支持模块化、封装、单独编译和代码重 164 | 用。一个包的源代码保存在一个或多个以.go为文件后缀名的源文件中. 165 | 在Go语言中包还可以让我们通过控制哪些名字是外部可见的来隐藏内部实现信息。在Go语言中,一个简单的规则 166 | 是:如果一个名字是大写字母开头的,那么该名字是导出的。 167 | 168 | 如果包中含有多个.go源文件,它们将按照发给编译器的顺序进行初始化,Go语言的构建工具首先会 169 | 将.go文件根据文件名排序,然后依次调用编译器编译。 170 | 对于在包级别声明的变量,如果有初始化表达式则用表达式初始化,还有一些没有初始化表达式的,例 171 | 如某些表格数据初始化并不是一个简单的赋值过程。在这种情况下,我们可以用一个特殊的init初始化 172 | 函数来简化初始化工作。每个文件都可以包含多个init初始化函数 173 | 174 | ```go 175 | 176 | func init() { } 177 | ``` 178 | 179 | 这样的init初始化函数除了不能被调用或引用外,其他行为和普通函数类似。在每个文件中的init初始 180 | 化函数,在程序开始执行时按照它们声明的顺序被自动调用。 181 | 每个包在解决依赖的前提下,以导入声明的顺序初始化,每个包只会被初始化一次。因此,如果一个p包 182 | 导入了m包,那么在p包初始化的时候可以认为m包必然已经初始化过了。初始化工作是自下而上进行的, 183 | main包最后被初始化。以这种方式,可以确保在main函数执行之前,所有依然的包都已经完成初始化工 184 | 作了。 185 | 186 | import导入包的用法: 187 | ```go 188 | import "github.com/tidwall/gjson" //通过包名gjson调用导出接口 189 | import json "github.com/tidwall/gjson" //通过别名json调用gjson 190 | import . "github.com/tidwall/gjson" //.符号表示,对包gjson的导出接口的调用直接省略包名 191 | import _ "github.com/tidwall/gjson" //_ 仅仅会初始化gjson,如初始化全局变量,调用init函数 192 | ``` 193 | 194 | #### 作用域 195 | 196 | 一个声明语句将程序中的实体和一个名字关联,比如一个函数或一个变量。声明语句的作用域是指源代 197 | 码中可以有效使用这个名字的范围。 198 | 不要将作用域和生命周期混为一谈。声明语句的作用域对应的是一个源代码的文本区域;它是一个编译 199 | 时的属性。一个变量的生命周期是指程序运行时变量存在的有效时间段,在此时间区域内它可以被程序 200 | 的其他部分引用;是一个运行时的概念。 201 | 202 | 语法块是由花括弧所包含的一系列语句,就像函数体或循环体花括弧对应的语法块那样。语法块内部声 203 | 明的名字是无法被外部语法块访问的。语法决定了内部声明的名字的作用域范围。我们可以这样理解, 204 | 语法块可以包含其他类似组批量声明等没有用花括弧包含的代码,我们称之为语法块。有一个语法块为 205 | 整个源代码,称为全局语法块;然后是每个包的包语法决;每个for、if和switch语句的语法决;每个 206 | switch或select的分支也有独立的语法决;当然也包括显式书写的语法块(花括弧包含的语句)。 207 | 208 | 声明语句对应的词法域决定了作用域范围的大小。对于内置的类型、函数和常量,比如int、len和true 209 | 等是在全局作用域的,因此可以在整个程序中直接使用。任何在在函数外部(也就是包级语法域)声明 210 | 的名字可以在同一个包的任何源文件中访问的。对于导入的包,例如tempconv导入的fmt包,则是对应源 211 | 文件级的作用域,因此只能在当前的文件中访问导入的fmt包,当前包的其它源文件无法访问在当前源文 212 | 件导入的包 213 | 214 | 215 | 在包级别,声明的顺序并不会影响作用域范围,因此一个先声明的可以引用它自身或者是引用后面的一 216 | 个声明,这可以让我们定义一些相互嵌套或递归的类型或函数。但是如果一个变量或常量递归引用了自 217 | 身,则会产生编译错误。 218 | 219 | ```go 220 | if f, err := os.Open(fname); err != nil { // compile error: unused: f 221 | return err 222 | } 223 | f.ReadByte() // compile error: undefined f 224 | f.Close() // compile error: undefined f 225 | ``` 226 | 227 | 变量f的作用域只有在if语句内,因此后面的语句将无法引入它,这将导致编译错误。你可能会收到一个 228 | 局部变量f没有声明的错误提示,具体错误信息依赖编译器的实现。 229 | -------------------------------------------------------------------------------- /src/chapter02/01.0.md: -------------------------------------------------------------------------------- 1 | ### For-learning-Go-Tutorial 2 | 3 | Go语言是谷歌2009发布的第二款开源编程语言 4 | 5 | Go语言专门针对多处理器系统应用程序的编程进行了优化,使用Go编译的程序可以媲美C或C++代码的速度,而且更加安全、支持并行进程。 6 | 7 | 因而一直想的是自己可以根据自己学习和使用Go语言编程的心得,写一本Go的书可以帮助想要学习Go语言的初学者快速入门开发和使用! 8 | 9 | #### 基本数据类型 10 | 11 | Go基本数据类型这块是基础所以需要着重去学习和实践运用。 12 | 13 | 基本数据类型有: 14 | * [整型](#整型) 15 | * [浮点型](#浮点型) 16 | * [复数](#复数) 17 | * [布尔型](#布尔型) 18 | * [字符串](#字符串) 19 | * [常量](#常量) 20 | * [整型运算](#整型运算) 21 | 22 | #### 整型 23 | 24 | Go语言同时提供了有符号和无符号类型的整数运算。 25 | 26 | ```go 27 | 有符号整形数类型: 28 | int8,长度:1字节, 取值范围:(-128 ~ 127) 29 | int16,长度:2字节,取值范围:(-32768 ~ 32767) 30 | int32,长度:4字节,取值范围:(-2,147,483,648 ~ 2,147,483,647) 31 | int64.长度:8字节,取值范围:(-9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807) 32 | 33 | 无符号整形数类型: 34 | uint8,长度:1字节, 取值范围:(0 ~ 255) 35 | uint16,长度:2字节,取值范围:(0 ~ 65535) 36 | uint32,长度:4字节,取值范围:(0 ~ 4,294,967,295) 37 | uint64.长度:8字节,取值范围:(0 ~ 18,446,744,073,709,551,615) 38 | ``` 39 | 40 | 字符是 UTF-8 编码的 Unicode 字符,Unicode 为每一个字符而非字形定义唯一的码值(即一个整数),例如 字符a 在 unicode 字符表是第 97 个字符,所以其对应的数值就是 97,也就是说对于Go语言处理字符时,97 和 a 都是指的是字符a,而 Go 语言将使用数值指代字符时,将这样的数值称呼为 rune 类型。 41 | rune类型是 Unicode 字符类型,和 int32 类型等价,通常用于表示一个 Unicode 码点。rune 和 int32 可以互换使用。 42 | 一个Unicode代码点通常由"U+"和一个以十六进制表示法表示的整数表示,例如英文字母'A'的Unicode代码点为"U+0041"。 43 | 44 | 此外rune类型的值需要由单引号"'"包裹,不过我们还可以用另外几种方式表示: 45 | 46 |

47 | 48 |

49 | 50 | rune类型值的表示中支持几种特殊的字符序列,即:转义符。 51 | 52 |

53 | 54 |

55 | 56 | byte是uint8类型的等价类型,byte类型一般用于强调数值是一个原始的数据而不是 一个小的整数。 57 | 58 | uintptr 是一种无符号的整数类型,没有指定具体的bit大小但是足以容纳指针。 uintptr类型只有在底层编程是才需要,特别是Go语言和C语言函数库或操作系统接口相交互的地方。 59 | 60 | 此外在这里还需要了解下进制的转换方便以后学习和使用: 61 | 62 | ```markdown 63 | 十进制整数: 使用0-9的数字表示且不以0开头。// 100 123455 64 | 八进制整数: 以0开头,0-7的数字表示。 // 0100 0600 65 | 十六进制整数: 以0X或者是0x开头,0-9|A-F|a-f组成 //0xff 0xFF12 66 | ``` 67 | #### 浮点型 68 | 69 | 浮点型。float32 精确到小数点后 7 位,float64 精确到小数点后 15 位。由于精确度的缘故,你在使用 `==` 或者 `!=` 来比较浮点数时应当非常小心。 70 | 71 | ```go 72 | 浮点型(IEEE-754 标准): 73 | float32:(+- 1e-45 -> +- 3.4 * 1e38)32位浮点类型 74 | 75 | float64:(+- 5 1e-324 -> 107 1e308)64位浮点类型 76 | ``` 77 | 浮点型中指数部分由"E"或"e"以及带正负号的10进制整数表示。例:`3.9E-2`表示浮点数`0.039`。`3.9E+1`表示浮点数39。 78 | 有时候浮点数类型值也可以被简化。比如39.0可以被简化为39。0.039可以被简化为.039。在Golang中浮点数的相关部分只能由10进制表示法表示。 79 | 80 | 81 | #### 复数 82 | ```go 83 | 复数类型: 84 | complex64: 由两个float32类型的值分别表示复数的实数部分和虚数部分 85 | 86 | complex128: 由两个float64类型的值表示复数的实数部分和虚数部分 87 | ``` 88 | 复数类型的值一般由浮点数表示的实数部分、加号"+"、浮点数表示的虚数部分以及小写字母"i"组成,例如: 89 | ```go 90 | var x complex128 = complex(1,2) //1+2i 91 | ``` 92 | 93 | 对于一个复数 c = complex(x, y) ,可以通过Go语言内置函数 real(z) 获得该复数的实 94 | 部,也就是 x ,通过 imag(c) 获得该复数的虚部,也就是 y 。 95 | 96 | #### 布尔型 97 | 在Go语言中,布尔值的类型为 bool,值是 true 或 false,布尔可以做3种逻辑运算,&&(逻辑且),||(逻辑或),!(逻辑非),布尔类型的值不支持其他类型的转换. 98 | 99 | 布尔值可以和&&(AND)和||(OR)操作符结合,并且可能会有短路行为:如果运算符左边值已经可以确定整个布尔表达式的值,那么运算符右边的值将不在被求值,因此下面的表达式总是安全的: 100 | 101 | ```go 102 | s != "" && s[0] == 'x' 103 | ``` 104 | 其中s[0]操作如果应用于空字符串将会导致panic异常。 105 | 106 | #### 字符串 107 | 在Go语言中,组成字符串的最小单位是字符,存储的最小单位是字节,字符串本身不支持修改。字节是数据存储的最小单元,每个字节的数据都可以用整数表示,例如一个字节储存的字符a,实际存储的是97而非字符的字形,将这个实际存储的内容用数字表示的类型,称之为byte。 108 | 109 | 字符串是不可变的字节序列,它可以包含任意数据,包括0值字节,但是主要还是为了人可读的文本。内置的 len()函数返回字符串的字节数。 110 | 111 | 字符串的表示法有两种,即:原生表示法和解释型表示法。原生表示法,需用用反引号"`"把字符序列包起来,如果用解释型表示法,则需要用双引号"""包裹字符序列。 112 | 113 | ```go 114 | var str1 string = "keke" 115 | var str2 string = `keke` 116 | ``` 117 | 这两种表示的区别是,前者表示的是所见即所得的(除了回车符)。后者所表示的值中转义符会起作用。字符串值是不可变的,如果我们创建了一个此类型的值,就不可能再对它本身做任何修改。 118 | 119 | ```go 120 | var str string // 声明一个字符串变量 121 | str = "hai keke" // 字符串赋值 122 | ch := str[0] // 取字符串的第一个字符 123 | ``` 124 | 125 | #### 常量 126 | 常量表达式的值在编译期计算,而不是在运行期。每种常量的潜在类型都是基础类型:boolean、string或数字。 127 | 128 | ```go 129 | const x, y int = 1, 2 // 多常量初始化 130 | const s = "Hello, KeKe!" // 类型推断 131 | const ( // 常量组 132 | a, b, c = 10, 20, 30 133 | bool = false 134 | ) 135 | 136 | func main() { 137 | 138 | const m = "20"// 未使用用局部常量不会引发编译错误。 139 | } 140 | ``` 141 | 142 | 枚举:关键字 iota 定义常量组中从 0 开始按行行计数的自自增枚举值。 143 | 144 | iota在const关键字出现时将被重置为0(const内部的第一行之前),const中每新增一行常量声明将使iota计数一次(iota可理解为const语句块中的行索引)。使用iota能简化定义,在定义枚举时很有用。 145 | 146 | ```go 147 | const ( 148 | Sunday = iota // 0 149 | Monday // 1,通常省略后续行行表达式。 150 | Tuesday // 2 151 | Wednesday // 3 152 | Thursday // 4 153 | Friday // 5 154 | Saturday // 6 155 | ) 156 | ``` 157 | 在同一常量组中,可以提供多个 iota,它们各自增⻓。 158 | ```go 159 | const ( 160 | A, B = iota, iota << 10 // 0, 0 << 10 161 | C, D // 1, 1 << 10 162 | ) 163 | ``` 164 | 容量大小的单位的自增: 165 | ```go 166 | const ( 167 | B = 1 << (10*iota) 168 | KB 169 | MB 170 | GB 171 | TB 172 | PB 173 | ) 174 | ``` 175 | bit就是位,也叫比特位,是计算机表示数据最小的单位,byte是字节。`1B(byte,字节,1byte就是1B)= 8 bit(位就是bit也是b)`,一个字符=2字节(2byte)。 176 | 177 | 178 | #### 整型运算 179 | 180 | 在整型运算中,算术运算、逻辑运算和比较运算,运算符优先级从上到下递减顺序排列: 181 | ```go 182 | * / % << >> & &^ 183 | + - | ^ 184 | == != < <= > >= 185 | && 186 | || 187 | 188 | ``` 189 | 在同一个优先级,使用左优先结合规则,但是使用括号可以明确优先顺序。 190 | 191 | bit位操作运算符: 192 | 193 | |符号 | 操作 |操作数是否区分符号 | 194 | |-----|-----------------|------------------| 195 | | & | 位运算 AND |No | 196 | | ^ | 位运算 XOR |No | 197 | | &^ | 位清空 (AND NOT) |No | 198 | | << | 左移 |Yes | 199 | | >> | 右移 |Yes | 200 | -------------------------------------------------------------------------------- /src/chapter04/01.0.md: -------------------------------------------------------------------------------- 1 | ### For-learning-Go-Tutorial 2 | 3 | Go语言是谷歌2009发布的第二款开源编程语言。 4 | 5 | Go语言专门针对多处理器系统应用程序的编程进行了优化,使用Go编译的程序可以媲美C或C++代码的速度,而且更加安全、支持并行进程。 6 | 7 | 因而一直想的是自己可以根据自己学习和使用Go语言编程的心得,写一本Go的书可以帮助想要学习Go语言的初学者快速入门开发和使用! 8 | 9 | #### 函数 10 | 11 | Go是编译型语言,因此与函数编写的顺序是无关的,但是为了更好的可读性,需要把main() 函数写在文件的前面,其他函数按照一定逻辑顺序进行编写(例如函数被调用的顺序)。 12 | 13 | 编写多个函数的主要目的是将一个需要很多行代码的复杂问题分解为一系列简单的任务(那就是函数)来解决。而且,同一个任务(函数)可以被调用多次,有助于代码重用。 14 | 15 | 当函数执行到代码块最后一行(结束的} 之前)或者 return 语句的时候会退出,其中 return 语句可以带有零个或多个参数,这些参数将作为返回值供调用者使用。简单的 return 语句也可以用来结束 for 死循环,或者结束一个协程(goroutine)。 16 | 17 | 在Go中主要有三种类型的函数: 18 | 19 | 1. 普通的带有名字的函数. 20 | 2. 匿名函数或者lambda函数. 21 | 3. 方法(Methods). 22 | 23 | 在函数这一章我们讲述的目录: 24 | 25 | * [函数参数与返回值](#函数参数与返回值) 26 | * [将函数作为参数传递](#将函数作为参数传递) 27 | * [内置函数](#内置函数) 28 | * [递归函数](#递归函数) 29 | * [匿名函数](#匿名函数) 30 | * [defer延迟函数](#defer延迟函数) 31 | * [panic异常和](#panic异常) 32 | * [recover捕获异常](#recover捕获异常) 33 | 34 | #### 函数参数与返回值 35 | 36 | 函数构成代码执行的逻辑结构。在Go语言中,函数的基本组成为:关键字 func 、函数名、参数列表、返回值、函数体和返回语句。除了main()、init()函数外,其它所有类型的函数都可以有参数与返回值。函数参数、返回值以及它们的类型被统称为函数签名。 37 | ```go 38 | 39 | func function_name( [parameter list] ) [return_types] { 40 | body(函数体) 41 | } 42 | ``` 43 | 函数定义: 44 | * func:函数由 func 开始声明 45 | * function_name:函数名称,函数名和参数列表一起构成了函数签名。 46 | * parameter list:参数列表,参数就像一个占位符,当函数被调用时,你可以将值传递给参数,这个值被称为实际参数。参数列表指定的是参数类型、顺序、及参数个数。参数是可选的,也就是说函数也可以不包含参数。 47 | * return_types:返回类型,函数返回一列值。return_types 是该列值的数据类型。有些功能不需要返回值,这种情况下 return_types 不是必须的。 48 | * body(函数体):函数定义的代码集合。 49 | 50 | 形式参数列表描述了函数的参数名以及参数类型。这些参数作为局部变量,其值由参数调用者提供。返回值列表描述了函数返回值的变量名以及类型。如果函数返回一个无名变量或者没有返回值,返回值列表的括号是可以省略的。如果一个函数声明不包括返回值列表,那么函数体执行完毕后,不会返回任何值。 51 | 52 | ```go 53 | // 这个函数计算两个int型输入数据的和,并返回int型的和 54 | func plus(a int, b int) int { 55 | // Go需要使用return语句显式地返回值 56 | return a + b 57 | } 58 | fmt.Println(plus(3,4)) //7 59 | ``` 60 | 在plus函数中a和b是形参名3和4是调用时的传入的实数,函数返回了一个int类型的值。返回值也可以像形式参数一样被命名。在这种情况下,每个返回值被声明成一个局部变量,并根据该返回值的类型,将其初始化为0。 如果一个函数在声明时,包含返回值列表,该函数必须以 return语句结尾,除非函数明显无法运行到结尾处。例如函数在结尾时调用了panic异常或函数中存在无限循环。 61 | 62 | ```markdown 63 | 注意:GO中包内的函数,类型和变量的对外可见性(可访问性)由函数名,类型和变量标示符首字母决定,大写对外可见,小写对外不可见 64 | 概括来说: 公有函数的名字以大写字母开头;私有函数的名字以小写字母开头 65 | ``` 66 | 67 | 你可能会偶尔遇到没有函数体的函数声明,这表示该函数不是以Go实现的。这样的声明定义了函数标识符。 68 | 69 | ```go 70 | package math 71 | 72 | func Sin(x float64) float //implemented in assembly language 73 | ``` 74 | 那么函数在使用的时候如何被调用呢? 75 | 76 | ``` 77 | package.Function(arg1, arg2, …, argn) 78 | ``` 79 | Function 是 pack 包里面的一个函数,括号里的是被调用函数的实参(argument):这些值被传递给被调用函数的形参(parameter)。函数被调用的时候,这些实参将被复制然后传递给被调用函数。函数一般是在其他函数里面被调用的,这个其他函数被称为调用函数(calling function)。函数能多次调用其他函数,这些被调用函数按顺序行,理论上,函数调用其他函数的次数是无穷的(直到函数调用栈被耗尽)。 80 | 81 | 在函数中有的时候可能也会遇到你要传递的参数的类型是多个(传递变长参数)如这样的函数: 82 | 83 | ```go 84 | func patent(a,b,c ...int) 85 | ``` 86 | 参数是采用 ...type 的形式传递,这样的函数称为变参函数. 87 | 88 | #### 将函数作为参数传递 89 | 90 | 参数数量可变的函数称为为可变参数函数。典型的例子就是fmt.Printf和类似函数。Printf首先接收一个的必备参数,之后接收任意个数的后续参数。 91 | 在声明可变参数函数时,需要在参数列表的最后一个参数类型之前加上省略符号“...”,这表示该函数会接收任意数量的该类型参数。 92 | ```go 93 | package main 94 | 95 | import ( 96 | "fmt" 97 | ) 98 | 99 | func main() { 100 | callback(1, Add) 101 | } 102 | 103 | func Add(a, b int) { 104 | fmt.Printf("The sum of %d and %d is: %d\n", a, b, a+b) 105 | } 106 | 107 | func callback(y int, f func(int, int)) { 108 | f(y, 5) // this becomes Add(1, 5) 109 | } 110 | 111 | ``` 112 | 输出结果为: 113 | ```go 114 | The sum of 1 and 5 is: 6 115 | ``` 116 | 117 | 118 | #### 内置函数 119 | 120 | Go语言拥有一些不需要进行导入操作就可以使用的内置函数。它们有时可以针对不同的类型进行操作,例如:len、cap 和 append,或必须用于系统级的操作,例如:panic。因而,它们需要直接获得编译器的支持。 121 | 122 | | 名称 | 说明 | 123 | | ---- | ------------------------------- | 124 | |close|用于关闭管道通信channel| 125 | |len、cap |len 用于返回某个类型的长度或数量(字符串、数组、切片、map 和管道);cap 是容量的意思,用于返回某个类型的最大容量(只能用于切片和 map)| 126 | |new、make |new 和 make 均是用于分配内存:new 用于值类型和用户定义的类型,如自定义结构,内建函数new分配了零值填充的元素类型的内存空间,并且返回其地址,一个指针类型的值。make 用于内置引用类型(切片、map 和管道)创建一个指定元素类型、长度和容量的slice。容量部分可以省略,在这种情况下,容量将等于长度。它们的用法就像是函数,但是将类型作为参数:new(type)、make(type)。new(T) 分配类型 T 的零值并返回其地址,也就是指向类型 T 的指针)。它也可以被用于基本类型:v := new(int)。make(T) 返回类型 T 的初始化之后的值,因此它比 new 进行更多的工作,new() 是一个函数,不要忘记它的括号| 127 | |copy、append|copy函数用于复制,copy返回拷贝的长度,会自动取最短的长度进行拷贝(min(len(src), len(dst))),append函数用于向slice追加元素| 128 | |panic、recover|两者均用于错误处理机制,使用panic抛出异常,抛出异常后将立即停止当前函数的执行并运行所有被defer的函数,然后将panic抛向上一层,直至程序carsh。recover的作用是捕获并返回panic提交的错误对象、调用panic抛出一个值、该值可以通过调用recover函数进行捕获。主要的区别是,即使当前goroutine处于panic状态,或当前goroutine中存在活动紧急情况,恢复调用仍可能无法检索这些活动紧急情况抛出的值。| 129 | |print、println|底层打印函数| 130 | |complex、real imag|用于创建和操作复数,imag返回complex的实部,real返回complex的虚部| 131 | |delete|从map中删除key对应的value| 132 | 133 | 内置接口: 134 | ```go 135 | type error interface { 136 | //只要实现了Error()函数,返回值为String的都实现了err接口 137 | Error() String 138 | } 139 | ``` 140 | 141 | #### 递归函数 142 | 143 | 当一个函数在其函数体内调用自身,则称之为递归。递归是一种强有力的技术特别是在处理数据结构的过程中. 144 | ```go 145 | package main 146 | 147 | import "fmt" 148 | 149 | func main() { 150 | result := 0 151 | for i := 0; i <= 10; i++ { 152 | result = processing(i) 153 | fmt.Printf("processing(%d) is: %d\n", i, result) 154 | } 155 | } 156 | 157 | func processing(n int) (res int) { 158 | if n <= 1 { 159 | res = 1 160 | } else { 161 | res = processing(n-1) + processing(n-2) 162 | } 163 | return 164 | } 165 | ``` 166 | 输出结果: 167 | ```go 168 | 169 | processing(0) is: 1 170 | processing(1) is: 1 171 | processing(2) is: 2 172 | processing(3) is: 3 173 | processing(4) is: 5 174 | processing(5) is: 8 175 | processing(6) is: 13 176 | processing(7) is: 21 177 | processing(8) is: 34 178 | processing(9) is: 55 179 | processing(10) is: 89 180 | ``` 181 | 182 | 在使用递归函数时经常会遇到的一个重要问题就是栈溢出:一般出现在大量的递归调用导致的程序栈内存分配耗尽。这个问题可以通过一个名为懒惰求值的技术解决,在 Go 语言中,我们可以使用管道(channel)和 goroutine也会通过这个方案来优化斐波那契数列的生成问题。 183 | 184 | 这样许多问题都可以使用优雅的递归来解决,比如说著名的快速排序算法。 185 | 186 | 187 | #### 匿名函数 188 | 189 | 当我们不希望给函数起名字的时候,可以使用匿名函数,例如:func(x, y int) int { return x + y }。 190 | 函数字面量的语法和函数声明相似,区别在于func关键字后没有函数名。函数值字面量是一种表达式,它的值被称为匿名函数(anonymous function)。匿名函数由一个不带函数名的函数声明和函数体组成.通常不希望再次使用(即只使用一次的)的函数可以定义为匿名函数. 191 | 192 | ```go 193 | 匿名函数结构: 194 | 195 | func() { 196 | //func body 197 | }() //花括号后加()表示函数调用,此处声明时为指定参数列表, 198 | 199 | 如: 200 | 201 | fun(a,b int) { 202 | fmt.Println(a+b) 203 | }(1,2) 204 | ``` 205 | 表示参数列表的第一对括号必须紧挨着关键字 func,因为匿名函数没有名称。花括号 {} 涵盖着函数体,最后的一对括号表示对该匿名函数的调用。 206 | 207 | 这样的一个函数不能够独立存在(编译器会返回错误:non-declaration statement outside function body),但可以被赋值于某个变量,即保存函数的地址到变量中:fn := func(x, y int) int { return x + y },然后通过变量名对函数进行调用:fn(1,2)。 208 | 209 | 除此之外,也可以直接对匿名函数进行调用:func(x, y int) int { return x + y } (3, 4)。 210 | 211 | 通常也会有使用匿名函数channel的情况如: 212 | 213 | ```go 214 | f := make(chan func() string, 2) 215 | f <- func() string { return "Hello, World!" } 216 | fmt.Println((<-f)()) 217 | ``` 218 | 在谈到匿名函数我们在补充下闭包函数,闭包是函数式语言中的概念,没有研究过函数式语言的用户可能很难理解闭包的强大,相关的概念超出了本书的范围。Go语言是支持闭包的,这里只是简单地讲一下在Go语言中闭包是如何实现的。 匿名函数是无需定义标示符(函数名)的函数;而闭包是指能够访问自由变量的函数。换句话说,定义在闭包中的函数可以”记忆”它被创建时候的环境。闭包函数=匿名函数+环境变量。 219 | 220 | ```go 221 | func f(i int) func() int { 222 | return func() int { 223 | i++ 224 | return i 225 | } 226 | } 227 | ``` 228 | 229 | 运行的结果: 230 | 231 | ```go 232 | c1 := f(0) 233 | c2 := f(0) 234 | c1() // reference to i, i = 0, return 1 235 | c2() // reference to another i, i = 0, return 1 236 | ``` 237 | 函数f返回了一个函数,返回的这个函数,返回的这个函数就是一个闭包。这个函数中本身是没有定义变量i的,而是引用了它所在的环境(函数f)中的变量i。 238 | 239 | c1跟c2引用的是不同的环境,在调用i++时修改的不是同一个i,因此两次的输出都是1。函数f每进入一次,就形成了一个新的环境,对应的闭包中,函数都是同一个函数,环境却是引用不同的环境。 240 | 变量i是函数f中的局部变量,假设这个变量是在函数f的栈中分配的,是不可以的。因为函数f返回以后,对应的栈就失效了,f返回的那个函数中变量i就引用一个失效的位置了。所以闭包的环境中引用的变量不能够在栈上分配。Go编译器通过逃逸分析自动识别出变量的作用域,在堆上分配内存,而不是在函数f的栈上。 241 | 242 | 接着我们继续分析逃逸分析: 243 | 244 | ```go 245 | func agency() *Cursor { 246 | var c Cursor 247 | c.X = 500 248 | noinline() 249 | return &c 250 | } 251 | ``` 252 | Cursor是一个结构体(这种写法在C语言中是不允许的,因为变量c是在栈上分配的,当函数agency返回后c的空间就失效了)。但是,在Go语言规范中有说明,这种写法在Go语言中合法的。语言会自动地识别出这种情况并在堆上分配c的内存,而不是函数agency的栈上。 253 | 254 | 为了验证这一点,可以观察函数agency生成的汇编代码: 255 | ```go 256 | MOVQ $type."".Cursor+0(SB),(SP) // 取变量c的类型,也就是Cursor 257 | PCDATA $0,$16 258 | PCDATA $1,$0 259 | CALL ,runtime.new(SB) // 调用new函数,相当于new(Cursor) 260 | PCDATA $0,$-1 261 | MOVQ 8(SP),AX // 取c.X的地址放到AX寄存器 262 | MOVQ $500,(AX) // 将AX存放的内存地址的值赋为500 263 | MOVQ AX,"".~r0+24(FP) 264 | ADDQ $16,SP 265 | ``` 266 | 识别出变量需要在堆上分配,是由编译器的一种叫escape analyze的技术实现的。如果输入命令: 267 | ```go 268 | go build --gcflags=-m main.go 269 | ``` 270 | 输出结果: 271 | ```go 272 | ./main.go:20: moved to heap: c 273 | ./main.go:23: &c escapes to heap 274 | ``` 275 | 表示c逃逸了,被移到堆中。escape analyze可以分析出变量的作用范围,这是对垃圾回收很重要的一项技术。 276 | 277 | 我们在回到闭包结构中,前面说过,闭包是函数和它所引用的环境。 278 | ```go 279 | type Closure struct { 280 | F func()() 281 | i *int 282 | } 283 | ``` 284 | 事实上,Go在底层确实就是这样表示一个闭包的。让我们看一下汇编代码: 285 | ```go 286 | func f(i int) func() int { 287 | return func() int { 288 | i++ 289 | return i 290 | } 291 | } 292 | 293 | 294 | MOVQ $type.int+0(SB),(SP) 295 | PCDATA $0,$16 296 | PCDATA $1,$0 297 | CALL ,runtime.new(SB) // 是不是很熟悉,这一段就是i = new(int) 298 | ... 299 | MOVQ $type.struct { F uintptr; A0 *int }+0(SB),(SP) // 这个结构体就是闭包的类型 300 | ... 301 | CALL ,runtime.new(SB) // 接下来相当于 new(Closure) 302 | PCDATA $0,$-1 303 | MOVQ 8(SP),AX 304 | NOP , 305 | MOVQ $"".func·001+0(SB),BP 306 | MOVQ BP,(AX) // 函数地址赋值给Closure的F部分 307 | NOP , 308 | MOVQ "".&i+16(SP),BP // 将堆中new的变量i的地址赋值给Closure的值部分 309 | MOVQ BP,8(AX) 310 | MOVQ AX,"".~r1+40(FP) 311 | ADDQ $24,SP 312 | RET , 313 | ``` 314 | 其中func·001是另一个函数的函数地址,也就是agency返回的那个函数。 315 | 316 | goroutine是常见的匿名函数,通常我们会使用关键字 go 启动了一个匿名函数作为 goroutine。 317 | 使用匿名函数或闭包创建 goroutine 时,除了将函数定义部分写在 go 的后面之外,还需要加上匿名函数的调用参数,格式如下: 318 | ```go 319 | go func( 参数列表 ){ 320 | 函数体 321 | }( 调用参数列表 ) 322 | ``` 323 | 其中: 324 | * 参数列表:函数体内的参数变量列表。 325 | * 函数体:匿名函数的代码。 326 | * 调用参数列表:启动 goroutine 时,需要向匿名函数传递的调用参数。 327 | 328 | ```go 329 | var x int64 = 20 330 | var y int64 = 10 331 | var wg sync.WaitGroup 332 | 333 | wg.Add(1) 334 | //定义一个匿名函数,并对该函数开启协程 335 | go func(x, y int64) { 336 | z := x+y 337 | fmt.Println("the reuslt value:",z) 338 | wg.Done() 339 | }(x,y) 340 | //由于这个函数是匿名函数,所以调用方式就直接是(x,y)去调用,不用输入函数名。 341 | 342 | wg.Wait() 343 | ``` 344 | 匿名函数也可以接受声明时指定的参数。我们指定匿名函数要接受两个参数:x,y都是int64的类型。 345 | 346 | Goroutine是异步执行的,有的时候为了防止在结束mian函数的时候结束掉Goroutine,所以需要同步等待,这个时候就需要用 WaitGroup了,在 sync 包中,提供了 WaitGroup ,它会等待它收集的所有 goroutine 任务全部完成。在WaitGroup里主要有三个方法: 347 | 348 | Add, 可以添加或减少 goroutine的数量. 349 | Done, 相当于Add(-1). 350 | Wait, 执行后会堵塞主线程,直到WaitGroup 里的值减至0. 351 | 352 | #### defer延迟函数 353 | Go语言的defer算是一个语言的新特性,至少对比当今主流编程语言如此. 354 | 355 | ```markdown 356 | A "defer" statement invokes a function whose execution is deferred to the moment the surrounding function returns, either because the surrounding function executed a return statement, reached the end of its function body, or because the corresponding goroutine is panicking. defer语句调用一个函数,这个函数执行会推迟,直到外围的函数返回,或者外围函数运行到最后,或者相应的goroutine panic 357 | ``` 358 | 359 | defer语句经常被用于处理成对的操作,如打开、关闭、连接、断开连接、加锁、释放锁。通过defer机制,不论函数逻辑多复杂,都能保证在任何执行路径下,资源被释放。释放资源的defer应该直接跟在请求资源的语句后。 360 | ```go 361 | f,err := os.Open(filename) 362 | if err != nil { 363 | panic(err) 364 | } 365 | defer f.Close() 366 | ``` 367 | 如果有多个defer表达式,调用顺序类似于栈,越后面的defer表达式越先被调用。 368 | 369 | 在处理其他资源时,也可以采用defer机制,比如对文件的操作: 370 | ```go 371 | package ioutil 372 | func ReadFile(filename string) ([]byte, error) { 373 | f, err := os.Open(filename) 374 | if err != nil { 375 | return nil, err 376 | } 377 | defer f.Close() 378 | return ReadAll(f) 379 | } 380 | ``` 381 | 也可以处理互斥锁: 382 | ```go 383 | var mu sync.Mutex 384 | var m = make(map[string]int) 385 | func lookup(key string) int { 386 | mu.Lock() 387 | defer mu.Unlock() 388 | return m[key] 389 | } 390 | ``` 391 | 调试复杂程序时,defer机制也常被用于记录何时进入和退出函数。 392 | 393 | 此外在使用defer函数的时候也会遇到些意外的情况,那就是defer使用时需要注意的坑: 394 | 先来看看几个例子。例1: 395 | ```go 396 | func f() (result int) { 397 | defer func() { 398 | result++ 399 | }() 400 | return 0 401 | } 402 | ``` 403 | 例2: 404 | ```go 405 | func f() (r int) { 406 | t := 5 407 | defer func() { 408 | t = t + 5 409 | }() 410 | return t 411 | } 412 | ``` 413 | 例3: 414 | ```go 415 | func f() (r int) { 416 | defer func(r int) { 417 | r = r + 5 418 | }(r) 419 | return 1 420 | } 421 | ``` 422 | 自己可以先跑下看看这三个例子是不是和自己想的不一样的呢!结果确实是的例1的正确答案不是0,例2的正确答案不是10,如果例3的正确答案不是6...... 423 | 424 | defer是在return之前执行的。这个在 [官方文档](https://golang.org/ref/spec#defer_statements)中是明确说明了的。要使用defer时不踩坑,最重要的一点就是要明白,return A这一条语句并不是一条原子指令! 425 | 426 | ```markdown 427 | 返回值 = A 428 | 调用defer函数 429 | 空的return 430 | ``` 431 | 接着我们看下例1,它可以改写成这样: 432 | ```go 433 | func f() (result int) { 434 | result = 0 //return语句不是一条原子调用,return xxx其实是赋值+ret指令 435 | func() { //defer被插入到return之前执行,也就是赋返回值和ret指令之间 436 | result++ 437 | }() 438 | return 439 | } 440 | ``` 441 | 所以例子1的这个返回值是1。 442 | 443 | 再看例2,它可以改写成这样: 444 | ```go 445 | func f() (r int) { 446 | t := 5 447 | r = t //赋值指令 448 | func() { //defer被插入到赋值与返回之间执行,这个例子中返回值r没被修改过 449 | t = t + 5 450 | } 451 | return //空的return指令 452 | } 453 | ``` 454 | 所以这个的结果是5。 455 | 456 | 最后看例3,它改写后变成: 457 | 458 | ```go 459 | func f() (r int) { 460 | r = 1 //给返回值赋值 461 | func(r int) { //这里改的r是传值传进去的r,不会改变要返回的那个r值 462 | r = r + 5 463 | }(r) 464 | return //空的return 465 | } 466 | ``` 467 | 所以这个例子3的结果是1 468 | 469 | defer确实是在return之前调用的。但表现形式上却可能不像。本质原因是return A语句并不是一条原子指令,defer被插入到了赋值 与ret之间,因此可能有机会改变最终的返回值。 470 | 471 | #### panic异常 472 | 错误和异常这两个是不同的概念,非常容易混淆。很多人习惯将一切非正常情况都看做错误,而不区分错误和异常,即使程序中可能有异常抛出,也将异常及时捕获并转换成错误。错误指的是可能出现问题的地方出现了问题,比如压缩一个文件时失败,这种情况在人们可以意料之中的事情;但是异常指的是不应该出现问题的地方出现了问题,比如引用了空指针,这种情况在人们的意料之外。因而,错误是业务过程的一部分,而异常不是。 473 | 474 | Golang中引入error接口类型作为错误处理的标准模式,如果函数要返回错误,则返回值类型列表中肯定包含error。error处理过程类似于C语言中的错误码,可逐层返回,直到被处理。 475 | 476 | Golang中引入两个内置函数panic和recover来触发和终止异常处理流程,同时引入关键字defer来延迟执行defer后面的函数。 477 | 478 | 一直等到包含defer语句的函数执行完毕时,延迟函数(defer后的函数)才会被执行,而不管包含defer语句的函数是通过return的正常结束,还是由于panic导致的异常结束。你可以在一个函数中执行多条defer语句,它们的执行顺序与声明顺序相反。 479 | 480 | 当程序运行时候,如果遇到引用空指针、下标越界或显式调用panic函数等情况,则会先触发panic函数的执行,然后调用延迟函数。调用者继续传递panic,因此该过程一直在调用栈中重复发生:函数停止执行,调用延迟执行函数等。如果一路在defer延迟函数中没有recover函数的调用,则会到达协程的起点,该协程结束,然后终止其他所有协程,包括主协程. 481 | 482 | 错误和异常从Golang机制上讲,就是error和panic的区别。很多其他语言也一样,比如C++/Java,没有error但有errno,没有panic但有throw。 483 | 484 | 一般而言,当panic异常发生时,程序会中断运行,并立即执行在该goroutine中被延迟的函数(defer 机制)。随后,程序崩溃并输出日志信 485 | 息。日志信息包括panic value和函数调用的堆栈跟踪信息。panic value通常是某种错误信息。对于每个goroutine,日志信息中都会有与之相对的,发生panic时的函数调用堆栈跟踪信息。通常,我们不需要再次运行程序去定位问题,日志信息已经提供了足够的诊断依据。因此,在我们填写问题报告时,一般会将panic异常和日志信息一并记录。 486 | 487 | ```go 488 | func panic(interface{}) 489 | ``` 490 | 虽然Go的panic机制类似于其他语言的异常,但panic的适用场景有一些不同。由于panic会引起程序的崩溃,因此panic一般用于严重错误,如程序内部的逻辑不一致。通常认为任何崩溃都表明代码中存在漏洞,所以对于大部分漏洞,我们应该使用Go提供的错误机制,而不是panic,尽量避免程序的崩溃。在健壮的程序中,任何可以预料到的错误,如不正确的输入、错误的配置或是失败的I/O操作都应该被优雅的处理,最好的处理方式,就是使用Go的错误机制。 491 | 492 | #### recover捕获异常 493 | 494 | 通常来说,不应该对panic异常做任何处理,但有时,也许我们可以从异常中恢复,至少我们可以在程序崩溃前,做一些操作。如果在deferred函数中调用了内置函数recover,并且定义该defer语句的函数发生了panic异常,recover会使程序从panic中恢复,并返回panic value。导致panic异常的函数不会继续运行,但能正常返回。在未发生panic时调用recover,recover会返回nil。 495 | 496 | ```go 497 | func recover() interface{} 498 | ``` 499 | recover 函数用来获取 panic 函数的参数信息,只能在延时调用 defer 语句调用的函数中直接调用才能生效,如果在 defer 语句中也调用 panic 函数,则只有最后一个被调用的 panic 函数的参数会被 recover 函数获取到。如果 goroutine 没有 panic,那调用 recover 函数会返回 nil。 500 | 501 | 让我们以语言解析器为例,说明recover的使用场景。考虑到语言解析器的复杂性,即使某个语言解析器目前工作正常,也无法肯定它没有漏洞。因此,当某个异常出现时,我们不会选择让解析器崩溃,而是会将panic异常当作普通的解析错误,并附加额外信息提醒用户报告此错误。 502 | 503 | ```go 504 | func Parse(input string) (s *Syntax, err error) { 505 | defer func() { 506 | if p := recover(); p != nil { 507 | err = fmt.Errorf("internal error: %v", p) 508 | } 509 | }() 510 | // ...parser... 511 | } 512 | ``` 513 | 从中可以看到defer函数帮助Parse从panic中恢复。在defer函数内部,panic value被附加到错误信息中;并用err变量接收错误信息,返回给调用者。我们也可以通过调用runtime.Stack往错误信息中添加完整的堆栈调用信息。 514 | 515 | 但是如果不加区分的恢复所有的panic异常,不是可取的做法;因为在panic之后,无法保证包级变量的状态仍然和我们预期一致。比如,对数据结构的一次重要更新没有被完整完成、文件或者网络连接没有被关闭、获得的锁没有被释放。此外,如果写日志时产生的panic被不加区分的恢复,可能会导致漏洞被忽略。 516 | 517 | 虽然把对panic的处理都集中在一个包下,有助于简化对复杂和不可以预料问题的处理,但作为被广泛遵守的规范,你不应该试图去恢复其他包引起的panic。公有的API应该将函数的运行失败作为error返回,而不是panic。同样的,你也不应该恢复一个由他人开发的函数引起的panic,比如说调用者传入的回调函数,因为你无法确保这样做是安全的。 518 | 519 | 因此安全的做法是有选择性的recover。换句话说,只恢复应该被恢复的panic异常,此外,这些异常所占的比例应该尽可能的低。为了标识某个panic是否应该被恢复,我们可以将panic value设置成特殊类型。在recover时对panic value进行检查,如果发现panic value是特殊类型,就将这个panic作为errror处理,如果不是,则按照正常的panic进行处理. 520 | -------------------------------------------------------------------------------- /src/chapter05/01.0.md: -------------------------------------------------------------------------------- 1 | ### For-learning-Go-Tutorial 2 | 3 | Go语言是谷歌2009发布的第二款开源编程语言。 4 | 5 | Go语言专门针对多处理器系统应用程序的编程进行了优化,使用Go编译的程序可以媲美C或C++代码的速度,而且更加安全、支持并行进程。 6 | 7 | 因而一直想的是自己可以根据自己学习和使用Go语言编程的心得,写一本Go的书可以帮助想要学习Go语言的初学者快速入门开发和使用! 8 | 9 | #### 方法(method) 10 | 11 | Golang方法的是作用在接收者(receiver)上的一个函数,接收者是某种类型的变量,因此Golang方法是一种特殊类型的函数。 12 | 13 | 接收者类型可以是(几乎)任何类型,不仅仅是结构体类型:任何类型都可以有方法,甚至可以是函数类型,可以是 `int`、`bool`、`string` 或数组的别名类型。但是接收者不能是一个接口类型,因为接口是一个抽象定义,但是方法却是具体实现;如果这样做会引发一个编译错误:`invalid receiver type…`。 14 | 15 | 最后接收者不能是一个指针类型,但是它可以是任何其他允许类型的指针。 16 | 17 | 对于方法来说,最简单的解释就是,在函数声明时,在其名字之前放上一个变量,即是一个方法。这个附加的参数会将该函数附加到这种类型上,即相当于为这种类型定义了一个独占的方法。 18 | 19 | ```go 20 | package geometry 21 | 22 | import "math" 23 | 24 | type Point struct{ X, Y float64 } 25 | 26 | // traditional function 27 | func Distance(p, q Point) float64 { 28 | return math.Hypot(q.X-p.X, q.Y-p.Y) 29 | } 30 | 31 | // same thing, but as a method of the Point type 32 | func (p Point) Distance(q Point) float64 { 33 | return math.Hypot(q.X-p.X, q.Y-p.Y) 34 | } 35 | ``` 36 | 37 | 上面的代码里那个附加的参数p,叫做方法的接收器(receiver),早期的面向对象语言中将调用一个方法称为“向一个对象发送消息”。 38 | 39 | 在Go语言中,我们并不会像其它语言那样用this或者self作为接收器,我们可以任意的选择接收器的名字。由于接收器的名字经常会被使用到,所以保持其在方法间传递时的一致性和简短性是不错的主意。这里的建议是可以使用其类型的第一个字母,比如这里使用了Point的首字母p。 40 | 41 | 在方法调用过程中,接收器参数一般会在方法名之前出现。这和方法声明是一样的,都是接收器参数在方法名字之前。 42 | 43 | ```go 44 | p := Point{1, 2} 45 | q := Point{4, 6} 46 | fmt.Println(Distance(p, q)) // "5", function call 47 | fmt.Println(p.Distance(q)) 48 | // "5", method call 49 | ``` 50 | 51 | 可以看到,上面的两个函数调用都是Distance,但是却没有发生冲突。第一个Distance的调用实际上用的是包级别的函数`geometry.Distance`,而第二个则是使用刚刚声明的Point,调用的是Point类下声明的`Point.Distance`方法。 52 | 53 | 这种`p.Distance`的表达式叫做选择器,因为他会选择合适的对应p这个对象的Distance方法来执行。选择器也会被用来选择一个struct类型的字段,比如p.X。由于方法和字段都是在同一命名空间,所以如果我们在这里声明一个X方法的话,编译器会报错,因为在调用p.X时会有歧义. 54 | 55 | 因为每种类型都有其方法的命名空间,我们在用Distance这个名字的时候,不同的Distance调用指向了不同类型里的Distance方法。让我们来定义一个Path类型,这个Path代表一个线段的集合,并且也给这个Path定义一个叫Distance的方法。 56 | 57 | ```go 58 | // A Path is a journey connecting the points with straight lines. 59 | type Path []Point 60 | // Distance returns the distance traveled along the path. 61 | func (path Path) Distance() float64 { 62 | sum := 0.0 63 | for i := range path { 64 | if i > 0 { 65 | sum += path[i-1].Distance(path[i]) 66 | } 67 | } 68 | return sum 69 | } 70 | ``` 71 | 72 | 这里的Path是一个命名的slice类型,而不是Point那样的struct类型,然而我们依然可以为它定义方法。在能够给任意类型定义方法这一点上,Go和很多其它的面向对象的语言不太一样。因此在Go语言里,我们为一些简单的数值、字符串、slice、map来定义一些附加行为很方便。方法可以被声明到任意类型,只要不是一个指针或者一个interface。 73 | 74 | 两个Distance方法有不同的类型。他们两个方法之间没有任何关系,尽管Path的Distance方法会在内部调用Point.Distance方法来计算每个连接邻接点的线段的长度。 75 | 76 | #### 基于指针对象的方法 77 | 78 | 当调用一个函数时,会对其每一个参数值进行拷贝,如果一个函数需要更新一个变量,或者函数的其中一个参数实在太大我们希望能够避免进行这种默认的拷贝,这种情况下我们就需要用到指针了。对应到我们这里用来更新接收器的对象的方法,当这个接受者变量本身比较大时,我们就可以用其指针而不是对象来声明方法. 79 | 80 | ```go 81 | func (p *Point) ScaleBy(factor float64) { 82 | p.X *= factor 83 | p.Y *= factor 84 | } 85 | ``` 86 | 87 | 这个方法的名字是(*Point).ScaleBy。这里的括号是必须的;没有括号的话这个表达式可能会被理解为*(Point.ScaleBy)。如果Point这个类有一个指针作为接收器的方法,那么所有Point的方法都必须有一个指针接收器,即使是那些并不需要这个指针接收器的函数。 88 | 89 | 通常情况下如果想要调用指针类型方法(*Point).ScaleBy,只要提供一个Point类型的指针即可. 90 | 91 | ```go 92 | r := &Point{1, 2} 93 | r.ScaleBy(2) 94 | fmt.Println(*r) // "{2, 4}" 95 | ``` 96 | 97 | 如果接收器p是一个Point类型的变量,并且其方法需要一个Point指针作为接收器,我们可以用下面这种简短的写法: 98 | ```go 99 | p.ScaleBy(2) 100 | ``` 101 | 102 | 编译器会隐式地帮我们用&p去调用ScaleBy这个方法。这种简写方法只适用于“变量”,包括struct里的字段比如p.X,以及array和slice内的元素比如perim[0]。我们不能通过一个无法取到地址的接收器来调用指针方法,比如临时变量的内存地址就无法获取得到: 103 | 104 | ```go 105 | Point{1, 2}.ScaleBy(2) // compile error: can't take address of Point literal 106 | ``` 107 | 但是我们可以用一个*Point这样的接收器来调用Point的方法,因为我们可以通过地址来找到这个变量,只要用解引用符号*来取到该变量即可。编译器在这里也会给我们隐式地插入*这个操作符,所以下面这两种写法等价的: 108 | 109 | ```go 110 | pptr.Distance(q) 111 | (*pptr).Distance(q) 112 | ``` 113 | 这里需要注意的地方: 114 | 115 | * 不管你的method的receiver是指针类型还是非指针类型,都是可以通过指针或者非指针类型进行调用的,编译器会帮你做类型转换。 116 | 117 | * 在声明一个method的receiver该是指针还是非指针类型时,你需要考虑两方面的内部,第一方面是这个对象本身是不是特别大,如果声明为非指针变量时,调用会产生一次拷贝;第二方面是如果你用指针类型作为receiver,那么你一定要注意,这种指针类型指向的始终是一块内存地址,就算你对其进行了拷贝。 118 | 119 | -------------------------------------------------------------------------------- /src/chapter06/01.0.md: -------------------------------------------------------------------------------- 1 | ### For-learning-Go-Tutorial 2 | 3 | Go语言是谷歌2009发布的第二款开源编程语言。 4 | 5 | Go语言专门针对多处理器系统应用程序的编程进行了优化,使用Go编译的程序可以媲美C或C++代码的速度,而且更加安全、支持并行进程。 6 | 7 | 因而一直想的是自己可以根据自己学习和使用Go语言编程的心得,写一本Go的书可以帮助想要学习Go语言的初学者快速入门开发和使用! 8 | 9 | 在 Golang 中,interface 是一个非常重要的概念和特性。 10 | 11 | #### 接口(Interface) 12 | 13 | 在Go语言中,函数和方法不太一样,有明确的概念区分。其他语言中,比如Java,一般来说,函数就是方法,方法就是函数,但是在Go语言中,函数是指不属于任何结构体、类型的方法,也就是说,函数是没有接收者的;而方法是有接收者的,我们说的方法要么是属于一个结构体的,要么属于一个新定义的类型的。 14 | 15 | 在 Golang 中,interface 是一种抽象类型,相对于抽象类型的是具体类型(concrete type):int,string。如下是 io 包里面的例子。 16 | 17 | ```go 18 | // Writer is the interface that wraps the basic Write method. 19 | // 20 | // Write writes len(p) bytes from p to the underlying data stream. 21 | // It returns the number of bytes written from p (0 <= n <= len(p)) 22 | // and any error encountered that caused the write to stop early. 23 | // Write must return a non-nil error if it returns n < len(p). 24 | // Write must not modify the slice data, even temporarily. 25 | // 26 | // Implementations must not retain p. 27 | type Writer interface { 28 | Write(p []byte) (n int, err error) 29 | } 30 | 31 | // Closer is the interface that wraps the basic Close method. 32 | // 33 | // The behavior of Close after the first call is undefined. 34 | // Specific implementations may document their own behavior. 35 | type Closer interface { 36 | Close() error 37 | } 38 | ``` 39 | 在 Golang中,interface是一组 method 的集合,是 `duck-type programming` 的一种体现。不关心属性(数据),只关心行为(方法)。具体使用中你可以自定义自己的 struct,并提供特定的 interface 里面的 method 就可以把它当成 interface 来使用。 40 | 41 | 下面是一种 interface 的典型用法,定义函数的时候参数定义成 interface,调用函数的时候就可以做到非常的灵活。 42 | ```go 43 | type FirstInterface interface{ 44 | Print() 45 | } 46 | 47 | func TestFunc(x FirstInterface) {} 48 | type PatentStruct struct {} 49 | func (pt PatentStruct) Print() {} 50 | 51 | func main() { 52 | var pt PatentStruct 53 | TestFunc(me) 54 | } 55 | ``` 56 | 57 | #### Why Interface 58 | 59 | * writing generic algorithm(泛型编程) 60 | * hiding implementation detail(隐藏具体实现) 61 | * providing interception points(提供拦截端点) 62 | 63 | #### writing generic algorithm 64 | 65 | 在 Golang 中并不支持泛型编程。在 C++ 等高级语言中使用泛型编程非常的简单,所以泛型编程一直是 Golang 诟病最多的地方。 66 | 67 | 但是使用 interface 我们可以实现泛型编程,我这里简单说一下,具体可以参考我前面给出来的那篇文章。比如我们现在要写一个泛型算法,形参定义采用 interface 就可以了,以标准库的 sort 为例。 68 | 69 | ```go 70 | package sort 71 | 72 | // A type, typically a collection, that satisfies sort.Interface can be 73 | // sorted by the routines in this package. The methods require that the 74 | // elements of the collection be enumerated by an integer index. 75 | type Interface interface { 76 | // Len is the number of elements in the collection. 77 | Len() int 78 | // Less reports whether the element with 79 | // index i should sort before the element with index j. 80 | Less(i, j int) bool 81 | // Swap swaps the elements with indexes i and j. 82 | Swap(i, j int) 83 | } 84 | 85 | ... 86 | 87 | // Sort sorts data. 88 | // It makes one call to data.Len to determine n, and O(n*log(n)) calls to 89 | // data.Less and data.Swap. The sort is not guaranteed to be stable. 90 | func Sort(data Interface) { 91 | // Switch to heapsort if depth of 2*ceil(lg(n+1)) is reached. 92 | n := data.Len() 93 | maxDepth := 0 94 | for i := n; i > 0; i >>= 1 { 95 | maxDepth++ 96 | } 97 | maxDepth *= 2 98 | quickSort(data, 0, n, maxDepth) 99 | } 100 | ``` 101 | 102 | Sort 函数的形参是一个 interface,包含了三个方法:Len(),Less(i,j int),Swap(i, j int)。 103 | 使用的时候不管数组的元素类型是什么类型(int, float, string…),只要我们实现了这三个方法就可以使用 Sort 函数,这样就实现了“泛型编程”。 104 | 有一点比较麻烦的是,我们需要将数组自定义一下。下面是一个例子。 105 | 106 | ````go 107 | type Person struct { 108 | Name string 109 | Age int 110 | } 111 | 112 | func (p Person) String() string { 113 | return fmt.Sprintf("%s: %d", p.Name, p.Age) 114 | } 115 | 116 | // ByAge implements sort.Interface for []Person based on 117 | // the Age field. 118 | type ByAge []Person //自定义 119 | 120 | func (a ByAge) Len() int { return len(a) } 121 | func (a ByAge) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 122 | func (a ByAge) Less(i, j int) bool { return a[i].Age < a[j].Age } 123 | 124 | func main() { 125 | people := []Person{ 126 | {"Bob", 31}, 127 | {"John", 42}, 128 | {"Michael", 17}, 129 | {"Jenny", 26}, 130 | } 131 | 132 | fmt.Println(people) 133 | sort.Sort(ByAge(people)) 134 | fmt.Println(people) 135 | } 136 | ```` 137 | 138 | #### 函数 139 | 140 | 函数和方法,虽然概念不同,但是定义非常相似。函数的定义声明没有接收者,所以我们直接在go文件里,go包之下定义声明即可。 141 | 142 | ```go 143 | func main() { 144 | sum := add(3, 4) 145 | fmt.Println(sum) 146 | } 147 | func add(m, n int) int { 148 | return m+ n 149 | } 150 | ``` 151 | 在上面我们定义了add就是一个函数,它的函数签名是func add(m, n int) int,没有接收者,直接定义在go的一个包之下,可以直接调用,比如例子中的main函数调用了add函数。例子中的这个函数名称是小写开头的add,所以它的作用域只属于所声明的包内使用,不能被其他包使用,如果我们把函数名以大写字母开头,该函数的作用域就大了,可以被其他包调用。 152 | 153 | 这也是Go语言中大小写的用处,比如Java中,就有专门的关键字来声明作用域private、protect、public等。 154 | 155 | ```go 156 | func Add(a,b int) int { 157 | return a + b 158 | } 159 | ``` 160 | 这个新定义的Add方法就是可以被其他包调用的. 161 | 162 | #### 方法 163 | 164 | 方法的声明和函数类似,他们的区别是:方法在定义的时候,会在func和方法名之间增加一个参数,这个参数就是接收者,这样我们定义的这个方法就和接收者绑定在了一起,称之为这个接收者的方法 165 | 166 | ```go 167 | type person struct { 168 | name string 169 | } 170 | func (p person) String() string{ 171 | return "the person name is "+p.name 172 | } 173 | ``` 174 | 175 | 留意例子中,func和方法名之间增加的参数(p person),这个就是接收者。现在我们说,类型person有了一个String方法,现在我们看下如何使用它。 176 | 177 | ```go 178 | func main() { 179 | p:=person{name:"你好"} 180 | fmt.Println(p.String()) 181 | } 182 | ``` 183 | 184 | Go语言里有两种类型的接收者:值接收者和指针接收者。我们上面的例子中,就是使用值类型接收者的示例。 185 | 使用值类型接收者定义的方法,在调用的时候,使用的其实是值接收者的一个副本,所以对该值的任何操作,不会影响原来的类型变量。 186 | 187 | ```go 188 | 189 | func main() { 190 | p:=person{name:"你好"} 191 | p.modify() //值接收者,修改无效 192 | fmt.Println(p.String()) 193 | } 194 | type person struct { 195 | name string 196 | } 197 | func (p person) String() string{ 198 | return "the person name is "+p.name 199 | } 200 | func (p person) modify(){ 201 | p.name = "真的好" 202 | } 203 | ``` 204 | 只需要改动一下,变成指针的接收者,就可以完成了修改。 205 | 206 | ```markdown 207 | 在调用方法的时候,传递的接收者本质上都是副本,只不过一个是这个值副本,一是指向这个值指针的副本。指针具有指向原有值的特性,所以修改了指针指向的值,也就修改了原有的值。我们可以简单的理解为值接收者使用的是值的副本来调用方法,而指针接收者使用实际的值来调用方法。 208 | ``` 209 | 在上面的例子中,有没有发现,我们在调用指针接收者方法的时候,使用的也是一个值的变量,并不是一个指针,如果我们使用下面的也是可以的。 210 | 211 | ```go 212 | p:=person{name:"年后"} 213 | (&p).modify() //指针接收者,修改有效 214 | ``` 215 | 216 | 这样也是可以的。如果我们没有这么强制使用指针进行调用,Go的编译器自动会帮我们取指针,以满足接收者的要求。 217 | 同样的,如果是一个值接收者的方法,使用指针也是可以调用的,Go编译器自动会解引用,以满足接收者的要求,比如例子中定义的String()方法,也可以这么调用: 218 | ```go 219 | p:=person{name:"你好"} 220 | fmt.Println((&p).String()) 221 | ``` 222 | 223 | 总之,方法的调用,既可以使用值,也可以使用指针,我们不必要严格的遵守这些,Go语言编译器会帮我们进行自动转义的,这大大方便了我们开发者。不管是使用值接收者,还是指针接收者,一定要搞清楚类型的本质:对类型进行操作的时候,是要改变当前值,还是要创建一个新值进行返回?这些就可以决定我们是采用值传递,还是指针传递。 224 | 225 | #### 多值返回 226 | 227 | Go语言支持函数方法的多值返回,也就说我们定义的函数方法可以返回多个值,比如标准库里的很多方法,都是返回两个值,第一个是函数需要返回的值,第二个是出错时返回的错误信息,这种的好处,我们的出错异常信息再也不用像Java一样使用一个Exception这么重的方式表示了,非常简洁。 228 | 229 | ```go 230 | func main() { 231 | file, err := os.Open("/usr/bin") 232 | if err != nil { 233 | log.Fatal(err) 234 | return 235 | } 236 | fmt.Println(file) 237 | } 238 | ``` 239 | 240 | 如果返回的值,我们不想使用,可以使用_进行忽略. 241 | 242 | ```go 243 | file, _ := os.Open("/usr/bin") 244 | ``` 245 | 246 | 多个值返回的定义也非常简单,看个例子。 247 | 248 | ```go 249 | func add(a, b int) (int, error) { 250 | return a + b, nil 251 | } 252 | ``` 253 | 254 | 函数方法声明定义的时候,采用逗号分割,因为时多个返回,还要用括号括起来。返回的值还是使用return 关键字,以逗号分割,和返回的声明的顺序一致。 255 | 256 | #### 可变参数 257 | 258 | 函数方法的参数,可以是任意多个,这种我们称之为可以变参数,比如我们常用的fmt.Println()这类函数,可以接收一个可变的参数。 259 | 260 | ```go 261 | func main() { 262 | fmt.Println("a","b","c") 263 | } 264 | ``` 265 | 266 | 可以变参数,可以是任意多个。我们自己也可以定义可以变参数,可变参数的定义,在类型前加上省略号…即可。 267 | ```go 268 | func main() { 269 | print("a","b","c") 270 | } 271 | func print (a ...interface{}){ 272 | for _,v:=range a{ 273 | fmt.Print(v) 274 | } 275 | fmt.Println() 276 | } 277 | ``` 278 | 例子中我们自己定义了一个接受可变参数的函数,效果和fmt.Println()一样。 279 | 280 | 可变参数本质上是一个数组,所以我们向使用数组一样使用它,比如例子中的 for range 循环。 281 | 282 | -------------------------------------------------------------------------------- /src/chapter07/01.0.md: -------------------------------------------------------------------------------- 1 | ### For-learning-Go-Tutorial 2 | 3 | Go语言是谷歌2009发布的第二款开源编程语言. 4 | 5 | Go语言专门针对多处理器系统应用程序的编程进行了优化,使用Go编译的程序可以媲美C或C++代码的速度,而且更加安全、支持并行进程。 6 | 7 | 因而一直想的是自己可以根据自己学习和使用Go语言编程的心得,写一本Go的书可以帮助想要学习Go语言的初学者快速入门开发和使用! 8 | 9 | #### 反射(reflection) 10 | 11 | 在运行时反射是程序检查其所拥有的结构,尤其是类型的一种能力;这是元编程的一种形式。它同时也是造成迷惑的来源。反射可以在运行时检查类型和变量,例如它的大小、方法和 动态 的调用这些方法。这对于没有源代码的包尤其有用。 12 | 13 | golang实现反射是通过reflect包来实现的, 让原本是静态类型的go具备了很多动态类型语言的特征。reflect包有两个数据类型,一个是Type,一个是Value。它定义了两个重要的类型, Type和Value. Type就是定义的类型的一个数据类型,Value是值的类型, `TypeOf` 和 `ValueOf`是获取Type和Value的方法。一个Type表示一个Go类型.它是一个接口, 有许多方法来区分类型以及检查它们的组成部分, 例如一个结构体的成员或一个函数的参数等. 唯一能反映 `reflect.Type` 实现的是接口的类型描述信息,也正是这个实体标识了接口值的动态类型. 14 | 15 | 接着我们开始我们使用Golang反射,通常在使用到Golang反射的时候会有三种定律: 16 | 17 | * 反射定律一:反射可以将“接口类型变量”转换为“反射类型对象”. 18 | 19 | 这里的反射类型指的是`reflect.Type`和`reflect.Value`.将接口类型变量转换为反射类型变量,是通过reflect包的TypeOf和ValueOf实现的。 20 | 21 | 下面我们来看看在reflect包中的TypeOf和ValueOf的定义: 22 | 23 | TypeOf 24 | 25 | ```go 26 | #TypeOf returns the reflection Type that represents the dynamic type of i. If i is a nil interface value, TypeOf returns nil. 27 | #TypeOf返回表示i的动态类型的反射Type。如果i是nil接口值,则TypeOf返回nil。 28 | 29 | func TypeOf(i interface{}) Type 30 | ``` 31 | 32 | ValueOf: 33 | 34 | ```go 35 | # ValueOf returns a new Value initialized to the concrete value stored in the interface i. ValueOf(nil) returns the zero Value 36 | # ValueOf返回一个新的Value,初始化为存储在接口i中的具体值。 ValueOf(nil)返回零值 37 | 38 | func ValueOf(i interface{}) Value 39 | ``` 40 | 然后我们可以使用reflect.ValueOf和reflect.TypeOf将接口类型变量分别转换为反射类型: 41 | 42 | ```go 43 | var p int = 10 44 | v1 := reflect.ValueOf(p)//返回Value类型对象,值为10 45 | 46 | t1 := reflect.TypeOf(p)//返回Type类型对象,值为int 47 | 48 | fmt.Println("v1:",v1) 49 | 50 | fmt.Println("t1",t1) 51 | 52 | v2 := reflect.ValueOf(&p)//返回Value类型对象,值为&p,变量p的地址 53 | 54 | t2 := reflect.TypeOf(&p)//返回Type类型对象,值为*int 55 | 56 | fmt.Println("v2:",v2) 57 | 58 | fmt.Println("t2:",t2) 59 | ``` 60 | 运行结果: 61 | ```go 62 | v1: 10 63 | t1: int 64 | v2: 0xc4200180b8 65 | t2: *int 66 | ``` 67 | 68 | 其中v1和v2中包含了接口中的实际值,t1和t2中包含了接口中的实际类型.由于`reflect.ValueOf`和`reflect.TypeOf`的参数类型都是interface{},空接口类型,而返回值的类型是`reflect.Value`和`reflect.Type`,中间的转换由reflect包来实现。所以就实现了接口类型变量到反射类型对象的转换. 69 | 70 | * 反射定律二:反射可以将“反射类型对象”转换为“接口类型变量”, 71 | 72 | 这里根据一个 `reflect.Value`类型的变量,我们可以使用Interface方法恢复其接口类型的值。事实上,这个方法会把 type和value信息打包并填充到一个接口变量中,然后返回. 73 | 74 | 下面我们来看看在reflect包中Value的定义: 75 | ```go 76 | func (v Value) Interface() (i interface{}) 77 | 78 | # Interface returns v's current value as an interface{}. It is equivalent to: 79 | # 接口将v的当前值作为接口{}返回。它相当于: 80 | 81 | var i interface{} = (v's underlying value) 82 | 83 | # It panics if the Value was obtained by accessing unexported struct fields. 84 | # 如果通过访问未导出的struct字段获得Value,则会发生混乱。 85 | ``` 86 | 然后我们可以使用Value将反射类型转换为接口类型变量: 87 | ```go 88 | var a int = 10 89 | 90 | v1 := reflect.ValueOf(&a) //返回Value类型对象,值为&a,变量a的地址 91 | 92 | t1 := reflect.TypeOf(&a) //返回Type类型对象,值为*int 93 | 94 | fmt.Println("v1:",v1) 95 | 96 | fmt.Println("t1:",t1) 97 | 98 | v2 := v1.Interface() //返回空接口变量 99 | 100 | v3 := v2.(*int) //类型断言,断定v1中type=*int 101 | 102 | fmt.Printf("%T %v\n", v3, v3) 103 | 104 | fmt.Println("v3:",*v3) 105 | ``` 106 | 运行的结果: 107 | ```go 108 | v1: 0xc420082010 109 | t1: *int 110 | *int 0xc420082010 111 | v3: 10 112 | ``` 113 | reflect.ValueOf 的逆操作是 reflect.Value.Interface方法.它返回一个 interface{}类型,装载着与reflect.Value相同的具体值,这样我们就可以将“反射类型对象”转换为“接口类型变量. 114 | 115 | 其实reflect.Value 和 interface{} 都能装载任意的值. 有所不同的是, 一个空的接口隐藏了值内部的表示方式和所有方法, 因此只有我们知道具体的动态类型才能使用类型断言来访问内部的值(就像上面那样), 内部值我们没法访问. 相比之下, 一个 Value 则有很多方法来检查其内容, 无论它的具体类型是什么. 116 | 117 | * 反射定律三:如果要修改反射类型对象,其值必须是“addressable” 118 | 119 | 在上面第一种反射定律将“接口类型变量”转换为“反射类型对象”我们可以知道,反射对象包含了接口变量中存储的值以及类型。如果反射对象中包含的值是原始值,那么可以通过反射对象修改原始值,如果反射对象中包含的值不是原始值(反射对象包含的是副本值或指向原始值的地址),那么该反射对象是不可以修改的。 120 | 121 | 那么我们可以通过CanSet函数可以判定反射对象是否可以修改。 122 | ```go 123 | # CanSet reports whether the value of v can be changed. A Value can be changed only if it is addressable and was not obtained by the use of unexported struct fields. If CanSet returns false, calling Set or any type-specific setter (e.g., SetBool, SetInt) will panic. 124 | 125 | # CanSet报告是否可以更改v的值.仅当值可寻址且未通过使用未导出的struct字段获取时,才能更改值。如果CanSet返回false,则调用Set或任何特定于类型的setter(例如,SetBool,SetInt)将会发生混乱。 126 | 127 | func (v Value) CanSet() bool 128 | ``` 129 | 注意:CanSet返回false,值是不可以修改的,如果返回true则是可以修改的. 130 | ```go 131 | var p float64 = 3.4 132 | 133 | v1 := reflect.ValueOf(&p) 134 | 135 | if v1.Kind() == reflect.Ptr && !v1.Elem().CanSet() { //判断是否为指针类型,元素是否可以修改 136 |    fmt.Println("cannot set value") 137 |    return 138 | } else { 139 |    v1 = v1.Elem() //实际取得的对象 140 |    fmt.Println("CanSet return bool:", v1.CanSet()) 141 | } 142 | 143 | v1.SetFloat(6.1) 144 | 145 | fmt.Println("p=",p) 146 | ``` 147 | 运行结果: 148 | ```go 149 | CanSet return bool: true 150 | p= 6.1 151 | ``` 152 | 从运行结果看值修改成功了,但是这里出现了Elem函数作用是用来获取原始值对应的反射对象. 153 | ```go 154 | # Elem returns the value that the interface v contains or that the pointer v points to. It panics if v's Kind is not Interface or Ptr. It returns the zero Value if v is nil. 155 | # Elem返回接口v包含的值或指针v指向的值。如果v的Kind不是Interface或Ptr,它会发生恐慌。如果v为nil,则返回零值。 156 | 157 | func (v Value) Elem() Value 158 | ``` 159 | 在这里要修改变量p的值,首先就要通过`reflect.ValueOf`来获取p的值类型, `refelct.ValueOf` 返回的值类型是变量p一份值拷贝,要修改变量p就要传递p的地址,从而返回p的地址对象,才可以进行对p变量值对修改操作。在得到p变量的地址值类型之后先调用Elem()返回地址指针指向的值的Value封装。然后通过Set方法进行修改赋值。 160 | 161 | 通过反射可以很容易的修改变量的值,我们首先要通过反射拿到这个字段的地址值类型,然后去判断反射返回类型是否为reflect.Ptr指针类型(通过指针才能操作对象地址中的值)同时还要判断这个元素是否可以修改,通过Kind()和Set来修改字段的值,然后就可以拿到我们修改的值了. 162 | 163 | 因此在反射中使用反射包提供 `refelct.TypeOf` 和 `refelct.ValueOf` 方法获得接口对象的类型,值和方法等。通过反射修改字段值等时候需要传入地址类型,并且需要检查反射返回值类型是否为refelct.Ptr,检查字段是否CanSet,检查字段是存在,然后通过Kind()来帮助赋值相应对类型值。 164 | 165 | 应用示例: 166 | ```go 167 | package main 168 | 169 | import ( 170 | "fmt" 171 | "reflect" 172 | ) 173 | 174 | type User struct { 175 | Name string `json:"name"` 176 | Gender string `json:"gender"` 177 | Age int `json:"age"` 178 | } 179 | 180 | func main() { 181 | types := reflect.TypeOf(&User{}).Elem() 182 | value := reflect.ValueOf(&User{}).Elem() 183 | fmt.Println("values Numfield:",value.NumField()) 184 | for i:=0;i go run reflect.go 209 | 10 210 | ``` 211 | 212 | 这里: 213 | 214 | 1. 调用 reflect.ValueOf 函数获取变量指针; 215 | 2. 调用 reflect.Value.Elem 方法获取指针指向的变量; 216 | 3. 调用 reflect.Value.SetInt 方法更新变量的值: 217 | 218 | 由于 Go 语言的函数调用都是值传递的,所以我们只能先获取指针对应的 reflect.Value,再通过 reflect.Value.Elem 方法迂回的方式得到可以被设置的变量,我们通过如下所示的代码理解这个过程: 219 | ```go 220 | func main() { 221 | i := 1 222 | v := &i 223 | *v = 10 224 | } 225 | ``` 226 | 227 | 如果不能直接操作 i 变量修改其持有的值,我们就只能获取 i 变量所在地址并使用 `*v` 修改所在地址中存储的整数。 228 | 229 | #### 反射的性能测试 230 | 231 | Golang提供了一个testing包,使得单元测试、性能测试尤为简单。只要新建一个以_test结尾的文件,然后使用命令go test就可以自动执行文件中的相应测试函数了(单元测试函数以Test开头,性能测试函数以Benchmark开头)。我们可以使用`golang testing`来做一下reflect的最简单的性能测试。 232 | 233 | Type:Type类型用来表示一个go类型。 234 | 235 | 不是所有go类型的Type值都能使用所有方法。请参见每个方法的文档获取使用限制。在调用有分类限定的方法时,应先使用Kind方法获知类型的分类。调用该分类不支持的方法会导致运行时的panic。 236 | 237 | reflect.Type中方法: 238 | ```go 239 | // 通用方法 240 | func (t *rtype) String() string // 获取 t 类型的字符串描述,不要通过 String 来判断两种类型是否一致。 241 | 242 | func (t *rtype) Name() string // 获取 t 类型在其包中定义的名称,未命名类型则返回空字符串。 243 | 244 | func (t *rtype) PkgPath() string // 获取 t 类型所在包的名称,未命名类型则返回空字符串。 245 | 246 | func (t *rtype) Kind() reflect.Kind // 获取 t 类型的类别。 247 | 248 | func (t *rtype) Size() uintptr // 获取 t 类型的值在分配内存时的大小,功能和 unsafe.SizeOf 一样。 249 | 250 | func (t *rtype) Align() int // 获取 t 类型的值在分配内存时的字节对齐值。 251 | 252 | func (t *rtype) FieldAlign() int // 获取 t 类型的值作为结构体字段时的字节对齐值。 253 | 254 | func (t *rtype) NumMethod() int // 获取 t 类型的方法数量。 255 | 256 | func (t *rtype) Method() reflect.Method // 根据索引获取 t 类型的方法,如果方法不存在,则 panic。 257 | 258 | // 如果 t 是一个实际的类型,则返回值的 Type 和 Func 字段会列出接收者。 如果 t 只是一个接口,则返回值的 Type 不列出接收者,Func 为空值。 259 | func (t *rtype) MethodByName(string) (reflect.Method, bool) // 根据名称获取 t 类型的方法。 260 | 261 | func (t *rtype) Implements(u reflect.Type) bool // 判断 t 类型是否实现了 u 接口。 262 | 263 | func (t *rtype) ConvertibleTo(u reflect.Type) bool // 判断 t 类型的值可否转换为 u 类型。 264 | 265 | func (t *rtype) AssignableTo(u reflect.Type) bool // 判断 t 类型的值可否赋值给 u 类型。 266 | 267 | func (t *rtype) Comparable() bool // 判断 t 类型的值可否进行比较操作 268 | 269 | // 注意对于:数组、切片、映射、通道、指针、接口 270 | func (t *rtype) Elem() reflect.Type // 获取元素类型、获取指针所指对象类型,获取接口的动态类型 271 | // 数值 272 | func (t *rtype) Bits() int // 获取数值类型的位宽,t 必须是整型、浮点型、复数型 273 | // 数组 274 | func (t *rtype) Len() int // 获取数组的元素个数 275 | // 映射 276 | func (t *rtype) Key() reflect.Type // 获取映射的键类型 277 | // 通道 278 | func (t *rtype) ChanDir() reflect.ChanDir // 获取通道的方向 279 | // 结构体 280 | func (t *rtype) NumField() int // 获取字段数量 281 | 282 | func (t *rtype) Field(int) reflect.StructField // 根据索引获取字段 283 | 284 | func (t *rtype) FieldByName(string) (reflect.StructField, bool) // 根据名称获取字段 285 | 286 | func (t *rtype) FieldByNameFunc(match func(string) bool) (reflect.StructField, bool) // 根据指定的匹配函数 math 获取字段 287 | 288 | func (t *rtype) FieldByIndex(index []int) reflect.StructField // 根据索引链获取嵌套字段 289 | 290 | // 函数 291 | func (t *rtype) NumIn() int // 获取函数的参数数量 292 | 293 | func (t *rtype) In(int) reflect.Type // 根据索引获取函数的参数信息 294 | 295 | func (t *rtype) NumOut() int // 获取函数的返回值数量 296 | 297 | func (t *rtype) Out(int) reflect.Type // 根据索引获取函数的返回值信息 298 | 299 | func (t *rtype) IsVariadic() bool // 判断函数是否具有可变参数。 300 | // 如果有可变参数,则 t.In(t.NumIn()-1) 将返回一个切片。 301 | ``` 302 | 303 | reflect.Value方法: 304 | 305 | reflect.Value.Kind():获取变量类别,返回常量. 306 | 307 | 用于获取值方法: 308 | ```go 309 | func (v Value) Int() int64 // 获取int类型值,如果 v 值不是有符号整型,则 panic。 310 | 311 | func (v Value) Uint() uint64 // 获取unit类型的值,如果 v 值不是无符号整型(包括 uintptr),则 panic。 312 | 313 | func (v Value) Float() float64 // 获取float类型的值,如果 v 值不是浮点型,则 panic。 314 | 315 | func (v Value) Complex() complex128 // 获取复数类型的值,如果 v 值不是复数型,则 panic。 316 | 317 | func (v Value) Bool() bool // 获取布尔类型的值,如果 v 值不是布尔型,则 panic。 318 | 319 | func (v Value) Len() int // 获取 v 值的长度,v 值必须是字符串、数组、切片、映射、通道。 320 | 321 | func (v Value) Cap() int // 获取 v 值的容量,v 值必须是数值、切片、通道。 322 | 323 | func (v Value) Index(i int) reflect.Value // 获取 v 值的第 i 个元素,v 值必须是字符串、数组、切片,i 不能超出范围。 324 | 325 | func (v Value) Bytes() []byte // 获取字节类型的值,如果 v 值不是字节切片,则 panic。 326 | 327 | func (v Value) Slice(i, j int) reflect.Value // 获取 v 值的切片,切片长度 = j - i,切片容量 = v.Cap() - i。 328 | // v 必须是字符串、数值、切片,如果是数组则必须可寻址。i 不能超出范围。 329 | 330 | func (v Value) Slice3(i, j, k int) reflect.Value // 获取 v 值的切片,切片长度 = j - i,切片容量 = k - i。 331 | // i、j、k 不能超出 v 的容量。i <= j <= k。 332 | // v 必须是字符串、数值、切片,如果是数组则必须可寻址。i 不能超出范围。 333 | 334 | func (v Value) MapIndex(key Value) reflect.Value // 根据 key 键获取 v 值的内容,v 值必须是映射。 335 | // 如果指定的元素不存在,或 v 值是未初始化的映射,则返回零值(reflect.ValueOf(nil)) 336 | 337 | func (v Value) MapKeys() []reflect.Value // 获取 v 值的所有键的无序列表,v 值必须是映射。 338 | // 如果 v 值是未初始化的映射,则返回空列表。 339 | 340 | func (v Value) OverflowInt(x int64) bool // 判断 x 是否超出 v 值的取值范围,v 值必须是有符号整型。 341 | 342 | func (v Value) OverflowUint(x uint64) bool // 判断 x 是否超出 v 值的取值范围,v 值必须是无符号整型。 343 | 344 | func (v Value) OverflowFloat(x float64) bool // 判断 x 是否超出 v 值的取值范围,v 值必须是浮点型。 345 | 346 | func (v Value) OverflowComplex(x complex128) bool // 判断 x 是否超出 v 值的取值范围,v 值必须是复数型。 347 | ``` 348 | 设置值方法: 349 | ```go 350 | func (v Value) SetInt(x int64) //设置int类型的值 351 | 352 | func (v Value) SetUint(x uint64) // 设置无符号整型的值 353 | 354 | func (v Value) SetFloat(x float64) // 设置浮点类型的值 355 | 356 | func (v Value) SetComplex(x complex128) //设置复数类型的值 357 | 358 | func (v Value) SetBool(x bool) //设置布尔类型的值 359 | 360 | func (v Value) SetString(x string) //设置字符串类型的值 361 | 362 | func (v Value) SetLen(n int) // 设置切片的长度,n 不能超出范围,不能为负数。 363 | 364 | func (v Value) SetCap(n int) //设置切片的容量 365 | 366 | func (v Value) SetBytes(x []byte) //设置字节类型的值 367 | 368 | func (v Value) SetMapIndex(key, val reflect.Value) //设置map的key和value,前提必须是初始化以后,存在覆盖、不存在添加 369 | ``` 370 | 371 | 其他的方法: 372 | ```go 373 | // 结构体相关: 374 | func (v Value) NumField() int // 获取结构体字段(成员)数量 375 | 376 | func (v Value) Field(i int) reflect.Value //根据索引获取结构体字段 377 | 378 | func (v Value) FieldByIndex(index []int) reflect.Value // 根据索引链获取结构体嵌套字段 379 | 380 | func (v Value) FieldByName(string) reflect.Value // 根据名称获取结构体的字段,不存在返回reflect.ValueOf(nil) 381 | 382 | func (v Value) FieldByNameFunc(match func(string) bool) Value // 根据匹配函数 match 获取字段,如果没有匹配的字段,则返回零值(reflect.ValueOf(nil)) 383 | 384 | 385 | // 通道相关: 386 | func (v Value) Send(x reflect.Value)// 发送数据(会阻塞),v 值必须是可写通道。 387 | 388 | func (v Value) Recv() (x reflect.Value, ok bool) // 接收数据(会阻塞),v 值必须是可读通道。 389 | 390 | func (v Value) TrySend(x reflect.Value) bool // 尝试发送数据(不会阻塞),v 值必须是可写通道。 391 | 392 | func (v Value) TryRecv() (x reflect.Value, ok bool) // 尝试接收数据(不会阻塞),v 值必须是可读通道。 393 | 394 | func (v Value) Close() // 关闭通道 395 | 396 | // 函数相关 397 | func (v Value) Call(in []Value) (r []Value) // 通过参数列表 in 调用 v 值所代表的函数(或方法)。函数的返回值存入 r 中返回。 398 | // 要传入多少参数就在 in 中存入多少元素。 399 | // Call 即可以调用定参函数(参数数量固定),也可以调用变参函数(参数数量可变)。 400 | 401 | func (v Value) CallSlice(in []Value) []Value // 调用变参函数 402 | ``` 403 | -------------------------------------------------------------------------------- /src/chapter09/01.0.md: -------------------------------------------------------------------------------- 1 | ### For-learning-Go-Tutorial 2 | 3 | Go语言是谷歌2009发布的第二款开源编程语言 4 | 5 | Go语言专门针对多处理器系统应用程序的编程进行了优化,使用Go编译的程序可以媲美C或C++代码的速度,而且更加安全、支持并行进程。 6 | 7 | 因而一直想的是自己可以根据自己学习和使用Go语言编程的心得,写一本Go的书可以帮助想要学习Go语言的初学者快速入门开发和使用! 8 | 9 | #### Channel 10 | 11 | Channel是Go中的一个核心类型,你可以把它看成一个管道,通过它并发核心单元就可以发送或者接收数据进行通讯(communication). 12 | 13 | Channel用于数据传递或数据共享,其本质上是一个先进先出的队列,使用Goroutine 和channel进行数据通讯简单高效,同时也线程安全,多个Goroutine可同时修改一个Channel,不需要加锁。 14 | 15 | 管道是一系列由channel联通的状态(stage),而每个状态是一组运行相同函数的Goroutine。在每个状态的Goroutine上: 16 | 17 | * 通过流入(inbound)channel接收上游的数值. 18 | * 运行一些函数来处理接收的数据,一般会产生新的数值. 19 | * 通过流出(outbound)channel将数值发给下游. 20 | 21 | 每个语态都会有任意个流入或者流出channel,除了第一个状态(只有流出channel)和最后一个状态(只有流入channel)。第一个状态有时被称作源或者生产者;最后一个状态有时被称作槽(sink)或者消费者。 22 | 23 | 我们先从简单开始使用内置的make函数,创建一个Channel: 24 | ```go 25 | ch := make(chan int) 26 | ```` 27 | 然而每个channel都有一个特殊的类型,也就是channels可发送数据的类型。一个可以发送int类型数据的channel一般写为chan int,一个channel有发送和接受两个主要操作,都是通信行为。一个发送语句将一个值从一个goroutine通过channel发送到另一个执行接收操作的goroutine。发送和接收两个操作都是用<-运算符。 28 | ```go 29 | ch <- p // 发送值p到Channel ch中 30 | p := <-ch // 从Channel ch中接收数据,并将数据赋值给p 31 | ``` 32 | 注意:在channel中箭头的指向是数据的流向. 33 | 34 | 和map类似,channel也一个对应make创建的底层数据结构的引用。当我们复制一个channel或用于函数参数传递时,我们只是拷贝了一个channel引用,因此调用者何被调用者将引用同一个channel对象。和其它的引用类型一样,channel的零值也是nil。两个相同类型的channel可以使用==运算符比较。如果两个channel引用的是相通的对象,那么比较的结果为真。一个channel也可以和nil进行比较。 35 | 36 | Channel类型的定义格式: 37 | 38 | ```go 39 | ChannelType = ( "chan" | "chan" "<-" | "<-" "chan" ) Type . 40 | ``` 41 | 42 | 它包括三种类型的定义。可选的<-代表channel的方向。如果没有指定方向,那么Channel就是双向的,既可以接收数据,也可以发送数据。 43 | 44 | ```go 45 | chan p // 可以接收和发送类型为p的数据 46 | chan<- float64 // 只可以用来发送 float64 类型的数据 47 | <-chan int // 只可以用来接收 int 类型的数据 48 | ``` 49 | 50 | 这里需要注意下:<-总是优先和最左边的类型结合。 51 | 52 | 使用make初始化Channel,我们还可以设置channel的容量,容量(capacity)代表Channel容纳的最多的元素的数量,代表Channel的缓存的大小。 53 | ```go 54 | ch = make(chan int) // 无缓冲 channel 55 | ch = make(chan int, 0) // 无缓冲 channel 56 | ch = make(chan int, 3) // 缓冲 channel容量是3 57 | ``` 58 | 如果没有设置容量,或者容量设置为0, 说明Channel没有缓存,只有sender和receiver都准备好了后它们的通讯(communication)才会发生(Blocking)。如果设置了缓存,就有可能不发生阻塞, 只有buffer满了后 send才会阻塞, 而只有缓存空了后receive才会阻塞。一个nil channel不会通信。 59 | 60 | ##### 无缓冲的Channels 61 | 62 | 无缓冲:发送和接收动作是同时发生的Channels的发送和接收操作将导致两个goroutine做一次同步操作。因为这个原因,无缓存Channels有时候也被称为同步Channels 63 | 。如果没有 goroutine 读取 channel (<- channel),则发送者 (channel <-) 会一直阻塞。 64 | 65 |

66 | 67 |

68 | 69 | ##### 缓冲的Channels 70 | 71 | 缓冲:缓冲 channel 类似一个有容量的队列。当队列满的时候发送者会阻塞;当队列空的时候接收者会阻塞。 72 | 73 |

74 | 75 |

76 | 77 | 此外在使用channel之后可以进行关闭,关闭channel后,对该channel的任何发送操作都将导致panic异常。对一个已经被close过的channel之行接收操作依然可以接受到之前已经成功发送的数据;如果channel中已经没有数据的话讲产生一个零值的数据。 78 | 79 | 使用内置的close函数就可以关闭一个channel: 80 | ```go 81 | close(ch) 82 | ``` 83 | 但是关于关闭channel 有几点需要注意: 84 | 85 | * 重复关闭 channel 会导致 panic. 86 | * 向关闭的 channel 发送数据会 panic. 87 | * 从关闭的 channel 读数据不会 panic,读出channel中已有的数据之后再读就是channel类似的默认值,比如 chan int 类型的channel关闭之后读取到的值为 0. 88 | 89 | 这里我们需要区分一下第三种channel 中的值是默认值还是channel 关闭了。可以使用 ok-idiom 方式,这种方式在 map 中比较常用. 90 | ```go 91 | ch := make(chan int, 10) 92 | ... 93 | close(ch) 94 | 95 | // ok-idiom 96 | val, ok := <-ch 97 | if ok == false { 98 | // channel closed 99 | } 100 | ``` 101 | 102 | #### Channel的典型用法 103 | 104 | * goroutine 使用channel通信 105 | ```go 106 | func main() { 107 | x := make(chan int) 108 | go func() { 109 | x <- 1 110 | }() 111 | <-x 112 | } 113 | ``` 114 | 115 | * select 116 | 117 | select语句选择一组可能的send操作和receive操作去处理。它类似switch,但是只是用来处理通讯(communication)操作。它的case可以是send语句,也可以是receive语句,亦或者default。 118 | 119 | receive语句可以将值赋值给一个或者两个变量。它必须是一个receive操作。 120 | 121 | select在一定程度上可以类比于linux中的 IO 多路复用中的 select。后者相当于提供了对多个 IO 事件的统一管理,而 Golang 中的 select 相当于提供了对多个 channel 的统一管理。当然这只是 select 在 channel 上的一种使用方法。 122 | ```go 123 | select { 124 | case e, ok := <-ch1: 125 | ... 126 | case e, ok := <-ch2: 127 | ... 128 | default: 129 | } 130 | ``` 131 | 这里需要注意的是 select 中的 break 只能跳到 select 这一层。select 使用的时候一般需要配合 for 循环使用,因为正常 select 里面的流程也就执行一遍。这么来看 select 中的 break 就稍显鸡肋了。所以使用 break 的时候一般配置 label 使用,label 定义在 for循环这一层。 132 | ```go 133 | for { 134 | select { 135 | ... 136 | } 137 | } 138 | ``` 139 | 140 | * range channel 141 | 142 | 使用range channel 我们可以直接取到 channel 中的值。当我们使用 range 来操作 channel 的时候,一旦 channel 关闭,channel 内部数据读完之后循环自动结束。 143 | ```go 144 | func consumer(ch chan int) { 145 | for x := range ch { 146 | fmt.Println(x) 147 | ... 148 | } 149 | } 150 | 151 | func producer(ch chan int) { 152 | for _, v := range values { 153 | ch <- v 154 | } 155 | } 156 | ``` 157 | 158 | * 超时控制 159 | 160 | select有很重要的一个应用就是超时处理。由于如果没有case需要处理,select语句就会一直阻塞着。这时候我们可能就需要一个超时操作,用来处理超时的情况通常在很多操作情况下都需要超时控制,我们可以利用 select 实现超时控制: 161 | ```go 162 | func main() { 163 | c1 := make(chan string, 1) 164 | go func() { 165 | time.Sleep(time.Second * 2) 166 | c1 <- "result 1" 167 | }() 168 | select { 169 | case res := <-c1: 170 | fmt.Println(res) 171 | case <-time.After(time.Second * 1): 172 | fmt.Println("timeout 1") 173 | } 174 | } 175 | ``` 176 | 这里利用的是time.After方法,它返回一个类型为<-chan Time的单向的channel,在指定的时间发送一个当前时间给返回的channel中。 177 | 178 | * channel同步 179 | 180 | channel可以用在goroutine之间的同步。 181 | 下面的例子main中goroutine通过done channel等待 mission完成任务。 mission做完任务后只需往channel发送一个数据就可以通知main goroutine任务完成。 182 | 183 | ```go 184 | func mission(done chan bool) { 185 | time.Sleep(time.Second) 186 | // 通知任务已完成 187 | done <- true 188 | } 189 | func main() { 190 | done := make(chan bool, 1) 191 | go mission(done) 192 | // 等待任务完成 193 | <-done 194 | } 195 | ``` 196 | 197 | 在Golang中的channel 将goroutine 隔离开,并发编程的时候可以将注意力放在 channel 上。在一定程度上,这个和消息队列的解耦功能还是挺像的。 198 | 199 | ##### 扇出和扇入 200 | 201 | 通常情况下多个函数可以同时从一个channel接收数据,直到channel关闭,这种情况被称作扇出。这是一种将工作分布给一组工作者的方法,目的是并行使用CPU和I/O。 202 | 203 | 如果一个函数同时接收并处理多个channel输入并转化为一个输出channel,直到所有的输入channel都关闭后,关闭输出channel,这种情况就被称作扇入。 204 | 205 | 但是main可以容易的通过关闭done channel来释放所有的发送者。关闭是个高效的发送给所有发送者的信号。我们扩展channel管道里的每个函数,让其以参数方式接收done,并通过defer语句在函数退出时执行关闭操作,这样main里所有的退出路径都会触发管道里的所有状态退出。 206 | 207 | ```go 208 | func main() { 209 | // 构建done channel,整个管道里分享done,并在管道退出时关闭这个channel 210 | // 以此通知所有Goroutine该推出了。 211 | done := make(chan struct{}) 212 | defer close(done) 213 | 214 | in := gen(done, 2, 3) 215 | 216 | // 发布sq的工作到两个都从in里读取数据的Goroutine 217 | c1 := sq(done, in) 218 | c2 := sq(done, in) 219 | 220 | // 处理来自output的第一个数值 221 | out := merge(done, c1, c2) 222 | fmt.Println(<-out) // 4 或者 9 223 | 224 | // done会通过defer调用而关闭 225 | } 226 | 227 | func sq(in <-chan int) <-chan int { 228 | out := make(chan int) 229 | go func() { 230 | for n := range in { 231 | out <- n*n 232 | } 233 | close(out) 234 | }() 235 | return out 236 | } 237 | ``` 238 | 239 | merge对每个流入channel启动一个Goroutine,并将流入的数值复制到流出channel,由此将一组channel转换到一个channel。一旦启动了所有的output Goroutine,merge函数会多启动一个Goroutine,这个Goroutine在所有的输入channel输入完毕后,关闭流出channel。sq函数是把上一个函数的chan最为参数,下一个输出的chan作为返回值。 240 | 241 | 但是往一个已经关闭的channel输出会产生异常(panic),所以一定要保证所有数据发送完成后再执行关闭。 242 | 243 | 所以发送Goroutine将发送操作替换为一个select语句,要么把数据发送给out,要么处理来自done的数值。done的类型是个空结构,因为具体数值并不重要:接收事件本身就指明了应当放弃继续发送给out的动作。而output Goroutine会继续循环处理流入的channel,c,而不会阻塞上游状态. 244 | 245 | ```go 246 | //gen函数启动一个Goroutine,将整数数列发送给channel,如果所有数都发送完成,关闭这个channel 247 | func gen(nums ...int) <-chan int { 248 | out := make(chan int, len(nums)) 249 | for _, n := range nums { 250 | out <- n 251 | } 252 | close(out) 253 | return out 254 | } 255 | 256 | // 从一个channel接收整数,并求整数的平方,发送给另一个channel. 257 | // mission的循环中退出,因为我们知道如果done已经被关闭了,也会关闭上游的gen状态. 258 | // mission通过defer语句,保证不管从哪个返回路径,它的out channel都会被关闭. 259 | 260 | 261 | func mission(in <-chan int) <-chan int { 262 | out := make(chan int) 263 | go func() { 264 | defer close(out) 265 | for n := range in { 266 | select { 267 | case out <- n * n: 268 | case <-done: 269 | return 270 | } 271 | } 272 | }() 273 | return out 274 | } 275 | 276 | 277 | func merge(cs ...<-chan int) <-chan int { 278 | var wg sync.WaitGroup 279 | out := make(chan int) 280 | 281 | // 为每个cs中的输入channel启动一个output Goroutine。outpu从c里复制数值直到c被关闭 282 | // 或者从done里接收到数值,之后output调用wg.Done 283 | output := func(c <-chan int) { 284 | for n := range c { 285 | select { 286 | case out <- n: 287 | case <-done: 288 | } 289 | } 290 | wg.Done() 291 | } 292 | wg.Add(len(cs)) 293 | for _, c := range cs { 294 | go output(c) 295 | } 296 | 297 | // 启动一个Goroutine,当所有output Goroutine都工作完后(wg.Done),关闭out, 298 | // 保证只关闭一次。这个Goroutine必须在wg.Add之后启动 299 | go func() { 300 | wg.Wait() 301 | close(out) 302 | }() 303 | return out 304 | } 305 | ``` 306 | 在channel模式中有个模式: 307 | 308 | * 状态会在所有发送操作做完后,关闭它们的流出channel 309 | 310 | * 状态会持续接收从流入channel输入的数值,直到channel关闭 311 | 312 | 这个模式使得每个接收状态可以写为一个range循环,并保证所有的Goroutine在将所有的数值发送成功给下游后立刻退出。 313 | 314 | 所以在构建channel的时候: 315 | 316 | * 状态会在所有发送操作做完后,关闭它们的流出channel. 317 | * 状态会持续接收从流入channel输入的数值,直到channel关闭或者其发送者被释放. 318 | 319 | 因而管道要么保证足够能存下所有发送数据的缓冲区,要么接收来自接收者明确的要放弃channel的信号,来保证释放发送者。 320 | -------------------------------------------------------------------------------- /src/chapter10/01.0.md: -------------------------------------------------------------------------------- 1 | ### For-learning-Go-Tutorial 2 | 3 | Go语言是谷歌2009发布的第二款开源编程语言. 4 | 5 | Go语言专门针对多处理器系统应用程序的编程进行了优化,使用Go编译的程序可以媲美C或C++代码的速度,而且更加安全、支持并行进程。 6 | 7 | 因而一直想的是自己可以根据自己学习和使用Go语言编程的心得,写一本Go的书可以帮助想要学习Go语言的初学者快速入门开发和使用! 8 | 9 | #### Goroutine并发处理 10 | 11 | 在了解Goroutine并发之前,我们需要先了解下进程和线程,并发与并行 (Concurrency and Parallelism)的概念。 12 | 13 | * 进程 14 | 15 | 进程是操作系统中进行保护和资源分配的基本单位,操作系统分配资源以进程为基本单位。 16 | 17 | cpu在切换程序的时候,如果不保存上一个程序的状态(也就是我们常说的context-上下文),直接切换下一个程序,就会丢失上一个程序的一系列状态,于是引入了进程这个概念,用以划分好程序运行时所需要的资源。因此进程就是一个程序运行时候的所需要的基本资源单位(也可以说是程序运行的一个实体)。 18 | 19 | * 线程 20 | 21 | 线程是进程的组成部分,它代表了一条顺序的执行流。 22 | 23 | cpu切换多个进程的时候,会花费不少的时间,因为切换进程需要切换到内核态,而每次调度需要内核态都需要读取用户态的数据,进程一旦多起来,cpu调度会消耗一大堆资源,因此引入了线程的概念,线程本身几乎不占有资源,他们共享进程里的资源,内核调度起来不会那么像进程切换那么耗费资源。 24 | 25 | * 协程 26 | 27 | 协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此,协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。线程和进程的操作是由程序触发系统接口,最后的执行者是系统;协程的操作执行者则是用户自身程序,goroutine也是协程。 28 | 29 |

30 | 31 |

32 | 33 | 进程是从操作系统获得基本的内存空间,所有的线程共享着进程的内存地址空间。此外,每个线程也会拥有自己私有的内存地址范围,其他线程不能访问。然而由于所有的线程共享进程的内存地址空间,所以线程间的通信就非常容易,通过共享进程级全局变量就可以实现线程间的通信。 34 | 35 | * 并发 36 | 37 | 并发是指程序的逻辑结构,交替做不同事的能力,在这里通常是不同程序交替执行的性能。 38 | 39 | * 并行 40 | 41 | 并行是指程序的运行状态,同时做不同事的能力,在这里通常是指不同程序同时执行的性能。 42 | 43 | 如果某个系统支持两个或者多个动作(Action)同时存在,那么这个系统就是一个**并发系统**。如果某个系统支持两个或者多个动作同时执行,那么这个系统就是一个**并行系统**。 44 | 45 | 并发系统与并行系统这两个定义之间的关键差异在于“存在”这个词。在并发程序中可以同时拥有两个或者多个线程。这意味着,如果程序在单核处理器上运行,那么这两个线程将交替地换入或者换出内存。这些线程是同时“存在”的——每个线程都处于执行过程中的某个状态。如果程序能够并行执行,那么就一定是运行在多核处理器上。 46 | 47 | 此时,程序中的每个线程都将分配到一个独立的处理器核上,因此可以同时运行。这里相信你已经能够得出结论——“并行”概念是“并发”概念的一个子集。也就是说,你可以编写一个拥有多个线程或者进程的并发程序,但如果没有多核处理器来执行这个程序,那么就不能以并行方式来运行代码。因此,凡是在求解单个问题时涉及多个执行流程的编程模式或者执行行为,都属于并发编程的范畴。 48 | 49 | 理解了并发和并行后,我们在看看Goroutine. 50 | 51 | Goroutine 的概念类似于线程,但 Goroutine 由 Go 程序运行时的调度和管理。Go 程序会将Goroutine 中的任务合理地分配给每个 CPU。Go 程序从 main 包的 main() 函数开始,在程序启动时,Go 程序就会为 main() 函数创建一个默认的 Goroutine。 52 | 53 | Goroutine是Go语言原生支持并发的具体实现,在Go中的代码都是运行在Goroutine中的。Goroutine占用的资源非常小(Go 1.4将每个Goroutine stack的size默认设置为2k),goroutine调度的切换也不用陷入(trap)操作系统内核层完成,代价很低。因此,一个Go程序中可以创建成千上万个并发的goroutine。 54 | 55 | 所有的Go代码都在goroutine中执行,即使是go的runtime也不例外。我们可以启动成千上万的goroutine,但是Go的runtime负责对goroutine进行调度。这里的调度就是决定何时哪个goroutine将获得资源开始执行、哪个goroutine应该停止执行让出资源、哪个goroutine应该被唤醒恢复执行等. 56 | 57 | 但是很多人其实并没有深入的了解过Goroutine的调度模型和原理,那么Goroutine是怎么实现调度的呢? 58 | 59 | Go的调度器内部有三个重要的结构:G P M. 60 | 61 | * G: 表示goroutine,存储了goroutine的执行stack信息、goroutine状态以及goroutine的任务函数等;另外G对象是可以重用的。 62 | 63 | ```go 64 | struct G { 65 | uintptr stackguard; // 分段栈的可用空间下界 66 | uintptr stackbase; // 分段栈的栈基址 67 | Gobuf sched; //进程切换时,利用sched域来保存上下文 68 | uintptr stack0; 69 | FuncVal* fnstart; // goroutine运行的函数 70 | void* param; // 用于传递参数,睡眠时其它goroutine设置param,唤醒时此goroutine可以获取 71 | int16 status; // 状态Gidle,Grunnable,Grunning,Gsyscall,Gwaiting,Gdead 72 | int64 goid; // goroutine的id号 73 | G* schedlink; 74 | M* m; // for debuggers, but offset not hard-coded 75 | M* lockedm; // G被锁定只能在这个m上运行 76 | uintptr gopc; // 创建这个goroutine的go表达式的pc 77 | ... 78 | } 79 | ``` 80 | 81 | 结构体G中的部分域如上所示。其中包含了栈信息stackbase和stackguard,还有运行的函数信息fnstart。这样就可以成为一个可执行的单元了,只要得到CPU就可以运行。goroutine切换时,上下文信息保存在结构体的sched域中。goroutine是轻量级的线程或者称为协程,切换时并不必陷入到操作系统内核中,所以保存过程很轻量。 82 | 83 | 而G中的Gobuf,只保存了当前栈指针,程序计数器,以及goroutine自身。 84 | ```go 85 | struct Gobuf { 86 | // The offsets of these fields are known to (hard-coded in) libmach. 87 | uintptr sp; 88 | byte* pc; 89 | G* g; 90 | ... 91 | } 92 | ``` 93 | 94 | 这里g是为了恢复当前goroutine的结构体G指针,运行时库中使用了一个常驻的寄存器`extern register G* g`,这个是当前goroutine的结构体G的指针。这样做是为了快速地访问goroutine中的信息. 95 | 96 | * P:表示逻辑processor 代表cpu,P的数量决定了系统内最大可并行的G的数量(系统的物理cpu核数>=P的数量);P的最大作用还是其拥有的各种G对象队列、链表、一些cache和状态。 97 | 98 | 在代码中结构体P的加入是为了提高Go程序的并发度,实现更好的调度。M代表OS线程。P代表Go代码执行时需要的资源。当M执行Go代码时,它需要关联一个P,当M为idle或者在系统调用中时,它也需要P。有刚好`GOMAXPROCS`个P。所有的P被组织为一个数组,在P上实现了工作流窃取的调度器。 99 | 100 | ```go 101 | struct P { 102 | Lock; 103 | uint32 status; // Pidle或Prunning等 104 | P* link; 105 | uint32 schedtick; // 每次调度时将它加一 106 | M* m; // 链接到它关联的M (nil if idle) 107 | MCache* mcache; 108 | 109 | G* runq[256]; 110 | int32 runqhead; 111 | int32 runqtail; 112 | 113 | // Available G's (status == Gdead) 114 | G* gfree; 115 | int32 gfreecnt; 116 | byte pad[64]; 117 | } 118 | ``` 119 | 在P中有一个Grunnable的goroutine队列,这是一个P的局部队列。当P执行Go代码时,它会优先从自己的这个局部队列中取,这时可以不用加锁,提高了并发度。如果发现这个队列空了,则去其它P的队列中拿一半过来,这样实现工作流窃取的调度。这种情况下是需要给调用器加锁的。 120 | 121 | * M: M代表着执行计算资源。在绑定有效的p后,进入schedule循环;而schedule循环的机制大致是从各种队列、p的本地队列中获取G,切换到G的执行栈上并执行G的函数,调用goexit做清理工作并回到m,如此反复。M并不保留G状态,这是G可以跨M调度的基础。 122 | 123 | M是machine的缩写,是对机器的抽象,每个m都是对应到一条操作系统的物理线程。M必须关联了P才可以执行Go代码,但是当它处理阻塞或者系统调用中时,可以不需要关联P。 124 | ```go 125 | struct M { 126 | G* g0; // 带有调度栈的goroutine 127 | G* gsignal; // signal-handling G 处理信号的goroutine 128 | void (*mstartfn)(void); 129 | G* curg; // M中当前运行的goroutine 130 | P* p; // 关联P以执行Go代码 (如果没有执行Go代码则P为nil) 131 | P* nextp; 132 | int32 id; 133 | int32 mallocing; //状态 134 | int32 throwing; 135 | int32 gcing; 136 | int32 locks; 137 | int32 helpgc; //不为0表示此m在做帮忙gc。helpgc等于n只是一个编号 138 | bool blockingsyscall; 139 | bool spinning; 140 | Note park; 141 | M* alllink; // 这个域用于链接allm 142 | M* schedlink; 143 | MCache *mcache; 144 | G* lockedg; 145 | M* nextwaitm; // next M waiting for lock 146 | GCStats gcstats; 147 | ... 148 | } 149 | ``` 150 | 和G类似,M中也有alllink域将所有的M放在allm链表中。lockedg是某些情况下,G锁定在这个M中运行而不会切换到其它M中去。M中还有一个MCache,是当前M的内存的缓存。M也和G一样有一个常驻寄存器变量,代表当前的M。同时存在多个M,表示同时存在多个物理线程。 151 | 152 | 结构体M中有两个G是需要关注一下的,一个是curg,代表结构体M当前绑定的结构体G。另一个是g0,是带有调度栈的goroutine,这是一个比较特殊的goroutine。普通的goroutine的栈是在堆上分配的可增长的栈,而g0的栈是M对应的线程的栈。所有调度相关的代码,会先切换到该goroutine的栈中再执行。 153 | 154 |

155 | 156 |

157 | 158 | G 代表 goroutine,M 可以看做真实的资源(OS Threads)。P是 G-M 的中间层,P是一个“逻辑Proccessor”,组织多个Goroutine跑在同一个 OS Thread 上。 159 | 160 | 对于G来说,P就是运行它的“CPU”,可以说:G的眼里只有P。但从Go scheduler视角来看,真正的“CPU”是M,只有将P和M绑定才能让P的runq中G得以真实运行起来。这样的P与M的关系,就好比Linux操作系统调度层面用户线程(user thread)与核心线程(kernel thread)的对应关系那样(N x M)。 161 | 162 | 163 | 一个 P上会挂着多个G,当一个G执行结束时,P会选择下一个 Goroutine 继续执行。而当一个Goroutine执行太久没有结束,这样就需要调度给后面的 Goroutine 运行的机会。所以,Go scheduler 除了在一个 Goroutine 执行结束时会调度后面的 Goroutine 执行,还会在正在被执行的 Goroutine 发生以下情况时让出当前 goroutine 的执行权,并调度后面的 Goroutine 执行:IO 操作,Channel 阻塞,system call,运行较长时间. 164 | 165 | 对于运行时间较长的Goroutine,scheduler会在其 G对象上打上一个标志( preempt),当这个 goroutine 内部发生函数调用的时候,会先主动检查这个标志,如果为 true ,就需要主动调用Gosched()来让出CPU。 166 | 167 | 然而如果G被阻塞在某个channel操作或network I/O操作上时,G会被放置到某个wait队列中,而M会尝试运行下一个runnable的G;如果此时没有runnable的G供m运行,那么m将解绑P,并进入sleep状态。当I/O available或channel操作完成,在wait队列中的G会被唤醒,标记为runnable,放入到某P的队列中,绑定一个M继续执行。 168 | 169 | 果G被阻塞在某个`system call`操作上,那么不光G会阻塞,执行该G的M也会解绑P(实质是被sysmon抢走了),与G一起进入sleep状态。如果此时有idle的M,则P与其绑定继续执行其他G;如果没有idle M,但仍然有其他G要去执行,那么就会创建一个新M。 170 | 171 | 当阻塞在syscall上的G完成syscall调用后,G会去尝试获取一个可用的P,如果没有可用的P,那么G会被标记为runnable,之前的那个sleep的M将再次进入sleep。 172 | 173 | #### 调度器状态的查看方法 174 | 175 | 在Go中调度器状态的查看方法是用: 176 | ```go 177 | > GODEBUG=schedtrace=1000 godoc -http=:6060 178 | 179 | SCHED 0ms: gomaxprocs=4 idleprocs=2 threads=5 spinningthreads=1 idlethreads=0 runqueue=0 [0 0 0 0] 180 | SCHED 1007ms: gomaxprocs=4 idleprocs=4 threads=27 spinningthreads=0 idlethreads=5 runqueue=0 [0 0 0 0] 181 | SCHED 2016ms: gomaxprocs=4 idleprocs=3 threads=27 spinningthreads=0 idlethreads=5 runqueue=0 [0 0 0 0] 182 | SCHED 3017ms: gomaxprocs=4 idleprocs=3 threads=27 spinningthreads=0 idlethreads=5 runqueue=0 [0 0 0 0] 183 | SCHED 4018ms: gomaxprocs=4 idleprocs=1 threads=27 spinningthreads=0 idlethreads=5 runqueue=0 [0 0 0 0] 184 | SCHED 5018ms: gomaxprocs=4 idleprocs=1 threads=27 spinningthreads=0 idlethreads=5 runqueue=0 [0 0 0 0] 185 | SCHED 6018ms: gomaxprocs=4 idleprocs=2 threads=27 spinningthreads=0 idlethreads=5 runqueue=0 [0 0 0 0] 186 | SCHED 7018ms: gomaxprocs=4 idleprocs=3 threads=27 spinningthreads=0 idlethreads=5 runqueue=0 [0 0 0 0] 187 | SCHED 8019ms: gomaxprocs=4 idleprocs=3 threads=27 spinningthreads=0 idlethreads=5 runqueue=0 [0 0 0 0] 188 | SCHED 9020ms: gomaxprocs=4 idleprocs=0 threads=27 spinningthreads=1 idlethreads=4 runqueue=0 [0 0 0 0] 189 | SCHED 10020ms: gomaxprocs=4 idleprocs=3 threads=27 spinningthreads=0 idlethreads=5 runqueue=0 [0 0 0 0] 190 | ``` 191 | GODEBUG这个Go运行时环境变量非常有用,我们通过给其传入不同的key=value,...组合,可以查看Go的runtime会输出不同的调试信息,比如在这里我们给GODEBUG传入了 ”schedtrace=1000″,其含义就是每1000ms,打印输出一次`goroutine scheduler`的状态,每次一行.其他的状态如下: 192 | ```go 193 | SCHED 1007ms: gomaxprocs=4 idleprocs=4 threads=27 spinningthreads=0 idlethreads=5 runqueue=0 [0 0 0 0] 194 | 195 | SCHED: 调试信息输出标志字符串,代表本行是goroutine scheduler的输出. 196 | 1007ms: 即从程序启动到输出这行日志的时间. 197 | gomaxprocs: P的数量. 198 | idleprocs: 处于idle状态的P的数量;通过gomaxprocs和idleprocs的差值,我们就可知道执行go代码的P的数量. 199 | threads: os threads的数量,包含scheduler使用的m数量,加上runtime自用的类似sysmon这样的thread的数量. 200 | spinningthreads: 处于自旋状态的os thread数量. 201 | idlethread: 处于idle状态的os thread的数量. 202 | runqueue=0: go scheduler全局队列中G的数量. 203 | [0 0 0 0]: 分别为0个P的local queue中的G的数量. 204 | ``` 205 | #### Goroutine的闭包函数 206 | 207 | 闭包函数可以直接引用外层代码定义的变量,需要注意的是,在闭包函数里面引用的是变量的地址,当goroutine被调度时,改地址的值才会被传递给goroutine 函数。 208 | 209 | 使用匿名函数或闭包创建 goroutine 时,除了将函数定义部分写在 go 的后面之外,还需要加上匿名函数的调用参数,格式如下: 210 | 211 | ```go 212 | go func( 参数列表 ){ 213 | 函数体 214 | }( 调用参数列表 ) 215 | ``` 216 | 其中: 217 | 218 | * 参数列表:函数体内的参数变量列表。 219 | * 函数体:匿名函数的代码。 220 | * 调用参数列表:启动 goroutine 时,需要向匿名函数传递的调用参数。 221 | 222 | #### Goroutine的使用 223 | 224 | 设置goroutine运行的CPU数量,最新版本的go已经默认已经设置了。 225 | 226 | ```go 227 | num := runtime.NumCPU() //获取主机的逻辑CPU个数 228 | runtime.GOMAXPROCS(num) //设置可同时执行的最大CPU数 229 | ``` 230 | 应用示例: 231 | ```go 232 | package main 233 | 234 | import ( 235 | "fmt" 236 | "time" 237 | ) 238 | 239 | func count(a int , b int ) { 240 | c := a+b 241 | fmt.Printf("%d + %d = %d\n",a,b,c) 242 | } 243 | 244 | func main() { 245 | for i :=0 ; i<10 ;i++{ 246 | go count(i,i+1) //启动10个goroutine 来计算 247 | } 248 | time.Sleep(time.Second * 3) // sleep作用是为了等待所有任务完成 249 | } 250 | ``` 251 | 由于goroutine是异步执行的,那很有可能出现主程序退出时还有goroutine没有执行完,此时goroutine也会跟着退出。此时如果想等到所有goroutine任务执行完毕才退出,go提供了sync包和channel来解决同步问题,当然如果你能预测每个goroutine执行的时间,你还可以通过time.Sleep方式等待所有的groutine执行完成以后在退出程序。 252 | 253 | * 使用sync包同步goroutine 254 | 255 | sync实现方式是: 256 | 257 | WaitGroup 等待一组goroutinue执行完毕. 主程序调用 Add 添加等待的goroutinue数量. 每个goroutinue在执行结束时调用 Done ,此时等待队列数量减1.,主程序通过Wait阻塞,直到等待队列为0. 258 | 259 | 应用示例: 260 | ```go 261 | package main 262 | 263 | import ( 264 | "fmt" 265 | "sync" 266 | ) 267 | 268 | func count(a ,b int,n *sync.WaitGroup){ 269 | c := a +b 270 | fmt.Printf("The Result of %d + %d=%d\n",a,b,c) 271 | defer n.Done() //goroutinue完成后, WaitGroup的计数-1 272 | } 273 | 274 | 275 | func main(){ 276 | var wg sync.WaitGroup 277 | for i:=0;i<10 ;i++{ 278 | wg.Add(1) // WaitGroup的计数加1 279 | go count(i,i+1,&wg) 280 | } 281 | wg.Wait() //等待所有goroutine执行完毕 282 | } 283 | ``` 284 | 运行 285 | ```go 286 | The Result of 9 + 10=19 287 | The Result of 7 + 8=15 288 | The Result of 8 + 9=17 289 | The Result of 5 + 6=11 290 | The Result of 0 + 1=1 291 | The Result of 1 + 2=3 292 | The Result of 2 + 3=5 293 | The Result of 3 + 4=7 294 | The Result of 6 + 7=13 295 | The Result of 4 + 5=9 296 | ``` 297 | 通过channel实现goroutine之间的同步: 298 | 299 | 通过channel能在多个groutine之间通讯,当一个goroutine完成时候向channel发送退出信号,等所有goroutine退出时候,利用for循环channe去channel中的信号,若取不到数据会阻塞原理,等待所有goroutine执行完毕,使用该方法有个前提是你已经知道了你启动了多少个goroutine。 300 | 301 | ```go 302 | package main 303 | 304 | import ( 305 | "fmt" 306 | "time" 307 | ) 308 | 309 | func count(a int , b int ,exitChan chan bool) { 310 | c := a+b 311 | fmt.Printf("The Result of %d + %d = %d\n",a,b,c) 312 | time.Sleep(time.Second*2) 313 | exitChan <- true 314 | } 315 | 316 | func main() { 317 | 318 | exitChan := make(chan bool,10) //声明并分配管道内存 319 | for i :=0 ; i<10 ;i++{ 320 | go count(i,i+1,exitChan) 321 | } 322 | for j :=0; j<10; j++{ 323 | <- exitChan //取信号数据,如果取不到则会阻塞 324 | } 325 | close(exitChan) // 关闭管道 326 | } 327 | ``` 328 | 运行: 329 | 330 | ```go 331 | The Result of 9 + 10 = 19 332 | The Result of 0 + 1 = 1 333 | The Result of 7 + 8 = 15 334 | The Result of 6 + 7 = 13 335 | The Result of 3 + 4 = 7 336 | The Result of 4 + 5 = 9 337 | The Result of 1 + 2 = 3 338 | The Result of 2 + 3 = 5 339 | The Result of 8 + 9 = 17 340 | The Result of 5 + 6 = 11 341 | ``` 342 | -------------------------------------------------------------------------------- /src/chapter11/01.0.md: -------------------------------------------------------------------------------- 1 | ### For-learning-Go-Tutorial 2 | 3 | Go语言是谷歌2009发布的第二款开源编程语言。 4 | 5 | Go语言专门针对多处理器系统应用程序的编程进行了优化,使用Go编译的程序可以媲美C或C++代码的速度,而且更加安全、支持并行进程。 6 | 7 | 因而一直想的是自己可以根据自己学习和使用Go语言编程的心得,写一本Go的书可以帮助想要学习Go语言的初学者快速入门开发和使用! 8 | 9 | #### Golang包详解 10 | 11 | Golang的标准包有很多,但是很多人应该没有具体去看过的Golang标准包详细内容. 12 | 13 | * [archive]() 14 | * [tar--tar包实现了tar格式压缩文件的存取]() 15 | * [zip--zip包提供了zip档案文件的读写服务]() 16 | * [bufio--bufio包实现了带缓存的I/O操作](x) 17 | * [builtin-builtin包为Go的预声明标识符提供了文档]() 18 | * [bytes--bytes包实现了操作[]byte的常用函数](x) 19 | * [compress]() 20 | * [bzip2--bzip2包实现bzip2的解压缩]() 21 | * [flate--flate包实现了deflate压缩数据格式,参见RFC 1951]() 22 | * [gzip--gzip包实现了gzip格式压缩文件的读写,参见RFC 1952]() 23 | * [lzw--lzw包实现了Lempel-Ziv-Welch数据压缩格式]() 24 | * [zlib--zlib包实现了对zlib格式压缩数据的读写,参见RFC 1950]() 25 | * [container]() 26 | * [heap--heap包提供了对任意类型(实现了heap.Interface接口)的堆操作]() 27 | * [list--list包实现了双向链表]() 28 | * [ring--ring实现了环形链表的操作]() 29 | * [context--context包定义了上下文类型,它跨API边界和进程之间传送截止时间,取消信号和其他请求范围的值]() 30 | * [crypto--crypto包搜集了常用的密码(算法)常量.]() 31 | * [aes--aes包实现了AES加密算法.]() 32 | * [cipher--cipher包实现了多个标准的用于包装底层块加密算法的加密算法实现.]() 33 | * [des--des包实现了DES标准和TDEA算法.]() 34 | * [dsa--dsa包实现FIPS 186-3定义的数字签名算法.]() 35 | * [elliptic--elliptic包实现了几条覆盖素数有限域的标准椭圆曲线.]() 36 | * [hmac--hmac包实现了规定的HMAC加密哈希信息认证码.]() 37 | * [md5--md5包实现了MD5哈希算法.]() 38 | * [rand--rand包实现了用于加解密的更安全的随机数生成器.]() 39 | * [rc4--rc4包实现了RC4加密算法.]() 40 | * [rsa--rsa包实现了PKCS#1规定的RSA加密算法.]() 41 | * [sha1--sha1包实现了SHA1哈希算法,参见RFC 3174.]() 42 | * [sha256--sha256包实现了SHA224和SHA256哈希算法,参见FIPS 180-4]() 43 | * [sha512--sha512包实现了SHA384和SHA512哈希算法,参见FIPS 180-2](.) 44 | * [subtle--package subtle实现了在加密代码中常用的功能,但需要仔细考虑才能正确使用]() 45 | * [tls--tls包实现了TLS 1.2,细节参见RFC 5246]() 46 | * [x509--x509包解析X.509编码的证书和密钥]() 47 | * [pkix--pkix包提供了共享的、低层次的结构体,用于ASN.1解析和X.509证书、CRL、OCSP的序列化]() 48 | * [database]() 49 | * [sql--sql包提供了通用的SQL(或类SQL)数据库接口]() 50 | * [driver--driver包定义了应被数据库驱动实现的接口,这些接口会被sql包使用]() 51 | * [debug]() 52 | * [dwarf--提供了对从可执行文件加载的DWARF调试信息的访问]() 53 | * [elf--实现了对ELF对象文件的访问]() 54 | * [gosym--访问Go语言二进制程序中的调试信息]() 55 | * [macho-实现了Mach-O对象文件的访问]() 56 | * [macho-实现了Mach-O对象文件的访问]() 57 | * [pe-实现了对PE(Microsoft Windows Portable Executable)文件的访问]() 58 | * [plan9obj--plan9obj包实现了对Plan 9 a.out对象文件的访问]() 59 | * [encoding--encoding包定义了供其它包使用的可以将数据在字节水平和文本表示之间转换的接口]() 60 | * [ascii85--ascii85 包是对 ascii85 的数据编码的实现]() 61 | * [asn1--asn1包实现了DER编码的ASN.1数据结构的解析,参见ITU-T Rec X.690]() 62 | * [base32--base32包实现了RFC 4648规定的base32编码]() 63 | * [base64--base64实现了RFC 4648规定的base64编码]() 64 | * [binary--binary包实现了简单的数字与字节序列的转换以及变长值的编解码]() 65 | * [csv--csv读写逗号分隔值(csv)的文件]() 66 | * [gob--gob包管理gob流——在编码器(发送器)和解码器(接受器)之间交换的binary值]() 67 | * [hex--hex包实现了16进制字符表示的编解码]() 68 | * [json--json包实现了json对象的编解码,参见RFC 4627]() 69 | * [pem--pem包实现了PEM数据编码(源自保密增强邮件协议)]() 70 | * [xml--xml包实现了一个简单的XML 1.0解析器,它可以理解XML名称空间]() 71 | * [errors--error包实现了用于错误处理的函数]() 72 | * [expvar--expvar包提供了公共变量的标准接口,如服务的操作计数器]() 73 | * [flag--flag 包实现命令行标签解析]() 74 | * [fmt--fmt 包实现了格式化I/O函数,类似于C的 printf 和 scanf](x) 75 | * [go]() 76 | * [ast--ast包声明了用于展示Go包中的语法树类型]() 77 | * [build--build包提供了构建Go包的工具]() 78 | * [constant--constant包实现表示无类型Go常量及其相应操作的值]() 79 | * [doc--doc包从Go AST中提取源代码文档]() 80 | * [format--format包实现Go源的标准格式]() 81 | * [importer--importer包提供对导出数据导入程序的访问]() 82 | * [parser--parser包为Go源文件实现解析器]() 83 | * [printer--printer包实现AST节点的打印]() 84 | * [scanner--scanner包为Go源文本实现扫描程序]() 85 | * [token--token包表示Go语言的词法标记的常量和标记的基本操作]() 86 | * [types--types包声明数据类型并实现Go包类型检查的算法]() 87 | * [hash--hash包提供hash函数的接口]() 88 | * [adler32--adler32包实现了Adler-32校验和算法,参见RFC 1950]() 89 | * [crc32--crc32包实现了32位循环冗余校验(CRC-32)的校验和算法]() 90 | * [crc64--crc64包实现64位循环冗余校验或CRC-64校验]() 91 | * [fnv--fnv包实现了FNV-1和FNV-1a(非加密hash函数)]() 92 | * [html--html包提供了用于转义和解转义HTML文本的函数]() 93 | * [template--template包(html/template)实现了数据驱动的模板,用于生成可对抗代码注入的安全HTML输出]() 94 | * [image--image实现了基本的2D图片库]() 95 | * [color--color包实现了基本的颜色库]() 96 | * [palette--palette包提供了标准的调色板]() 97 | * [draw--draw包提供组装图片的方法]() 98 | * [gif--gif包实现了GIF图片的解码]() 99 | * [jpeg--jpeg包实现了jpeg格式图像的编解码]() 100 | * [png--png包实现了PNG图像的编码和解码]() 101 | * [index]() 102 | * [suffixarray--suffixarrayb包通过使用内存中的后缀树实现了对数级时间消耗的子字符串搜索]() 103 | * [io--io包为I/O原语提供了基础的接口](x) 104 | * [ioutil--ioutil包实现了一些I/O的工具函数](x) 105 | * [log--log包实现了简单的日志服务]() 106 | * [syslog--syslog包提供一个简单的系统日志服务的接口]() 107 | * [math](math--math包提供了基本常数和数学函数) 108 | * [big--big包实现了(大数的)高精度运算.]() 109 | * [cmplx--cmplx包为复数提供了基本的常量和数学函数]() 110 | * [rand--rand包实现了伪随机数生成器]() 111 | * [mime--mime实现了MIME的部分规定]() 112 | * [multipart--multipart实现了MIME的multipart解析,参见RFC 2046]() 113 | * [quotedprintable--quotedprintable包实现RFC 2045指定的quoted-printable编码]() 114 | * [net--net包提供了可移植的网络I/O接口,包括TCP/IP、UDP、域名解析和Unix域socket]() 115 | * [http--http包提供了HTTP客户端和服务端的实现]() 116 | * [cgi--cgi包实现了RFC3875协议描述的CGI(公共网关接口)]() 117 | * [cookiejar--cookiejar包实现了保管在内存中的符合RFC6265标准的http.CookieJar接口]() 118 | * [fcgi--fcgi包实现了FastCGI协议]() 119 | * [httptest--httptest包提供HTTP测试的单元工具]() 120 | * [httptrace--httptrace包提供了跟踪HTTP客户端请求中的事件的机制]() 121 | * [httputil--httputil包提供了HTTP公用函数,是对net/http包的更常见函数的补充]() 122 | * [pprof--pprof包通过提供HTTP服务返回runtime的统计数据,这个数据是以pprof可视化工具规定的返回格式返回的]() 123 | * [mail--mail包实现了解析邮件消息的功能]() 124 | * [rpc--rpc包提供了一个方法来通过网络或者其他的I/O连接进入对象的外部方法]() 125 | * [jsonrpc--jsonrpc包使用了rpc的包实现了一个JSON-RPC的客户端解码器和服务端的解码器]() 126 | * [smtp--smtp包实现了简单邮件传输协议(SMTP),参见RFC 5321]() 127 | * [textproto--textproto实现了对基于文本的请求/回复协议的一般性支持,包括HTTP、NNTP和SMTP]() 128 | * [url--url包解析URL并实现了查询的逸码,参见RFC 3986]() 129 | * [os--os包提供了操作系统函数的不依赖平台的接口]() 130 | * [exec--exec包执行外部命令]() 131 | * [signal--signal包实现了对输入信号的访问]() 132 | * [user--user包允许通过名称或ID查询用户帐户]() 133 | * [path--path包实现了对斜杠分隔的路径的实用操作函数]() 134 | * [filepath--filepath包实现了兼容各操作系统的文件路径的实用操作函数]() 135 | * [plugin--plugin包实现了Go插件的加载和符号解析]() 136 | * [reflect--reflect包实现了运行时反射,允许程序操作任意类型的对象](x) 137 | * [regexp--regexp包实现了正则表达式搜索]() 138 | * [syntax--syntax包将正则表达式解析为语法树]() 139 | * [runtime--runtime包含与Go的运行时系统进行交互的操作,例如用于控制Go程的函数]() 140 | * [cgo--cgo包含有cgo工具生成的代码的运行时支持]() 141 | * [debug--debug包含有程序在运行时调试其自身的功能]() 142 | * [pprof--pprof包按照可视化工具pprof所要求的格式写出运行时分析数据]() 143 | * [race--race包实现了数据竞争检测逻辑]() 144 | * [trace--trace包实现了Go执行追踪]() 145 | * [sort--sort包为切片及用户定义的集合的排序操作提供了原语]() 146 | * [strconv--strconv包实现了基本数据类型和其字符串表示的相互转换](x) 147 | * [strings--strings包完成对字符串的主要操作](x) 148 | * [sync--sync包提供了互斥锁这类的基本的同步原语]() 149 | * [atomic--atomic包提供了底层的原子性内存原语,这对于同步算法的实现很有用]() 150 | * [Map--线程安全的map]() 151 | * [Mutex--互斥锁]() 152 | * [Cond--条件变量]() 153 | * [Pool--临时对象池]() 154 | * [Once--执行一次]() 155 | * [RWMutex--读写操作的互斥锁]() 156 | * [waitGroup--用于等待一组goroutine 结束]() 157 | * [syscall--syscall包含低级操作系统原语的接口]() 158 | * [testing--testing包为Go的自动测试提供支持]() 159 | * [iotest--iotest包实现了主要用于读和写的测试]() 160 | * [quick--quick包实现实用程序功能,以帮助进行黑盒测试]() 161 | * [text]() 162 | * [scanner--scanner包提供对utf-8文本的token扫描服务]() 163 | * [tabwriter--tabwriter包实现了写入过滤器(tabwriter.Writer),可以将输入的缩进修正为正确的对齐文本]() 164 | * [template--template包实现了数据驱动的用于生成文本输出的模板]() 165 | * [parse--parse包为文本/模板和html/模板定义的模板构建解析树]() 166 | * [time--time包提供了时间的显示和测量用的函数]() 167 | * [unicode--unicode 包提供了一些测试Unicode码点属性的数据和函数]() 168 | * [utf16--utf16包实现了对UTF-16序列的编码和解码]() 169 | * [utf8--utf8包实现支持UTF-8文本编码的函数和常量]() 170 | * [unsafe--unsafe包含有关于Go程序类型安全的所有操作]() 171 | -------------------------------------------------------------------------------- /src/chapter12/01.0.md: -------------------------------------------------------------------------------- 1 | ### For-learning-Go-Tutorial 2 | 3 | Go语言是谷歌2009发布的第二款开源编程语言。 4 | 5 | Go语言专门针对多处理器系统应用程序的编程进行了优化,使用Go编译的程序可以媲美C或C++代码的速度,而且更加安全、支持并行进程。 6 | 7 | 因而一直想的是自己可以根据自己学习和使用Go语言编程的心得,写一本Go的书可以帮助想要学习Go语言的初学者快速入门开发和使用! 8 | 9 | #### Grpc与Protobuf 10 | 11 | ```markdown 12 | A high performance, open source, general RPC framework that puts mobile and HTTP/2 first. 13 | ``` 14 | Grpc 是一个高性能、开源、通用的RPC框架,由Google推出,基于HTTP/2协议标准设计开发,默认采用Protocol Buffers数据序列化协议,支持多种开发语言。gRPC提供了一种简单的方法来精确的定义服务,并且为客户端和服务端自动生成可靠的功能库。 15 | 16 | Grpc主要特点: 17 | 18 | * Grpc使用ProtoBuf来定义服务,ProtoBuf是由Google开发的一种数据序列化协议(类似于XML、JSON、hessian)。ProtoBuf能够将数据进行序列化,并广泛应用在数据存储、通信协议等方面。 19 | 20 | * Grpc支持多种语言,并能够基于语言自动生成客户端和服务端功能库。目前已提供了C版本grpc、Java版本`grpc-java` 和 Go版本`grpc-go`,其它语言的版本正在积极开发中,其中,grpc支持C、C++、Node.js、Python、Ruby、Objective-C、PHP和C#等语言,`grpc-java`已经支持Android开发。 21 | 22 | * Grpc基于HTTP/2标准设计,所以相对于其他RPC框架,Grpc带来了更多强大功能,如双向流、头部压缩、多复用请求等。这些功能给移动设备带来重大益处,如节省带宽、降低TCP链接次数、节省CPU使用和延长电池寿命等。同时,Grpc还能够提高了云端服务和Web应用的性能。Grpc既能够在客户端应用,也能够在服务器端应用,从而以透明的方式实现客户端和服务器端的通信和简化通信系统的构建。 23 | 24 | 在Grpc客户端中可以直接调用不同服务器上的远程程序,使用姿势看起来就像调用本地程序一样,很容易去构建分布式应用和服务。和很多RPC系统一样,服务端负责实现定义好的接口并处理客户端的请求,客户端根据接口描述直接调用需要的服务。客户端和服务端可以分别使用Grpc支持的不同语言实现。 25 | 26 | 然而编译Grpc需要Protobufer编译器,那么Protobufer又是什么呢? 27 | 28 |

29 | 30 |

31 | 32 | Google Protocol Buffer( 简称 Protobuf) 是 Google 公司内部的混合语言数据标准,目前已经正在使用的有超过 48,162 种报文格式定义和超过 12,183 个 `.proto` 文件。他们用于 RPC 系统和持续数据存储系统。 33 | 34 | `Protocol buffers` 在序列化数据方面,它是灵活的,高效是一种轻便高效的结构化数据存储格式。相比较于 XML 来说,Protocol buffers 更加小巧,简单。当你定义了要处理的数据的数据结构之后,就可以利用 `Protocol buffers` 的代码生成工具生成相关的代码。 35 | 36 | 甚至你也可以在无需重新部署程序的情况下更新数据结构。只需使用 Protobuf 对数据结构进行一次描述,即可利用各种不同语言或从各种不同数据流中对你的结构化数据轻松读写。`Protocol buffers`可以用于结构化数据串行化,或者说序列化。它很适合做数据存储或 RPC 数据交换格式。可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。目前提供了 C++、Java、Python 三种语言的 API。 37 | 38 | protocol buffers的特性: 39 | 40 | * 自动生成的序列化和反序列化代码避免了手动解析。 41 | * 除用于 RPC(远程过程调用)请求之外,现在开始将protocol buffers 用作持久存储数据的便捷自描述格式(例如,在Bigtable中)。 42 | * 服务器的 RPC 接口可以先声明为协议的一部分,然后用 protocol compiler 生成基类,用户可以使用服务器接口的实际实现来覆盖它们。 43 | 44 | #### Protobuf 安装 45 | 46 | 首先我们可以从[Protobuf](https://github.com/google/protobuf/releases)获取编译器 protoc. 47 | ```bash 48 | > wget https://github.com/google/protobuf/releases/download/v2.6.1/protobuf-2.6.1.tar.gztar zxvf protobuf-2.6.1.tar.gz 49 | > cd protobuf-2.6.1./configure 50 | > make 51 | > make install 52 | > protoc -h 53 | ``` 54 | 然后我们需要获取goprotobuf 提供的 Protobuf 插件 protoc-gen-go(被放置于$GOPATH/bin 下,$GOPATH/bin 应该被加入PATH环境变量,以便 protoc 能够找到 protoc-gen-go) 55 | 56 | 此插件被 protoc 使用,用于编译 .proto 文件为Go源文件,通过此源文件可以使用定义在 .proto 文件中的消息。 57 | ```bash 58 | > go get github.com/golang/protobuf/protoc-gen-go 59 | 60 | > cd github.com/golang/protobuf/protoc-gen-go 61 | 62 | > go build 63 | 64 | > go install 65 | 66 | > vim ~/.bashrc 67 | 将$GOPATH/bin 加入环境变量 68 | 69 | > source ~/.bashrc 70 | ``` 71 | 最后我们需要获取 goprotobuf提供的支持库,包含诸如编码(marshaling)、解码(unmarshaling)等功能: 72 | 73 | ```bash 74 | > go get github.com/golang/protobuf/proto 75 | > cd github.com/golang/protobuf/proto 76 | > go build 77 | > go install 78 | ``` 79 | 80 | #### proto3 定义 message 81 | 82 | 上面我们知道了protobuf怎么安装的,接着我们需要了解下protobuf的语法规则是什么 83 | 84 | Protobuf 语法定义: 85 | 86 | 要想使用protobuf必须得先定义proto文件。所以得先熟悉protobuf的消息定义的相关语法。下面就来介绍 87 | 首先我们先定义一个proto文件,结构如下: 88 | 89 | ```protobufer 90 | syntax = "proto3"; 91 | 92 | package pb; 93 | 94 | service NewService { 95 | string msn = 1; 96 | string streamKey = 2; 97 | } 98 | ``` 99 | 100 | * 文件的第一行指定了你正在使用proto3语法:如果你没有指定这个,编译器会使用proto2。这个指定语法行必须是文件的非空非注释的第一个行。 101 | * NewService消息格式有2个字段,在消息中承载的数据分别对应于每一个字段。其中每个字段都有一个名字和一种类型。 102 | 103 | #### 分配字段编号 104 | 105 | 在NewService所有字段都是标量类型:两个string类型 msn 和 streamKey。当然,你也可以为字段指定其他的合成类型,包括枚举(enumerations)或其他消息类型。 106 | 正如消息中的结构每个消息定义中的每个字段都有唯一的编号。这些字段编号用于标识消息二进制格式中的字段,并且在使用消息类型后不应更改。这里需要注意下,范围 1 到 15 中的字段编号需要一个字节进行编码,包括字段编号和字段类型.范围 16 至 2047 中的字段编号需要两个字节。所以你应该保留数字 1 到 15 作为非常频繁出现的消息元素。请记住为将来可能添加的频繁出现的元素留出一些空间。 107 | 108 | 最小的标识号可以从1开始,最大到`2^29 - 1, or 536,870,911`。不可以使用其中的[19000-19999]( (从FieldDescriptor::kFirstReservedNumber 到 FieldDescriptor::kLastReservedNumber))的标识号, Protobuf协议实现中对这些进行了预留。如果非要在.proto文件中使用这些预留标识号,编译时就会报警。同样你也不能使用早期保留的标识号。 109 | 110 | #### 指定字段规则 111 | 112 | 所指定的消息字段修饰符必须是如下之一: 113 | 114 | * singular:一个格式良好的消息应该有0个或者1个这种字段(但是不能超过1个). 115 | * repeated:在一个格式良好的消息中,这种字段可以重复任意多次(包括0次)。重复的值的顺序会被保留。 116 | 117 | 在proto3中,repeated的标量域默认情况下可以使用packed. 118 | 119 | 如果想要了解更多可以查看[Protocol Buffer编码原理](https://developers.google.com/protocol-buffers/docs/encoding?hl=zh-cn#packed). 120 | 121 | #### 保留标识符(Reserved) 122 | 123 | 如果你通过删除或者注释所有域,以后的用户可以重用标识号当你重新更新类型的时候。如果你使用旧版本加载相同的.proto文件这会导致严重的问题,包括数据损坏、隐私错误等等。现在有一种确保不会发生这种情况的方法就是指定保留标识符(and/or names, which can also cause issues for JSON serialization不明白什么意思),protocol buffer的编译器会警告未来尝试使用这些域标识符的用户。 124 | 125 | ```go 126 | message Foo { 127 | reserved 2, 7, 9 to 11; 128 | reserved "foo", "bar"; 129 | } 130 | ``` 131 | 注意:不要在同一行reserved声明中同时声明域名字和标识号 132 | 133 | #### .proto文件生成内容 134 | 135 | 当用protocol buffer编译器来运行.proto文件时,编译器将生成所选择语言的代码,这些代码可以操作在.proto文件中定义的消息类型,包括获取、设置字段值,将消息序列化到一个输出流中,以及从一个输入流中解析消息。 136 | 137 | * 对go来说,编译器会位每个消息类型生成了一个`.pd.go`文件。 138 | * 对C++来说,编译器会为每个`.proto`文件生成一个`.h`文件和一个`.cc`文件,`.proto`文件中的每一个消息有一个对应的类。 139 | * 对Java来说,编译器为每一个消息类型生成了一个`.java`文件,以及一个特殊的Builder类(该类是用来创建消息类接口的)。 140 | * 对Python来说,有点不太一样——Python编译器为`.proto`文件中的每个消息类型生成一个含有静态描述符的模块,,该模块与一个元类(metaclass)在运行时(runtime)被用来创建所需的Python数据访问类。 141 | * 对于Ruby来说,编译器会为每个消息类型生成了一个`.rb`文件。 142 | * 对于Objective-C来说,编译器会为每个消息类型生成了一个`pbobjc.h`文件和pbobjcm文件,`.proto`文件中的每一个消息有一个对应的类。 143 | * 对于C#来说,编译器会为每个消息类型生成了一个`.cs`文件,`.proto`文件中的每一个消息有一个对应的类。 144 | 145 | #### 各个语言标量类型对应关系 146 | 147 |

148 | 149 |

150 | 151 | 如果需要了解更多可以查看[Protocol Buffer编码原理](https://developers.google.com/protocol-buffers/docs/encoding?hl=zh-cn#packed). 了 152 | 153 | #### 消息解析默认值 154 | 155 | 当一个消息被解析的时候,如果被编码的信息不包含一个特定的singular元素,被解析的对象锁对应的域被设置位一个默认值,对于不同类型指定如下: 156 | * 对于strings,默认是一个空string. 157 | * 对于bytes,默认是一个空的bytes. 158 | * 对于bools,默认是false. 159 | * 对于数值类型,默认是0. 160 | * 对于枚举,默认是第一个定义的枚举值,必须为0. 161 | * 对于消息类型(message),域没有被设置,确切的消息是根据语言确定的,更详细的可以查看[API Reference](https://developers.google.com/protocol-buffers/docs/reference/overview?hl=zh-cn). 162 | 163 | 对于可重复域的默认值是空(通常情况下是对应语言中空列表)。 164 | 165 | 注意:对于标量消息域,一旦消息被解析,就无法判断域释放被设置为默认值(例如,例如boolean值是否被设置为false)还是根本没有被设置。你应该在定义你的消息类型时非常注意。例如,比如你不应该定义boolean的默认值false作为任何行为的触发方式。也应该注意如果一个标量消息域被设置为标志位,这个值不应该被序列化传输。 166 | 167 | #### 枚举 168 | 169 | 当需要定义一个消息类型的时候,可能想为一个字段指定某“预定义值序列”中的一个值。在message消息定义中添加一个枚举(enum)并且为每个可能的值定义一个常量就可以了。 170 | ```go 171 | message NewService { 172 | string msn = 1; 173 | string streamKey = 2; 174 | enum Corpus { 175 | UNIVERSAL = 0; 176 | WEB = 1; 177 | IMAGES = 2; 178 | LOCAL = 3; 179 | NEWS = 4; 180 | PRODUCTS = 5; 181 | VIDEO = 6; 182 | } 183 | Corpus corpus = 4; 184 | } 185 | ``` 186 | Corpus枚举的第一个常量映射为0:每个枚举类型必须将其第一个类型映射为0,这是因为: 187 | * 枚举为 0 的是作为零值,当不赋值的时候,就会是零值。 188 | * 这个零值必须为第一个元素,为了兼容proto2语义,枚举类的第一个值总是默认值. 189 | 190 | 枚举常量必须在32位整型值的范围内。因为enum值是使用可变编码方式的,对负数不够高效,因此不推荐在enum中使用负数。如上例所示,可以在 一个消息定义的内部或外部定义枚举——这些枚举可以在.proto文件中的任何消息定义里重用。当然也可以在一个消息中声明一个枚举类型,而在另一个不同 的消息中使用它——采用MessageType.EnumType的语法格式。 191 | 192 | 当对一个使用了枚举的.proto文件运行protocol buffer编译器的时候,生成的代码中将有一个对应的enum(对Java或C++来说),或者一个特殊的EnumDescriptor类(对 Python来说),它被用来在运行时生成的类中创建一系列的整型值符号常量(symbolic constants)。 193 | 194 | 除此之外在反序列化的过程中,无法被识别的枚举值,将会被保留在 messaage消息中。因为消息反序列化时如何表示是依赖于语言的。在支持指定符号范围之外的值的开放枚举类型的语言中,例如 C++ 和 Go,未知的枚举值只是存储为其基础整数表示。在诸如 Java 之类的封闭枚举类型的语言中,枚举值会被用来标识未识别的值,并且特殊的访问器可以访问到底层整数。 195 | 196 | 在其他情况下,如果消息被序列化,则无法识别的值仍将与消息一起序列化。 197 | 198 | #### 使用其他消息类型 199 | 200 | 我们可以将其他消息类型用作字段类型。例如我们ServiceResponse消息中包含Result消息,此时可以在相同的.proto文件中定义一个Result消息类型,然后在ServiceResponse消息中指定一个Result类型的字段,如: 201 | ```go 202 | message ServiceResponse { 203 | repeated Result results = 1; 204 | } 205 | 206 | message Result { 207 | string name = 1; 208 | string streamKey = 2; 209 | repeated string publishURL = 3; 210 | } 211 | ``` 212 | #### 导入定义proto文件 213 | 214 | Result消息类型与ServiceResponse是定义在同一文件中的。如果想要使用的消息类型已经在其他.proto文件中已经定义过了呢? 215 | 我们也可以通过导入(importing)其他.proto文件中的定义来使用它们。要导入其他.proto文件的定义,你需要在你的文件中添加一个导入声明,如: 216 | ```go 217 | import "keke/service.proto"; 218 | ``` 219 | 默认情况下你只能使用直接导入的.proto文件中的定义. 然而,有时候你需要移动一个.proto文件到一个新的位置, 可以不直接移动.proto文件, 只需放入一个伪 .proto 文件在老的位置, 然后使用import public转向新的位置。import public 依赖性会通过任意导入包含import public声明的proto文件传递。例如: 220 | ```go 221 | import public "elegance.proto"; 222 | ``` 223 | ```go 224 | import "old.proto"; 225 | ``` 226 | 编译可以通过下面的命令: 227 | ```bash 228 | > protoc elegance.proto --go_out=plugins=grpc:. 229 | ``` 230 | 如果没有给出标志,编译器会搜索编译命令被调用的目录。通常你只要指定proto_path标志为你的工程根目录就好。并且指定好导入的正确名称就好。 231 | 232 | #### 允许嵌套 233 | 234 | Protocol Buffers 在定义 message时候允许嵌套组合成更加复杂的消息: 235 | ```go 236 | message ServiceResponse { 237 | message Result { 238 | string name = 1; 239 | string streamKey = 2; 240 | repeated string publishURL = 3;; 241 | } 242 | repeated Result results = 1; 243 | } 244 | ``` 245 | 如果你想在它的父消息类型的外部重用这个消息类型,你需要以Parent.Type的形式使用它,如: 246 | ```go 247 | message SomeOtherMessage { 248 | ServiceResponse.Result result = 1; 249 | } 250 | ``` 251 | 当然,你也可以将消息嵌套任意多层,如: 252 | ```go 253 | message Outer { // Level 0 254 | message MiddleA { // Level 1 255 | message Inner { // Level 2 256 | int64 ival = 1; 257 | bool booly = 2; 258 | } 259 | } 260 | message MiddleB { // Level 1 261 | message Inner { // Level 2 262 | int32 ival = 1; 263 | bool booly = 2; 264 | } 265 | } 266 | } 267 | ``` 268 | #### 更新一个消息类型 269 | 270 | 如果后面发现之前定义 message需要增加新的字段了,这个时候就体现出 Protocol Buffer 的优势了,更新消息而不破坏已有代码是非常简单的,不需要改动之前的代码。不过需要满足以下 10 条规则: 271 | 1. 不要改动原有字段的数据结构。 272 | 273 | 2. 如果你增加新的字段,使用旧格式的字段仍然可以被你新产生的代码所解析。你应该记住这些元素的默认值这样你的新代码就可以以适当的方式和旧代码产生的数据交互。相似的,通过新代码产生的消息也可以被旧代码解析:只不过新的字段会被忽视掉。注意,未被识别的字段会在反序列化的过程中丢弃掉,所以如果消息再被传递给新的代码,新的字段依然是不可用的(这和proto2中的行为是不同的,在proto2中未定义的域依然会随着消息被序列化). 274 | 275 | 3. 非required的字段可以移除——只要它们的标识号在新的消息类型中不再使用(更好的做法可能是重命名那个字段,例如在字段前添加“OBSOLETE_”前缀,那样的话,使用的.proto文件的用户将来就不会无意中重新使用了那些不该使用的标识号). 276 | 277 | 4. int32, uint32, int64, uint64,和bool是全部兼容的,这意味着可以将这些类型中的一个转换为另外一个,而不会破坏向前、 向后的兼容性。如果解析出来的数字与对应的类型不相符,那么结果就像在C++中对它进行了强制类型转换一样(例如,如果把一个64位数字当作int32来 读取,那么它就会被截断为32位的数字)。 278 | 279 | 5. sint32和sint64是互相兼容的,但是它们与其他整数类型不兼容。 280 | 281 | 6. string和bytes是兼容的——只要bytes是有效的UTF-8编码。 282 | 283 | 7.嵌套消息与bytes是兼容的——只要bytes包含该消息的一个编码过的版本。 284 | 285 | 8. fixed32与sfixed32兼容,而fixed64与sfixed64兼容。 286 | 287 | 9. enum 就数组而言,是可以与 int32,uint32,int64 和 uint64 兼容(请注意,如果它们不适合,值将被截断)。但是请注意,当消息反序列化时,客户端代码可能会以不同的方式对待它们:例如,未识别的 proto3 枚举类型将保留在消息中,但消息反序列化时如何表示是与语言相关的。(这点和语言相关,上面提到过了)Int 域始终只保留它们的值。 288 | 289 | 10. 将单个值更改为新的成员是安全和二进制兼容的。如果你确定一次没有代码设置多个字段,则将多个字段移至新的字段可能是安全的。将任何字段移到现有字段中都是不安全的。(注意字段和值的区别,字段是 field,值是 value) 290 | 291 | 10. 枚举类型与int32,uint32,int64和uint64相兼容(注意如果值不相兼容则会被截断),然而在客户端反序列化之后他们可能会有不同的处理方式,例如,未识别的proto3枚举类型会被保留在消息中,但是他的表示方式会依照语言而定。int类型的字段总会保留他们的. 292 | 293 | #### 未知字段 294 | 295 | 未知数字段是 protocol buffers 序列化的数据,表示解析器无法识别的字段。例如,当一个旧的二进制文件解析由新的二进制文件发送的新数据的数据时,这些新的字段将成为旧的二进制文件中的未知字段。 296 | 297 | 在Proto3 中可以成功解析未知字段的消息,但是,实现可能会或可能不会支持保留这些未知字段。因此你不应该依赖保存或删除未知域。对于大多数 Google protocol buffers 实现,未知字段在 proto3 中无法通过相应的 proto 运行时访问,并且在反序列化时被丢弃和遗忘。这是与 proto2 的不同行为,其中未知字段总是与消息一起保存并序列化。 298 | 299 | #### Map 300 | 301 | 如果你希望创建一个关联Map映射,protocol buffer提供了一种便捷的语法: 302 | ```go 303 | map map_field = N; 304 | ``` 305 | 306 | 其中key_type可以是任意Integer或者string类型(所以,除了floating和bytes的任意标量类型都是可以的)value_type可以是任意类型。 307 | 308 | 例如,如果你希望创建一个project的映射,每个Projecct使用一个string作为key,你可以像下面这样定义: 309 | ```go 310 | map projects = 3; 311 | ``` 312 | * Map的字段可以是repeated. 313 | * 序列化后的顺序和map迭代器的顺序是不确定的,所以你不要期望以固定顺序处理Map. 314 | * 当为.proto文件产生生成文本格式的时候,map会按照key 的顺序排序,数值化的key会按照数值排序. 315 | * 从序列化中解析或者融合时,如果有重复的key则后一个key不会被使用,当从文本格式中解析map时,如果存在重复的key. 316 | 317 | 这里成map的API现在对于所有proto3支持的语言都可用,更详细的可以查看[API Reference](https://developers.google.com/protocol-buffers/docs/reference/overview?hl=zh-cn). 318 | 319 | #### 向后兼容性问题 320 | map语法序列化后等同于如下内容,因此即使是不支持map语法的protocol buffer实现也是可以处理你的数据的: 321 | ```go 322 | message MapFieldEntry { 323 | key_type key = 1; 324 | value_type value = 2; 325 | } 326 | 327 | repeated MapFieldEntry map_field = N; 328 | ``` 329 | #### 包及名称的解析 330 | 为了防止不同的消息类型有命名冲突我们也可以为.proto文件新增一个可选的package声明符。如 331 | ```go 332 | package config; 333 | message Open { ... } 334 | ``` 335 | 在其他的消息格式定义中也可以使用包名+消息名的方式来定义域的类型,如: 336 | ```go 337 | message Foo { 338 | ... 339 | required foo.bar.Open open = 1; 340 | ... 341 | } 342 | ``` 343 | 包的声明符会根据使用语言的不同影响生成的代码: 344 | * 对于C++,产生的类会被包装在C++的命名空间中,如上例中的Open会被封装在 foo::bar空间中; - 对于Java,包声明符会变为java的一个包,除非在.proto文件中提供了一个明确有java_package; 345 | * 对于 Python,这个包声明符是被忽略的,因为Python模块是按照其在文件系统中的位置进行组织的。 346 | * 对于Go,包可以被用做Go包名称,除非你显式的提供一个option go_package在你的.proto文件中。 347 | * 对于Ruby,生成的类可以被包装在内置的Ruby名称空间中,转换成Ruby所需的大小写样式 (首字母大写;如果第一个符号不是一个字母,则使用PB_前缀),例如Open会在Foo::Bar名称空间中。 348 | * 对于javaNano包会使用Java包,除非你在你的文件中显式的提供一个option java_package。 349 | * 对于C#包可以转换为PascalCase后作为名称空间,除非你在你的文件中显式的提供一个option csharp_namespace,例如,Open会在Foo.Bar名称空间中. 350 | 351 | Protocol buffer语言中类型名称的解析:首先从最内部开始查找,依次向外进行,每个包会被看作是其父类包的内部类。 352 | 353 | ProtocolBuffer编译器会解析.proto文件中定义的所有类型名。 对于不同语言的代码生成器会知道如何来指向每个具体的类型,即使它们使用了不同的规则。 354 | 355 | #### 定义服务(Service) 356 | 357 | 如果我们想要将消息类型用在RPC(远程方法调用)系统中,可以在.proto文件中定义一个RPC服务接口,protocol buffer编译器将会根据所选择的不同语言生成服务接口代码及存根。如,想要定义一个RPC服务并具有一个方法,该方法能够接收 NewRequest并返回一个NewResponse,此时可以在.proto文件中进行如下定义: 358 | ```go 359 | service NewService { 360 | rpc Search (NewRequest) returns (NewRequest); 361 | } 362 | ``` 363 | 364 | 对于使用protocol buffer的RPC框架就是Grpc,Grpc在使用protocl buffer时非常有效,如果使用特殊的protocol buffer插件可以直接从.proto文件中产生相关的RPC代码。 365 | 366 | #### JSON 映射 367 | 368 | Proto3 支持 JSON 中的规范编码,使系统之间共享数据变得更加容易。编码在下表中按类型逐个描述。 369 | 370 | 如果 JSON 编码数据中缺少值或其值为空,则在解析为 protocol buffer 时,它将被解释为适当的默认值。如果一个字段在协议缓冲区中具有默认值,默认情况下它将在 JSON 编码数据中省略以节省空间。具体 Mapping 的实现可以提供选项决定是否在 JSON 编码的输出中发送具有默认值的字段。 371 | 372 |

373 | 374 |

375 | 376 | #### Protocol Buffer命名规范 377 | 378 | Protocol Buffer的消息体定义采用驼峰命名法: 379 | ```go 380 | message newService { 381 | required string name = 1; 382 | } 383 | ``` 384 | 枚举类型采用驼峰命名法。枚举类型首字母大写开头。每个枚举值全部大写,并且采用下划线分隔法命名。 385 | ```go 386 | enum Foo { 387 | FIRST_VALUE = 0; 388 | SECOND_VALUE = 1; 389 | } 390 | ``` 391 | 消息体中每行用分号结束,不是逗号. 392 | 393 | 消息体中服务名和方法名都采用驼峰命名法。并且首字母都大写开头. 394 | ```go 395 | service NewService { 396 | rpc Search (NewRequest) returns (NewRequest); 397 | } 398 | ``` 399 | -------------------------------------------------------------------------------- /src/chapter13/01.0.md: -------------------------------------------------------------------------------- 1 | ### For-learning-Go-Tutorial 2 | 3 | Go语言是谷歌2009发布的第二款开源编程语言。 4 | 5 | Go语言专门针对多处理器系统应用程序的编程进行了优化,使用Go编译的程序可以媲美C或C++代码的速度,而且更加安全、支持并行进程。 6 | 7 | 因而一直想的是自己可以根据自己学习和使用Go语言编程的心得,写一本Go的书可以帮助想要学习Go语言的初学者快速入门开发和使用! 8 | 9 | #### Golang逃逸分析 10 | 11 | 通常在编译代码的时候,编译器根据分析,判断将变量分配在栈或堆上。函数定义中,一般将局部变量和参数分配到栈上(stack frame)上。但是,如果编译器不能确定在函数返回(return)时,变量是否被引用(reference)分配到堆上;如果局部变量非常大,也应分配在堆上。 12 | 13 | 如果对变量取地址(*和&操作),则有可能分配在堆上。此外,还需要进行逃逸分析(escape analytic),判断return后变量是否被引用,不引用分配到栈上,引用分配到堆上。 14 | 15 | 在golang中逃逸分析是一种确定指针动态范围的方法,可以分析在程序的哪些地方可以访问到指针。它涉及到指针分析和形状分析。当一个变量(或对象)在子程序中被分配时,一个指向变量的指针可能逃逸到其它执行线程中,或者去调用子程序。如果使用尾递归优化(通常在函数编程语言中是需要的),对象也可能逃逸到被调用的子程序中。 16 | 17 | 如果一个子程序分配一个对象并返回一个该对象的指针,该对象可能在程序中的任何一个地方被访问到——这样指针就成功“逃逸”了。如果指针存储在全局变量或者其它数据结构中,它们也可能发生逃逸,这种情况是当前程序中的指针逃逸。 逃逸分析需要确定指针所有可以存储的地方,保证指针的生命周期只在当前进程或线程中。 18 | 19 | 但是golang 编译器决定变量应该分配到什么地方时会进行逃逸分析,下面我们看段代码: 20 | 21 | ```go 22 | package main 23 | 24 | import () 25 | 26 | func foo() *int { 27 | var x int 28 | return &x 29 | } 30 | 31 | func bar() int { 32 | x := new(int) 33 | *x = 1 34 | return *x 35 | } 36 | 37 | func main() {} 38 | ``` 39 | 40 | 运行后: 41 | 42 | ```go 43 | > go run -gcflags '-m -l' escape.go 44 | ./main.go:6: moved to heap: x 45 | ./main.go:7: &x escape to heap 46 | ./main.go:11: bar new(int) does not escape 47 | ``` 48 | 49 | foo() 中的 x 最后在堆上分配,而 bar() 中的 x 最后分配在了栈上。在官网 (golang.org) FAQ 上有一个关于变量分配的问题如下: 50 | 51 | From a correctness standpoint, you don’t need to know. Each variable in Go exists as long as there are references to it. 52 | The storage location chosen by the implementation is irrelevant to the semantics of the language. 53 | 54 | The storage location does have an effect on writing efficient programs. When possible, the Go compilers will allocate variables that are local to a function in that function’s stack frame. 55 | 56 | However, if the compiler cannot prove that the variable is not referenced after the function returns, 57 | then the compiler must allocate the variable on the garbage-collected heap to avoid dangling pointer errors. 58 | Also, if a local variable is very large, it might make more sense to store it on the heap rather than the stack. 59 | 60 | In the current compilers, if a variable has its address taken, that variable is a candidate for allocation on the heap. 61 | However, a basic escape analysis recognizes some cases when such variables will not live past the return from the function and can reside on the stack. 62 | 63 | 翻译过来就是: 64 | 65 | 如何得知变量是分配在栈(stack)上还是堆(heap)上? 66 | 67 | 准确地说,你并不需要知道。Golang 中的变量只要被引用就一直会存活,存储在堆上还是栈上由内部实现决定而和具体的语法没有关系。 68 | 69 | 知道变量的存储位置确实和效率编程有关系。如果可能,Golang 编译器会将函数的局部变量分配到函数栈帧(stack frame)上。然而,如果编译器不能确保变量在函数 return之后不再被引用,编译器就会将变量分配到堆上。而且,如果一个局部变量非常大,那么它也应该被分配到堆上而不是栈上。 70 | 71 | 当前情况下,如果一个变量被取地址,那么它就有可能被分配到堆上。然而,还要对这些变量做逃逸分析,如果函数return之后,变量不再被引用,则将其分配到栈上。 72 | 73 | 其实在golang中所有静态内存的其实分配都是在 stack 上进行的,而函数体在执行结束出栈后所有在栈上分配的内存都将得到释放,如果此时直接返回当前作用域变量的指针,这在下层函数的寻址行为就会因为出栈的内存释放而造成空指针异常。这个时候我们就得需要用到malloc在堆上(heap)动态分配内存,自己管理内存的生命周期,自己手动释放才是安全的方式。 74 | 75 | 然而`escape analysis`的存在让go完美规避了这些问题,编译器在编译时对代码做了分析,如果发现当前作用域的变量没有超出函数范围,则会自动在stack上分配,如果找不到了,则会在heap上分配。这样其实开发者就不用太关心堆栈的使用边界,在代码层面上完全不需要关心内存的分配,把底层要考虑的问题交给编译器,同时也减小了gc回收的压力。 76 | 77 | go在一定程度消除了堆和栈的区别,因为go在编译的时候进行逃逸分析,来决定一个对象放栈上还是放堆上,不逃逸的对象放栈上,可能逃逸的放堆上。 78 | 79 | 那么逃逸分析的作用是什么呢? 80 | 81 | 1.逃逸分析的好处是为了减少gc的压力,不逃逸的对象分配在栈上,当函数返回时就回收了资源,不需要gc标记清除。 82 | 83 | 2.逃逸分析完后可以确定哪些变量可以分配在栈上,栈的分配比堆快,性能好(逃逸的局部变量会在堆上分配 ,而没有发生逃逸的则有编译器在栈上分配)。 84 | 85 | 3.同步消除,如果你定义的对象的方法上有同步锁,但在运行时,却只有一个线程在访问,此时逃逸分析后的机器码,会去掉同步锁运行。 86 | 87 | 我们在开发的时候其实也可以自己去设置和查看逃逸分析的log,我们可以分析逃逸日志,只要在编译的时候加上-gcflags '-m',但是我们为了不让编译时自动内连函数,一般会加-l参数,最终为-gcflags '-m -l'.在main中可以用: 88 | 89 | ```go 90 | go run -gcflags '-m -l' main.go 91 | ``` 92 | 93 | 那么接着就会有个问题什么时候会逃逸,什么时候不会逃逸呢? 94 | 95 | 接下来我们看个例子: 96 | 97 | ```go 98 | package main 99 | 100 | type Elegance struct{} 101 | 102 | func main() { 103 | var e Elegance 104 | p := &e 105 | _ = *identity(y) 106 | } 107 | 108 | func identity(m *Elegance) *Elegance { 109 | return m 110 | } 111 | ``` 112 | 运行后: 113 | ```go 114 | main.go:11: leaking param: m to result ~r1 level=0 115 | main.go:7: main &e does not escape 116 | ``` 117 | 在这里的m变量是“流式”,因为identity这个函数仅仅输入一个变量,又将这个变量作为返回输出,但identity并没有引用m,所以这个变量没有逃逸,而e没有被引用,且生命周期也在mian里,e没有逃逸,分配在栈上。 118 | 119 | 通常在go中函数都是运行在栈上的,在栈声明临时变量分配内存,函数运行完毕在回收该段栈空间,并且每个函数的栈空间都是独立的,不能被访问到的。但是在某些情况下,栈上的空间需要在该函数被释放后依旧能访问到,这时候就涉及到内存的逃逸了: 120 | 121 | ```go 122 | type data struct { 123 | name string 124 | } 125 | 126 | func patent1()data{ 127 | p := data{"keke"} 128 | return p 129 | } 130 | 131 | func patent2() *data { 132 | p := data{"jame"} 133 | return &p 134 | } 135 | func main(){ 136 | p1 := patent1() 137 | p2 := patent2() 138 | } 139 | ``` 140 | 141 | 这里的patent1和patent2函数都有返回值,唯一不同的地方是patent1返回data结构体,patent2返回data结构体指针。在大多数语言例如C类似patent2的函数是不对的,因为p是一个临时变量,返回过后就会被释放掉,返回毫无意义。但是在golang中,这种语法是允许的,它能正确的把p的地址返回到上层调用函数而不被释放。 142 | 143 | 这样该函数在运行完毕后肯定是要释放的,内部分配的临时内存也要释放,所以p也应该被释放。而为了让p能被正确返回到上层调用,golang采取了一种内存策略,把p从栈拿到堆的中去,此时p就不会跟随patent2一同消亡了,这个过程就是逃逸。 144 | -------------------------------------------------------------------------------- /src/chapter16/01.0.md: -------------------------------------------------------------------------------- 1 | ### For-learning-Go-Tutorial 2 | 3 | Go语言是谷歌2009发布的第二款开源编程语言。 4 | 5 | Go语言专门针对多处理器系统应用程序的编程进行了优化,使用Go编译的程序可以媲美C或C++代码的速度,而且更加安全、支持并行进程。 6 | 7 | 因而一直想的是自己可以根据自己学习和使用Go语言编程的心得,写一本Go的书可以帮助想要学习Go语言的初学者快速入门开发和使用! 8 | 9 | #### Go排序算法及其性能比较 10 | 11 | 在我们开发的时候有的时候需要对一个数据集合进行排序,这时候我们就需要用到了排序算法,而Go的标准库提供了排序的包sort,实现了int,float64和string三种基础类型的排序接口。所有排序调用sort.Sort,内部根据排序数据个数自动切换最适合的排序算法(插入排序.快排和堆排序)。 12 | 13 | 因为Go中排序的包sort,里面是由三种排序算法(插入排序.快排和堆排序)具体实现的,因此真的排序算法又可以分为内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。 14 | 常见的内部排序算法有:插入排序、希尔排序、选择排序、冒泡排序、归并排序、快速排序、堆排序、基数排序等。 15 | 16 | 排序算法又分为稳定性算法和不稳定性算法: 17 | 18 | * 稳定的排序算法: 冒泡排序、插入排序、归并排序和基数排序。 19 | 20 | * 不是稳定的排序算法: 选择排序、快速排序、希尔排序、堆排序。 21 | 22 | 在Go排序算法这一章讲述的目录: 23 | 24 | * [冒泡排序](#冒泡排序) 25 | * [选择排序](#选择排序) 26 | * [插入排序](#插入排序) 27 | * [希尔排序](#希尔排序) 28 | * [归并排序](#归并排序) 29 | * [快速排序](#快速排序) 30 | * [堆排序](#堆排序) 31 | * [桶排序](#桶排序) 32 | * [计数排序](#计数排序) 33 | * [基数排序](#基数排序) 34 | 35 | #### Go的标准包sort排序 36 | 37 | 因此对数据集合排序时不必考虑应当选择哪一种排序方法,只要实现了`sort.Interface`定义的三个方法:获取数据集合长度的(Len)方法、比较两个元素大小的(Less)方法和交换两个元素位置的(Swap)方法,就可以顺利对数据集合进行排序。sort包会根据实际数据自动选择高效的排序算法。 38 | 39 | 标准库提供一个通用接口,只要实现了这个接口,就可以通过调用 sort.Sort 来排序。 40 | ```go 41 | type Interface interface { 42 | // Len is the number of elements in the collection. 获取数据集合元素个数 43 | Len() int 44 | // Less returns whether the element with index i should sort 45 | // before the element with index j. 如果index为i的元素小于index为j的元素,则返回true,否则返回false 46 | Less(i, j int) bool 47 | // Swap swaps the elements with indexes i and j. 交换i和j索引的两个元素的位置 48 | Swap(i, j int) 49 | } 50 | ``` 51 | 52 | 接下来我们看一个测试: 53 | 54 | ```go 55 | import ( 56 | "fmt" 57 | "sort" 58 | ) 59 | 60 | func main() { 61 | 62 | // int 类型的排序 63 | a := []int{60, 5, 50, 32, 100} 64 | fmt.Println(a) // [60 5 50 32 100] 65 | sort.Ints(a) // sort.Sort(IntSlice(a)) 的封装 66 | fmt.Println(a) // [5 32 50 60 100],默认的 Less() 实现的是升序 67 | sort.Sort(sort.Reverse(sort.IntSlice(a))) 68 | fmt.Println(a) // [100 60 50 32 5] 实现降序排列 69 | 70 | //float类型的排序 71 | b := []float64{60.23,5.23,50.99,76.32,100.39,20.21} 72 | fmt.Println(b) // [60.23 5.23 50.99 76.32 100.39 20.21] 73 | sort.Float64s(b) // sort.Float64Slice(b) 74 | fmt.Println(b) // [5.23 20.21 50.99 60.23 76.32 100.39] 75 | sort.Sort(sort.Reverse(sort.Float64Slice(b))) 76 | fmt.Println(a) // [100.39 76.32 60.23 50.99 20.21 5.23] 实现降序排列 77 | } 78 | ``` 79 | 80 | 这里需要注意的是,默认的`sort.Less`实现的是升序排列,如果想要让结果降序,可以先用`sort.Reverse`包装一次。这个调用会得到一个reverse的类型,包含一个 Interface 的匿名字段,其中Less函数与Interface里的相反,从而实现逆序。 81 | 82 | 如果我们要对自定义的数据类型进行排序,需要实现 sort.Interface 接口,也就是实现 Len、Less 和 Swap 三个函数。很多场景下 Len 和 Swap 基本上和数据类型无关,所以实际上只有 Less 会有差别。 83 | 84 | 例如在app市场中app下载排行榜,知道appId和对应的下载量,需要把数据根据下载量进行排序。 85 | 86 | ```go 87 | import ( 88 | "fmt" 89 | "math/rand" 90 | "sort" 91 | ) 92 | 93 | type DownloadItem struct { 94 | AppId int // appID 95 | DownloadTimes int // 下载次数 96 | } 97 | 98 | func (d DownloadItem) String() string{ 99 | return fmt.Sprintf("AppId:%d,DownloadTimes:%d",d.AppId,d.DownloadTimes) 100 | } 101 | 102 | type DownloadCollection []*DownloadItem 103 | 104 | func (d DownloadCollection)Len() int{ 105 | return len(d) 106 | } 107 | 108 | func (d DownloadCollection)Swap(i int,j int){ 109 | d[i],d[j] = d[j],d[i] 110 | } 111 | 112 | // 根据app下载量降序排列 113 | func (d DownloadCollection)Less(i int,j int) bool{ 114 | return d[i].DownloadTimes >d[j].DownloadTimes 115 | } 116 | 117 | func main() { 118 | a := make(DownloadCollection,5) 119 | for i := 0; i < len(a); i++ { 120 | a[i] = &DownloadItem{i + 1, rand.Intn(1000)} 121 | } 122 | 123 | fmt.Println(a) 124 | sort.Sort(a) 125 | fmt.Println(a) 126 | } 127 | ``` 128 | 129 | 可以看到为排序的数据是: 130 | 131 | ```go 132 | [AppId:1,DownloadTimes:81 AppId:2,DownloadTimes:887 AppId:3,DownloadTimes:847 AppId:4,DownloadTimes:59 AppId:5,DownloadTimes:81] 133 | ``` 134 | 排序后的顺序是: 135 | ```go 136 | [AppId:2,DownloadTimes:887 AppId:3,DownloadTimes:847 AppId:1,DownloadTimes:81 AppId:5,DownloadTimes:81 AppId:4,DownloadTimes:59] 137 | ``` 138 | 139 | 在了解了Go的sort包排序之后我们继续探索下当今最流行的十大排序算法,然后做个梳理和总结方便我们以后可以学习和回顾. 140 | #### 冒泡排序 141 | 142 | **冒泡排序(Bubble Sort)** 是一种计算机科学领域的较简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,此时该数列就已经排序完成。个算法的名字由来是因为越大的元素会经由交换慢慢“浮”到数列的顶端(升序或降序排列),就如同碳酸饮料中二氧化碳的气泡最终会上浮到顶端一样,故名“冒泡排序”。冒泡排序还有一种优化算法,就是立一个 flag,当在一趟序列遍历中元素没有发生交换,则证明该序列已经有序。 143 | 144 | ##### 算法原理 145 | 146 | 冒泡排序算法的原理如下: 147 | 148 | * 比较相邻的元素。如果第一个比第二个大,就交换他们两个。 149 | * 对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。 150 | * 针对所有的元素重复以上的步骤,除了最后一个。 151 | * 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。 152 | 153 |

154 | 155 |

156 | 157 | 冒泡算法实现: 158 | 159 | ```go 160 | func main() { 161 | var arr = []int{9,10,11,5,3,4,27,2,1,3,20} 162 | //升序 163 | bubbleAscendingSort(arr) 164 | //降序 165 | bubbleDescendingSort(arr) 166 | } 167 | 168 | //升序 169 | func bubbleAscendingSort(arr []int) { 170 | for i :=0; i < len(arr)-1; i++ { 171 | for j := i+1; j< len(arr); j++ { 172 | if (arr[i] > arr[j]) { 173 | arr[i],arr[j] = arr[j],arr[i] 174 | } 175 | } 176 | } 177 | 178 | fmt.Println("bubbleAscendingSort:",arr) 179 | } 180 | 181 | //降序 182 | func bubbleDescendingSort(arr []int) { 183 | for i :=0; i < len(arr)-1; i++ { 184 | for j := i+1; j< len(arr); j++ { 185 | if (arr[i] < arr[j]) { 186 | arr[i],arr[j] = arr[j],arr[i] 187 | } 188 | } 189 | } 190 | 191 | fmt.Println("bubbleDescendingSort:",arr) 192 | } 193 | ``` 194 | 195 | 运行结果: 196 | ```go 197 | bubbleAscendingSort: [1 2 3 3 4 5 9 10 11 20 27] 198 | 199 | bubbleDescendingSort: [27 20 11 10 9 5 4 3 3 2 1] 200 | ``` 201 | 202 | 203 | #### 选择排序 204 | 205 | **选择排序(Selection sort)** 是一种简单直观的排序算法。它的工作原理如下。首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。 206 | 207 | 选择排序的主要优点与数据移动有关。如果某个元素位于正确的最终位置上,则它不会被移动。选择排序每次交换一对元素,它们当中至少有一个将被移到其最终位置上,因此对n个元素的表进行排序总共进行至多n-1次交换。在所有的完全依靠交换去移动元素的排序方法中,选择排序属于非常好的一种。 208 | 209 | ##### 算法原理 210 | 211 | 选择排序算法的原理如下: 212 | 213 | * 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置 214 | 215 | * 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。 216 | 217 | * 重复第二步,直到所有元素均排序完毕。 218 | 219 |

220 | 221 |

222 | 223 | 选择排序算法实现: 224 | ```go 225 | func main() { 226 | var arr = []int{19,28,17,5,13,4,6,7,9,3,10} 227 | //升序 228 | selectAscendingSort(arr) 229 | //降序 230 | selectDescendingSort(arr) 231 | } 232 | 233 | //升序 234 | func selectAscendingSort(arr []int) { 235 | l := len(arr) 236 | m := len(arr) - 1 237 | for i := 0; i < m; i++ { 238 | k := i 239 | for j := i+1; j < l; j++ { 240 | if arr[k] > arr[j] { 241 | k = j 242 | } 243 | } 244 | if k != i { 245 | arr[k],arr[i] = arr[i],arr[k] 246 | } 247 | } 248 | 249 | fmt.Println("selectAscendingSort:",arr) 250 | } 251 | 252 | //降序 253 | func selectDescendingSort(arr []int) { 254 | l := len(arr) 255 | m := len(arr) - 1 256 | for i := 0; i < m; i++ { 257 | k := i 258 | for j := i+1; j < l; j++ { 259 | if arr[k] < arr[j] { 260 | k = j 261 | } 262 | } 263 | if k != i { 264 | arr[k],arr[i] = arr[i],arr[k] 265 | } 266 | } 267 | 268 | fmt.Println("selectDescendingSort:",arr) 269 | } 270 | 271 | ``` 272 | 运行结果: 273 | 274 | ```go 275 | selectDescendingSort: [3 4 5 6 7 9 10 13 17 19 28] 276 | 277 | selectAscendingSort: [28 19 17 13 10 9 7 6 5 4 3] 278 | ``` 279 | #### 插入排序 280 | 281 | **插入排序(Insertion Sort)** 是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。 282 | 283 | ##### 算法原理 284 | 285 | 插入排序算法原理: 286 | 287 | * 将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。 288 | 289 | * 从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。) 290 | 291 | 如果比较操作的代价比交换操作大的话,可以采用二分查找法来减少比较操作的数目。该算法可以认为是插入排序的一个变种,称为二分查找插入排序。 292 | 293 |

294 | 295 |

296 | 297 | 插入排序算法实现: 298 | ```go 299 | 300 | func main() { 301 | var arr = []int{19,13,27,15,3,4,26,12,1,0} 302 | insertSort(arr) 303 | fmt.Println("insertSort:",arr) 304 | } 305 | 306 | func insertSort(arr []int) { 307 | n := len(arr) 308 | if n < 2 { 309 | return 310 | } 311 | for i := 1; i < n; i++ { 312 | for j := i; j >0 && arr[j] < arr[j-1]; j-- { 313 | arr[j], arr[j-1] = arr[j-1], arr[j] 314 | } 315 | } 316 | } 317 | ``` 318 | 运行结果: 319 | ```go 320 | insertSort: [0 1 3 4 12 13 15 19 26 27] 321 | ``` 322 | 323 | #### 希尔排序 324 | 325 | **希尔排序(Shell Sort)**,又称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序算法。 326 | 327 | 希尔排序是基于插入排序的以下两点性质而提出改进方法的: 328 | 329 | * 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率; 330 | * 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位; 331 | 332 | 希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。 333 | 334 | ##### 算法原理 335 | 336 | 希尔排序算法原理: 337 | * 选择一个增量序列 t1,t2,……,tk,其中 ti > tj, tk = 1. 338 | * 按增量序列个数 k,对序列进行 k 趟排序. 339 | * 每趟排序,根据对应的增量 ti,将待排序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序。仅增量因子为 1 时,整个序列作为一个表来处理,表长度即为整个序列的长度. 340 | 341 | 希尔排序算法实现: 342 | ```go 343 | func main() { 344 | var arr = []int{19,8,27,15,3,17,6,2,1,0} 345 | shellSort(arr) 346 | fmt.Println("shellSort:",arr) 347 | } 348 | func shellSort(arr []int) { 349 | n := len(arr) 350 | h := 1 351 | 352 | //寻找合适的间隔h 353 | for h < n/3 { 354 | h = 3*h +1 355 | } 356 | 357 | for h >= 1 { 358 | for i := h; i < n; i++ { 359 | for j := i; j >= h && arr[j] < arr[j-1]; j -= h { 360 | arr[j], arr[j-1] = arr[j-1], arr[j] 361 | } 362 | } 363 | h /= 3 364 | } 365 | } 366 | ``` 367 | 运行结果: 368 | ```go 369 | shellSort: [0 1 2 3 6 8 15 17 19 27] 370 | ``` 371 | #### 归并排序 372 | 373 | **归并排序(Merge sort)** 是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。 374 | 375 | 作为一种典型的分而治之思想的算法应用,归并排序的实现由两种方法: 376 | 377 | * 自上而下的递归(所有递归的方法都可以用迭代重写,所以就有了第 2 种方法). 378 | * 自下而上的迭代. 379 | 380 | ##### 算法原理 381 | 382 | 归并排序算法原理: 383 | * 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列. 384 | * 设定两个指针,最初位置分别为两个已经排序序列的起始位置. 385 | * 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置. 386 | * 重复上一步直到某一指针达到序列尾; 387 | * 将另一序列剩下的所有元素直接复制到合并序列尾. 388 | 389 |

390 | 391 |

392 | 393 | 归并排序算法实现: 394 | ```go 395 | func main() { 396 | array := []int{55, 94, 87, 12, 4, 32, 11,8, 39, 42, 64, 53, 70, 12, 9} 397 | fmt.Println("before MergeSort",array) 398 | array = MergeSort(array) 399 | fmt.Println("after MergeSort:",array) 400 | } 401 | 402 | func MergeSort(array []int) []int{ 403 | n := len(array) 404 | if n < 2 { 405 | return array 406 | } 407 | key := n / 2 408 | left := MergeSort(array[0:key]) 409 | right := MergeSort(array[key:]) 410 | return merge(left, right) 411 | } 412 | 413 | func merge(left []int, right []int) []int{ 414 | tmp := make([]int, 0) 415 | i, j := 0,0 416 | for i < len(left) && j < len(right) { 417 | if left[i] < right[j]{ 418 | tmp = append(tmp, left[i]) 419 | i ++ 420 | }else{ 421 | tmp = append(tmp, right[j]) 422 | j ++ 423 | } 424 | } 425 | tmp = append(tmp, left[i:]...) 426 | tmp = append(tmp, right[j:]...) 427 | return tmp 428 | } 429 | ``` 430 | 运行结果: 431 | ```go 432 | before MergeSort [55 94 87 12 4 32 11 8 39 42 64 53 70 12 9] 433 | after MergeSort: [4 8 9 11 12 12 32 39 42 53 55 64 70 87 94] 434 | ``` 435 | 436 | #### 快速排序 437 | 438 | **快速排序(Quicksort)**,又称划分交换排序(partition-exchange sort),简称快排,是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要 Ο(nlogn) 次比较。在最坏状况下则需要 Ο(n2) 次比较,但这种状况并不常见。事实上,快速排序通常明显比其他 Ο(n log n) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。 439 | 440 | 快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。 441 | 442 | 快速排序又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。 443 | 444 | 快速排序的名字起的是简单,当听到这个名字的时候其实你就知道它存在的意义,就是快速排序,而且效率高!它是处理大数据最快的排序算法之一了。 445 | ```markdown 446 | 快速排序的最坏运行情况是 O(n²),比如说顺序数列的快排。但它的平摊期望时间是 O(nlogn),且 O(nlogn) 记号中隐含的常数因子很小,比复杂度稳定等于 O(nlogn) 的归并排序要小很多。所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。 447 | ``` 448 | 449 | ##### 算法原理 450 | 快速排序的算法原理: 451 | * 从数列中挑出一个元素,称为 “基准”(pivot). 452 | * 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作. 453 | * 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序. 454 | 455 | 递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会退出,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。 456 | 457 |

458 | 459 |

460 | 461 | 快速排序算法实现: 462 | ```go 463 | 464 | func main() { 465 | var arr = []int{19,8,16,15,23,34,6,3,1,0,2,9,7} 466 | quickAscendingSort(arr, 0, len(arr)-1) 467 | fmt.Println("quickAscendingSort:",arr) 468 | 469 | quickDescendingSort(arr, 0, len(arr)-1) 470 | fmt.Println("quickDescendingSort:",arr) 471 | } 472 | 473 | //升序 474 | func quickAscendingSort(arr []int, start, end int) { 475 | if (start < end) { 476 | i, j := start, end 477 | key := arr[(start + end)/2] 478 | for i <= j { 479 | for arr[i] < key { 480 | i++ 481 | } 482 | for arr[j] > key { 483 | j-- 484 | } 485 | if i <= j { 486 | arr[i], arr[j] = arr[j], arr[i] 487 | i++ 488 | j-- 489 | } 490 | } 491 | 492 | if start < j { 493 | quickAscendingSort(arr, start, j) 494 | } 495 | if end > i { 496 | quickAscendingSort(arr, i, end) 497 | } 498 | } 499 | } 500 | 501 | //降序 502 | func quickDescendingSort(arr []int, start, end int) { 503 | if (start < end) { 504 | i, j := start, end 505 | key := arr[(start + end)/2] 506 | for i <= j { 507 | for arr[i] > key { 508 | i++ 509 | } 510 | for arr[j] < key { 511 | j-- 512 | } 513 | if i <= j { 514 | arr[i], arr[j] = arr[j], arr[i] 515 | i++ 516 | j-- 517 | } 518 | } 519 | 520 | if start < j { 521 | quickDescendingSort(arr, start, j) 522 | } 523 | if end > i { 524 | quickDescendingSort(arr, i, end) 525 | } 526 | } 527 | } 528 | ``` 529 | 运行结果: 530 | ```go 531 | quickAscendingSort: [0 1 2 3 6 7 8 9 15 16 19 23 34] 532 | quickDescendingSort: [34 23 19 16 15 9 8 7 6 3 2 1 0] 533 | ``` 534 | #### 堆排序 535 | **堆排序(Heapsort)** 是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。 536 | 537 | 堆排序可以说是一种利用堆的概念来排序的选择排序。分为两种方法: 538 | * 大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列. 539 | * 小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列. 540 | 541 | ##### 算法原理 542 | 堆排序的算法原理: 543 | * 创建一个堆 H[0……n-1]. 544 | * 把堆首(最大值)和堆尾互换. 545 | * 把堆的尺寸缩小 1,并调用 shift_down(0),目的是把新的数组顶端数据调整到相应位置. 546 | * 重复步骤2,直到堆的尺寸为 1. 547 |

548 | 549 |

550 | 551 | 堆排序算法实现: 552 | ```go 553 | func main() { 554 | array := []int{52,16,37,2,3,32,12,27,19,42,29,13,37,12,9} 555 | HeapSort(array) 556 | fmt.Println("HeapSort:",array) 557 | } 558 | 559 | func HeapSort(array []int) { 560 | m := len(array) 561 | s := m/2 562 | for i := s; i > -1; i-- { 563 | heap(array, i, m-1) 564 | } 565 | for i := m-1; i > 0; i-- { 566 | array[i], array[0] = array[0], array[i] 567 | heap(array, 0, i-1) 568 | } 569 | } 570 | 571 | func heap(array []int, i, end int){ 572 | l := 2*i+1 573 | if l > end { 574 | return 575 | } 576 | n := l 577 | r := 2*i+2 578 | if r <= end && array[r]>array[l]{ 579 | n = r 580 | } 581 | if array[i] > array[n]{ 582 | return 583 | } 584 | array[n], array[i] = array[i], array[n] 585 | heap(array, n, end) 586 | } 587 | ``` 588 | 运行结果: 589 | ```go 590 | HeapSort: [2 3 9 12 12 13 16 19 27 29 32 37 37 42 52] 591 | ``` 592 | #### 桶排序 593 | 594 | **桶排序(Bucket sort)**,工作的原理是将数组分到有限数量的桶里。每个桶再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)。桶排序是鸽巢排序的一种归纳结果。 595 | 桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点: 596 | 1. 在额外空间充足的情况下,尽量增大桶的数量. 597 | 598 | 2. 使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中. 599 | 600 | ##### 算法原理 601 | 602 | 桶排序的算法原理: 603 | 604 | * 设置一个定量的数组当作空桶子. 605 | * 寻访序列,并且把项目一个一个放到对应的桶子去. 606 | * 对每个不是空的桶子进行排序. 607 | * 从不是空的桶子里把项目再放回原来的序列中. 608 | 609 | 桶排序算法实现: 610 | ```go 611 | 612 | func main() { 613 | array := []int{31,16,37,2,13,32,10,27,7,42,29,18,28,12,9,} 614 | BucketSort(array) 615 | fmt.Println("BucketSort:",array) 616 | } 617 | 618 | func sortInBucket(bucket []int) {//此处实现插入排序方式,其实可以用任意其他排序方式 619 | length := len(bucket) 620 | if length == 1 {return} 621 | 622 | for i := 1; i < length; i++ { 623 | backup := bucket[i] 624 | j := i -1; 625 | //将选出的被排数比较后插入左边有序区 626 | for j >= 0 && backup < bucket[j] {//注意j >= 0必须在前边,否则会数组越界 627 | bucket[j+1] = bucket[j]//移动有序数组 628 | j -- //反向移动下标 629 | } 630 | bucket[j + 1] = backup //插队插入移动后的空位 631 | } 632 | } 633 | //获取数组最大值 634 | func getMaxInArr(arr []int) int{ 635 | max := arr[0] 636 | for i := 1; i < len(arr); i++ { 637 | if arr[i] > max{ max = arr[i]} 638 | } 639 | return max 640 | } 641 | 642 | //桶排序 643 | func BucketSort(arr []int) []int { 644 | //桶数 645 | num := len(arr) 646 | //k(数组最大值) 647 | max := getMaxInArr(arr) 648 | //二维切片 649 | buckets := make([][]int, num) 650 | 651 | //分配入桶 652 | index := 0 653 | for i := 0; i < num; i++ { 654 | index = arr[i] * (num-1) /max//分配桶index = value * (n-1) /k 655 | 656 | buckets[index] = append(buckets[index], arr[i]) 657 | } 658 | //桶内排序 659 | tmpPos := 0 660 | for i := 0; i < num; i++ { 661 | bucketLen := len(buckets[i]) 662 | if bucketLen > 0{ 663 | sortInBucket(buckets[i]) 664 | 665 | copy(arr[tmpPos:], buckets[i]) 666 | 667 | tmpPos += bucketLen 668 | } 669 | } 670 | 671 | return arr 672 | 673 | ``` 674 | 运行结果: 675 | ```go 676 | BucketSort: [2 7 9 10 12 13 16 18 27 28 29 31 32 37 42] 677 | ``` 678 | 679 | #### 计数排序 680 | 681 | **计数排序(Counting sort)** 是一种稳定的线性时间排序算法.计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。计数排序使用一个额外的数组C,其中第i个元素是待排序数组A中值等于i的元素的个数。然后根据数组C 来将A中的元素排到正确的位置。 682 | 683 | ##### 算法原理 684 | 685 | 计数排序的算法原理: 686 | 687 | * 找出待排序的数组中最大和最小的元素. 688 | * 统计数组中每个值为i的元素出现的次数,存入数组C的第i项. 689 | * 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加). 690 | * 反向填充目标数组:将每个元素 i放在新数组的第C[i]项,每放一个元素就将C[i]减去1. 691 | 692 |

693 | 694 |

695 | 696 | 697 | 计数排序算法实现: 698 | ```go 699 | func main() { 700 | array := []int{69,16,48,2,3,32,10,27,17,42,29,8,28,12,9,} 701 | countingSort(array,array[0]) 702 | fmt.Println("BucketSort:",array) 703 | } 704 | 705 | func countingSort(arr []int, maxValue int) []int { 706 | bucketLen := maxValue + 1 707 | bucket := make([]int, bucketLen) // 初始为0的数组 708 | 709 | sortedIndex := 0 710 | length := len(arr) 711 | 712 | for i := 0; i < length; i++ { 713 | bucket[arr[i]] += 1 714 | } 715 | 716 | for j := 0; j < bucketLen; j++ { 717 | for bucket[j] > 0 { 718 | arr[sortedIndex] = j 719 | sortedIndex += 1 720 | bucket[j] -= 1 721 | } 722 | } 723 | 724 | return arr 725 | } 726 | ``` 727 | 运行结果: 728 | ```go 729 | countingSort: [2 3 8 9 10 12 16 17 27 28 29 32 42 48 69] 730 | ``` 731 | 732 | #### 基数排序 733 | **基数排序(Radix sort)** 是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。 734 | 735 | ##### 算法原理 736 | 737 | 基数排序的算法原理: 738 | 739 | * 将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。 740 | 741 | * 基数排序的方式可以采用LSD(Least significant digital)或MSD(Most significant digital),LSD的排序方式由键值的最右边开始,而MSD则相反,由键值的最左边开始。 742 | 743 | LSD 基数排序动图演示: 744 |

745 | 746 |

747 | 748 | 749 | 基数排序算法实现: 750 | ```go 751 | 752 | func main() { 753 | array := []int{12, 3, 8, 5, 9, 11, 23, 36,20,28,21} 754 | fmt.Println("before radixSort:",array) 755 | 756 | radixSort(array) 757 | fmt.Println("after radixSort:",array) 758 | } 759 | 760 | //获取数组的最大值 761 | func maxValue(arr []int) (ret int) { 762 | ret = 1 763 | var key int = 10 764 | for i := 0; i < len(arr); i++ { 765 | for arr[i] >= key { 766 | key = key * 10 767 | ret++ 768 | } 769 | } 770 | return 771 | } 772 | 773 | func radixSort(arr []int) { 774 | key := maxValue(arr) 775 | tmp := make([]int, len(arr), len(arr)) 776 | count := new([10]int) 777 | radix := 1 778 | var i, j, k int 779 | for i = 0; i < key; i++ { //进行key次排序 780 | for j = 0; j < 10; j++ { 781 | count[j] = 0 782 | } 783 | for j = 0; j < len(arr); j++ { 784 | k = (arr[j] / radix) % 10 785 | count[k]++ 786 | } 787 | 788 | for j = 1; j < 10; j++ { //将tmp中的为准依次分配给每个桶 789 | count[j] = count[j-1] + count[j] 790 | } 791 | for j = len(arr) - 1; j >= 0; j-- { 792 | k = (arr[j] / radix) % 10 793 | tmp[count[k]-1] = arr[j] 794 | count[k]-- 795 | } 796 | for j = 0; j > 32) 46 | w := uint32(state) 47 | if race.Enabled && delta > 0 && v == int32(delta) { 48 | // The first increment must be synchronized with Wait. 49 | // Need to model this as a read, because there can be 50 | // several concurrent wg.counter transitions from 0. 51 | race.Read(unsafe.Pointer(semap)) 52 | } 53 | if v < 0 { 54 | panic("sync: negative WaitGroup counter") 55 | } 56 | if w != 0 && delta > 0 && v == int32(delta) { 57 | panic("sync: WaitGroup misuse: Add called concurrently with Wait") 58 | } 59 | if v > 0 || w == 0 { 60 | return 61 | } 62 | // This goroutine has set counter to 0 when waiters > 0. 63 | // Now there can't be concurrent mutations of state: 64 | // - Adds must not happen concurrently with Wait, 65 | // - Wait does not increment waiters if it sees counter == 0. 66 | // Still do a cheap sanity check to detect WaitGroup misuse. 67 | if *statep != state { 68 | panic("sync: WaitGroup misuse: Add called concurrently with Wait") 69 | } 70 | // Reset waiters count to 0. 71 | *statep = 0 72 | for ; w != 0; w-- { 73 | runtime_Semrelease(semap, false) 74 | } 75 | } 76 | ``` 77 | 78 | Done()方法将WaitGroup计数器减1。 79 | 80 | ```go 81 | func (wg *WaitGroup) Done() { 82 | wg.Add(-1) 83 | } 84 | ``` 85 | 86 | Wait()直到WaitGroup计数器为零。 87 | ```go 88 | func (wg *WaitGroup) Wait() { 89 | statep, semap := wg.state() 90 | if race.Enabled { 91 | _ = *statep // trigger nil deref early 92 | race.Disable() 93 | } 94 | for { 95 | state := atomic.LoadUint64(statep) 96 | v := int32(state >> 32) 97 | w := uint32(state) 98 | if v == 0 { 99 | // Counter is 0, no need to wait. 100 | if race.Enabled { 101 | race.Enable() 102 | race.Acquire(unsafe.Pointer(wg)) 103 | } 104 | return 105 | } 106 | // Increment waiters count. 107 | if atomic.CompareAndSwapUint64(statep, state, state+1) { 108 | if race.Enabled && w == 0 { 109 | // Wait must be synchronized with the first Add. 110 | // Need to model this is as a write to race with the read in Add. 111 | // As a consequence, can do the write only for the first waiter, 112 | // otherwise concurrent Waits will race with each other. 113 | race.Write(unsafe.Pointer(semap)) 114 | } 115 | runtime_Semacquire(semap) 116 | if *statep != 0 { 117 | panic("sync: WaitGroup is reused before previous Wait has returned") 118 | } 119 | if race.Enabled { 120 | race.Enable() 121 | race.Acquire(unsafe.Pointer(wg)) 122 | } 123 | return 124 | } 125 | } 126 | } 127 | ``` 128 | 129 | 应用示例: 130 | ```go 131 | func main(){ 132 | var wg sync.WaitGroup 133 | 134 | for i:=0;i<5;i=i+1{ 135 | wg.Add(1) 136 | go func(n int) { 137 | //defer wg.Done(),注意这个Done的位置,是另一个函数 138 | defer wg.Add(-1) 139 | EchoNumber(n) 140 | }(i) 141 | } 142 | wg.Wait() 143 | } 144 | 145 | func EchoNumber(i int){ 146 | time.Sleep(time.Millisecond *2000) 147 | fmt.Println(i) 148 | 149 | } 150 | ``` 151 | 运行: 152 | ```go 153 | 3 154 | 4 155 | 2 156 | 0 157 | 1 158 | ``` 159 | 这个应用示例很简单,是将每次循环的数量过3秒钟输出。那么,这个程序如果不用WaitGroup,那么将看不见输出结果。因为Goroutine还没执行完,主线程已经执行完毕。注释的`defer wg.Done()`和`defer wg.Add(-1)`作用一样。 160 | -------------------------------------------------------------------------------- /src/chapter20/01.0.md: -------------------------------------------------------------------------------- 1 | #### For-learning-Go-Tutorial 2 | 在 Golang中,Go编程标准和规范对于一个人的认知很重要.因而一开始就希望可以养成好的习惯! 3 | 4 | #### Go异步抢占式调度 5 | 6 | go 1.14 版本带来了一个非常重要的特性:异步抢占的调度模式。之前通过解释协程调度原理中提到,协程是用户态实现的自我调度单元,每个协程都是君子才能维护和谐的调度秩序,如果出现了流氓(占着 cpu 不放的协程)那就是是无可奈何的。 7 | 8 | go1.14 之前的版本所谓的抢占调度是怎么样的呢下面我们一起看下? 9 | 10 | 1. 如果 `sysmon` 监控线程发现有个协程 A 执行之间太长了(或者 gc 场景,或者 stw 场景),那么会友好的在这个 A 协程的某个字段设置一个抢占标记. 11 | 2. 协程 A 在 call 一个函数的时候,会复用到扩容栈(morestack)的部分逻辑,检查到抢占标记之后,让出 cpu,切到调度主协程里. 12 | 13 | 这样 A 就算是被抢占了。我们注意到,A 调度权被抢占有个前提:A 必须主动 call 函数,这样才能有走到 morestack 的机会(能抢占君子的调度,无法抢占流氓的调度权). 14 | 15 | 这里我们用一个 P(处理器),用来确保是单处理器的场景. 16 | 17 | 通过golang 的 GMP 模型:调度单元 G,线程 M,队列 P,由于 P 只有一个,所以每时每刻有效执行的 M 只会有一个,也就是单处理器的场景(旁白:个人理解有所不同,有些人喜欢直接把 P 理解成处理器,我这里把 P 说成队列是从实现的角度来讲的). 18 | 19 | 通过打开 golang 调试器 `trace` 工具(可以直观的观察调度的情况),搞一个纯计算且耗时的函数 `calculateSum`. 20 | 21 | 下面创建一个名为 `main.go` 的文件,写入以下内容: 22 | 23 | ```go 24 | package main 25 | 26 | import ( 27 | "fmt" 28 | "os" 29 | "runtime" 30 | "runtime/trace" 31 | "sync" 32 | ) 33 | 34 | func calculateSum(w *sync.WaitGroup, p int) { 35 | defer w.Done() 36 | var sum, n int64 37 | for ; n < 10000; n++ { 38 | sum += n 39 | } 40 | fmt.Println(p, sum) 41 | } 42 | 43 | func main() { 44 | runtime.GOMAXPROCS(1) 45 | 46 | f, _ := os.Create("trace.output") 47 | defer f.Close() 48 | 49 | _ = trace.Start(f) 50 | defer trace.Stop() 51 | 52 | var wg sync.WaitGroup 53 | for i := 0; i < 10; i++ { 54 | wg.Add(1) 55 | go calculateSum(&wg, i) 56 | } 57 | wg.Wait() 58 | } 59 | 60 | ``` 61 | 我们分别看下 go1.13, go.14 对于这个程序的表现区别。 62 | 63 | trace 这个就再简单提下,trace 是 golang 内置的一种调试手段,能够 trace 一段时间程序的运行情况。能看到: 64 | 65 | * 协程的调度运行情况; 66 | * 跑在每个处理器 P 上协程情况; 67 | * 协程出发的事件链; 68 | * 编译、运行的程序: 69 | 70 | ```bash 71 | > go build -gcflags "-N -l" ./main.go 72 | > ./main 73 | ``` 74 | 这样在本地就能生成一个 `trace.output` 文件; 75 | 76 | 分析 trace 输出: 77 | ```bash 78 | > go tool trace -http=":8080" ./trace.output 79 | ``` 80 | 81 | 这样就可以直接用浏览器来方便查看分析的结果,如下: 82 | 83 |

84 | 85 |

86 | 87 | 详细解析: 88 | ```markdown 89 | * View trace:查看跟踪(这个是今天要使用的重点),能看到一段时间内 goroutine 的调度执行情况,包括事件触发链; 90 | * Goroutine analysis:Goroutine 分析,能看到这段时间所有 goroutine 执行的一个情况,执行堆栈,执行时间; 91 | * Network blocking profile:网络阻塞概况(分析网络的一些消耗) 92 | * Synchronization blocking profile:同步阻塞概况(分析同步锁的一些情况) 93 | * Syscall blocking profile:系统调用阻塞概况(分析系统调用的消耗) 94 | * Scheduler latency profile:调度延迟概况(函数的延迟占比) 95 | * User defined tasks:自定义任务 96 | * User defined regions:自定义区域 97 | * Minimum mutator utilization:Mutator 利用率使用情况 98 | ``` 99 | 100 | 所以我们要是分析抢占只需要分析`View trace`。 101 | 102 | * 横坐标为时间线,表示采样的顺序时间; 103 | * 纵坐标为采样的指标,分为两大块:STATS,PROCS 104 | 105 | 注意: 这些采样值都要配合时间轴来看,理解成是一些快照数据. 106 | 107 |

108 | 109 |

110 | 111 | STATS: 112 | 113 | 处于上半区,展示的有三个指标: `Goroutines`,`Heap`,`Threads`,鼠标点击彩色的图样,就能看到这一小段时间的采样情况. 114 | 115 | * Goroutines:展示某个时间 GCWaiting,Runnable,Running 三种状态的协程个数; 116 | * Heap:展示某个时间的 NextGC,Allocated 的值; 117 | * Threads:展示 InSyscall,Running 两个状态的线程数量情况; 118 | 119 | PROCS: 120 | 121 | 显示每个处理器当时正在处理的协程,事件,和一些具体运行时信息, Proc 的个数由 GOMAXPROCS 参数控制,默认和机器核心数一致. 122 | 123 | 124 | 点击一个协程区域,就会显示这个时间段的情况,有一些指标: 125 | ```markdown 126 | * Start:开始时间(就是时间轴上的刻度). 127 | * Wall Duration:持续时间(这个 goroutine 在这个处理器上连续执行的小段时间). 128 | * Start Stack Trace:协程调用栈(切进来执行的 goroutine 调用栈). 129 | * End Stack Trace:切走时候时候的调用栈. 130 | * Incoming flow:触发切入的事件. 131 | * Outgoing flow:触发切走的事件. 132 | * Preceding events:这个协程相关的之前所有的事件. 133 | * Follwing events:这个协程相关的之后所有的事件. 134 | * All connected:这个协程相关的所有事件. 135 | ``` 136 | 137 |

138 | 139 |

140 | 141 | 142 | 从trace图中我们可以看出: 143 | 144 | 只有一个处理器(Proc 0)调度协程;因为我们代码里面设置 GOMAXPROCS = 1, 程序运行的总时间还是 16s(虽然 10 个 goroutine 是并发运行的,但是你只有一个处理器,所以时间肯定是一样的,但如果你有多个处理器的时候,就不一样了);这个 goroutine 只执行了 20ms 就让出处理器了; 145 | 我们大概知道,main.go 里面 `calculateSum`函数在我的机器上大概需要 1.6s 的时间,所以执行 20ms 就切走了肯定是还没有执行完的,是被强制抢占了处理器; 146 | 147 | 因此可以从`go1.14` 看出抢占式任务调度,让 goroutine 任务的调度执行更加公平,避免了流氓协程降低整个系统吞吐能力的情况发生;通过 trace 工具图形化展示了go1.14 的调度执行情况,从 trace 结果来看,非常直观. 148 | 149 | 我们理解了抢占调度带来的好处,并且形象的观测到了,并且还发现了 runtime.asyncPreempt 这个函数(是通过异步信号来实现的); 150 | 151 | #### License 152 | This is free software distributed under the terms of the MIT license 153 | -------------------------------------------------------------------------------- /src/example/EX.0.1.md: -------------------------------------------------------------------------------- 1 | ### For-learning-Go-Tutorial 2 | Go语言各种值类型,包括字符串、整数、浮点数、布尔值等。下面是一些基本示例。 3 | 4 | Go语言最主要的特性: 5 | * 自动垃圾回收 6 | * 更丰富的内置类型 7 | * 函数多返回值 8 | * 错误处理 9 | * 匿名函数和闭包 10 | * 类型和接口 11 | * 并发编程 12 | * 反射 13 | * 语言交互性 14 | 15 | 接着熟悉下Go环境变量: 16 | 17 | ```bash 18 | > go env 19 | GO111MODULE="auto" 20 | GOARCH="amd64" 21 | GOBIN="/Users/admin/goTest/bin" 22 | GOCACHE="/Users/admin/Library/Caches/go-build" 23 | GOENV="/Users/admin/Library/Application Support/go/env" 24 | GOEXE="" 25 | GOFLAGS="" 26 | GOHOSTARCH="amd64" 27 | GOHOSTOS="darwin" 28 | GOINSECURE="" 29 | GOMODCACHE="/Users/admin/goTest/pkg/mod" 30 | GONOPROXY="" 31 | GONOSUMDB="" 32 | GOOS="darwin" 33 | GOPATH="/Users/admin/goTest" 34 | GOPRIVATE="" 35 | GOPROXY="https://goproxy.cn,direct" 36 | GOROOT="/usr/local/go" 37 | GOSUMDB="sum.golang.org" 38 | GOTMPDIR="" 39 | GOTOOLDIR="/usr/local/go/pkg/tool/darwin_amd64" 40 | GCCGO="gccgo" 41 | AR="ar" 42 | CC="clang" 43 | CXX="clang++" 44 | CGO_ENABLED="1" 45 | GOMOD="" 46 | CGO_CFLAGS="-g -O2" 47 | CGO_CPPFLAGS="" 48 | CGO_CXXFLAGS="-g -O2" 49 | CGO_FFLAGS="-g -O2" 50 | CGO_LDFLAGS="-g -O2" 51 | PKG_CONFIG="pkg-config" 52 | GOGCCFLAGS="-fPIC -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fdebug-prefix-map=/var/folders/kp/3yqnp9cj4f3_9539b06q4yyc0000gn/T/go-build238604917=/tmp/go-build -gno-record-gcc-switches -fno-common" 53 | ``` 54 | 下面详细解释下关于Go的环境变量中的各个参数的含义: 55 | 56 | | 参数 | 含义 | 57 | | --------------- |------------------------------------------------------------------------| 58 | |GO111MODULE | 是一个环境变量, on 仍将强制使用Go模块,off 强制Go表现出GOPATH方式,auto 是默认模式。| 59 | |GOARCH | 程序构建环境的目标计算架构 | 60 | |GOBIN | 该环境变量的值为 Go 程序的可执行文件的目录 | 61 | |GOCACHE | 存储编译后信息的缓存目录 | 62 | |GOENV | 本地的go环境文件存储位置 | 63 | |GOEXE | 可执行文件名后缀(".exe" on Windows) | 64 | |GOFLAGS | 当给定标志被当前命令已知时,默认情况下以空格分隔的-flag = value设置列表将应用于go命令。 | 65 | |GOHOSTARCH | Go工具链二进制文件的体系结构(GOARCH)。| 66 | |GOINSECURE | 逗号分隔的模块路径前缀列表(按Go的path.Match语法),应始终以不安全的方式获取。仅适用于直接获取的依赖项。 | 67 | |GOMODCACHE | Go命令将存储下载的模块的目录。| 68 | |GOOS | 为其编译代码的操作系统。例如linux,darwin,windows,netbsd。| 69 | |GOPATH | Go 命令依赖一个重要的环境变量,GOPATH允许多个目录,当有多个目录时,请注意分隔符.| 70 | |GOPRIVATE | Go的私有模块仓库 | 71 | |GOPROXY | Go的mod代理 | 72 | |GOROOT | Go安装目录的绝对路径| 73 | |GOSUMDB | 要使用的校验和数据库的名称,以及可选的公用密钥和URL。 | 74 | |GOTMPDIR | Go命令将在其中写入临时源文件,程序包和二进制文件的目录。 | 75 | |GOTOOLDIR | Go tool的安装目录| 76 | |GCCGO | 为"go build -compiler = gccgo"运行的gccgo命令。 | 77 | |AR | 使用gccgo编译器进行构建时用于处理库归档文件的命令。 | 78 | |CC | 用于编译C代码 | 79 | |CXX | 用于编译C代码 | 80 | |CGO_ENABLED | 是否支持cgo命令 | 81 | |GOMOD | 主模块go.mod的绝对路径。如果启用了模块感知模式,但没有go.mod,则GOMOD将为os.DevNull | 82 | 83 | 如果你想查看更加详细的可以通过命令: 84 | ```bash 85 | > go help environment 86 | ``` 87 | 88 | 下面我以一个简单的string例子开始吧。 89 | 90 | ```go 91 | package main 92 | import "fmt" 93 | func main() { 94 | fmt.Println("go" + "lang") 95 | 96 | fmt.Println("1+1 =", 1+1) 97 | fmt.Println("7.0/3.0 =", 7.0/3.0) 98 | fmt.Println(true && false) 99 | fmt.Println(true || false) 100 | fmt.Println(!true) 101 | 102 | } 103 | ``` 104 | 打印输出: 105 | ```go 106 | $ go run values.go 107 | golang 108 | 1+1 = 2 109 | 10.0/2.0 = 5 110 | false 111 | true 112 | false 113 | ``` 114 | 这是最基本的语法,相信通过这个简单的例子大家已经初步对Go语言由了基本认识,那接下来开启Go的世界! 115 | -------------------------------------------------------------------------------- /src/example/EX.0.2.md: -------------------------------------------------------------------------------- 1 | ### For-learning-Go-Tutorial 2 | 3 | Go语言各种值类型,包括字符串、整数、浮点数、布尔值等。下面是一些基本示例。 4 | 5 | Go语言最主要的特性: 6 | 7 | * 自动垃圾回收 8 | * 更丰富的内置类型 9 | * 函数多返回值 10 | * 错误处理 11 | * 匿名函数和闭包 12 | * 类型和接口 13 | * 并发编程 14 | * 反射 15 | * 语言交互性 16 | 17 | #### Go语言案例总结 18 | 19 | 最近一直在写一部针对初入门Golang开发人员的书,我觉得最好的状态就是学习了基础之后,练习,然后做项目中不断的提高! 20 | 21 | GO 例子目录: 22 | 23 | * [Go 数组](#Go数组) 24 | * [Go map](#map) 25 | * [Go 函数定义](#Go函数定义) 26 | * [Go 方法](#Go方法) 27 | * [Go 结构体](#Go结构体) 28 | * [Go Channel](#Channel) 29 | * [Go 闭包函数](#Go闭包函数) 30 | * [Go Defer函数](#Defer函数) 31 | * [Go 接口]() 32 | * [Go Base64编码]() 33 | * [Go 状态协程]() 34 | * [Go Panic]() 35 | * [Go 散列函数]() 36 | * [Go 工作池]() 37 | * [Go 进程触发]() 38 | * [Go 时间戳]() 39 | * [Go 时间格式化和解析]() 40 | * [Go 通道缓冲]() 41 | * [Go IO]() 42 | * [Go 压缩]() 43 | 44 | #### Go数组 45 | 46 | * 数组是一个具有相同数据类型的元素组成的固定长度的有序集合。 47 | * 在Go语言中,数组是值类型,长度是类型的组成部分,也就是说"[10]int"和“[20]int”是完全不同的两种数组类型。 48 | * 同类型的两个数组支持"=="和"!="比较,但是不能比较大小。 49 | * 数组作为参数时,函数内部不改变数组内部的值,除非是传入数组的指针。 50 | * 数组的指针:*[2,3,4]int 51 | * 指针数组:[3]*int 52 | 53 | ```go 54 | package main 55 | 56 | import "fmt" 57 | 58 | func main() { 59 | // 这里我们创建了一个具有5个元素的整型数组 60 | // 元素的数据类型和数组长度都是数组的一部分 61 | // 默认情况下,数组元素都是零值 62 | // 对于整数,零值就是0 63 | var a [5]int 64 | fmt.Println("emp:", a) 65 | 66 | // 我们可以使用索引来设置数组元素的值,就像这样 67 | // "array[index] = value" 或者使用索引来获取元素值, 68 | // 就像这样"array[index]" 69 | a[4] = 100 70 | fmt.Println("set:", a) 71 | fmt.Println("get:", a[4]) 72 | 73 | // 内置的len函数返回数组的长度 74 | fmt.Println("len:", len(a)) 75 | 76 | // 这种方法可以同时定义和初始化一个数组 77 | b := [5]int{1, 2, 3, 4, 5} 78 | fmt.Println("dcl:", b) 79 | 80 | // 数组都是一维的,但是你可以把数组的元素定义为一个数组 81 | // 来获取多维数组结构 82 | var twoD [2][3]int 83 | for i := 0; i < 2; i++ { 84 | for j := 0; j < 3; j++ { 85 | twoD[i][j] = i + j 86 | } 87 | } 88 | fmt.Println("2d: ", twoD) 89 | } 90 | ``` 91 | 输出结果为: 92 | ```go 93 | emp: [0 0 0 0 0] 94 | set: [0 0 0 0 100] 95 | get: 100 96 | len: 5 97 | dcl: [1 2 3 4 5] 98 | 2d: [[0 1 2] [1 2 3]] 99 | ``` 100 | 拥有固定长度是数组的一个特点,但是这个特点有时候会带来很多不便,尤其在一个集合元素个数不固定的情况下。这个时候我们更多地使用切片。 101 | 102 | #### map 103 | 104 | map是Go语言内置的关联数据类型。因为数组是索引对应数组元素,而字典是键对应值。 105 | ```go 106 | package main 107 | 108 | import "fmt" 109 | 110 | func main() { 111 | 112 | // 创建一个字典可以使用内置函数make 113 | // "make(map[键类型]值类型)" 114 | m := make(map[string]int) 115 | 116 | // 使用经典的"name[key]=value"来为键设置值 117 | m["k1"] = 9 118 | m["k2"] = 22 119 | 120 | // 用Println输出字典,会输出所有的键值对 121 | fmt.Println("map:", m) 122 | 123 | // 获取一个键的值 "name[key]". 124 | v1 := m["k1"] 125 | fmt.Println("v1: ", v1) 126 | 127 | // 内置函数返回字典的元素个数 128 | fmt.Println("len:", len(m)) 129 | 130 | // 内置函数delete从字典删除一个键对应的值 131 | delete(m, "k2") 132 | fmt.Println("map:", m) 133 | 134 | // 根据键来获取值有一个可选的返回值,这个返回值表示字典中是否 135 | // 存在该键,如果存在为true,返回对应值,否则为false,返回零值 136 | // 有的时候需要根据这个返回值来区分返回结果到底是存在的值还是零值 137 | // 比如字典不存在键x对应的整型值,返回零值就是0,但是恰好字典中有 138 | // 键y对应的值为0,这个时候需要那个可选返回值来判断是否零值。 139 | _, ok := m["k2"] 140 | fmt.Println("ok:", ok) 141 | 142 | // 可以用 ":=" 同时定义和初始化一个字典 143 | n := map[string]int{"foo": 1, "bar": 2} 144 | fmt.Println("map:", n) 145 | } 146 | ``` 147 | 输出结果为: 148 | ```go 149 | map: map[k1:9 k2:22] 150 | v1: 9 151 | len: 2 152 | map: map[k1:9] 153 | ok: false 154 | map: map[foo:1 bar:2] 155 | ``` 156 | #### Go函数定义 157 | 158 | 函数是Go语言的重要内容。 159 | ```go 160 | package main 161 | 162 | import "fmt" 163 | 164 | // 这个函数计算两个int型输入数据的和,并返回int型的和 165 | func plus(a int, b int) int { 166 | // Go需要使用return语句显式地返回值 167 | return a + b 168 | } 169 | 170 | func main() { 171 | // 函数的调用方式很简单 172 | // "名称(参数列表)" 173 | res := plus(1, 2) 174 | fmt.Println("1+2 =", res) 175 | } 176 | ``` 177 | 178 | 输出结果为: 179 | 180 | ```go 181 | 1+2 = 3 182 | ``` 183 | 184 | #### Go方法 185 | 186 | 通常的函数定义叫做函数,定义在结构体上面的函数叫做该结构体的方法。从某种意义上说,方法是函数的“语法糖”。当函数与某个特定的类型绑定,那么它就是一个方法。也证因为如此,我们可以将方法“还原”成函数。 187 | 188 | ```go 189 | package main 190 | 191 | import "fmt" 192 | 193 | type rect struct { 194 | width, height int 195 | } 196 | 197 | // 这个area方法有一个限定类型*rect, 198 | // 表示这个函数是定义在rect结构体上的方法 199 | func (r *rect) area() int { 200 | return r.width * r.height 201 | } 202 | 203 | // 方法的定义限定类型可以为结构体类型 204 | // 也可以是结构体指针类型 205 | // 区别在于如果限定类型是结构体指针类型 206 | // 那么在该方法内部可以修改结构体成员信息 207 | func (r rect) perim() int { 208 | return 2*r.width + 2*r.height 209 | } 210 | 211 | func main() { 212 | r := rect{width: 10, height: 5} 213 | 214 | // 调用方法 215 | fmt.Println("area: ", r.area()) 216 | fmt.Println("perim:", r.perim()) 217 | 218 | // Go语言会自动识别方法调用的参数是结构体变量还是 219 | // 结构体指针,如果你要修改结构体内部成员值,那么使用 220 | // 结构体指针作为函数限定类型,也就是说参数若是结构体 221 | //变量,仅仅会发生值拷贝。 222 | rp := &r 223 | fmt.Println("area: ", rp.area()) 224 | fmt.Println("perim:", rp.perim()) 225 | } 226 | ``` 227 | 228 | 输出结果为: 229 | ```go 230 | area: 50 231 | perim: 30 232 | area: 50 233 | perim: 30 234 | ``` 235 | #### Go结构体 236 | 237 | #### Channel 238 | 239 | channel常用的10中操作: 240 | 241 | 1. 使用for range读channel 242 | 243 | 场景:当需要不断从channel读取数据时. 244 | 245 | 使用for-range读取channel,这样既安全又便利,当channel关闭时,for循环会自动退出,无需主动监测channel是否关闭,可以防止读取已经关闭的channel,造成读到数据为通道所存储的数据类型的零值。 246 | 247 | ```go 248 | for x := range ch{ 249 | fmt.Println(x) 250 | } 251 | ``` 252 | 2. 使用v,ok := <-ch + select操作判断channel是否关闭 253 | 254 | 场景:判断channel是否关闭.其中ok的含义是: 255 | ```markdown 256 | true:读到通道数据,不确定是否关闭,可能channel还有保存的数据,但channel已关闭。 257 | false:通道关闭,无数据读到。 258 | ``` 259 | 从关闭的channel读值读到是channel所传递数据类型的零值,这个零值有可能是发送者发送的,也可能是channel关闭了。 260 | 261 | 情况1:当chanrecv返回(false,false)时,本质是select操作失败了,所以相关的case会阻塞,不会执行: 262 | 263 | ```go 264 | func main() { 265 | ch := make(chan int) 266 | select { 267 | case v, ok := <-ch: 268 | fmt.Printf("v: %v, ok: %v\n", v, ok) 269 | default: 270 | fmt.Println("nothing") 271 | } 272 | } 273 | ``` 274 | 情况2:下面的结果会是零值和false: 275 | 276 | ```go 277 | func main() { 278 | ch := make(chan int) 279 | // 增加关闭 280 | close(ch) 281 | 282 | select { 283 | case v, ok := <-ch: 284 | fmt.Printf("v: %v, ok: %v\n", v, ok) 285 | } 286 | } 287 | ``` 288 | 289 | 向channel写数据然后关闭,依然可以从已关闭channel读到有效数据,但channel关闭且没有数据时,读不到有效数据,ok为false,可以确定当前channel已关闭。 290 | ```go 291 | func main() { 292 | ch := make(chan int, 1) 293 | 294 | // 发送1个数据关闭channel 295 | ch <- 1 296 | close(ch) 297 | print("close channel\n") 298 | 299 | // 不停读数据直到channel没有有效数据 300 | for { 301 | select { 302 | case v, ok := <-ch: 303 | print("v: ", v, ", ok:", ok, "\n") 304 | if !ok { 305 | print("channel is close\n") 306 | return 307 | } 308 | default: 309 | print("nothing\n") 310 | } 311 | } 312 | } 313 | 314 | // 结果 315 | // close channel 316 | // v: 1, ok:true 317 | // v: 0, ok:false 318 | // channel is close 319 | ``` 320 | 321 | 3. 使用select处理多个channel 322 | 323 | 场景:需要对多个通道进行同时处理,但只处理最先发生的channel时. 324 | 325 | select可以同时监控多个通道的情况,只处理未阻塞的case。当通道为nil时,对应的case永远为阻塞,无论读写。 326 | 327 | 这里需要注意下特殊关注:普通情况下,对nil的通道写操作是要panic的。 328 | 329 | ```go 330 | // 分配job时,如果收到关闭的通知则退出,不分配job 331 | func (h *Handler) handle(job *Job) { 332 | select { 333 | case h.jobCh<-m: 334 | return 335 | case <-h.stopCh: 336 | return 337 | } 338 | } 339 | ``` 340 | 341 | 4. 使用channel的声明控制读写权限 342 | 343 | 场景:协程对某个通道只读或只写时. 344 | 345 | 目的: 346 | 347 | * 使代码更易读、更易维护, 348 | * 防止只读协程对通道进行写数据,但通道已关闭,造成panic, 349 | 350 | 通常如果协程对某个channel只有写操作,则这个channel声明为只写。如果协程对某个channel只有读操作,则这个channe声明为只读。 351 | ```go 352 | // 只有generator进行对outCh进行写操作,返回声明 353 | // <-chan int,可以防止其他协程乱用此通道,造成隐藏bug 354 | func generator(int n) <-chan int { 355 | outCh := make(chan int) 356 | go func(){ 357 | for i:=0;i= len(src) { 280 | err = errShortInput 281 | break Loop 282 | } 283 | if validateOnly { 284 | break 285 | } 286 | size = 2 287 | update(src[n] + src[n+1]< b 297 | func Compare(a, b []byte) int { 298 | for i := 0; i < len(a) && i < len(b); i++ { 299 | switch { 300 | case a[i] > b[i]: 301 | return 1 302 | case a[i] < b[i]: 303 | return -1 304 | } 305 | } 306 | switch { 307 | case len(a) > len(b): 308 | return 1 309 | case len(a) < len(b): 310 | return -1 311 | } 312 | return 0 313 | } 314 | 315 | ``` 316 | 317 | 使用类型断言的语法和关键字类型: 318 | ```go 319 | var t interface{} 320 | t = functionOfSomeType() 321 | switch t := t.(type) { 322 | default: 323 | fmt.Printf("unexpected type %T\n", t) // %T prints whatever type t has 324 | case bool: 325 | fmt.Printf("boolean %t\n", t) // t has type bool 326 | case int: 327 | fmt.Printf("integer %d\n", t) // t has type int 328 | case *bool: 329 | fmt.Printf("pointer to boolean %t\n", *t) // t has type *bool 330 | case *int: 331 | fmt.Printf("pointer to integer %d\n", *t) // t has type *int 332 | } 333 | 334 | ``` 335 | * return 336 | 337 | 尽早return:一旦有错误发生,马上返回 338 | ```go 339 | f, err := os.Open(name) 340 | if err != nil { 341 | return err 342 | } 343 | d, err := f.Stat() 344 | if err != nil { 345 | f.Close() 346 | return err 347 | } 348 | codeUsing(f, d) 349 | 350 | ``` 351 | 352 | #### 11.方法的接收器 353 | * 名称 一般采用strcut的第一个字母且为小写,而不是this或者其他不符合的命名习惯 354 | ```go 355 | type Buffer struct { 356 | buf []byte // contents are the bytes buf[off : len(buf)] 357 | off int // read at &buf[off], write at &buf[len(buf)] 358 | bootstrap [64]byte // memory to hold first slice; helps small buffers avoid allocation. 359 | lastRead readOp // last read operation, so that Unread* can work correctly. 360 | 361 | // FIXME: it would be advisable to align Buffer to cachelines to avoid false 362 | // sharing. 363 | } 364 | 365 | func (b *Buffer) Bytes() []byte { return b.buf[b.off:] } 366 | ``` 367 | 368 | * 如果接收者是map,slice或者chan,不要用指针传递 369 | ```go 370 | //Map 371 | package main 372 | 373 | import ( 374 | "fmt" 375 | ) 376 | 377 | type mp map[string]string 378 | 379 | func (m mp) Set(k, v string) { 380 | m[k] = v 381 | } 382 | 383 | func main() { 384 | m := make(mp) 385 | m.Set("k", "v") 386 | fmt.Println(m) 387 | } 388 | ``` 389 | 390 | ```go 391 | //Channel 392 | package main 393 | 394 | import ( 395 | "fmt" 396 | ) 397 | 398 | type ch chan interface{} 399 | 400 | func (c ch) Push(i interface{}) { 401 | c <- i 402 | } 403 | 404 | func (c ch) Pop() interface{} { 405 | return <-c 406 | } 407 | 408 | func main() { 409 | c := make(ch, 1) 410 | c.Push("i") 411 | fmt.Println(c.Pop()) 412 | } 413 | 414 | ``` 415 | * 如果需要对slice进行修改,通过返回值的方式重新赋值 416 | 417 | ```go 418 | //Slice 419 | package main 420 | 421 | import ( 422 | "fmt" 423 | ) 424 | 425 | type slice []byte 426 | 427 | func main() { 428 | s := make(slice, 0) 429 | s = s.addOne(42) 430 | fmt.Println(s) 431 | } 432 | 433 | func (s slice) addOne(b byte) []byte { 434 | return append(s, b) 435 | } 436 | 437 | ``` 438 | * 如果接收者是含有sync.Mutex或者类似同步字段的结构体,必须使用指针传递避免复制 439 | 440 | ```go 441 | 442 | package main 443 | 444 | import ( 445 | "sync" 446 | ) 447 | 448 | type T struct { 449 | m sync.Mutex 450 | } 451 | 452 | func (t *T) lock() { 453 | t.m.Lock() 454 | } 455 | 456 | func main() { 457 | t := new(T) 458 | t.lock() 459 | } 460 | ``` 461 | 462 | * 如果接收者是大的结构体或者数组,使用指针传递会更有效率。 463 | 464 | ```go 465 | package main 466 | 467 | import ( 468 | "fmt" 469 | ) 470 | 471 | type T struct { 472 | data [1024]byte 473 | } 474 | 475 | func (t *T) Get() byte { 476 | return t.data[0] 477 | } 478 | 479 | func main() { 480 | t := new(T) 481 | fmt.Println(t.Get()) 482 | } 483 | ``` 484 | * append 485 | ```go 486 | 487 | var a, b []int 488 | b = append(b, a...) 489 | 490 | ``` 491 | 492 | * 使用strings.TrimPrefix 去掉前缀,strings.TrimSuffix去掉后缀 493 | ```go 494 | var s1 = "a value" 495 | var s2 = "a" 496 | var s3 = strings.TrimPrefix(s1, s2) 497 | ``` 498 | 499 | ```go 500 | var s1 = "value" 501 | var s2 = "e" 502 | var s3 = strings.TrimSuffix(s1,s2) 503 | ``` 504 | 505 | 506 | #### 12.使用工具检查你的代码 507 | * [go fmt](https://golang.org/pkg/fmt/) 508 | * [go vet](https://golang.org/cmd/vet/) 509 | * [go simple](https://github.com/dominikh/go-tools/tree/master/cmd/gosimple) 510 | * [go lint](https://github.com/golang/lint) 511 | * [errcheck](https://github.com/kisielk/errcheck) 512 | * [misspell](https://github.com/client9/misspell) 513 | 514 | 515 | #### 参考文档 516 | 517 | * [Effective Go](https://golang.org/doc/effective_go.html) 518 | 519 | 520 | #### License 521 | 522 | This is free software distributed under the terms of the MIT license 523 | -------------------------------------------------------------------------------- /src/spec/03.0.md: -------------------------------------------------------------------------------- 1 | #### For-learning-Go-Tutorial 2 | 3 | 我们详细的探索和了解下slice 和数组的区别: 4 | 5 | slice 的底层数据是数组,slice 是对数组的封装,它描述一个数组的片段。两者都可以通过下标来访问单个元素。 6 | 7 | 数组是定长的,长度定义好之后,不能再更改。在 Go 中,数组是不常见的,因为其长度是类型的一部分,限制了它的表达能力,比如 [3]int 和 [4]int 就是不同的类型。 8 | 9 | 而切片则非常灵活,它可以动态地扩容。切片的类型和长度无关。 10 | 11 | 数组就是一片连续的内存, slice 实际上是一个结构体,包含三个字段:长度、容量、底层数组。 12 | 13 | slice 的结构: 14 | ```go 15 | // runtime/slice.go 16 | type slice struct { 17 | array unsafe.Pointer // 元素指针 18 | len int // 长度 19 | cap int // 容量 20 | } 21 | ``` 22 | 23 | slice 的数据结构如下: 24 | 25 |

26 | 27 |

28 | 29 | 注意,底层数组是可以被多个 slice 同时指向的,因此对一个 slice 的元素进行操作是有可能影响到其他 slice 的。 30 | 31 | Go示例代码: 32 | ```markdown 33 | package main 34 | 35 | import "fmt" 36 | 37 | func main() { 38 | slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} 39 | s1 := slice[2:5] 40 | s2 := s1[2:6:7] 41 | 42 | s2 = append(s2, 100) 43 | s2 = append(s2, 200) 44 | 45 | s1[2] = 20 46 | 47 | fmt.Println(s1) 48 | fmt.Println(s2) 49 | fmt.Println(slice) 50 | } 51 | ``` 52 | 53 | 运行结果: 54 | ```bash 55 | [2 3 20] 56 | [4 5 6 7 100 200] 57 | [0 1 2 3 20 5 6 7 100 9] 58 | ``` 59 | 60 | s1 从 slice 索引2(闭区间)到索引5(开区间,元素真正取到索引4),长度为3,容量默认到数组结尾,为8。 s2 从 s1 的索引2(闭区间)到索引6(开区间,元素真正取到索引5),容量到索引7(开区间,真正到索引6),为5。 61 | 62 | 这里需要注意的是: 63 | ```markdown 64 | 操作 含义 65 | s[n] 切片s中索引位置为n的项. 66 | s[:] 初始化切片s,是数组的引用。 67 | s[low:] 从切片s的索引位置low到len(s)-1处所获得的切片. 68 | s[:high] 从切片s的索引位置0到high处所获得的切片,len=high. 69 | s[low:high] 从切片s的索引位置low到high处所获得的切片,len=high-low. 70 | s[low:high:max] 从切片s的索引位置low到high处所获得的切片,len=high-low,cap=max-low. 71 | len(s) 切片s的长度,总是<=cap(s). 72 | cap(s) 切片s的容量,总是>=len(s). 73 | ``` 74 | 75 | 因此: 76 | 77 |

78 | 79 |

80 | 81 | 接着,向 s2 尾部追加一个元素 100: 82 | ```markdown 83 | s2 = append(s2, 100) 84 | ``` 85 | s2 容量刚好够,直接追加。不过,这会修改原始数组对应位置的元素。这一改动,数组和 s1 都可以看得到。 86 |

87 | 88 |

89 | 90 | 再次向 s2 追加元素200: 91 | ```markdown 92 | s2 = append(s2, 100) 93 | ``` 94 | 95 | 这时,s2 的容量不够用,该扩容了。于是,s2 另起炉灶,将原来的元素复制新的位置,扩大自己的容量。并且为了应对未来可能的 append 带来的再一次扩容,s2 会在此次扩容的时候多留一些 buffer,将新的容量将扩大为原始容量的2倍,也就是10了。 96 | 97 |

98 | 99 |

100 | 101 | 最后,修改 s1 索引为2位置的元素: 102 | 103 | ```markdown 104 | s1[2] = 20 105 | ``` 106 | 107 | 这次只会影响原始数组相应位置的元素。它影响不到 s2 了。 108 | 109 |

110 | 111 |

112 | 113 | 最后在注意下,打印 s1 的时候,只会打印出 s1 长度以内的元素。所以,只会打印出3个元素,虽然它的底层数组不止3个元素。 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | #### License 129 | 130 | This is free software distributed under the terms of the MIT license 131 | --------------------------------------------------------------------------------