├── .vscode └── settings.json ├── Ansible └── Ansible2.2 Notebook │ ├── 1. Inventory.md │ ├── 2. Pattern.md │ ├── 3. Ad-hoc Commands.md │ └── 4. Playbook.md ├── C_Lib └── Function&Macro │ ├── std_assert_assert.md │ ├── std_ctype_isalnum.md │ ├── std_ctype_isalpha.md │ ├── std_ctype_iscntrl.md │ ├── std_ctype_isdigit.md │ ├── std_ctype_isgraph.md │ ├── std_ctype_islower.md │ ├── std_ctype_isprint.md │ ├── std_ctype_ispunct.md │ ├── std_ctype_isspace.md │ ├── std_ctype_isupper.md │ ├── std_ctype_isxdigit.md │ ├── std_ctype_tolower.md │ ├── std_ctype_toupper.md │ ├── std_locale_localeconv.md │ ├── std_locale_setlocale.md │ ├── std_math_acos.md │ ├── std_math_asin.md │ ├── std_math_atan.md │ ├── std_math_atan2.md │ ├── std_math_ceil.md │ ├── std_math_cos.md │ ├── std_math_cosh.md │ ├── std_math_exp.md │ ├── std_math_floor.md │ ├── std_math_fmod.md │ ├── std_math_frexp.md │ ├── std_math_ldexp.md │ ├── std_math_log.md │ ├── std_math_log10.md │ ├── std_math_modf.md │ ├── std_math_pow.md │ ├── std_math_powf.md │ ├── std_math_sin.md │ ├── std_math_sinh.md │ ├── std_math_sqrt.md │ ├── std_math_tan.md │ └── std_math_tanh.md ├── Docker ├── 2018-10-09 使用基于Docker的phpmyadmin访问本地MySQL.md └── 2019-06-15 基于 Container 网络共享机制的抓包实践.md ├── GoMicro └── 2019-07-04 关于微服务架构中服务通信的思考.md ├── GoPackages ├── 2016-03-22 redigo 使用连接池和不使用连接池的性能对比测试.md ├── 2016-03-22 redigo 基础知识讲解之客户端连接.md ├── 2018-09-06 k8s 依赖库中的wait功能.md ├── 2018-09-14 k8s 依赖库中的go-restful框架.md └── 2018-11-05 基于Websocket的简易webshell实现.md ├── Golang ├── 2015-11-30 Go标准库strings中的一些容易令人混淆的方法.md ├── 2015-12-19 Go里面除了引用语义之外的类型都可以作为字典的key.md ├── 2016-02-16 Go中根据函数名称调用指定函数的使用方法简介.md ├── 2016-10-24 理解golang的匿名函数的变量作用域.md ├── 2017-05-31 Go语言包之 strings.markdown ├── 2018-09-15 Go中Context的使用方法.md ├── 2020-05-12 Go Module私有库支持HTTP的方案.md ├── 2020-06-29 Go HTTP重用底层TCP连接需要注意的关键点.md ├── 2020-07-01 Go HTTP 默认的Client对象使用注意事项.md ├── Code │ └── go-restful │ │ └── src │ │ ├── build.sh │ │ ├── main.go │ │ └── userservice │ │ └── userservice.go └── images │ ├── tcp_reuse_no.png │ └── tcp_reuse_yes.png ├── Go示例学 ├── Go Base64编码.markdown ├── Go Defer.markdown ├── Go Exit.markdown ├── Go JSON支持.markdown ├── Go Line Filters.markdown ├── Go Panic.markdown ├── Go SHA1 散列.markdown ├── Go String与Byte切片之间的转换.markdown ├── Go URL解析.markdown ├── Go for循环.markdown ├── Go if..else if..else 条件判断.markdown ├── Go range函数.markdown ├── Go switch语句.markdown ├── Go 互斥.markdown ├── Go 信号处理.markdown ├── Go 关闭通道.markdown ├── Go 写入文件.markdown ├── Go 函数命名返回值.markdown ├── Go 函数回调.markdown ├── Go 函数多返回值.markdown ├── Go 函数定义.markdown ├── Go 切片.markdown ├── Go 原子计数器.markdown ├── Go 变量.markdown ├── Go 可变长参数列表.markdown ├── Go 命令行参数.markdown ├── Go 命令行参数标记.markdown ├── Go 字典.markdown ├── Go 字符串操作函数.markdown ├── Go 字符串格式化.markdown ├── Go 工作池.markdown ├── Go 常量.markdown ├── Go 并行功能.markdown ├── Go 并行通道Channel.markdown ├── Go 打点器.markdown ├── Go 指针.markdown ├── Go 排序.markdown ├── Go 接口.markdown ├── Go 数值.markdown ├── Go 数字解析.markdown ├── Go 数组.markdown ├── Go 方法.markdown ├── Go 时间.markdown ├── Go 时间戳.markdown ├── Go 时间格式化和解析.markdown ├── Go 正则表达式.markdown ├── Go 状态协程.markdown ├── Go 环境变量.markdown ├── Go 经典hello world.md ├── Go 结构体.markdown ├── Go 自定义排序.markdown ├── Go 计时器.markdown ├── Go 请求处理频率控制.markdown ├── Go 读取文件.markdown ├── Go 超时.markdown ├── Go 进程执行.markdown ├── Go 进程触发.markdown ├── Go 递归函数.markdown ├── Go 通道方向.markdown ├── Go 通道的同步功能.markdown ├── Go 通道缓冲.markdown ├── Go 通道选择Select.markdown ├── Go 遍历通道.markdown ├── Go 错误处理.markdown ├── Go 闭包函数.markdown ├── Go 随机数.markdown ├── Go 集合功能.markdown ├── Go 非阻塞通道.markdown └── README.md ├── Go轻松学 ├── go_tutorial_0_what_to_learn.md ├── go_tutorial_10_use_package_test.md ├── go_tutorial_1_how_to_install_go.md ├── go_tutorial_2_data_type.md ├── go_tutorial_3_variable.md ├── go_tutorial_4_control_structure.md ├── go_tutorial_5_array_slice_map.md ├── go_tutorial_6_func.md ├── go_tutorial_7_pointer.md ├── go_tutorial_8_struct_interface.md ├── go_tutorial_9_parallel_compute.md ├── golang_500x500.png └── qrcode_duokr.jpg ├── Jenkins ├── 2018-10-09 Jenkins常用API汇总.md └── images │ ├── jenkins-add-git-repo.png │ ├── jenkins-create-a-freestyle-job.png │ ├── private-key-in-jenkins.png │ └── public-key-in-gitlab.png ├── Kubernetes ├── 2018-09-11 kubelet使用imagePullSecret来拉取镜像.md ├── 2018-09-13 kubelet报错 broken pipe for writing log.md ├── 2018-09-20 Kubernetes API访问鉴权之Basic模式.md ├── 2019-06-11 Kubernetes 中使用插件 sniff 进行网络抓包.md ├── 2019-06-12 Kubernetes 中如何开发一个kubectl的插件命令.md ├── 2019-06-15 自定义Ingress的默认404页面.md ├── 2020-12-04 无头(Headless)Service的使用方法.md └── images │ └── default-backend-404.png ├── README.md ├── ScalaTutorial ├── README.md ├── default_parameter_values.md ├── named_arguments.md └── scala-tutorial │ ├── NamedParameter$.class │ ├── NamedParameter.class │ ├── default_parameter_value.scala │ └── named_parameter.scala └── Translations ├── 2018-09-17 Go并发模型: context.md ├── 2019-06-20 深入了解 Go 的类型系统.md └── closer-look-at-go-type-system ├── snippet-figure1.png ├── snippet-figure2.png └── snippet-figure3.png /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | } -------------------------------------------------------------------------------- /Ansible/Ansible2.2 Notebook/2. Pattern.md: -------------------------------------------------------------------------------- 1 | # Pattern 2 | 3 | 1. 匹配模式用于过滤出待管理的目标主机。对于Playbook来讲,就是要将IT流程应用在哪些主机上,例如: 4 | ``` 5 | $ ansible -m -a 6 | ``` 7 | 2. 使用 `all` 或 `*` 指向所有的主机; 8 | 3. 支持指向具体的 Host 或者是通过名称指定一组 Host: 9 | ``` 10 | a) one.example.com 11 | b) one.example.com:two.exmaple.com # 其中冒号(:)表示 OR,是并集 12 | c) 192.0.2.50 13 | d) 192.0.2.* 14 | ``` 15 | 4. 支持指向某个 Group 或者一组 Group 16 | ``` 17 | a) webservers 18 | b) webservers:dbservers 19 | ``` 20 | 5. 支持使用排除法 (A-B) 21 | ``` 22 | a) webservers:!phoenix # 表示在webservers中但是不在phoenix中的Host 23 | ``` 24 | 6. 支持使用 `&` 来指定 A 和 B 的两个组的交集 25 | ``` 26 | a) webservers:&staging 27 | ``` 28 | 7. 支持使用组合式(由左向右计算) 29 | ``` 30 | a) webservers:dbservers:&staging:!phoenix 31 | ``` 32 | 8. 支持使用变量插值的方式以支持在 Playbook 中通过选项 `-e` 来传值 33 | ``` 34 | webservers:!{{excluded}}:&{{required}} 35 | ``` 36 | 9. 支持使用通配符和组合式方案 37 | ``` 38 | a) *.example.com 39 | b) *.com 40 | c) one*.com:dbservers 41 | ``` 42 | 10. 支持以一个组的所以来获取组中的一些Host作为目标 43 | ``` 44 | [webservers] 45 | cobweb 46 | webbing 47 | weber 48 | ``` 49 | ``` 50 | a) webservers[0] 返回第一个元素 cobweb 51 | b) webservers[-1] 返回最后一个元素 weber 52 | c) webservers[0:1] 返回第一个和第二个元素 cobweb 和 webbing 53 | d) webservers[1:] 返回第一个元素到最后的所有元素,即 webbing 和 weber 54 | ``` 55 | 11. 支持使用正则表达式来过滤Host,使用 `~` 开始匹配 56 | ``` 57 | a) ~(web|db).*\.example\.com 58 | ``` 59 | 12. 可以给 ansible 或者 ansible-playbook 指定一个 limit 的选项来排除一些 Host,比如进一步限定Host在 datacenters 组中; 60 | ``` 61 | $ ansible-playbook site.yml --limit datacenters 62 | ``` 63 | 如果希望从文件中读取一组Host,可以使用 `@` 前缀后面跟上文件名 64 | ``` 65 | $ ansible-playbook site.yml --limit @retry_hosts.txt 66 | ``` -------------------------------------------------------------------------------- /Ansible/Ansible2.2 Notebook/3. Ad-hoc Commands.md: -------------------------------------------------------------------------------- 1 | # Ad-hoc Command 2 | 3 | ## Ad-hoc 命令 4 | 5 | Ad-hoc 命令就是在命令行可以即时执行的命令。 6 | 7 | - 并行执行(parallelism) & Shell 命令 8 | - 文件传输(File Transfer) 9 | - 管理包(Managing Packages) 10 | - 用户和组(User & Group) 11 | - 从源码控制部署(Deploy From Source Control) 12 | - 管理服务(Managing Services) 13 | - 有限执行时间的操作(Time Limited Background Operations) 14 | - 收集Facts(Collecting Facts) 15 | 16 | ## 并行执行 & Shell命令 17 | 18 | ``` 19 | $ ansible dbservers -a "/sbin/roboot" -f 10 # 同时使用10个进程来操作 20 | ``` 21 | 22 | ``` 23 | a) 上面的命令使用的是默认的模块 command 24 | b)默认的并发值为5,可以按照需要调整 25 | c)command 模块不支持扩展的 Shell语法,比如 pipling 和 redirects(尽管 shell 变量还是可以使用的),如果需要 shell 的特定语法,可以使用 shell 模块 26 | 27 | $ ansible dbservers -m shell -a 'echo $TERM' 28 | ``` 29 | 30 | ## 文件传输 31 | 32 | ``` 33 | $ ansible dbservers -m copy -a "src=/etc/hosts dest=/tmp/hosts" 34 | ``` 35 | 36 | 如果使用 Playbook 的话,还可以使用 template 模块支持更多的功能。 37 | 38 | 可以使用 file 模块来支持设置文件的属性(chown)和更改文件的权限(chmod)。 39 | 40 | ``` 41 | $ ansible dbservers -m file -a 'dest=/tmp/hello.txt mode=600' 42 | 43 | $ ansible dbservers -m file -a 'dest=/tmp/hello.txt mode=600 owner=root group=root' 44 | ``` 45 | 46 | 另外 file 模块也支持用来创建目录。 47 | 48 | ``` 49 | $ ansible dbservers -m file -a 'dest=/tmp/workspace/ mode=755 owner=root group=root state=directory' 50 | ``` 51 | 52 | file 模块也支持递归删除目录和文件。 53 | 54 | ``` 55 | $ ansible dbservers -m file -a 'dest=/tmp/workspace/ state=absent' 56 | ``` 57 | 58 | ## 管理包(使用 yum / apt 模块等) 59 | 60 | ``` 61 | # 确保package存在,但不更新 62 | $ ansible dbservers -m yum -a 'name=curl state=present' 63 | 64 | # 确保指定的版本存在 65 | $ ansible dbservers -m yum -a 'name=curl-7.29 state=present' 66 | 67 | # 确保是最新的版本 68 | $ ansible dbservers -m yum -a 'name=curl state=latest' 69 | 70 | # 确保package没有安装 71 | $ ansible dbservers -m yum -a 'name=curl state=absent' 72 | ``` 73 | 74 | ## 用户和组(User & Group) 75 | 76 | ``` 77 | a) 创建或管理已有用户 78 | b)删除已有的用户 79 | ``` 80 | 81 | ``` 82 | $ ansible all -m user -a 'name=jenkins password=' 83 | 84 | $ ansible all -m user -a 'name=jenkins state=absent' 85 | ``` 86 | 87 | ## 从源码控制部署 88 | 89 | ``` 90 | $ ansible webservers -m git -a 'repo=https://git.xxx.com/xxx/repo.git dest=/srv/myapp version=HEAD' 91 | ``` 92 | 93 | ## 管理系统服务(启动,停止,重启) 94 | 95 | ``` 96 | $ ansible webservers -m service -a 'name=httpd state=started' 97 | 98 | $ ansible webservers -m service -a 'name=httpd state=restarted' 99 | 100 | $ ansible webservers -m service -a 'name=httpd state=stopped' 101 | ``` 102 | 103 | ## 有限时间的任务后台执行 104 | 105 | 长时间执行的任务可以放后台执行,并且可以稍后检测状态,一般用于长时间执行的任务或者软件更新。 106 | 107 | ``` 108 | # 其中 -B 表示超时时间,-P 表示轮询结果的时间间隔 109 | $ ansible all -B 300 -P 0 -a '/bin/xxx --do-stuff' 110 | ``` 111 | 112 | 也可以使用 `async_status` 模块来检测任务的状态,传入上面命令返回的Job ID即可。 113 | 114 | ``` 115 | $ ansible all -m async_status -a 'jid=xxx' 116 | ``` 117 | 118 | 轮询功能是内置的,可以按照如下的方式设置,例如每隔1分钟(60s)轮询一次结果。 119 | 120 | ``` 121 | $ ansible all -B 1800 -P 60 -a '/bin/xxx --do-stuff' 122 | ``` 123 | 124 | ## 收集Facts 125 | 126 | Facts 是 Playbook 中的 Section 中描述的表示一个系统状态的变量,可以用来实现Task的条件执行,也可以用来获取系统的信息。 127 | 128 | ``` 129 | # 查看系统所有的 Facts 130 | $ ansible all -m setup 131 | ``` -------------------------------------------------------------------------------- /Ansible/Ansible2.2 Notebook/4. Playbook.md: -------------------------------------------------------------------------------- 1 | # Playbook 2 | 3 | ## Playbook 基础 4 | 5 | 1. Playbook 是 Ansible 的配置,部署和编排语言。它可以用来定义一个希望在远程系统上面应用的策略或者一组IT工作流; 6 | 2. Playbook 的内容通过 YAML 来描述,每个 Playbook 可以由一个或者多个 Play 组成; 7 | 3. Play 的目标是将一组Host映射到已经定义好的角色上面;这些角色由 Task 来表示,所谓的 Task 是指一组对 Ansible 模块功能的调用; 8 | 4. 通过在一个 Playbook 中组织起一组 Play,我们可以实现多机器的部署编排; 9 | 5. 下面是一个只有一个 Play 的例子: 10 | ``` 11 | - hosts: webservers 12 | vars: 13 | http_port: 80 14 | max_clients: 200 15 | remote_user: root 16 | tasks: 17 | - name: ensure apache is at the latest version 18 | yum: name=httpd state=latest 19 | - name: write the apache config file 20 | template: src=/srv/httpd.j2 dest=/etc/httpd.conf 21 | notify: 22 | - restart apache 23 | - name: ensure apache is running (and enable it at boot) 24 | service: name=httpd state=started enabled=yes 25 | handlers: 26 | - name: restart apache 27 | service: name=httpd state=restarted 28 | ``` 29 | 6. 当 Task 的参数比较长或者模块有很多参数的时候,可以把参数部分拆分到多行上面来提高可读性,以下是另外一个使用 YAML 字典来模块提供参数的方法: 30 | ``` 31 | - hosts: webservers 32 | vars: 33 | http_port: 80 34 | max_clients: 200 35 | remote_user: root 36 | tasks: 37 | - name: ensure apache is at the latest version 38 | yum: 39 | name: httpd 40 | state: latest 41 | - name: write the apache config file 42 | template: 43 | src: /srv/httpd.j2 44 | dest: /etc/httpd.conf 45 | notify: 46 | - restart apache 47 | - name: ensure apache is running (and enable it at boot) 48 | service: 49 | name: httpd 50 | state: started 51 | enabled: yes 52 | handlers: 53 | - name: restart apache 54 | service: 55 | name: httpd 56 | state: restarted 57 | ``` 58 | 7. 一个Playbook可以包含多个Play,可以在一个 Playbook 中先定义面向 webservers 的操作,然后在定义面向 databases 的操作,例如: 59 | ``` 60 | - hosts: webservers 61 | remote_user: root 62 | 63 | tasks: 64 | - name: ensure apache is at the latest version 65 | yum: 66 | name: httpd 67 | state: latest 68 | - name: write the apache config file 69 | template: 70 | src: /srv/httpd.j2 71 | dest: /etc/httpd.conf 72 | 73 | - hosts: databases 74 | remote_user: root 75 | 76 | tasks: 77 | - name: ensure postgresql is at the latest version 78 | yum: 79 | name: postgresql 80 | state: latest 81 | name: ensure that postgresql is started 82 | service: 83 | name: postgresql 84 | state: started 85 | ``` 86 | 8. Playbook 中的 Play 执行顺序是从上到下,即按照在 Playbook 中定义的先后次序来依次执行; 87 | 88 | ## Hosts & Users 89 | 90 | 1. 对 Playbook 中的 Play 来讲,首先选择目标主机和所使用的目标远程用户; 91 | 2. `hosts` 为基于一个或者多个组或者主机匹配的 Pattern组成,彼此之间用冒号分隔, `remote_user` 为远程用户; 92 | ``` 93 | - hosts: webservers 94 | remote_user: root 95 | ``` 96 | 3. `remote_user` 参数一般称之为`user`,在 Ansible 1.4 中这个参数重命名为 `remote_user` 以此来和提供用户管理的 `user` 模块区分开; 97 | 4. `remote_user` 可以基于每个 Task 来定义(这个也是在 Ansible 1.4 中支持的): 98 | ``` 99 | - hosts: webservers 100 | remote_user: root 101 | tasks: 102 | - name: test connection 103 | ping: 104 | remote_user: yourname 105 | ``` 106 | 5. 也支持通过权限放大来以其他用户身份运行: 107 | ``` 108 | - hosts: webservers 109 | remote_user: yourname 110 | become: yes 111 | ``` 112 | 也可以基于某个 Task 来切换身份: 113 | ``` 114 | - hosts: webservers 115 | remote_user: yourname 116 | tasks: 117 | - service: 118 | name: ngixn 119 | state: started 120 | become: yes 121 | become_method: sudo 122 | ``` 123 | 可以以自身身份登入,然后以其他非root身份执行任务: 124 | ``` 125 | - hosts: webservers 126 | remote_user: yourname 127 | become: yes 128 | become_user: postgres 129 | ``` 130 | 也可以使用其他的权限放大的方法: 131 | ``` 132 | - hosts: webservers 133 | remote_user: yourname 134 | become: yes 135 | become_method: su 136 | ``` 137 | 6. 如果希望为 sudo 指定一个密码,可以在运行 ansible-playbook 的时候添加选项 --ask-become-pass 。如果在运行一个带有 become 的 Playbook 卡住的时候,很有可能是卡在了权限放大提示输入密码环节; 138 | 7. 可以在 Ansible 2.4 版本之后控制 Hosts 的运行顺序,默认的顺序就是 Inventory 中定义的顺序; 139 | ``` 140 | - hosts: all 141 | order: sorted 142 | gather_facts: False 143 | tasks: 144 | - debug: var=inventory_hostname 145 | ``` 146 | 其中的 `order` 参数决定 Hosts 的运行顺序,默认情况下为 `inventory`,支持的可选值如下: 147 | ``` 148 | inventory 默认参数,为 Inventory 中 Host 的定义顺序; 149 | reverse_inventory 为 Inventory 中 Host 的定义顺序的逆序 150 | sorted 按照字母方式的 Host 名称排序 151 | reverse_sorted 按照字母方式的 Host 名称逆序排序 152 | shuffle 以随机的顺序来运行 153 | ``` -------------------------------------------------------------------------------- /C_Lib/Function&Macro/std_assert_assert.md: -------------------------------------------------------------------------------- 1 | #功能 2 | 断言是C语言中用来进行异常处理的一种方式。在程序中启用断言时,当特定的 3 | 断言表达式值为假的时候,程序将终止执行并向stderr输出一条信息。 4 | #原型 5 | 6 | `#include ` 7 | `void assert(int expression)` 8 | 9 | #实例 10 | 首先看一下基本用法 11 | ```c 12 | #include 13 | #define NDEBUG 14 | #include 15 | #include 16 | int 17 | main(int argc, char *argv[]) 18 | { 19 | FILE *fp; 20 | fp = fopen("test1.txt", "w"); 21 | assert(fp); 22 | fclose(fp); 23 | 24 | fp = fopen("test2.txt", "r"); 25 | assert(fp); 26 | fclose(fp); 27 | return 0; 28 | } 29 | ``` 30 | 假设上面的test1.txt和test2.txt都不存在,那么第一个以只写方式打开 31 | test1.txt时,fp不为NULL,所以assert的表达式为真。当第二次以只读 32 | 的方式读取不存在的test2.txt时,那么fp就为NULL了,这样assert断言 33 | 就为假,所以这段程序的输出结果为: 34 | ```shell 35 | Assertion failed: (fp), function main, file assert1.c, line 11. 36 | Abort trap: 6 37 | ``` 38 | #备注 39 | 1. assert是一个宏,并不是一个函数。 40 | 2. 使用assert的缺点是,频繁的调用会极大地影响程序的性能,增加额外的开销。 41 | 3. assert功能只有在调试模式下生效。 42 | 4. 可以在调试结束后,通过在引用`#include `之前添加 43 | `#define NDEBUG`来禁用assert调用。 44 | 5. 断言可以用来在函数开始处检查传入参数的合法性。 45 | 6. 每个assert只检验一个条件,因为同时检验多个条件时,如果断言失败,无法只管地 46 | 判断是那个条件失败了。 47 | 7. 不能使用判断断言条件时同时改变表达式中变量值的语句。比如`assert(i++<10)` 48 | 这样的语句一旦断言失败,`i++`也不会去执行。 49 | -------------------------------------------------------------------------------- /C_Lib/Function&Macro/std_ctype_isalnum.md: -------------------------------------------------------------------------------- 1 | #功能 2 | 判断字符变量c是否为字母或数字。 3 | 即当c为字母a-z或A-Z或者为0-9时,返回非零值,否则返回零。 4 | #原型 5 | `#include ` 6 | `extern int isalnum(int c)` 7 | #实例 8 | 这个函数很简单,这里的字符c是ASCII字符,即'a'-'z','A'-'Z','0'-'9'。 9 | ```c 10 | #include 11 | #include 12 | int 13 | main(int argc, char *argv[]) 14 | { 15 | int c = 'a'; 16 | printf("'%c' -> %d\n", c, isalnum(c)); 17 | c = '>'; 18 | printf("'%c' -> %d\n", c, isalnum(c)); 19 | c = '1'; 20 | printf("'%c' -> %d\n", c, isalnum(c)); 21 | c = 48; 22 | printf("'%c' -> %d\n", c, isalnum(c)); 23 | return 0; 24 | } 25 | ``` 26 | 输出结果为 27 | ```shell 28 | 'a' -> 1 29 | '>' -> 0 30 | '1' -> 1 31 | '0' -> 1 32 | ``` 33 | #备注 34 | 注意这里的字符`c`是ASCII字符,比如将上面的`c`赋值为`48`,那么对应ASCII字符为`'0'`。 35 | -------------------------------------------------------------------------------- /C_Lib/Function&Macro/std_ctype_isalpha.md: -------------------------------------------------------------------------------- 1 | #功能 2 | 判断字符c是否为英文字母。 3 | 即当c为字母a-z或A-Z时,返回非零值,否则返回零。 4 | #原型 5 | `#include ` 6 | `extern int isalpha(int c)` 7 | #实例 8 | 我们写一个小程序,将输入数据中的字母都过滤出来。 9 | 输入数据以';'作为结尾。 10 | ```c 11 | #include 12 | #include 13 | int 14 | main(int argc, char *argv[]) 15 | { 16 | int c; 17 | while ((c = getchar()) != ';') { 18 | if (isalpha(c)) { 19 | putchar(c); 20 | } 21 | } 22 | putchar('\n'); 23 | return 0; 24 | } 25 | ``` 26 | 测试结果: 27 | ```shell 28 | apple12orange13banana16 melon10; 29 | appleorangebananamelon 30 | ``` 31 | #备注 32 | 这里的字母都是ASCII字母,从'a'-'z'和'A'-'Z'。 33 | -------------------------------------------------------------------------------- /C_Lib/Function&Macro/std_ctype_iscntrl.md: -------------------------------------------------------------------------------- 1 | #功能 2 | 判断字符c是否为控制字符,即c的ASCII值在0x00-0x1F之间或0x7F(DEL)时,返回非零值,否则返回零值。 3 | #原型 4 | `#include` 5 | `extern int iscntrl(int c)` 6 | #实例 7 | 我们来看一个简单的例子。 8 | ```c 9 | #include 10 | #include 11 | int 12 | main(int argc, char *argv[]) 13 | { 14 | int c; 15 | c = 'a'; 16 | printf("%x:%s\n", c, iscntrl(c) ? "yes" : "no"); 17 | c = 0x0d; 18 | printf("%x:%s\n", c, iscntrl(c) ? "yes" : "no"); 19 | c = 0x7f; 20 | printf("%x:%s\n", c, iscntrl(c) ? "yes" : "no"); 21 | return 0; 22 | } 23 | ``` 24 | 输出结果为: 25 | ```shell 26 | 61:no 27 | d:yes 28 | 7f:yes 29 | ``` 30 | #备注 31 | 控制字符(Control Character),出现于特定的信息文本中,表示某一控制功能的字符。 32 | 在ASCII码中,第0~31号及第127号(共33个)是控制字符或通讯专用字符,如控制符:LF(换行)、CR(回车)、FF(换页)、DEL(删除)、BS(退格)、BEL(振铃)等;通讯专用字符:SOH(文头)、EOT(文尾)、ACK(确认)等。 33 | -------------------------------------------------------------------------------- /C_Lib/Function&Macro/std_ctype_isdigit.md: -------------------------------------------------------------------------------- 1 | #功能 2 | 判断字符c是否是数字字符,即如果是'0'-'9',则返回非零值,否则返回零值。 3 | #原型 4 | `#include` 5 | `extern int isdigit(int c)` 6 | #实例 7 | 我们来检测一个字符数组里面有哪些字符是数字字符。 8 | ```c 9 | #include 10 | #include 11 | #define ARRAY_SIZE 7 12 | int 13 | main(int argc, char *argv[]) 14 | { 15 | char test_array[ARRAY_SIZE] = {'a', '1', '3', 'b', 'd', '4', '0'}; 16 | int i; 17 | for (i = 0; i < ARRAY_SIZE; i++) { 18 | if (isdigit(test_array[i])) { 19 | printf("%c", test_array[i]); 20 | } 21 | } 22 | printf("\n"); 23 | return 0; 24 | } 25 | ``` 26 | 输出结果为: 27 | ```shell 28 | 1340 29 | ``` 30 | #备注 31 | 这个函数检查的是数字字符,而不是数字。 32 | -------------------------------------------------------------------------------- /C_Lib/Function&Macro/std_ctype_isgraph.md: -------------------------------------------------------------------------------- 1 | #功能 2 | 判断一个字符是否为可显示字符,即输出后能看到的字符。 3 | #原型 4 | `#include` 5 | `extern int isgraph(int c)` 6 | #实例 7 | 我们一样看看一个字符数组中有哪些可显示字符。 8 | ```c 9 | #include 10 | #include 11 | #define ARRAY_SIZE 7 12 | int 13 | main(int argc, char *argv[]) 14 | { 15 | char test_array[ARRAY_SIZE] = {' ', '\t', 'a', ',', '2', ')', '='}; 16 | int i; 17 | for (i = 0; i < ARRAY_SIZE; i++) { 18 | if (isgraph(test_array[i])) { 19 | printf("'%c' is graph character\n", test_array[i]); 20 | } 21 | } 22 | return 0; 23 | } 24 | ``` 25 | 输出结果为: 26 | ```shell 27 | 'a' is graph character 28 | ',' is graph character 29 | '2' is graph character 30 | ')' is graph character 31 | '=' is graph character 32 | ``` 33 | #备注 34 | 可显示字符是除了空白字符(' ')之外的,所有能够被打印出来的字符。相关函数为isprint。 35 | -------------------------------------------------------------------------------- /C_Lib/Function&Macro/std_ctype_islower.md: -------------------------------------------------------------------------------- 1 | #功能 2 | 判断参数c是否是小写英文字母,如果是返回非零值,否则返回零值。 3 | #原型 4 | `#include` 5 | `extern int islower(int c)` 6 | #实例 7 | 我们看一个简单的例子。 8 | ```c 9 | #include 10 | #include 11 | int 12 | main(int argc, char *argv[]) 13 | { 14 | char str[8] = "AbcDEFG"; 15 | char *p = str; 16 | while (*p) { 17 | if (islower(*p)) { 18 | printf("%c is lower-case character\n", *p); 19 | } 20 | p++; 21 | } 22 | return 0; 23 | } 24 | ``` 25 | 输出结果为: 26 | ```shell 27 | b is lower-case character 28 | c is lower-case character 29 | ``` 30 | #备注 31 | 这个函数仅支持英文字母。相关的函数为isupper。 32 | -------------------------------------------------------------------------------- /C_Lib/Function&Macro/std_ctype_isprint.md: -------------------------------------------------------------------------------- 1 | #功能 2 | 检查传入的参数c是否是可打印字符。如果是则返回非零值,否则返回零值。 3 | 所谓的可打印字符就是所有的非控制字符。 4 | #原型 5 | `#include` 6 | `extern int isprint(int c)` 7 | #实例 8 | 我们看一个简单的例子。找出所有可打印的字符。 9 | ```c 10 | #include 11 | #include 12 | int 13 | main (int argc, char *argv[]) 14 | { 15 | char str[7] = { 'a', '1', '\r', ' ', '\n', '\t', ' ' }; 16 | char *p = str; 17 | while (*p) 18 | { 19 | if (iscntrl (*p)) 20 | { 21 | printf ("'%c' is printable character\n", *p); 22 | } 23 | p++; 24 | } 25 | return 0; 26 | } 27 | ``` 28 | 输出结果为: 29 | ```shell 30 | 'a' is printable character 31 | '1' is printable character 32 | ' ' is printable character 33 | ``` 34 | #备注 35 | 所有的非控制字符都是可打印字符。可打印字符比可显示字符多了空格字符(' '),相关函数为isgraph。 36 | -------------------------------------------------------------------------------- /C_Lib/Function&Macro/std_ctype_ispunct.md: -------------------------------------------------------------------------------- 1 | #功能 2 | 检查参数c是否是标点字符。如果是则返回非零值,否则返回零值。 3 | 所谓的标点字符是指那些不是字母或数字字符(isalnum)的可显示字符(isgraph)。 4 | #原型 5 | `#include` 6 | `extern int ispunct(int c)` 7 | #实例 8 | 我们看一个简单的例子。找出所有标点字符。 9 | ```c 10 | #include 11 | #include 12 | int 13 | main (int argc, char *argv[]) 14 | { 15 | char str[7] = { 'a', '1', ',', ' ', '\n', '\t', ';' }; 16 | char *p = str; 17 | while (*p) 18 | { 19 | if (ispunct(*p)) 20 | { 21 | printf ("'%c' is punctuation character\n", *p); 22 | } 23 | p++; 24 | } 25 | return 0; 26 | } 27 | ``` 28 | 输出结果为: 29 | ```shell 30 | ',' is punctuation character 31 | ';' is punctuation character 32 | ``` 33 | #备注 34 | 这个函数只支持英文标点符号。编程的时候注意输入法的切换。 35 | -------------------------------------------------------------------------------- /C_Lib/Function&Macro/std_ctype_isspace.md: -------------------------------------------------------------------------------- 1 | #功能 2 | 检查参数c是否是空白(white-space)字符。如果是则返回非零值,否则返回零值。 3 | #原型 4 | `#include` 5 | `extern int isspace(int c)` 6 | #实例 7 | 我们看一个简单的例子。找出所有空白字符。 8 | ```c 9 | #include 10 | #include 11 | int 12 | main (int argc, char *argv[]) 13 | { 14 | char str[7] = { 'a', '1', ',', ' ', '\n', '\t', ';' }; 15 | char *p = str; 16 | while (*p) 17 | { 18 | if (isspace(*p)) 19 | { 20 | printf ("%d - '%c' is white-space character\n", *p, *p); 21 | } 22 | p++; 23 | } 24 | return 0; 25 | } 26 | ``` 27 | 输出结果为: 28 | ```shell 29 | 32 - ' ' is white-space character 30 | 10 - ' 31 | ' is white-space character 32 | 9 - ' ' is white-space character 33 | ``` 34 | #备注 35 | 标准的空白字符有: 36 | |字符 |ASCII值 |描述 | 37 | |----------|----------|---------------------| 38 | |' ' |(0x20) |空格字符(SPC) | 39 | |'\t' |(0x09) |水平制表位(TAB) | 40 | |'\n' |(0x0a) |新行(LF) | 41 | |'\v' |(0x0b) |垂直制表位(VT) | 42 | |'\f' |(0x0c) |feed(FF) | 43 | |'\r' |(0x0d) |回车符(CR) | 44 | 45 | 从上面我们可以看到空白字符不仅仅指的是空格字符(' ')。 46 | -------------------------------------------------------------------------------- /C_Lib/Function&Macro/std_ctype_isupper.md: -------------------------------------------------------------------------------- 1 | #功能 2 | 判断参数c是否是大写英文字母,如果是返回非零值,否则返回零值。 3 | #原型 4 | `#include` 5 | `extern int isupper(int c)` 6 | #实例 7 | 我们看一个简单的例子。 8 | ```c 9 | #include 10 | #include 11 | int 12 | main(int argc, char *argv[]) 13 | { 14 | char str[8] = "AbcDEFG"; 15 | char *p = str; 16 | while (*p) { 17 | if (isupper(*p)) { 18 | printf("%c is upper-case character\n", *p); 19 | } 20 | p++; 21 | } 22 | return 0; 23 | } 24 | ``` 25 | 输出结果为: 26 | ```shell 27 | A is upper-case character 28 | D is upper-case character 29 | E is upper-case character 30 | F is upper-case character 31 | G is upper-case character 32 | ``` 33 | #备注 34 | 这个函数仅支持英文字母。相关的函数为islower。 35 | -------------------------------------------------------------------------------- /C_Lib/Function&Macro/std_ctype_isxdigit.md: -------------------------------------------------------------------------------- 1 | #功能 2 | 检查参数c是否是十六进制字符。如果是返回非零值,否则返回零值。 3 | #原型 4 | `#include` 5 | `extern int isxdigit(int c)` 6 | #实例 7 | ```c 8 | #include 9 | #include 10 | int 11 | main (int argc, char *argv[]) 12 | { 13 | char str[7] = { 'a', '1', ',', ' ', '\n', '\t', ';' }; 14 | char *p = str; 15 | while (*p) 16 | { 17 | if (isxdigit(*p)) 18 | { 19 | printf ("'%c' is xdigit character\n", *p, *p); 20 | } 21 | p++; 22 | } 23 | return 0; 24 | } 25 | ``` 26 | 输出结果为: 27 | ```shell 28 | 'a' is xdigit character 29 | '1' is xdigit character 30 | ``` 31 | #备注 32 | 十六进制数以0x开头。可以包含的字符有'0'-'9','a'-'f','A'-'F'。 33 | -------------------------------------------------------------------------------- /C_Lib/Function&Macro/std_ctype_tolower.md: -------------------------------------------------------------------------------- 1 | #功能 2 | 将英文大写字母转换为小写字母。 3 | #原型 4 | `#include` 5 | `extern int tolower(int c)` 6 | #实例 7 | ```c 8 | #include 9 | #include 10 | int 11 | main (int argc, char *argv[]) 12 | { 13 | char *str = "DUOKR SCHOOL"; 14 | char *p = str; 15 | while (*p) 16 | { 17 | printf ("%c", tolower (*p)); 18 | p++; 19 | } 20 | printf ("\n"); 21 | return 0; 22 | } 23 | ``` 24 | 输出结果为: 25 | ```shell 26 | duokr school 27 | ``` 28 | #备注 29 | 如果存在对应的小写字母,则函数返回小写字母,否则保持不变。比如对于数字字符来讲,就直接返回数字字符。这个函数只支持英文字母。相关函数为toupper。 30 | -------------------------------------------------------------------------------- /C_Lib/Function&Macro/std_ctype_toupper.md: -------------------------------------------------------------------------------- 1 | #功能 2 | 将英文小写字母转换为大写字母。 3 | #原型 4 | `#include` 5 | `extern int toupper(int c)` 6 | #实例 7 | ```c 8 | #include 9 | #include 10 | int 11 | main (int argc, char *argv[]) 12 | { 13 | char *str = "duokr school"; 14 | char *p = str; 15 | while (*p) 16 | { 17 | printf ("%c", toupper (*p)); 18 | p++; 19 | } 20 | printf ("\n"); 21 | return 0; 22 | } 23 | 24 | ``` 25 | 输出结果为: 26 | ```shell 27 | DUOKR SCHOOL 28 | ``` 29 | #备注 30 | 如果存在对应的大写字母,则函数返回大写字母,否则保持不变。比如对于数字字符来讲,就直接返回数字字符。这个函数只支持英文字母。相关函数为tolower。 31 | -------------------------------------------------------------------------------- /C_Lib/Function&Macro/std_locale_localeconv.md: -------------------------------------------------------------------------------- 1 | #功能 2 | 这个函数用来获取位置相关的信息。这些信息以结构体`lconv`的对象返回。 3 | 4 | #原型 5 | `#include` 6 | `struct lconv *localeconv(void)` 7 | #实例 8 | ```c 9 | #include 10 | #include 11 | 12 | int main (int argc, char *argv[]) 13 | { 14 | struct lconv *lc; 15 | 16 | setlocale(LC_MONETARY, "it_IT"); 17 | lc = localeconv(); 18 | printf("Local Currency Symbol: %s\n",lc->currency_symbol); 19 | printf("International Currency Symbol: %s\n",lc->int_curr_symbol); 20 | 21 | setlocale(LC_MONETARY, "en_US"); 22 | lc = localeconv(); 23 | printf("Local Currency Symbol: %s\n",lc->currency_symbol); 24 | printf("International Currency Symbol: %s\n",lc->int_curr_symbol); 25 | 26 | setlocale(LC_MONETARY, "en_GB"); 27 | lc = localeconv(); 28 | printf ("Local Currency Symbol: %s\n",lc->currency_symbol); 29 | printf ("International Currency Symbol: %s\n",lc->int_curr_symbol); 30 | 31 | printf("Decimal Point = %s\n", lc->decimal_point); 32 | 33 | return 0; 34 | } 35 | ``` 36 | 输出结果为: 37 | ```shell 38 | Local Currency Symbol: EUR 39 | International Currency Symbol: EUR 40 | Local Currency Symbol: $ 41 | International Currency Symbol: USD 42 | Local Currency Symbol: £ 43 | International Currency Symbol: GBP 44 | Decimal Point = . 45 | ``` 46 | #备注 47 | 这个lconv的结构体定义为: 48 | ```c 49 | typedef struct { 50 | char *decimal_point; 51 | char *thousands_sep; 52 | char *grouping; 53 | char *int_curr_symbol; 54 | char *currency_symbol; 55 | char *mon_decimal_point; 56 | char *mon_thousands_sep; 57 | char *mon_grouping; 58 | char *positive_sign; 59 | char *negative_sign; 60 | char int_frac_digits; 61 | char frac_digits; 62 | char p_cs_precedes; 63 | char p_sep_by_space; 64 | char n_cs_precedes; 65 | char n_sep_by_space; 66 | char p_sign_posn; 67 | char n_sign_posn; 68 | } lconv; 69 | ``` 70 | 71 | -------------------------------------------------------------------------------- /C_Lib/Function&Macro/std_locale_setlocale.md: -------------------------------------------------------------------------------- 1 | #功能 2 | 这个函数用来设置位置相关的信息。 3 | 4 | **参数说明** 5 | 6 | - category 7 | 这个参数是一个命名常量。用来指定locale设定所影响的函数种类,即影响哪些函数的功能。 8 | - locale 9 | 如果locale指定为NULL或者空字符串(""),那么locale的值为环境变量中以上面的category参数为键的环境变量值。 10 | 11 | **返回值** 12 | 13 | 函数调用成功后,返回一个和locale集相关的字符串,如果调用失败则返回NULL。 14 | 15 | #原型 16 | `#include` 17 | `char *setlocale(int category, const char *locale)` 18 | #实例 19 | ```c 20 | #include 21 | #include 22 | #include 23 | 24 | int 25 | main () 26 | { 27 | time_t currtime; 28 | struct tm *timer; 29 | char buffer[80]; 30 | 31 | time (&currtime); 32 | timer = localtime (&currtime); 33 | 34 | printf ("Locale is: %s\n", setlocale (LC_ALL, "en_GB")); 35 | strftime (buffer, 80, "%c", timer); 36 | printf ("Date is: %s\n", buffer); 37 | 38 | 39 | printf ("Locale is: %s\n", setlocale (LC_ALL, "de_DE")); 40 | strftime (buffer, 80, "%c", timer); 41 | printf ("Date is: %s\n", buffer); 42 | 43 | return (0); 44 | } 45 | ``` 46 | 输出结果为: 47 | ```shell 48 | Locale is: en_GB 49 | Date is: Thu 23 Aug 2012 06:39:32 MST 50 | Locale is: de_DE 51 | Date is: Do 23 Aug 2012 06:39:32 MST 52 | ``` 53 | #备注 54 | 函数参数category的所有常量值如下: 55 | |常量值 |描述 | 56 | |-------------|-------------------------------| 57 | |LC_ALL |影响下面所有函数 | 58 | |LC_COLLATE |影响字符串比较,查看strcoll | 59 | |LC_CTYPE |影响字符分类和转换,例如strtoupper| 60 | |LC_MONETARY |影响货币格式化,查看localeconv | 61 | |LC_NUMERIC |影响十进制分隔符,查看localeconv | 62 | |LC_TIME |影响日期和时间格式化,查看strftime | 63 | |LC_MESSAGES |影响系统响应信息 | 64 | -------------------------------------------------------------------------------- /C_Lib/Function&Macro/std_math_acos.md: -------------------------------------------------------------------------------- 1 | #功能 2 | 计算参数x的反余弦值,x的值范围为[-1,1],函数返回值范围为[0,PI]。 3 | #原型 4 | `#include` 5 | `double acos(double x)` 6 | #实例 7 | 现在的例子介绍了acos的用法。 8 | ```c 9 | #include 10 | #include 11 | 12 | #define PI 3.14159265 13 | 14 | int 15 | main() 16 | { 17 | double x, ret, val; 18 | 19 | x = 0.9; 20 | val = 180.0 / PI; 21 | 22 | ret = acos(x) * val; 23 | printf("The arc cosine of %lf is %lf degrees\n", x, ret); 24 | 25 | return (0); 26 | } 27 | ``` 28 | 输出结果为: 29 | ```shell 30 | The arc cosine of 0.900000 is 25.841933 degrees 31 | ``` 32 | #备注 33 | 上面的例子中,我们将结果转换为了角度数显示。因为acos的直接返回结果是弧度数。 34 | -------------------------------------------------------------------------------- /C_Lib/Function&Macro/std_math_asin.md: -------------------------------------------------------------------------------- 1 | #功能 2 | 计算参数x的反正弦值,x的值范围为[-1,1],函数返回值范围为[-PI/2,PI/2]。 3 | #原型 4 | `#include` 5 | `double asin(double x)` 6 | #实例 7 | 现在的例子介绍了asin的用法。 8 | ```c 9 | #include 10 | #include 11 | 12 | #define PI 3.14159265 13 | 14 | int main () 15 | { 16 | double x, ret, val; 17 | x = 0.9; 18 | val = 180.0 / PI; 19 | 20 | ret = asin(x) * val; 21 | printf("The arc sine of %lf is %lf degrees\n", x, ret); 22 | 23 | return(0); 24 | } 25 | ``` 26 | 输出结果为: 27 | ```shell 28 | The arc sine of 0.900000 is 64.158067 degrees 29 | ``` 30 | #备注 31 | 上面的例子中,我们将结果转换为了角度数显示。因为asin的直接返回结果是弧度数。 32 | -------------------------------------------------------------------------------- /C_Lib/Function&Macro/std_math_atan.md: -------------------------------------------------------------------------------- 1 | #功能 2 | 计算参数x的反正切值,返回结果范围为[-PI/2,PI/2]。 3 | #原型 4 | `#include` 5 | `double atan(double x)` 6 | #实例 7 | ```c 8 | #include 9 | #include 10 | 11 | #define PI 3.14159265 12 | 13 | int 14 | main() 15 | { 16 | double x, ret, val; 17 | x = 1.0; 18 | val = 180.0 / PI; 19 | 20 | ret = atan(x) * val; 21 | printf("The arc tangent of %lf is %lf degrees\n", x, ret); 22 | 23 | return (0); 24 | } 25 | ``` 26 | 输出结果为: 27 | ```shell 28 | The arc tangent of 1.000000 is 45.000000 degrees 29 | ``` 30 | #备注 31 | 上面的例子中,我们将结果转换为了角度数显示。因为atan的直接返回结果是弧度数。 32 | -------------------------------------------------------------------------------- /C_Lib/Function&Macro/std_math_atan2.md: -------------------------------------------------------------------------------- 1 | #功能 2 | 计算y/x的反正切值,返回结果范围为[-PI,PI]。其中y为纵坐标的值,x为横坐标的值。 3 | 返回值会自动根据y和x的正负来确定,比如y=1,x=-1和y=-1,x=1,虽然它们的y/x的 4 | 值都是-1,但是y/x的反正切值是不一样的。 5 | #原型 6 | `#include` 7 | `double atan2(double y, double x)` 8 | #实例 9 | ```c 10 | #include 11 | #include 12 | 13 | #define PI 3.14159265 14 | 15 | int main () 16 | { 17 | double x, y, ret, val; 18 | 19 | x = -7.0; 20 | y = 7.0; 21 | val = 180.0 / PI; 22 | 23 | ret = atan2 (y,x) * val; 24 | printf("The arc tangent of x = %lf, y = %lf ", x, y); 25 | printf("is %lf degrees\n", ret); 26 | 27 | return(0); 28 | } 29 | ``` 30 | 输出结果: 31 | ```shell 32 | The arc tangent of x = -7.000000, y = 7.000000 is 135.000000 degrees 33 | ``` 34 | #备注 35 | 需要注意的就是在y/x的值一样的情况下,所返回的atan2值也有可能会不同,因为所返回的值还会根据y和x的正负来判断。 36 | -------------------------------------------------------------------------------- /C_Lib/Function&Macro/std_math_ceil.md: -------------------------------------------------------------------------------- 1 | #功能 2 | 这个函数返回大于或等于参数x的最小整数值。 3 | #原型 4 | `#include` 5 | `double ceil(double x)` 6 | #实例 7 | ```c 8 | #include 9 | #include 10 | 11 | int main () 12 | { 13 | float val1, val2, val3, val4; 14 | 15 | val1 = 1.6; 16 | val2 = 1.2; 17 | val3 = 2.8; 18 | val4 = 2.3; 19 | 20 | printf ("ceil(%f) = %.1lf\n", val1, ceil(val1)); 21 | printf ("ceil(%f) = %.1lf\n", val2, ceil(val2)); 22 | printf ("ceil(%f) = %.1lf\n", val3, ceil(val3)); 23 | printf ("ceil(%f) = %.1lf\n", val4, ceil(val4)); 24 | 25 | return(0); 26 | } 27 | ``` 28 | 输出结果为: 29 | ```shell 30 | ceil(1.600000) = 2.0 31 | ceil(1.200000) = 2.0 32 | ceil(2.800000) = 3.0 33 | ceil(2.300000) = 3.0 34 | ``` 35 | #备注 36 | 这个函数和四舍五入并不相同,而是向上取得一个最靠近这个参数的整数值。 37 | -------------------------------------------------------------------------------- /C_Lib/Function&Macro/std_math_cos.md: -------------------------------------------------------------------------------- 1 | #功能 2 | 计算一个弧度法表示的角度x的余弦值。 3 | #原型 4 | `#include` 5 | `double cos(double x)` 6 | #实例 7 | 我们来看一个小例子: 8 | ```c 9 | #include 10 | #include 11 | 12 | #define PI 3.14159265 13 | 14 | int 15 | main (int argc, char *argv[]) 16 | { 17 | double x, ret, val; 18 | 19 | x = 60.0; 20 | val = PI/180; 21 | ret = cos( x*val ); 22 | printf("The cosine of %lf is %lf\n", x, ret); 23 | 24 | x = 90.0; 25 | val = PI/180; 26 | ret = cos( x*val ); 27 | printf("The cosine of %lf is %lf\n", x, ret); 28 | 29 | return 0; 30 | } 31 | ``` 32 | 输出结果为: 33 | ```shell 34 | The cosine of 60.000000 is 0.500000 35 | The cosine of 90.000000 is 0.000000 36 | ``` 37 | #备注 38 | 角度有两种表示方法,一种就是角度数,另一种是弧度数。两者的转换如上面例子所示。 39 | `弧度数=角度数*(PI/180)`。这个函数参数是角度的弧度数表示。 -------------------------------------------------------------------------------- /C_Lib/Function&Macro/std_math_cosh.md: -------------------------------------------------------------------------------- 1 | #功能 2 | 计算参数x的双曲余弦函数值。 3 | #原型 4 | `#include` 5 | `double cosh(double x)` 6 | #实例 7 | ```c 8 | #include 9 | #include 10 | 11 | int main () 12 | { 13 | double x; 14 | 15 | x = 0.5; 16 | printf("The hyperbolic cosine of %lf is %lf\n", x, cosh(x)); 17 | 18 | x = 1.0; 19 | printf("The hyperbolic cosine of %lf is %lf\n", x, cosh(x)); 20 | 21 | x = 1.5; 22 | printf("The hyperbolic cosine of %lf is %lf\n", x, cosh(x)); 23 | 24 | return(0); 25 | } 26 | ``` 27 | 输出结果为: 28 | ```shell 29 | The hyperbolic cosine of 0.500000 is 1.127626 30 | The hyperbolic cosine of 1.000000 is 1.543081 31 | The hyperbolic cosine of 1.500000 is 2.352410 32 | ``` 33 | #备注 34 | 双曲余弦函数的公式为`cosh(x)=(exp(x) + exp(-x)) / 2.0`。 35 | -------------------------------------------------------------------------------- /C_Lib/Function&Macro/std_math_exp.md: -------------------------------------------------------------------------------- 1 | #功能 2 | 计算参数x的指数值,指数的底为e。 3 | #原型 4 | `#include` 5 | `double exp(double x)` 6 | #实例 7 | ```c 8 | #include 9 | #include 10 | 11 | int main () 12 | { 13 | double x = 0; 14 | 15 | printf("The exponential value of %lf is %lf\n", x, exp(x)); 16 | printf("The exponential value of %lf is %lf\n", x+1, exp(x+1)); 17 | printf("The exponential value of %lf is %lf\n", x+2, exp(x+2)); 18 | 19 | return(0); 20 | } 21 | ``` 22 | 输出结果为: 23 | ```shell 24 | The exponential value of 0.000000 is 1.000000 25 | The exponential value of 1.000000 is 2.718282 26 | The exponential value of 2.000000 is 7.389056 27 | ``` 28 | #备注 29 | 这个函数对应的数学公式为ex。 30 | -------------------------------------------------------------------------------- /C_Lib/Function&Macro/std_math_floor.md: -------------------------------------------------------------------------------- 1 | #功能 2 | 这个函数返回小于或等于参数x的最大整数值。 3 | #原型 4 | `#include` 5 | `double floor(double x)` 6 | #实例 7 | ```c 8 | #include 9 | #include 10 | 11 | int main () 12 | { 13 | float val1, val2, val3, val4; 14 | 15 | val1 = 1.6; 16 | val2 = 1.2; 17 | val3 = 2.8; 18 | val4 = 2.3; 19 | 20 | printf("floor(%f) = %.1lf\n", val1, floor(val1)); 21 | printf("floor(%f) = %.1lf\n", val2, floor(val2)); 22 | printf("floor(%f) = %.1lf\n", val3, floor(val3)); 23 | printf("floor(%f) = %.1lf\n", val4, floor(val4)); 24 | 25 | return(0); 26 | } 27 | ``` 28 | 输出结果为: 29 | ```shell 30 | floor(1.600000) = 1.0 31 | floor(1.200000) = 1.0 32 | floor(2.800000) = 2.0 33 | floor(2.300000) = 2.0 34 | ``` 35 | #备注 36 | 这个函数和四舍五入不同,而是向下取得一个最靠近这个参数的整数值。 37 | -------------------------------------------------------------------------------- /C_Lib/Function&Macro/std_math_fmod.md: -------------------------------------------------------------------------------- 1 | #功能 2 | 这个函数返回x对y求余的余数。x是分子,y是分母。 3 | #原型 4 | `#include` 5 | `double fmod(double x, double y)` 6 | #实例 7 | ```c 8 | #include 9 | #include 10 | 11 | int main () 12 | { 13 | float a, b; 14 | int c; 15 | a = 9.2; 16 | b = 3.7; 17 | c = 2; 18 | printf("Remainder of %f / %d is %lf\n", a, c, fmod(a,c)); 19 | printf("Remainder of %f / %f is %lf\n", a, b, fmod(a,b)); 20 | 21 | return(0); 22 | } 23 | ``` 24 | 输出结果为: 25 | ```shell 26 | Remainder of 9.200000 / 2 is 1.200000 27 | Remainder of 9.200000 / 3.700000 is 1.800000 28 | ``` 29 | #备注 30 | 这个函数是浮点数求余。 31 | -------------------------------------------------------------------------------- /C_Lib/Function&Macro/std_math_frexp.md: -------------------------------------------------------------------------------- 1 | #功能 2 | 首先看一下函数对应的公式`x=尾数(mantissa)*2^指数(exponent)`。 3 | 而这个函数就是根据参数x来分解出尾数(mantissa)和指数的(exponent)。 4 | #原型 5 | `#include` 6 | `double frexp(double x, int *exponent)` 7 | #实例 8 | ```c 9 | #include 10 | #include 11 | 12 | int main () 13 | { 14 | double x = 1024, fraction; 15 | int e; 16 | 17 | fraction = frexp(x, &e); 18 | printf("x = %.2lf = %.2lf * 2^%d\n", x, fraction, e); 19 | 20 | return(0); 21 | } 22 | ``` 23 | 输出结果为: 24 | ```shell 25 | x = 1024.00 = 0.50 * 2^11 26 | ``` 27 | #备注 28 | 这个函数将参数x分解为尾数和指数部分。如果参数x不为0,那么正常分解出尾数和指数,其中尾数的绝对值范围为[-0.5,1)。 29 | 如果x为0,那么尾数和指数均为0。 30 | 31 | 在上面的例子中,因为x不为0,所以即使1024可以表示为210,也会被分解为0.5的尾数和11的指数。 32 | -------------------------------------------------------------------------------- /C_Lib/Function&Macro/std_math_ldexp.md: -------------------------------------------------------------------------------- 1 | #功能 2 | 首先看一下函数对应的公式为y=x*2^exponent。其中y为函数计算的结果。 3 | #原型 4 | `#include` 5 | `double ldexp(double x, int exponent)` 6 | #实例 7 | ```c 8 | #include 9 | #include 10 | 11 | int main () 12 | { 13 | double x, ret; 14 | int n; 15 | 16 | x = 0.65; 17 | n = 3; 18 | ret = ldexp(x ,n); 19 | printf("%f * 2^%d = %f\n", x, n, ret); 20 | 21 | return(0); 22 | } 23 | ``` 24 | 输出结果为: 25 | ```shell 26 | 0.650000 * 2^3 = 5.200000 27 | ``` 28 | #备注 29 | 这个函数和frexp是逆过程。 30 | -------------------------------------------------------------------------------- /C_Lib/Function&Macro/std_math_log.md: -------------------------------------------------------------------------------- 1 | #功能 2 | 计算参数x的自然(e)对数值。 3 | #原型 4 | `#include` 5 | `double log(double x)` 6 | #实例 7 | ```c 8 | #include 9 | #include 10 | 11 | int main () 12 | { 13 | double x, ret; 14 | x = 2.7; 15 | 16 | /* 求 log(2.7) */ 17 | ret = log(x); 18 | printf("log(%lf) = %lf\n", x, ret); 19 | 20 | return(0); 21 | } 22 | ``` 23 | 输出结果为: 24 | ```shell 25 | log(2.700000) = 0.993252 26 | ``` 27 | #备注 28 | 这个函数对应的数学公式为ln(x)。 29 | -------------------------------------------------------------------------------- /C_Lib/Function&Macro/std_math_log10.md: -------------------------------------------------------------------------------- 1 | #功能 2 | 计算参数x的底数为10的对数值。 3 | #原型 4 | `#include` 5 | `double log10(double x)` 6 | #实例 7 | ```c 8 | #include 9 | #include 10 | 11 | int main () 12 | { 13 | double x, ret; 14 | x = 10000; 15 | 16 | ret = log10(x); 17 | printf("log10(%lf) = %lf\n", x, ret); 18 | 19 | return(0); 20 | } 21 | ``` 22 | 输出结果为: 23 | ```shell 24 | log10(10000.000000) = 4.000000 25 | ``` 26 | #备注 27 | 这个函数对应的数学公式为lg(x)。 28 | -------------------------------------------------------------------------------- /C_Lib/Function&Macro/std_math_modf.md: -------------------------------------------------------------------------------- 1 | #功能 2 | 返回一个double类型参数的小数部分并将整数部分以整型指针方式返回。 3 | #原型 4 | `#include` 5 | `double modf(double x, double *integer)` 6 | #实例 7 | ```c 8 | #include 9 | #include 10 | 11 | int main () 12 | { 13 | double x, fractpart, intpart; 14 | 15 | x = 8.123456; 16 | fractpart = modf(x, &intpart); 17 | 18 | printf("Integral part = %lf\n", intpart); 19 | printf("Fraction Part = %lf \n", fractpart); 20 | 21 | return(0); 22 | } 23 | ``` 24 | 输出结果为: 25 | ```shell 26 | Integral part = 8.000000 27 | Fraction Part = 0.123456 28 | ``` 29 | #备注 30 | 这个函数就是把一个double类型的数拆分为小数部分和整数部分。 31 | -------------------------------------------------------------------------------- /C_Lib/Function&Macro/std_math_pow.md: -------------------------------------------------------------------------------- 1 | #功能 2 | 计算参数x的y次方值。 3 | #原型 4 | `#include` 5 | `double pow(double x, double y)` 6 | #实例 7 | ```c 8 | #include 9 | #include 10 | 11 | int main () 12 | { 13 | printf("Value 8.0 ^ 3 = %lf\n", pow(8.0, 3)); 14 | 15 | printf("Value 3.05 ^ 1.98 = %lf\n", pow(3.05, 1.98)); 16 | 17 | return(0); 18 | } 19 | ``` 20 | 输出结果为: 21 | ```shell 22 | Value 8.0 ^ 3 = 512.000000 23 | Value 3.05 ^ 1.98 = 9.097324 24 | ``` 25 | #备注 26 | 这个函数对应的数学公式为xy。 27 | -------------------------------------------------------------------------------- /C_Lib/Function&Macro/std_math_powf.md: -------------------------------------------------------------------------------- 1 | #功能 2 | 这个函数和pow功能相同,也是计算x的y次方。区别在于传入的参数为float类型。 3 | #原型 4 | `#include` 5 | `float powf(float x, float y)` 6 | #实例 7 | ```c 8 | #include 9 | #include 10 | 11 | int main () 12 | { 13 | printf("Value 8.0 ^ 3 = %lf\n", pow(8.0, 3)); 14 | 15 | printf("Value 3.05 ^ 1.98 = %lf\n", pow(3.05, 1.98)); 16 | 17 | return(0); 18 | } 19 | ``` 20 | 输出结果为: 21 | ```shell 22 | Value 8.0 ^ 3 = 512.000000 23 | Value 3.05 ^ 1.98 = 9.097324 24 | ``` 25 | #备注 26 | 这个函数和pow的区别在于所传入的参数为float,结果也为float。 27 | -------------------------------------------------------------------------------- /C_Lib/Function&Macro/std_math_sin.md: -------------------------------------------------------------------------------- 1 | #功能 2 | 计算一个弧度法表示的角度x的正弦值。 3 | #原型 4 | `#include` 5 | `double sina(double x)` 6 | #实例 7 | 我们来看一个小例子: 8 | ```c 9 | #include 10 | #include 11 | 12 | #define PI 3.14159265 13 | 14 | int 15 | main (int argc, char *argv[]) 16 | { 17 | double x, ret, val; 18 | 19 | x = 45.0; 20 | val = PI / 180; 21 | ret = sin(x*val); 22 | printf("The sine of %lf is %lf", x, ret); 23 | 24 | return(0); 25 | } 26 | ``` 27 | 输出结果为: 28 | ```shell 29 | The sine of 45.000000 is 0.707107 30 | ``` 31 | #备注 32 | 角度有两种表示方法,一种就是角度数,另一种是弧度数。两者的转换如上面例子所示。 33 | `弧度数=角度数*(PI/180)`。这个函数参数是角度的弧度数表示。 34 | -------------------------------------------------------------------------------- /C_Lib/Function&Macro/std_math_sinh.md: -------------------------------------------------------------------------------- 1 | #功能 2 | 计算参数x的双曲正弦函数值。 3 | #原型 4 | `#include` 5 | `double sinh(double x)` 6 | #实例 7 | ```c 8 | #include 9 | #include 10 | 11 | int main () 12 | { 13 | double x, ret; 14 | x = 0.5; 15 | 16 | ret = sinh(x); 17 | printf("The hyperbolic sine of %lf is %lf\n", x, ret); 18 | 19 | return(0); 20 | } 21 | ``` 22 | 输出结果为: 23 | ```shell 24 | The hyperbolic sine of 0.500000 is 0.521095 25 | ``` 26 | #备注 27 | 双曲正弦函数的公式为`sinh(x)=(exp(x) - exp(-x)) / 2.0`。 28 | -------------------------------------------------------------------------------- /C_Lib/Function&Macro/std_math_sqrt.md: -------------------------------------------------------------------------------- 1 | #功能 2 | 计算参数x的平方根。 3 | #原型 4 | `#include` 5 | `double sqrt(double x)` 6 | #实例 7 | ```c 8 | #include 9 | #include 10 | 11 | int main () 12 | { 13 | 14 | printf("Square root of %lf is %lf\n", 4.0, sqrt(4.0) ); 15 | printf("Square root of %lf is %lf\n", 5.0, sqrt(5.0) ); 16 | 17 | return(0); 18 | } 19 | ``` 20 | 输出结果为: 21 | ```shell 22 | Square root of 4.000000 is 2.000000 23 | Square root of 5.000000 is 2.236068 24 | ``` 25 | #备注 26 | 很简单,没啥补充的。 27 | -------------------------------------------------------------------------------- /C_Lib/Function&Macro/std_math_tan.md: -------------------------------------------------------------------------------- 1 | #功能 2 | 计算一个弧度法表示的角度x的正切值。 3 | #原型 4 | `#include` 5 | `double tan(double x)` 6 | #实例 7 | 我们来看一个小例子: 8 | ```c 9 | #include 10 | #include 11 | 12 | #define PI 3.14159265 13 | 14 | int 15 | main (int argc, char *argv[]) 16 | { 17 | double x, ret, val; 18 | 19 | x = 45.0; 20 | val = PI / 180; 21 | ret = tan(x*val); 22 | printf("The tan of %lf is %lf", x, ret); 23 | 24 | return(0); 25 | } 26 | ``` 27 | 输出结果为: 28 | ```shell 29 | The tan of 45.000000 is 1.000000 30 | ``` 31 | #备注 32 | 角度有两种表示方法,一种就是角度数,另一种是弧度数。两者的转换如上面例子所示。 33 | `弧度数=角度数*(PI/180)`。这个函数参数是角度的弧度数表示。 -------------------------------------------------------------------------------- /C_Lib/Function&Macro/std_math_tanh.md: -------------------------------------------------------------------------------- 1 | #功能 2 | 计算参数x的双曲正切函数值。 3 | #原型 4 | `#include` 5 | `double tanh(double x)` 6 | #实例 7 | ```c 8 | #include 9 | #include 10 | 11 | int main () 12 | { 13 | double x, ret; 14 | x = 0.5; 15 | 16 | ret = tanh(x); 17 | printf("The hyperbolic tangent of %lf is %lf\n", x, ret); 18 | 19 | return(0); 20 | } 21 | ``` 22 | 输出结果为: 23 | ```shell 24 | The hyperbolic tangent of 0.500000 is 0.462117 25 | ``` 26 | #备注 27 | 双曲正切函数的公式为`tanh(x) = sinh(x) / cosh(x)`。 28 | -------------------------------------------------------------------------------- /Docker/2018-10-09 使用基于Docker的phpmyadmin访问本地MySQL.md: -------------------------------------------------------------------------------- 1 | 这个帖子主要是讲如何在本地 Docker 里面跑一个 phpmyadmin 来访问安装在本机的 MySQL 8.0 数据库。 2 | 3 | 注意这里的数据库版本是 MySQL 8.0,因为这个版本的有些命令和之前的版本已经不同了,有微小而合理的调整。 4 | 5 | ## 拉取 phpmyadmin 镜像到本地 6 | 7 | ``` 8 | $ docker pull phpmyadmin/phpmyadmin 9 | ``` 10 | 11 | 这个镜像是 phpmyadmin 的官方镜像。 12 | 13 | ## 配置本地的 MySQL 允许远程访问 14 | 15 | 因为 phpmyadmin 是跑在 Docker 里面的,而 MySQL 是跑在本地的物理机器上面的,所以对于 phpmyadmin 来说,这个 MySQL 服务器就是个远程的服务器。 16 | 我们需要设置允许 phpmyadmin 远程访问这个数据库服务器。 17 | 18 | 很多时候我们图开发方便,很可能会直接用下面的命令来开放 MySQL 数据库的远程访问。 19 | 20 | ``` 21 | mysql> grant all privileges on *.* to root@'%' with grant option; 22 | ``` 23 | 24 | 但是这个命令在 MySQL 8.0 上面会有如下的报错: 25 | 26 | >ERROR 1410 (42000): You are not allowed to create a user with GRANT 27 | 28 | 最主要是 MySQL 8.0 版本不再允许 root 远程访问了。在这种情况下,我们就要创建自己的用户来配置远程访问。 29 | 30 | ``` 31 | $ create user 'duoke' identified by 'duoke'; 32 | $ grant all privileges on clouddeploy.* to 'duoke'@'%' with grant option; 33 | $ flush privileges; 34 | ``` 35 | 36 | 上面我们创建了一个名称为 `duoke` 的用户,密码也是 `duoke`,然后允许它远程访问数据库 `clouddeploy`。 37 | 38 | ## 启动 phpmyadmin 的 Docker 容器 39 | 40 | 上面的准备工作做好之后,我们就可以启动 phpmyadmin 的容器了,在启动容器之前,我们需要解决一个问题就是 phpmyadmin 的容器是通过环境变量的方式来指定要访问的 MySQL 服务器的地址的,可以指定为域名,主机名或者直接IP地址。我们一般开发的时候电脑会从公司带到家里,再从家里带到公司,一般公司和家里分配的动态IP地址是不一样的,所以选择指定IP肯定不合适。所以这个地方我们可以指定为一个主机名,这个主机名并不一定需要是本地主机名,更不可以是 localhost。为了方便识别,我们就叫它 phpmydmin-mysql-server ,然后在本地的 `/etc/hosts` 文件里面添加主机名到本地机器IP的地址映射。 41 | 42 | ``` 43 | #192.168.1.102 phpmyadmin-mysql-server 44 | 10.120.112.39 phpmyadmin-mysql-server 45 | ``` 46 | 47 | 然后启动容器的命令如下: 48 | 49 | ``` 50 | $ docker run -d -e PMA_HOST=phpmyadmin-mysql-server -p 9090:80 phpmyadmin/phpmyadmin 51 | ``` 52 | 53 | 然后我们就可以通过地址 http://localhost:9090 访问到 phpmyadmin了,使用上面创建的用户名和密码登陆即可。 54 | 55 | 这样,我们每次到公司就切换 `/etc/hosts` 文件里面的IP映射就可以了。 56 | -------------------------------------------------------------------------------- /Docker/2019-06-15 基于 Container 网络共享机制的抓包实践.md: -------------------------------------------------------------------------------- 1 | # 基于 Container 网络共享机制的抓包实践 2 | 3 | ## 背景 4 | 5 | 假设存在一个容器,提供的服务是 HTTP 或者 RPC 的服务。由于出于简单可维护的目的,这个容器的基础镜像里面没有带上任何和网络抓包相关的功能。那么如何能搞对这样的容器进行抓包,以分析业务上面可能存在的问题呢? 6 | 7 | ## 共享网络 8 | 9 | Docker 的容器之间可以通过共享网络空间的方式,来让多个容器实现网络互通。这个意思直接一点就是如果你到每个容器内部去访问 localhost 监听的服务,无论这个服务在哪个容器里面,都能够访问成功。这就为网络抓包提供了基础。 10 | 11 | ## 专用镜像 12 | 13 | 我们可以使用一个专用的 tcpdump 的镜像来进行抓包。镜像名称为 corfr/tcpdump:latest ,可以使用 docker pull 直接下载。 14 | 15 | ``` 16 | $ docker pull corfr/tcpdump:latest 17 | ``` 18 | 19 | ## 抓包实践 20 | 21 | 我们现在用一个提供简单 HTTP 服务的镜像来进行测试。 22 | 23 | 下载测试镜像 24 | 25 | ``` 26 | $ docker pull jemygraw/echo-go:1.0 27 | ``` 28 | 29 | 启动测试容器 30 | 31 | ``` 32 | $ docker run -p 8080:8080 jemygraw/echo-go:1.0 /home/app/echo-go -port 8080 33 | ``` 34 | 35 | 查看测试容器ID 36 | 37 | ``` 38 | $ docker ps 39 | 5de30e950459 jemygraw/echo-go:1.0 "/home/app/echo-go -…" 10 minutes ago Up 10 minutes 0.0.0.0:8080->8080/tcp dreamy_benz 40 | ``` 41 | 42 | 启动抓包镜像,注意使用 `--network` 参数来共享测试容器的网络。 43 | 44 | ``` 45 | $ docker run --network container:5de30e950459 corfr/tcpdump:latest -i any -U -w - 46 | ``` 47 | 48 | 由于 `corfr/tcpdump:latest` 镜像构建的时候使用了 `ENTRYPOINT` 指定了入口命令为 `/usr/sbin/tcpdump` ,所以这里我们指定命令的选项参数即可。 49 | 50 | 这个时候我们可以尝试访问 `http://localhost:8080/` 就能够在抓包容器的输出中看到抓包结果了。 51 | 52 | ## 真实案例 53 | 54 | Kubernetes 的插件命令 sniff 就是使用了上面的技术来实现特权模式下通过旁观的抓包容器共享目标容器的网络来实现抓包的。该项目地址在:[https://github.com/eldadru/ksniff](https://github.com/eldadru/ksniff) ,有兴趣可以研究。 55 | 56 | -------------------------------------------------------------------------------- /GoMicro/2019-07-04 关于微服务架构中服务通信的思考.md: -------------------------------------------------------------------------------- 1 | # 微服务中 RPC 和 RESTFUL 之间的区别 2 | 3 | 这篇文章大概是没有什么结构的,基本上是想到什么写什么。不管对不对先把思考的东西写下来再说。 4 | 5 | 之所以思考微服务的问题,是一直想搞清楚Restful的接口规范和RPC的接口规范到底有什么不同,以及它们彼此适用的场景。 6 | 7 | 事实上在最近的一个系统中,我一直使用的是Restful的接口,因为这个系统是前后端分离的模式,前端用React,而后端用的是Go。而这个系统的服务对象是产品团队,他们的服务都是基于SpringCloud的微服务框架。 8 | 9 | 为了能够让大家能够更加轻松地使用Spring框架来开发应用,SpringBoot出现了,而为了能够让Spring更好地支持微服务SpringCloud出现了。所以SpringBoot是传统的单体应用解决方案,而SpringCloud是微服务架构的解决方案。 10 | 11 | 总之为了简单,下面都叫做Spring服务。 12 | 13 | 理论上Spring服务之间的调用采用Restful和RPC都可以。Restful和RPC规范的技术风格这个网上都有,但是技术选型的基本策略倒是没看到多少,这个就是为什么我要自己想一想写一写的原因,或许哪里也有但是我没看到,反正就这样了。 14 | 15 | Chris大佬说不存在所谓的“微服务”,而是微服务架构。而且这个微服务和服务的大小没关系,而是基于服务功能性把不同的功能内聚为一个服务,从原有的单体应用中剥离。 16 | 17 | 剥离之后的各个服务拥有自己独立的数据库,独立的业务逻辑,和其他的业务系统之间使用接口互相调用。那么问题来了,到底使用Restful还是RPC接口呢?我们这里说的RPC接口主要是指基于Protobuff这类调用的RPC。因为如果单从字面意思去理解Restful本身也是RPC接口。 18 | 19 | 经过一段时间的思考,我个人认为在内部进行通信的系统采用基于Protobuff这类型的RPC方案会比较好。而如果一个服务需要对外提供接口,比如是一个直接面向移动APP或者是Web前端的服务,采用Restful会比较好。 20 | 21 | 这种思考的结果基于的最基本的原则就是协同开发的效率。 22 | 23 | 协同开发的效率主要取决于两点,第一个是参与协同的服务角色,另外一个则是企业发展过程中沉淀的基础设施。 24 | 25 | 比如服务 A 和服务 B 都是使用 SpringBoot 或者 Golang 开发的后端服务,彼此通信最佳方式是使用 RPC。采取这个方式要基于一个事实就是服务 A 和服务 B 之间存在着大量的通信。在存在大量通信的服务之间,如果采用Restful的接口,那么还需要去定义接口的表现层,然后描述具体的参数和响应。这个在存在大量接口交互的情况下,是一个庞大的工作量,另外接口的变动更新也变得复杂。在微服务框架下,出于高可用的情况,服务的实例一般会有多个,这种带来了服务发现和转发的问题。在基于RPC的调用中,我们可以使用 API 网关完成这个工作。而在 Restful 的接口中,更多的要依赖运维层面的控制。 26 | 27 | 当然如果这个时候出现了服务 M 和 W ,分别代表了移动端和Web端。那么这个时候如果要和一个后端的服务 S 通信,那么最佳的方案是使用 Restful 风格的接口。因为在面向用户交互的过程中,更多的是定义和维护系统中的实体对象,简单来说就是面向对象的CRUD,这个使用Restful接口最方便了,而且这种CRUD的接口很少发生变动,即使变动维护成本也很低。 28 | 29 | 另外一般在企业当中,前端应用包括移动端和Web端都有了比较成熟的基础设施,一般就是基于流行开源框架基础之上进一步封装的集成了各个企业内部的一些基础服务比如统一登陆形成了框架。这些框架优先也会选择使用 Restful 的接口去和后端进行交互。这种情况下,接口文档是必不可少的。而RPC的模式下,写好框架里面语言层面的 interface 的注释基本上就好了。 30 | 31 | 总之呢,上面的描述适用于一些比较大型的系统交互。当然了Chris大佬说过,如果不是大型的系统,搞什么微服务架构,单体应用用用好嘞。 32 | 33 | 当然了,基于RPC的调用在网络协议方面可以采用HTTP或者是TCP,而Restful一般就只能用HTTP了。在大量接口交互下,RPC还有性能提升的空间。 34 | 35 | 就写那么多吧,再想想如果不对,重写。 36 | 37 | EOF 38 | 39 | -------------------------------------------------------------------------------- /GoPackages/2016-03-22 redigo 使用连接池和不使用连接池的性能对比测试.md: -------------------------------------------------------------------------------- 1 | 我们知道很多数据库连接驱动都提供连接池功能,以复用底层的TCP连接,同样redigo也提供连接池功能,这里我们将里面Go语言自身的基准测试来看一下,使用连接池的情况下,对程序性能的提升。我们主要从运行时间和内存消耗这两方面作对比测试。测试代码如下: 2 | 3 | ``` 4 | package redis 5 | 6 | import ( 7 | "github.com/garyburd/redigo/redis" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | func BenchmarkNoPool(b *testing.B) { 13 | for i := 0; i < b.N; i++ { 14 | func() { 15 | rdsConn, gErr := redis.Dial("tcp", "0.0.0.0:6379") 16 | if gErr != nil { 17 | b.Error(gErr) 18 | b.FailNow() 19 | } 20 | defer rdsConn.Close() 21 | 22 | if _, dErr := rdsConn.Do("SET", "a", "apple"); dErr != nil { 23 | b.Error(dErr) 24 | b.FailNow() 25 | } 26 | 27 | if reply, rErr := rdsConn.Do("GET", "a"); rErr != nil { 28 | b.Error(rErr) 29 | b.FailNow() 30 | } else { 31 | if _, ok := reply.([]byte); ok { 32 | //b.Log(string(replyBytes)) 33 | } else { 34 | b.Error("Err: get value by string key") 35 | } 36 | } 37 | }() 38 | } 39 | } 40 | 41 | func BenchmarkWithPool(b *testing.B) { 42 | rdsPool := &redis.Pool{ 43 | MaxIdle: 100, 44 | IdleTimeout: time.Second * 300, 45 | Dial: func() (redis.Conn, error) { 46 | conn, cErr := redis.Dial("tcp", "0.0.0.0:6379") 47 | if cErr != nil { 48 | return nil, cErr 49 | } 50 | return conn, nil 51 | }, 52 | } 53 | 54 | for i := 0; i < b.N; i++ { 55 | func() { 56 | rdsConn := rdsPool.Get() 57 | defer rdsConn.Close() 58 | 59 | if _, dErr := rdsConn.Do("SET", "a", "apple"); dErr != nil { 60 | b.Error(dErr) 61 | b.FailNow() 62 | } 63 | 64 | if reply, rErr := rdsConn.Do("GET", "a"); rErr != nil { 65 | b.Error(rErr) 66 | b.FailNow() 67 | } else { 68 | if _, ok := reply.([]byte); ok { 69 | //b.Log(string(replyBytes)) 70 | } else { 71 | b.Error("Err: get value by string key") 72 | } 73 | } 74 | }() 75 | } 76 | } 77 | ``` 78 | 79 | **运行时间:** 80 | 81 | ``` 82 | ➜ redis go test -bench=BenchmarkNoPool 83 | testing: warning: no tests to run 84 | PASS 85 | BenchmarkNoPool-8 10000 193323 ns/op 86 | ok _/Users/jemy/Projects/redigo_demo/redis 1.961s 87 | ➜ redis go test -bench=BenchmarkWithPool 88 | testing: warning: no tests to run 89 | PASS 90 | BenchmarkWithPool-8 20000 79167 ns/op 91 | ok _/Users/jemy/Projects/redigo_demo/redis 2.369s 92 | ``` 93 | 94 | 从上面运行时间的对比来看,使用了连接池的程序胜出。 95 | 96 | **内存消耗:** 97 | 98 | ``` 99 | ➜ redis go test -bench=BenchmarkNoPool -benchmem 100 | testing: warning: no tests to run 101 | PASS 102 | BenchmarkNoPool-8 10000 183410 ns/op 9208 B/op 25 allocs/op 103 | ok _/Users/jemy/Projects/redigo_demo/redis 1.863s 104 | ➜ redis go test -bench=BenchmarkWithPool -benchmem 105 | testing: warning: no tests to run 106 | PASS 107 | BenchmarkWithPool-8 20000 79970 ns/op 288 B/op 11 allocs/op 108 | ok _/Users/jemy/Projects/redigo_demo/redis 2.388s 109 | ``` 110 | 111 | 同样,从上面的内存消耗方面,带连接池的也是胜出。 112 | 113 | **结论:** 114 | 115 | 所以,如果你的程序需要大规模地操作Redis数据库,放心地使用连接池吧。 -------------------------------------------------------------------------------- /GoPackages/2016-03-22 redigo 基础知识讲解之客户端连接.md: -------------------------------------------------------------------------------- 1 | redigo 是 Go 语言操作 Redis 数据库的客户端库,就像 mysql 的驱动程序库一样, redigo 和 Redis 数据库之间的交互主要通过 Go 语言来实现。 2 | 3 | 我们都知道 Redis 数据库和客户端之间的交互主要是通过TCP协议来完成,所以很自然地 redigo 中就首先定义了一个叫做 `Conn` 的表示数据库连接的接口。在这个接口中,定义了一个客户端数据库连接可以做的所有的事情。接口定义(在redis.go中)如下: 4 | 5 | ``` 6 | // Conn 代表了一个到 Redis 数据库的连接 7 | type Conn interface { 8 | // 关闭这个连接 9 | Close() error 10 | 11 | // Err 方法在连接断开的时候返回一个非 nil 的值。 12 | // 返回的值是底层网络连接过程中遇到的第一个错误或者是协议解析错误 13 | // 应用程序应该关闭断开的连接。 14 | Err() error 15 | 16 | // Do 方法向服务器发送一个命令并返回命令的执行结果 17 | Do(commandName string, args ...interface{}) (reply interface{}, err error) 18 | 19 | // Send 方法将命令发送到输出缓冲中 20 | Send(commandName string, args ...interface{}) error 21 | 22 | // Flush 方法将输出缓冲中的命令发送到服务器 23 | Flush() error 24 | 25 | // Receive 从服务器接受一个命令返回结果 26 | Receive() (reply interface{}, err error) 27 | } 28 | 29 | ``` 30 | 31 | 从上面的接口我们可以看出,每一个到 Redis 数据库的客户端连接都是独立的,所以很自然的编程范式是: 32 | 33 | ``` 34 | conn := ... 35 | defer conn.Close() 36 | 37 | ``` 38 | 39 | 另外 `Do` 方法的调用等同于 `Send-Flush-Receive` 的组合调用。 40 | 41 | 既然上面已经定义了一个代表 Redis 客户端到服务器端的接口,必然需要实现这个接口,我们才能在业务代码中创建连接,发送命令,获取命令的返回结果或者是输出错误。 42 | 43 | 这个接口的实现在`conn.go`中。 44 | 45 | 在`conn.go`中,定义了一个包内部的结构体,这个结构体实现了上面的接口`Conn`中的所有方法,另外在这个文件中,还提供了创建客户端连接的方法。这个结构体就是`conn`,定义如下: 46 | 47 | ``` 48 | // conn 是 Conn 接口的底层实现 49 | type conn struct { 50 | 51 | // 互斥锁 52 | mu sync.Mutex 53 | pending int 54 | err error 55 | 56 | // 底层TCP连接 57 | conn net.Conn 58 | 59 | // 网络输入流 60 | readTimeout time.Duration 61 | br *bufio.Reader 62 | 63 | // 网络输出流 64 | writeTimeout time.Duration 65 | bw *bufio.Writer 66 | 67 | // Scratch space for formatting argument length. 68 | // '*' or '$', length, "\r\n" 69 | lenScratch [32]byte 70 | 71 | // Scratch space for formatting integers and floats. 72 | numScratch [40]byte 73 | } 74 | 75 | ``` 76 | 77 | 这个结构体实现的方法如下: 78 | 79 | ``` 80 | func (c *conn) Close() error 81 | func (c *conn) Err() error 82 | func (c *conn) Do(cmd string, args ...interface{}) (interface{}, error) 83 | func (c *conn) Send(cmd string, args ...interface{}) error 84 | func (c *conn) Flush() error 85 | func (c *conn) Receive() (reply interface{}, err error) 86 | 87 | ``` 88 | 89 | 另外在这个文件`conn.go`还定义了一个`xDialer`结构体,该结构体定义了相关的建立底层TCP连接的方法。 90 | 91 | 为了方便我们创建客户端连接,上面的文件`conn.go`中还定义了两个帮助函数: 92 | 93 | ``` 94 | func Dial(network, address string) (Conn, error) 95 | func DialTimeout(network, address string, connectTimeout, readTimeout, writeTimeout time.Duration) (Conn, error) 96 | 97 | ``` 98 | 99 | 好了,介绍了这些内容,基本上我们也可以看一个小例子了,向数据库写入一个字符串再从中读取。 100 | 101 | ``` 102 | package main 103 | 104 | import ( 105 | "fmt" 106 | "github.com/garyburd/redigo/redis" 107 | ) 108 | 109 | func main() { 110 | rdsConn, gErr := redis.Dial("tcp", "0.0.0.0:6379") 111 | if gErr != nil { 112 | fmt.Println(gErr) 113 | return 114 | } 115 | defer rdsConn.Close() 116 | 117 | if _, dErr := rdsConn.Do("SET", "a", "apple"); gErr != nil { 118 | fmt.Println(dErr) 119 | return 120 | } 121 | 122 | if reply, rErr := rdsConn.Do("GET", "a"); rErr != nil { 123 | fmt.Println(rErr) 124 | return 125 | } else { 126 | if replyBytes, ok := reply.([]byte); ok { 127 | fmt.Println(string(replyBytes)) 128 | } else { 129 | fmt.Println("Err: get value by string key") 130 | } 131 | } 132 | } 133 | 134 | ``` -------------------------------------------------------------------------------- /GoPackages/2018-09-06 k8s 依赖库中的wait功能.md: -------------------------------------------------------------------------------- 1 | # k8s 依赖库中的 wait 库功能 2 | 3 | 该库提供了很多基于周期性执行的方法,以及约束周期性执行的方法。 4 | 5 | ## 周期性执行一个函数 6 | 7 | 在某些情况下,我们需要周期性地执行一些动作,比如发送心跳请求给master,那么可以使用 wait 库中的 Forever 功能。 8 | 这里给一个简单的例子,每隔一秒钟输出当前的时间。 9 | 10 | ``` 11 | package main 12 | 13 | import ( 14 | "fmt" 15 | "time" 16 | 17 | "k8s.io/apimachinery/pkg/util/wait" 18 | ) 19 | 20 | func main() { 21 | wait.Forever(func() { 22 | fmt.Println(time.Now().String()) 23 | }, time.Second) 24 | } 25 | ``` 26 | 27 | ## 带StopSignal的周期性执行函数 28 | 29 | 上面的 Wait 函数其实是 Util 的变体,Util 本身还带有一个 stopSignal 选项。比如我们要删除一个CDN资源,然后删除之后周期性地检查文件是否还可以访问。可以用下面的逻辑。我们这里用counter来代替检查资源状态的判断逻辑。 30 | 31 | ``` 32 | package main 33 | 34 | import ( 35 | "fmt" 36 | "time" 37 | 38 | "k8s.io/apimachinery/pkg/util/wait" 39 | ) 40 | 41 | var stopSignal = make(chan struct{}) 42 | 43 | func main() { 44 | var counter = 1 45 | wait.Until(func() { 46 | if counter > 10 { 47 | close(stopSignal) 48 | } 49 | fmt.Println(time.Now().String()) 50 | counter++ 51 | }, time.Second, stopSignal) 52 | 53 | } 54 | ``` 55 | 56 | 57 | ## sync.WaitGroup 的封装及扩展 58 | 59 | 最简单的是对WaitGroup的简单封装 60 | 61 | ``` 62 | package main 63 | 64 | import ( 65 | "fmt" 66 | 67 | "k8s.io/apimachinery/pkg/util/wait" 68 | ) 69 | 70 | func main() { 71 | g := wait.Group{} 72 | for i := 0; i < 100; i++ { 73 | j := i 74 | g.Start(func() { 75 | fmt.Println(j) 76 | }) 77 | } 78 | g.Wait() 79 | } 80 | ``` 81 | 82 | 我们再假设一个场景,老大说大家去抓网页,差不多抓满1000个网页就结束。这个时候大家并发去抓,想要同步是比较困难的,另外什么时候通知大家结束也比较麻烦。这里,我们可以用下面的这样的框架代码。 83 | 84 | ``` 85 | package main 86 | 87 | import ( 88 | "fmt" 89 | "time" 90 | 91 | "sync/atomic" 92 | 93 | "k8s.io/apimachinery/pkg/util/wait" 94 | ) 95 | 96 | var stopSignal = make(chan struct{}) 97 | 98 | func main() { 99 | g := wait.Group{} 100 | var counter int32 101 | for i := 0; i < 100; i++ { 102 | j := i 103 | g.StartWithChannel(stopSignal, func(stopCh <-chan struct{}) { 104 | for { 105 | //quit if 106 | if atomic.LoadInt32(&counter) > 1000 { 107 | return 108 | } 109 | //otherwise 110 | select { 111 | case <-stopSignal: 112 | return 113 | default: 114 | fmt.Println(j, time.Now().String()) 115 | atomic.AddInt32(&counter, 1) 116 | <-time.After(time.Second) 117 | } 118 | } 119 | }) 120 | } 121 | g.Wait() 122 | } 123 | ``` 124 | 125 | 刚刚的场景还可以使用`StartWithContext`方法来实现。 126 | 127 | ``` 128 | package main 129 | 130 | import ( 131 | "context" 132 | "fmt" 133 | "time" 134 | 135 | "sync/atomic" 136 | 137 | "k8s.io/apimachinery/pkg/util/wait" 138 | ) 139 | 140 | func main() { 141 | g := wait.Group{} 142 | var counter int32 143 | ctx, cancelFunc := context.WithCancel(context.Background()) 144 | for i := 0; i < 100; i++ { 145 | j := i 146 | g.StartWithContext(ctx, func(ctx context.Context) { 147 | for { 148 | //quit if 149 | if atomic.LoadInt32(&counter) > 1000 { 150 | cancelFunc() //fire cancel signal 151 | } 152 | //otherwise 153 | select { 154 | case <-ctx.Done(): //cancel signal received 155 | return 156 | default: 157 | fmt.Println(j, time.Now().String()) 158 | atomic.AddInt32(&counter, 1) 159 | <-time.After(time.Second) 160 | } 161 | } 162 | }) 163 | } 164 | g.Wait() 165 | } 166 | ``` -------------------------------------------------------------------------------- /GoPackages/2018-11-05 基于Websocket的简易webshell实现.md: -------------------------------------------------------------------------------- 1 | 我们在很多场合都看到过基于浏览器的 shell,你可以在里面输入一些和你本机相同的命令,然后从远程服务器获得对应的输出。 2 | 3 | 本篇文章就是用来讲解这个基于 web 的 shell 的实现方法的。我们之所以研究这个问题,另一方面也是因为 kubernetes 的 Dashboard 里面也包含了这个功能。 4 | 在研究 kubernetes 的 Dashboard 的时候,我们会发现那个功能是基于 WebSocket 来实现的。所以本篇也就是讲解基于 WebSocket 的 shell 实现方法。 5 | 6 | ## 思路 7 | 8 | 我们如果仔细地思考一下,其实这个 web shell 的主要功能就是将这个命令发送到远程服务器,然后远程服务器执行这个命令,然后把结果返回给客户端就可以了。所以在这个客户端和服务器的交互场景下,有很多的方案可以选择,比如直接使用 HTTP 协议或者使用 TCP 协议,那么为什么 kubernetes 在实现的使用使用 web socket协议呢? 9 | 10 | 在进行一个技术方案的选型的时候,最重要的就是深入了解各个方案的利弊,以及它们最适用的场景。所以我们可以对比下基于 TCP,HTTP 和 WebSocket 三种协议实现这个 webshell 的优缺点。 11 | 12 | |协议|类别|特点| 13 | |---|---|---| 14 | |WebSocket|七层(应用层)|兼容HTTP的80端口和HTTPS的443端口,可以运行在HTTP或HTTPS协议上,全双工协议,基于事件驱动的交互方式,客户端不需要轮询服务端的执行结果| 15 | |HTTP(s)|七层(应用层)|HTTP需要保持长连接来维持客户端和服务器之间的不断的命令执行交互,否则频繁的短连接性能损耗严重,另外客户端需要主动轮训服务端的执行结果| 16 | |TCP|四层(传输层)|我就是个裸的传输层协议啦,HTTPS(s)和WebSocket都最终依赖我| 17 | 18 | 19 | 从原理上讲,大家最后都是需要依赖TCP协议来进行数据传输,所以如果坚持用TCP协议实现 webshell 当然是可以的,没有任何问题。 20 | 但是协议的抽象目的就是简化问题的解决方案以及解决旧有方案的缺点,HTTP的出现就是一种规范化的TCP协议应用,否则按照大家各自定义自己的数据格式的做法,这个互联网还是不要搞了,没法搞。你能脑补出每个公司每天都在互相接入对方的协议开发自己的应用么?画面太美,不敢想,不敢想。 21 | 22 | 那么我们就看看 WebSokcet 的出现简化和解决了 HTTP 协议的哪些问题就可以了。 23 | 24 | 对于交互式场景的应用,最重要的就是等待回复的不确定性,比如你发个消息给对方,对方什么时候回复是不确定的,你执行了一个远端的命令,这个命令什么时候执行完毕也是不确定的,在HTTP协议中,解决这种不确定性的方案是什么呢?轮询!你不是没有办法告诉我么?我自己去问行不?可以,来问吧,周期性地询问一下。 25 | 26 | 轮询的做法有什么问题呢?首先就是轮询周期的设定,你怎么设定这个时间呢?周期太短,白白浪费那么多建立连接,断开连接的动作,就像很久以前谈恋爱的小伙子,每隔一分钟去问一下传达室的大爷,今天刚寄出的信有没有回复。大爷很快就口吐白沫了。但是要是每隔一天去问,看上去好像不错,但是万一那个信是上午到的,结果你下午才去拿,那白白痛苦等待了一个上午不是。所以这个方案不好,但是没办法。 27 | 28 | 刚刚说了轮询的第一个缺点,第二个就是你总是轮询,整个路上全是你来来往往的身影,占用带宽不是,浪费连接不是。 29 | 30 | 那么 WebSocket 的出现,就可以解决这个问题了,大爷说,小伙子信发出去了不着急,等信到了大爷通知你,甚至主动把信送给你,你看好不好。 31 | 32 | 这就完美解决问题了嘛。 33 | 34 | ## 代码 35 | 36 | BB那么久,好歹说清楚 WebSocket 哪里好了,简单贴个能跑的代码吧。能跑就是说能展示原理,但是别直接拷贝就上线了,不好。 37 | 38 | 先上客户端,就是模拟每次发一个命令过去,然后命令后面接上换行符,算是简单的协议格式。 39 | 40 | 41 | remoteshell-client.go 42 | 43 | ``` 44 | package main 45 | 46 | import ( 47 | "flag" 48 | "golang.org/x/net/websocket" 49 | "bufio" 50 | "fmt" 51 | "os" 52 | ) 53 | 54 | func main() { 55 | var origin string 56 | var url string 57 | flag.StringVar(&origin, "origin", "", "websocket origin") 58 | flag.StringVar(&url, "url", "", "websocket remote url") 59 | flag.Parse() 60 | 61 | ws, err := websocket.Dial(url, "", origin) 62 | if err != nil { 63 | panic(err) 64 | return 65 | } 66 | buffer := make([]byte, 40960) 67 | bScanner := bufio.NewScanner(os.Stdin) 68 | fmt.Print("> ") 69 | for bScanner.Scan() { 70 | line := bScanner.Text() 71 | ws.Write([]byte(line + "\r\n")) 72 | num, err := ws.Read(buffer) 73 | if err != nil { 74 | ws.Close() 75 | return 76 | } 77 | fmt.Println(string(buffer[:num])) 78 | fmt.Print("> ") 79 | } 80 | } 81 | ``` 82 | 83 | 执行方法: 84 | 85 | ``` 86 | $ ./remoteshell-client -url 'ws://localhost:9001/remote/shell' -origin 'http://localhost:9001' 87 | ``` 88 | 89 | 服务端就是处理这个请求并给个回复了,因为这个连接是一直都在的,所以读数据就是直接for循环去读。我们在客户端发送数据的时候,给每条数据加了一个换行符,所以服务端就可以按行来读了。 90 | 91 | ``` 92 | package main 93 | 94 | import ( 95 | "flag" 96 | "fmt" 97 | "net/http" 98 | "os/exec" 99 | "bytes" 100 | "strings" 101 | "golang.org/x/net/websocket" 102 | "bufio" 103 | "os" 104 | ) 105 | 106 | func RemoteShell(ws *websocket.Conn) { 107 | bScanner := bufio.NewScanner(ws) 108 | currentWorkingDir, _ := os.Getwd() 109 | fmt.Println("current working dir", currentWorkingDir) 110 | 111 | for bScanner.Scan() { 112 | // parse command 113 | cmd := bScanner.Text() 114 | fmt.Println(cmd) 115 | cmdItems := strings.Split(cmd, " ") 116 | cmdName := cmdItems[0] 117 | 118 | var cmdArgs []string 119 | if len(cmdItems) >= 2 { 120 | cmdArgs = cmdItems[1:] 121 | } 122 | 123 | // execute command 124 | cmdOutput := bytes.NewBuffer(nil) 125 | cmdExec := exec.Command(cmdName, cmdArgs...) 126 | cmdExec.Dir = currentWorkingDir 127 | cmdExec.Stdout = cmdOutput 128 | cmdExec.Stderr = cmdOutput 129 | err := cmdExec.Run() 130 | if err != nil { 131 | fmt.Println(err) 132 | ws.Write([]byte(err.Error())) 133 | } else { 134 | ws.Write(cmdOutput.Bytes()) 135 | } 136 | } 137 | } 138 | 139 | func main() { 140 | var host string 141 | var port int 142 | flag.StringVar(&host, "host", "0.0.0.0", "host to listen") 143 | flag.IntVar(&port, "port", 9001, "port to listen") 144 | flag.Parse() 145 | 146 | //handler 147 | http.Handle("/remote/shell", websocket.Handler(RemoteShell)) 148 | 149 | //listen 150 | endPoint := fmt.Sprintf("%s:%d", host, port) 151 | err := http.ListenAndServe(endPoint, nil) 152 | if err != nil { 153 | fmt.Println(err) 154 | return 155 | } 156 | } 157 | ``` 158 | 159 | 执行方法: 160 | 161 | ``` 162 | $ ./remoteshell-server 163 | ``` -------------------------------------------------------------------------------- /Golang/2015-11-30 Go标准库strings中的一些容易令人混淆的方法.md: -------------------------------------------------------------------------------- 1 | 主要先介绍如下的两组方法: 2 | 3 | ``` 4 | func ToTitle(s string) string 5 | func Title(s string) string 6 | ``` 7 | 8 | 和 9 | 10 | ``` 11 | func TrimRight(s string, cutset string) string 12 | func TrimSuffix(s, suffix string) string 13 | ``` 14 | 15 | 但从方法的字面意义上面看,好像效果差不多,其实差距大着呢。 16 | 17 | 例如 `ToTitle`的意思是: 18 | >ToTitle returns a copy of the string s with all Unicode letters mapped to their title case. 19 | >将字符串中所有的Unicode字符都替换为对应的大写形式,然后返回一个新的字符串 20 | 21 | 而`Title`的意思是: 22 | >Title returns a copy of the string s with all Unicode letters that begin words mapped to their title case. 23 | >将字符串中每个单词的首字母替换为对应的大写形式,然后返回一个新的字符串 24 | 25 | **差别就在于`所有的Unicode字符`和`每个单词的首字母。** 26 | 27 | 所以如下的代码输出就好理解了。 28 | 29 | ``` 30 | package main 31 | 32 | import ( 33 | "fmt" 34 | "strings" 35 | ) 36 | 37 | func main() { 38 | msg := "i am a message to test func Title and ToTitle" 39 | 40 | //I AM A MESSAGE TO TEST FUNC TITLE AND TOTITLE 41 | fmt.Println(strings.ToTitle(msg)) 42 | 43 | //I Am A Message To Test Func Title And ToTitle 44 | fmt.Println(strings.Title(msg)) 45 | } 46 | ``` 47 | 48 | ------ 49 | 50 | 然后就是`TrimRight`和`TrimSuffix`了,这两个方法甚至坑过大牛(:-P)。 51 | 52 | ``` 53 | func TrimRight(s string, cutset string) string 54 | func TrimSuffix(s, suffix string) string 55 | ``` 56 | 57 | 其中`TrimRight`方法的意思如下: 58 | >TrimRight returns a slice of the string s, with all trailing Unicode code points contained in cutset removed. 59 | >将s尾部的所有在cutset中指定的字符移除掉,然后返回一个新的切片 60 | 61 | 而`TrimSuffix`方法的意思如下: 62 | >TrimSuffix returns s without the provided trailing suffix string. If s doesn't end with suffix, s is returned unchanged. 63 | >如果s以suffix结尾,则返回一个新的移除了suffix的s的切片,如果不已suffix结尾,则返回s本身 64 | 65 | **差别就在于一个是移除s中所有的在cutset中的字符,另外一个是移除尾部子字符串** 66 | 67 | 那么如下的代码就很容易理解了: 68 | 69 | ``` 70 | package main 71 | 72 | import ( 73 | "fmt" 74 | "strings" 75 | ) 76 | 77 | func main() { 78 | msg := "i love golang home" 79 | 80 | //i love golang 81 | fmt.Println(strings.TrimSuffix(msg, "home")) 82 | 83 | //i love golang 84 | fmt.Println(strings.TrimRight(msg, "omhe")) 85 | 86 | //i love golang 87 | fmt.Println(strings.TrimRight(msg, "home")) 88 | 89 | //i love golang 90 | fmt.Println(strings.TrimRight(msg, "meho")) 91 | 92 | //... 93 | } 94 | 95 | ``` 96 | 97 | OK,如果你理解了如上方法的差异,基本上就能防坑了。 98 | 99 | 另外相似的方法差距也可以类推: 100 | 101 | ``` 102 | func TrimLeft(s string, cutset string) string 103 | func TrimPrefix(s, prefix string) string 104 | ``` 105 | 106 | -------------------------------------------------------------------------------- /Golang/2015-12-19 Go里面除了引用语义之外的类型都可以作为字典的key.md: -------------------------------------------------------------------------------- 1 | 在Go里面,可以用基本类型作为字典的键,比如 整型,符点型,字符串,数组,结构体,指针类型等。例如下面的代码是OK的。 2 | 3 | ``` 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | ) 9 | 10 | func main() { 11 | s1 := [...]string{"hello", "world"} 12 | s2 := [...]string{"hello", "world"} 13 | 14 | m1 := make(map[[2]string]int, 0) 15 | m1[s1] = len(s1) 16 | m1[s2] = len(s2) 17 | 18 | fmt.Println(m1) 19 | } 20 | ``` 21 | 22 | ``` 23 | package main 24 | 25 | import ( 26 | "fmt" 27 | ) 28 | 29 | type Dog struct { 30 | Name string 31 | Age int 32 | } 33 | 34 | func (this *Dog) String() string { 35 | return fmt.Sprintf("Name: %s, Age: %d", this.Name, this.Age) 36 | } 37 | 38 | func main() { 39 | 40 | d1 := Dog{ 41 | "Jemy", 26, 42 | } 43 | d2 := Dog{ 44 | "Dolphin", 25, 45 | } 46 | 47 | dogs := make(map[Dog]string) 48 | dogs[d1] = d1.String() 49 | dogs[d2] = d2.String() 50 | 51 | fmt.Println(dogs) 52 | } 53 | ``` 54 | 55 | ``` 56 | package main 57 | 58 | import ( 59 | "fmt" 60 | ) 61 | 62 | func main() { 63 | s := [...]string{"hello", "world"} 64 | p1 := &s 65 | p2 := &s 66 | 67 | pmap := make(map[*[2]string]int) 68 | pmap[p1] = len(s) 69 | pmap[p2] = len(s) 70 | 71 | fmt.Println(pmap) 72 | } 73 | ``` 74 | 75 | 但是唯独切片是不可以作为字典的键的。 76 | 77 | ``` 78 | package main 79 | 80 | import ( 81 | "fmt" 82 | ) 83 | 84 | func main() { 85 | s1 := []string{"hello", "world"} 86 | s2 := []string{"hello", "world"} 87 | 88 | m1 := make(map[[]string]int, 0) 89 | m1[s1] = len(s1) 90 | m1[s2] = len(s2) 91 | 92 | fmt.Println(m1) 93 | } 94 | ``` 95 | 96 | -------------------------------------------------------------------------------- /Golang/2016-02-16 Go中根据函数名称调用指定函数的使用方法简介.md: -------------------------------------------------------------------------------- 1 | 根据函数名称调用对应的方法,在Web框架中应用十分广泛。我们以`beego`中的路由配置来介绍,比如有如下的路由: 2 | 3 | ``` 4 | beego.Router("/login",&controllers.LoginController{},"get:LoginPage;post:LoginAction") 5 | 6 | ``` 7 | 8 | 在上面的路由定义中,我们让`/login`既可以接收`GET`方法也可以接收`POST`方法,并且两个方法对应到`LoginController`中的不同的函数调用。这里我们就会在路由匹配之后,能够根据`LoginPage`函数名称和`LoginAction`函数名称,在`LoginController`找到对应的方法,这个就是`函数按名称调用`的应用实例。 9 | 10 | 下面,我们以一个简单的计算器程序来介绍`函数按名称调用`的使用方法。 11 | 12 | ``` 13 | package main 14 | 15 | import ( 16 | "fmt" 17 | "reflect" 18 | ) 19 | 20 | var opFuncs = map[string]string{"+": "Add", 21 | "-": "Minus", 22 | "*": "Multi", 23 | "/": "Divide", 24 | } 25 | 26 | type Calculator struct { 27 | } 28 | 29 | func (this *Calculator) Add(a, b int) int { 30 | return a + b 31 | } 32 | 33 | func (this *Calculator) Minus(a, b int) int { 34 | return a - b 35 | } 36 | 37 | func (this *Calculator) Multi(a, b int) int { 38 | return a * b 39 | } 40 | 41 | func (this *Calculator) Divide(a, b int) int { 42 | return a / b 43 | } 44 | 45 | func main() { 46 | var a int = 10 47 | var b int = 20 48 | 49 | calc := &Calculator{} 50 | 51 | for funcOp, funcName := range opFuncs { 52 | fn, _ := reflect.TypeOf(calc).MethodByName(funcName) 53 | 54 | params := make([]reflect.Value, 3) 55 | 56 | params[0] = reflect.ValueOf(calc) 57 | params[1] = reflect.ValueOf(a) 58 | params[2] = reflect.ValueOf(b) 59 | 60 | v := fn.Func.Call(params) 61 | fmt.Println(fmt.Sprintf("%d %s %d = %d", a, funcOp, b, v[0].Int())) 62 | } 63 | } 64 | 65 | ``` 66 | 67 | 上面的代码输出结果为: 68 | 69 | ``` 70 | 10 + 20 = 30 71 | 10 - 20 = -10 72 | 10 * 20 = 200 73 | 10 / 20 = 0 74 | 75 | ``` 76 | 77 | 最主要的是根据函数所在的结构体对象根据函数名称获取函数值,即通过`reflect.TypeOf(calc).MethodByName(funcName)` 来获取函数的值,然后再使用`[]reflect.Value`来组装函数调用所需要的参数,其中最重要的是`params[0]`必须是函数所在结构体对象的值,其他的则为对应函数的参数了。最后使用`fn.Func.Call`方法调用函数即可。 -------------------------------------------------------------------------------- /Golang/2016-10-24 理解golang的匿名函数的变量作用域.md: -------------------------------------------------------------------------------- 1 | 这里有一个例子代码,通过该代码可以理解golang的匿名函数的一些特点: 2 | 3 | ``` 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | ) 9 | 10 | func temp(msg string) func() string { 11 | return func() string { 12 | msg = msg[:len(msg)-1] 13 | return msg 14 | } 15 | } 16 | 17 | func main() { 18 | t1 := temp("hello world") 19 | fmt.Println(t1()) 20 | fmt.Println(t1()) 21 | fmt.Println(t1()) 22 | } 23 | ``` 24 | 25 | 这段代码的输出为: 26 | 27 | ``` 28 | hello worl 29 | hello wor 30 | hello wo 31 | ``` 32 | 33 | 从上面的代码,可以看出`t1`就是`temp`函数内部的匿名函数的引用,每次调用的时候,都相当于匿名函数在`temp`函数的作用域范围内执行一次,因为上一次执行修改了传入的参数`msg`,所以再次运行的时候,将以上一次得到的`msg`作为新的执行参数。 -------------------------------------------------------------------------------- /Golang/2017-05-31 Go语言包之 strings.markdown: -------------------------------------------------------------------------------- 1 | #Go语言包之 strings 2 | **Contains**(s, substr string) bool 3 | 检查substr是否在字符串s中,如果存在返回true,否则返回false。 4 | ```go 5 | package main 6 | import ( 7 | "fmt" 8 | "strings" 9 | ) 10 | func main(){ 11 | s:="hello i am jemy, who are you, what's your name" 12 | fmt.Println(strings.Contains(s,"name")) 13 | } 14 | //true 15 | ``` 16 | **ContainsAny**(s, chars string) bool 17 | 检查字符串s中是否存在chars字符串中的任何一个Unicode字符,如果存在则返回true,否则返回false。 18 | 比如下面的例子: 19 | ```go 20 | strings.ContainsAny(s, "xa") //true 21 | string.ContainsAny(s, "x") //false 22 | ``` 23 | **ContainsRune**(s string, r rune) bool 24 | 检查字符串s中是否存在Unicode字符r,如果存在返回true,否则返回false。 25 | ```go 26 | s:="hello i am jemy, who are you, what's your name" 27 | r:=`x` 28 | fmt.Println(strings.ContainsAny(s,r)) 29 | //false 30 | ``` 31 | **Count(s, sep string)** int 32 | 检查字符串sep在字符串s中出现的总次数 33 | ```go 34 | s:="we are a team, we are all boys, where are the girls" 35 | sep:="are" 36 | fmt.Println(strings.Count(s,sep)) 37 | //3 38 | ``` 39 | **EqualFold**(s, t string) bool 40 | 检查以UTF-8编码方式解析的字符串s和t是否相同,忽略大小写。 41 | ```go 42 | t:="你好,我是小猪猪A" 43 | s:="你好,我是小猪猪a" 44 | fmt.Println(strings.EqualFold(s,t)) 45 | //true 46 | ``` 47 | **Fields**(s string) []string 48 | Fields方法将字符串s用空白字符分割,空白字符由**unicode.IsSpace**所定义。方法返回分割后的字符串数组。如果字符串s仅仅包含空白字符的话,则返回空数组。 49 | ```go 50 | s1="we are a team we can fight together " 51 | fmt.Println(strings.Fields(s)) 52 | s2=" " 53 | fmt.Println(strings.Fields(s)) 54 | //[we are a team we can fight together] 55 | //[] 56 | ``` 57 | 58 | **FieldsFunc**(s string, f func(rune) bool) []string 59 | 这个方法判断字符串s中的每个Unicode字符,如果发现用c作为函数f的参数能够让f返回true的话,则用这个字符c来分割字符串s。并返回分割后的字符串数组。如果s字符串中所有的字符都可以让函数f返回true,或者字符串s为空,那么返回一个空字符串数组。 60 | ```go 61 | package main 62 | import ( 63 | "fmt" 64 | "strings" 65 | ) 66 | func is_a_or_b(c rune) bool{ 67 | result:=false 68 | if c>='a' && c <='b'{ 69 | result=true 70 | } 71 | return result 72 | } 73 | func is_我(c rune) bool{ 74 | result:=false 75 | if c == '我'{ 76 | result=true 77 | } 78 | return result 79 | } 80 | func main(){ 81 | s:="we are a team 我是大猪猪 " 82 | r_array:=strings.FieldsFunc(s, is_a_or_b) 83 | for i:=0; i' 62 | git config --global url."git@git.abc.com:".insteadOf "http://git.abc.com/" 63 | ``` 64 | 65 | 其中那个 替换为 Gitlab 里面的访问 AccessToken即可,自己生成一个。 66 | 67 | 最后,就可以使用 go build 啦。 -------------------------------------------------------------------------------- /Golang/2020-06-29 Go HTTP重用底层TCP连接需要注意的关键点.md: -------------------------------------------------------------------------------- 1 | # Go HTTP 重用底层TCP连接需要注意的关键点 2 | 3 | ## 前言 4 | 5 | 在写这篇文章之前,我在社区搜索了一下,找到了一个相关的帖子 [can't assign requested address 错误解决](https://gocn.vip/topics/8470),还是 @astaxie 自己写的。当然这里我之所以重复再写一个新帖子,是希望给大家提供一种新的验证的方式。 6 | 7 | ## 问题 8 | 9 | 有一次我在看某个项目(可能是kafka吧,记不清楚了)的源码的时候,我发现它的注释里面特别提到一句话,说是要读取完 `http.Response` 的 `Body`并关闭它,否则不会重用底层的 TCP 连接。我想了想为什么它这里一定要特别提出来呢?关闭 `http.Response` 不是一个常识性动作么?比如一般写代码我们都会遵循下面的模式: 10 | 11 | ```go 12 | resp, err := http.Get("http://www.example.com") 13 | if err != nil { 14 | return err 15 | } 16 | defer resp.Body.Close() 17 | 18 | respBody, err := ioutil.ReadAll(resp.Body) 19 | // ... 20 | ``` 21 | 22 | 在结合实际的场景之后,我发现其实有的时候问题出在`我们并不总是`会去读取完整个`http.Response` 的 `Body`。为什么这么说呢? 23 | 24 | 在常见的 API 开发的业务逻辑中,我们会定义一个 JSON 的对象来反序列化 `http.Response` 的 `Body`,但是通常在反序列化这个回复之前,我们会做一些 http 的 `StatusCode` 检查,比如当 `StatusCode` 为 `200` 的时候,我们才去读取 `http.Response` 的 `Body`,如果不是 `200`,我们就直接返回一个包装好的错误。比如下面的模式: 25 | 26 | ```go 27 | resp, err := http.Get("http://www.example.com") 28 | if err != nil { 29 | return err 30 | } 31 | defer resp.Body.Close() 32 | 33 | if resp.StatusCode == http.StatusOK { 34 | var apiRet APIRet 35 | decoder := json.NewDecoder(resp.Body) 36 | err := decoder.Decode(&apiRet) 37 | // ... 38 | } 39 | ``` 40 | 41 | 如果代码是按照上面的这种方式写的话,那么在请求异常的时候,会导致大量的底层 TCP 无法重用,所以我们稍微改进下就可以了。 42 | 43 | ```go 44 | resp, err := http.Get("http://www.example.com") 45 | if err != nil { 46 | return err 47 | } 48 | defer resp.Body.Close() 49 | 50 | if resp.StatusCode == http.StatusOK { 51 | var apiRet APIRet 52 | decoder := json.NewDecoder(resp.Body) 53 | err := decoder.Decode(&apiRet) 54 | // ... 55 | }else{ 56 | io.Copy(ioutil.Discard, resp.Body) 57 | // ... 58 | } 59 | ``` 60 | 61 | 我们通过直接将 `http.Response` 的 `Body` 丢弃掉就可以了。 62 | 63 | ## 原因 64 | 65 | 在 Go 的源码中,关于这个问题有特别的注释。 66 | 67 | ```go 68 | // Body represents the response body. 69 | // 70 | // The response body is streamed on demand as the Body field 71 | // is read. If the network connection fails or the server 72 | // terminates the response, Body.Read calls return an error. 73 | // 74 | // The http Client and Transport guarantee that Body is always 75 | // non-nil, even on responses without a body or responses with 76 | // a zero-length body. It is the caller's responsibility to 77 | // close Body. The default HTTP client's Transport may not 78 | // reuse HTTP/1.x "keep-alive" TCP connections if the Body is 79 | // not read to completion and closed. 80 | // 81 | // The Body is automatically dechunked if the server replied 82 | // with a "chunked" Transfer-Encoding. 83 | // 84 | // As of Go 1.12, the Body will also implement io.Writer 85 | // on a successful "101 Switching Protocols" response, 86 | // as used by WebSockets and HTTP/2's "h2c" mode. 87 | Body io.ReadCloser 88 | ``` 89 | 90 | 其中提到了必须将 `http.Response` 的 `Body` 读取完毕并且关闭后,才会重用底层的 TCP 连接。 91 | 92 | ## 实验 93 | 94 | 为了验证一把上面的问题,我们写了一个简单的对比实验,并且通过 Wireshark 抓包分析了一下。这里使用的是 https://www.oschina.net 作为例子,由于这个站点用的是 HTTPS,所以重用了 TCP 的话,那么一次建立 TLS 连接后面就不用重建了,非常方便观察。 95 | 96 | ### 重用了TCP连接 97 | 98 | ```go 99 | package main 100 | 101 | import ( 102 | "net/http" 103 | ) 104 | 105 | func main() { 106 | count := 100 107 | for i := 0; i < count; i++ { 108 | resp, err := http.Get("https://www.oschina.net") 109 | if err != nil { 110 | panic(err) 111 | } 112 | 113 | io.Copy(ioutil.Discard, resp.Body) 114 | resp.Body.Close() 115 | } 116 | } 117 | ``` 118 | 119 | ![images/tcp_reuse_yes.png](./images/tcp_reuse_yes.png) 120 | 121 | ### 未重用TCP连接 122 | 123 | ```go 124 | package main 125 | 126 | import ( 127 | "net/http" 128 | ) 129 | 130 | func main() { 131 | count := 100 132 | for i := 0; i < count; i++ { 133 | resp, err := http.Get("https://www.oschina.net") 134 | if err != nil { 135 | panic(err) 136 | } 137 | 138 | //io.Copy(ioutil.Discard, resp.Body) 139 | resp.Body.Close() 140 | } 141 | } 142 | ``` 143 | 144 | ![images/tcp_reuse_no.png](./images/tcp_reuse_no.png) 145 | 146 | ## 小结 147 | 148 | 学无止境,小心翼翼。 -------------------------------------------------------------------------------- /Golang/2020-07-01 Go HTTP 默认的Client对象使用注意事项.md: -------------------------------------------------------------------------------- 1 | # Go HTTP 默认的Client对象使用注意事项 2 | 3 | ## 前言 4 | 5 | 前面写过一篇帖子介绍了Go HTTP中重用TCP连接需要注意的问题,看上去阅读量很好。所有打算再介绍下Go HTTP中使用默认的Client时需要注意的事项。 6 | 7 | ## 问题 8 | 9 | 在一个系统中,通过HTTP调用第三方的服务是很常见的,而且稍微大的一点的系统,可能需要调用不同的组件来完成一项工作。这个时候就需要注意一些细节问题。 10 | 11 | 我们知道Go的`net/http`库里面有很多方法可以直接使用。比如常见的 `http.Get`和`http.Post`。一般情况下我们用这两个方法来写个脚本什么的都没啥问题。但是需要注意的是在大型系统中,千万不要直接使用这些方法。具体原因就在这些方法的定义中。 12 | 13 | ### http.Get 14 | 15 | ```go 16 | func Get(url string) (resp *Response, err error) { 17 | return DefaultClient.Get(url) 18 | } 19 | ``` 20 | 21 | ### http.Post 22 | 23 | ```go 24 | func Post(url, contentType string, body io.Reader) (resp *Response, err error) { 25 | return DefaultClient.Post(url, contentType, body) 26 | } 27 | ``` 28 | 29 | ### http.DefaultClient 30 | 31 | 上面的两个方法里面都使用了`http.DefaultClient`对象,这个对象是`net/http`包里面提供的可以即时使用的HTTP Client对象,它的定义如下: 32 | 33 | ```go 34 | // DefaultClient is the default Client and is used by Get, Head, and Post. 35 | var DefaultClient = &Client{} 36 | ``` 37 | 38 | 问题出在哪里?问题就出在这个`DefaultClient`是个指针,换句话说这个对象在 Go 的并发编程中是不安全的。 39 | 40 | ## 实验 41 | 42 | ```go 43 | package main 44 | 45 | import ( 46 | "fmt" 47 | "net/http" 48 | "time" 49 | ) 50 | 51 | func main() { 52 | // 初始化超时时间为 1 秒 53 | http.DefaultClient.Timeout = time.Second 54 | go func() { 55 | ticker := time.NewTicker(time.Second * 5) 56 | count := 1 57 | for { 58 | select { 59 | case <-ticker.C: 60 | // 每隔 5 秒,更新一下超时时间 61 | http.DefaultClient.Timeout = time.Second * time.Duration(count) 62 | count++ 63 | } 64 | } 65 | }() 66 | 67 | // 不断请求 Google,会触发超时,如果没有超时,说明你已经违法,😄 68 | for i := 0; i < 100; i++ { 69 | startTime := time.Now() 70 | func() { 71 | resp, err := http.Get("https://www.google.com") 72 | if err != nil { 73 | return 74 | } 75 | defer resp.Body.Close() 76 | }() 77 | 78 | // 打印下运行数据,开始时间,超时时间 79 | fmt.Println(fmt.Sprintf("Run %d:", i+1), "Start:", startTime.Format("15:04:05"), 80 | "Timeout:", time.Since(startTime)) 81 | 82 | // 每隔 1 秒请求一次 83 | <-time.After(time.Second) 84 | } 85 | } 86 | ``` 87 | 88 | 运行情况: 89 | 90 | ```s 91 | Run 1: Start: 21:37:42 Timeout: 1.002390001s 92 | Run 2: Start: 21:37:44 Timeout: 1.005189409s 93 | Run 3: Start: 21:37:46 Timeout: 1.001791553s 94 | Run 4: Start: 21:37:48 Timeout: 1.000847131s 95 | Run 5: Start: 21:37:50 Timeout: 1.0042284s 96 | Run 6: Start: 21:37:52 Timeout: 2.001313209s 97 | Run 7: Start: 21:37:55 Timeout: 2.000255175s 98 | Run 8: Start: 21:37:58 Timeout: 3.005502974s 99 | Run 9: Start: 21:38:02 Timeout: 4.005494172s 100 | Run 10: Start: 21:38:07 Timeout: 5.001988372s 101 | Run 11: Start: 21:38:13 Timeout: 6.000908119s 102 | Run 12: Start: 21:38:20 Timeout: 7.003262543s 103 | Run 13: Start: 21:38:28 Timeout: 9.000410503s 104 | Run 14: Start: 21:38:38 Timeout: 11.004758151s 105 | Run 15: Start: 21:38:50 Timeout: 13.002290813s 106 | ``` 107 | 108 | 其实不需要这个例子,你也能够明白在并发环境不能直接用共享的变量,否则会出问题的。比如A系统超时时间和B系统超时时间完全不同,结果因为用了共享的 `http.DefaultClient`,就混在一起了。 109 | 110 | ## 小结 111 | 112 | 学无止境,小心翼翼。 -------------------------------------------------------------------------------- /Golang/Code/go-restful/src/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | PROJ_DIR=$(cd ..;pwd) 3 | export GOPATH=$GOPATH:$PROJ_DIR 4 | go build main.go -------------------------------------------------------------------------------- /Golang/Code/go-restful/src/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "userservice" 7 | 8 | "github.com/emicklei/go-restful" 9 | ) 10 | 11 | func main() { 12 | restful.Add(userservice.New()) 13 | log.Fatal(http.ListenAndServe(":9090", nil)) 14 | } 15 | -------------------------------------------------------------------------------- /Golang/Code/go-restful/src/userservice/userservice.go: -------------------------------------------------------------------------------- 1 | package userservice 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/emicklei/go-restful" 7 | ) 8 | 9 | type User struct { 10 | Id, Name string 11 | } 12 | 13 | func New() *restful.WebService { 14 | service := new(restful.WebService) 15 | service.Path("/users").Consumes(restful.MIME_XML, restful.MIME_JSON). 16 | Produces(restful.MIME_XML, restful.MIME_JSON) 17 | 18 | service.Route(service.GET("/{user-id}").To(FindUser)) 19 | service.Route(service.POST("").To(UpdateUser)) 20 | service.Route(service.PUT("/{user-id}").To(CreateUser)) 21 | service.Route(service.DELETE("/{user-id}").To(RemoveUser)) 22 | return service 23 | } 24 | 25 | func FindUser(request *restful.Request, response *restful.Response) { 26 | id := request.PathParameter("user-id") 27 | // here you would fetch user from some persistence system 28 | usr := &User{Id: id, Name: "John Doe"} 29 | response.WriteEntity(usr) 30 | } 31 | 32 | func UpdateUser(request *restful.Request, response *restful.Response) { 33 | usr := new(User) 34 | err := request.ReadEntity(&usr) 35 | // here you would update the user with some persistence system 36 | if err == nil { 37 | response.WriteEntity(usr) 38 | } else { 39 | response.WriteError(http.StatusInternalServerError, err) 40 | } 41 | } 42 | 43 | func CreateUser(request *restful.Request, response *restful.Response) { 44 | usr := User{Id: request.PathParameter("user-id")} 45 | err := request.ReadEntity(&usr) 46 | // here you would create the user with some persistence system 47 | if err == nil { 48 | response.WriteEntity(usr) 49 | } else { 50 | response.WriteError(http.StatusInternalServerError, err) 51 | } 52 | } 53 | 54 | func RemoveUser(request *restful.Request, response *restful.Response) { 55 | // here you would delete the user from some persistence system 56 | } 57 | -------------------------------------------------------------------------------- /Golang/images/tcp_reuse_no.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jemygraw/TechDoc/38794deee04bafa5f1510c634f23c4f992762b39/Golang/images/tcp_reuse_no.png -------------------------------------------------------------------------------- /Golang/images/tcp_reuse_yes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jemygraw/TechDoc/38794deee04bafa5f1510c634f23c4f992762b39/Golang/images/tcp_reuse_yes.png -------------------------------------------------------------------------------- /Go示例学/Go Base64编码.markdown: -------------------------------------------------------------------------------- 1 | # Go Base64编码 2 | Go提供了对base64编码和解码的内置支持 3 | ```go 4 | package main 5 | 6 | // 这种导入包的语法将默认的base64起了一个别名b64,这样 7 | // 我们在下面就可以直接使用b64表示这个包,省点输入量 8 | import b64 "encoding/base64" 9 | import "fmt" 10 | 11 | func main() { 12 | 13 | // 这里是我们用来演示编码和解码的字符串 14 | data := "abc123!?$*&()'-=@~" 15 | 16 | // Go支持标准的和兼容URL的base64编码。 17 | // 我们这里使用标准的base64编码,这个 18 | // 函数需要一个`[]byte`参数,所以将这 19 | // 个字符串转换为字节数组 20 | sEnc := b64.StdEncoding.EncodeToString([]byte(data)) 21 | fmt.Println(sEnc) 22 | 23 | // 解码一个base64编码可能返回一个错误, 24 | // 如果你不知道输入是否是正确的base64 25 | // 编码,你需要检测一些解码错误 26 | sDec, _ := b64.StdEncoding.DecodeString(sEnc) 27 | fmt.Println(string(sDec)) 28 | fmt.Println() 29 | 30 | // 使用兼容URL的base64编码和解码 31 | uEnc := b64.URLEncoding.EncodeToString([]byte(data)) 32 | fmt.Println(uEnc) 33 | uDec, _ := b64.URLEncoding.DecodeString(uEnc) 34 | fmt.Println(string(uDec)) 35 | } 36 | ``` 37 | 运行结果 38 | ``` 39 | YWJjMTIzIT8kKiYoKSctPUB+ 40 | abc123!?$*&()'-=@~ 41 | 42 | YWJjMTIzIT8kKiYoKSctPUB- 43 | abc123!?$*&()'-=@~ 44 | ``` 45 | 这两种方法都将原数据编码为base64编码,区别在于标准的编码后面是`+`,而兼容URL的编码方式后面是`-`。 -------------------------------------------------------------------------------- /Go示例学/Go Defer.markdown: -------------------------------------------------------------------------------- 1 | # Go Defer 2 | Defer 用来保证一个函数调用会在程序执行的最后被调用。通常用于资源清理工作。 3 | 4 | ```go 5 | package main 6 | 7 | import "fmt" 8 | import "os" 9 | 10 | // 假设我们想创建一个文件,然后写入数据,最后关闭文件 11 | func main() { 12 | // 在使用createFile得到一个文件对象之后,我们使用defer 13 | // 来调用关闭文件的方法closeFile,这个方法将在main函数 14 | // 最后被执行,也就是writeFile完成之后 15 | f := createFile("/tmp/defer.txt") 16 | // Windows下面使用这个语句 17 | // f := createFile("D:\\Temp\\defer.txt") 18 | defer closeFile(f) 19 | writeFile(f) 20 | } 21 | 22 | func createFile(p string) *os.File { 23 | fmt.Println("creating") 24 | f, err := os.Create(p) 25 | if err != nil { 26 | panic(err) 27 | } 28 | return f 29 | } 30 | 31 | func writeFile(f *os.File) { 32 | fmt.Println("writing") 33 | fmt.Fprintln(f, "data") 34 | 35 | } 36 | 37 | func closeFile(f *os.File) { 38 | fmt.Println("closing") 39 | f.Close() 40 | } 41 | ``` 42 | 运行结果 43 | ``` 44 | creating 45 | writing 46 | closing 47 | ``` 48 | 使用defer来调用closeFile函数可以保证在main函数结束之前,关闭文件的操作一定会被执行。 -------------------------------------------------------------------------------- /Go示例学/Go Exit.markdown: -------------------------------------------------------------------------------- 1 | # Go Exit 2 | 使用`os.Exit`可以给定一个状态,然后立刻退出程序运行。 3 | ```go 4 | package main 5 | 6 | import "fmt" 7 | import "os" 8 | 9 | func main() { 10 | // 当使用`os.Exit`的时候defer操作不会被运行, 11 | // 所以这里的``fmt.Println`将不会被调用 12 | defer fmt.Println("!") 13 | 14 | // 退出程序并设置退出状态值 15 | os.Exit(3) 16 | } 17 | ``` 18 | 注意,Go和C语言不同,main函数并不返回一个整数来表示程序的退出状态,而是将退出状态作为`os.Exit`函数的参数。 19 | 20 | 如果你使用`go run`来运行程序,将会有如下输出 21 | ``` 22 | exit status 3 23 | ``` 24 | 如果你使用`go build`先编译程序,然后再运行可执行文件,程序将不会有输出。 25 | 如果你想查看程序的返回值,*nix系列系统下面使用如下方法: 26 | ``` 27 | $ ./go_exit 28 | $ echo $? 29 | 3 30 | ``` -------------------------------------------------------------------------------- /Go示例学/Go JSON支持.markdown: -------------------------------------------------------------------------------- 1 | # Go JSON支持 2 | Go内置了对JSON数据的编码和解码,这些数据的类型包括内置数据类型和自定义数据类型。 3 | 4 | ```go 5 | package main 6 | 7 | import "encoding/json" 8 | import "fmt" 9 | import "os" 10 | 11 | // 我们使用两个结构体来演示自定义数据类型的JSON数据编码和解码。 12 | type Response1 struct { 13 | Page int 14 | Fruits []string 15 | } 16 | type Response2 struct { 17 | Page int `json:"page"` 18 | Fruits []string `json:"fruits"` 19 | } 20 | 21 | func main() { 22 | 23 | // 首先我们看一下将基础数据类型编码为JSON数据 24 | bolB, _ := json.Marshal(true) 25 | fmt.Println(string(bolB)) 26 | 27 | intB, _ := json.Marshal(1) 28 | fmt.Println(string(intB)) 29 | 30 | fltB, _ := json.Marshal(2.34) 31 | fmt.Println(string(fltB)) 32 | 33 | strB, _ := json.Marshal("gopher") 34 | fmt.Println(string(strB)) 35 | 36 | // 这里是将切片和字典编码为JSON数组或对象 37 | slcD := []string{"apple", "peach", "pear"} 38 | slcB, _ := json.Marshal(slcD) 39 | fmt.Println(string(slcB)) 40 | 41 | mapD := map[string]int{"apple": 5, "lettuce": 7} 42 | mapB, _ := json.Marshal(mapD) 43 | fmt.Println(string(mapB)) 44 | 45 | // JSON包可以自动地编码自定义数据类型。结果将只包括自定义 46 | // 类型中的可导出成员的值并且默认情况下,这些成员名称都作 47 | // 为JSON数据的键 48 | res1D := &Response1{ 49 | Page: 1, 50 | Fruits: []string{"apple", "peach", "pear"}} 51 | res1B, _ := json.Marshal(res1D) 52 | fmt.Println(string(res1B)) 53 | 54 | // 你可以使用tag来自定义编码后JSON键的名称 55 | res2D := &Response2{ 56 | Page: 1, 57 | Fruits: []string{"apple", "peach", "pear"}} 58 | res2B, _ := json.Marshal(res2D) 59 | fmt.Println(string(res2B)) 60 | 61 | // 现在我们看看解码JSON数据为Go数值 62 | byt := []byte(`{"num":6.13,"strs":["a","b"]}`) 63 | 64 | // 我们需要提供一个变量来存储解码后的JSON数据,这里 65 | // 的`map[string]interface{}`将以Key-Value的方式 66 | // 保存解码后的数据,Value可以为任意数据类型 67 | var dat map[string]interface{} 68 | 69 | // 解码过程,并检测相关可能存在的错误 70 | if err := json.Unmarshal(byt, &dat); err != nil { 71 | panic(err) 72 | } 73 | fmt.Println(dat) 74 | 75 | // 为了使用解码后map里面的数据,我们需要将Value转换为 76 | // 它们合适的类型,例如我们将这里的num转换为期望的float64 77 | num := dat["num"].(float64) 78 | fmt.Println(num) 79 | 80 | // 访问嵌套的数据需要一些类型转换 81 | strs := dat["strs"].([]interface{}) 82 | str1 := strs[0].(string) 83 | fmt.Println(str1) 84 | 85 | // 我们还可以将JSON解码为自定义数据类型,这有个好处是可以 86 | // 为我们的程序增加额外的类型安全并且不用再在访问数据的时候 87 | // 进行类型断言 88 | str := `{"page": 1, "fruits": ["apple", "peach"]}` 89 | res := &Response2{} 90 | json.Unmarshal([]byte(str), &res) 91 | fmt.Println(res) 92 | fmt.Println(res.Fruits[0]) 93 | 94 | // 上面的例子中,我们使用bytes和strings来进行原始数据和JSON数据 95 | // 之间的转换,我们也可以直接将JSON编码的数据流写入`os.Writer` 96 | // 或者是HTTP请求回复数据。 97 | enc := json.NewEncoder(os.Stdout) 98 | d := map[string]int{"apple": 5, "lettuce": 7} 99 | enc.Encode(d) 100 | } 101 | ``` 102 | 运行结果 103 | ``` 104 | true 105 | 1 106 | 2.34 107 | "gopher" 108 | ["apple","peach","pear"] 109 | {"apple":5,"lettuce":7} 110 | {"Page":1,"Fruits":["apple","peach","pear"]} 111 | {"page":1,"fruits":["apple","peach","pear"]} 112 | map[num:6.13 strs:[a b]] 113 | 6.13 114 | a 115 | &{1 [apple peach]} 116 | apple 117 | {"apple":5,"lettuce":7} 118 | ``` -------------------------------------------------------------------------------- /Go示例学/Go Line Filters.markdown: -------------------------------------------------------------------------------- 1 | # Go Line Filters 2 | Line Filters翻译一下大概是行数据过滤器。简单一点就是一个程序从标准输入stdin读取数据,然后处理一下,将处理的结果输出到标准输出stdout。grep和sed就是常见的行数据过滤器。 3 | 这里有一个行数据过滤器的例子,是把一个输入文本转换为大写的文本。你可以使用这种方式来实现你自己的Go Line Filters。 4 | ```go 5 | package main 6 | 7 | import ( 8 | "bufio" 9 | "fmt" 10 | "os" 11 | "strings" 12 | ) 13 | 14 | func main() { 15 | 16 | // 使用缓冲scanner来包裹无缓冲的`os.Stdin`可以让我们 17 | // 方便地使用`Scan`方法,这个方法会将scanner定位到下 18 | // 一行的位置 19 | scanner := bufio.NewScanner(os.Stdin) 20 | 21 | for scanner.Scan() { 22 | // `Text`方法从输入中返回当前行 23 | ucl := strings.ToUpper(scanner.Text()) 24 | 25 | // 输出转换为大写的行 26 | fmt.Println(ucl) 27 | } 28 | 29 | // 在`Scan`过程中,检查错误。文件结束不会被当作一个错误 30 | if err := scanner.Err(); err != nil { 31 | fmt.Fprintln(os.Stderr, "error:", err) 32 | os.Exit(1) 33 | } 34 | } 35 | ``` 36 | 运行结果 37 | ``` 38 | hello world 39 | HELLO WORLD 40 | how are you 41 | HOW ARE YOU 42 | ``` -------------------------------------------------------------------------------- /Go示例学/Go Panic.markdown: -------------------------------------------------------------------------------- 1 | # Go Panic 2 | Panic表示的意思就是有些意想不到的错误发生了。通常我们用来表示程序正常运行过程中不应该出现的,或者我们没有处理好的错误。 3 | 4 | ```go 5 | package main 6 | 7 | import "os" 8 | 9 | func main() { 10 | 11 | // 我们使用panic来检查预期不到的错误 12 | panic("a problem") 13 | 14 | // Panic的通常使用方法就是如果一个函数 15 | // 返回一个我们不知道怎么处理的错误的 16 | // 时候,直接终止执行。 17 | _, err := os.Create("/tmp/file") 18 | if err != nil { 19 | panic(err) 20 | } 21 | } 22 | ``` 23 | 运行结果 24 | ``` 25 | panic: a problem 26 | 27 | goroutine 1 [running]: 28 | runtime.panic(0x44e060, 0xc0840031b0) 29 | C:/Users/ADMINI~1/AppData/Local/Temp/2/bindist667667715/go/src/pkg/runtime/panic.c:266 +0xc8 30 | main.main() 31 | D:/GoDoc/go_panic.go:8 +0x58 32 | exit status 2 33 | ``` 34 | 和其他的编程语言不同的是,Go并不使用exception来处理错误,而是通过函数返回值返回错误代码。 -------------------------------------------------------------------------------- /Go示例学/Go SHA1 散列.markdown: -------------------------------------------------------------------------------- 1 | # Go SHA1 散列 2 | SHA1散列经常用来计算二进制或者大文本数据的短标识值。git版本控制系统用SHA1来标识受版本控制的文件和目录。这里介绍Go中如何计算SHA1散列值。 3 | Go在`crypto/*`包里面实现了几个常用的散列函数。 4 | ```go 5 | package main 6 | 7 | import "crypto/sha1" 8 | import "fmt" 9 | 10 | func main() { 11 | s := "sha1 this string" 12 | 13 | // 生成一个hash的模式是`sha1.New()`,`sha1.Write(bytes)` 14 | // 然后是`sha1.Sum([]byte{})`,下面我们开始一个新的hash 15 | // 示例 16 | h := sha1.New() 17 | 18 | // 写入要hash的字节,如果你的参数是字符串,使用`[]byte(s)` 19 | // 把它强制转换为字节数组 20 | h.Write([]byte(s)) 21 | 22 | // 这里计算最终的hash值,Sum的参数是用来追加而外的字节到要 23 | // 计算的hash字节里面,一般来讲,如果上面已经把需要hash的 24 | // 字节都写入了,这里就设为nil就可以了 25 | bs := h.Sum(nil) 26 | 27 | // SHA1散列值经常以16进制的方式输出,例如git commit就是 28 | // 这样,所以可以使用`%x`来将散列结果格式化为16进制的字符串 29 | fmt.Println(s) 30 | fmt.Printf("%x\n", bs) 31 | } 32 | ``` 33 | 运行结果 34 | ``` 35 | sha1 this string 36 | cf23df2207d99a74fbe169e3eba035e633b65d94 37 | ``` -------------------------------------------------------------------------------- /Go示例学/Go String与Byte切片之间的转换.markdown: -------------------------------------------------------------------------------- 1 | #Go String与Byte切片之间的转换 2 | 3 | String转换到Byte数组时,每个byte(byte类型其实就是uint8)保存字符串对应字节的数值。 4 | 5 | 注意Go的字符串是UTF-8编码的,每个字符长度是不确定的,一些字符可能是1、2、3或者4个字节结尾。 6 | 7 | 示例1: 8 | 9 | ```go 10 | package main 11 | 12 | import "fmt" 13 | 14 | func main() { 15 | 16 | s1 := "abcd" 17 | b1 := []byte(s1) 18 | fmt.Println(b1) // [97 98 99 100] 19 | 20 | s2 := "中文" 21 | b2 := []byte(s2) 22 | fmt.Println(b2) // [228 184 173 230 150 135], unicode,每个中文字符会由三个byte组成 23 | 24 | r1 := []rune(s1) 25 | fmt.Println(r1) // [97 98 99 100], 每个字一个数值 26 | 27 | r2 := []rune(s2) 28 | fmt.Println(r2) // [20013 25991], 每个字一个数值 29 | 30 | } 31 | ``` -------------------------------------------------------------------------------- /Go示例学/Go URL解析.markdown: -------------------------------------------------------------------------------- 1 | # Go URL解析 2 | URL提供了一种统一访问资源的方式。我们来看一下Go里面如何解析URL。 3 | ```go 4 | package main 5 | 6 | import "fmt" 7 | import "net/url" 8 | import "strings" 9 | 10 | func main() { 11 | 12 | // 我们将解析这个URL,它包含了模式,验证信息, 13 | // 主机,端口,路径,查询参数和查询片段 14 | s := "postgres://user:pass@host.com:5432/path?k=v#f" 15 | 16 | // 解析URL,并保证没有错误 17 | u, err := url.Parse(s) 18 | if err != nil { 19 | panic(err) 20 | } 21 | 22 | // 可以直接访问解析后的模式 23 | fmt.Println(u.Scheme) 24 | 25 | // User包含了所有的验证信息,使用 26 | // Username和Password来获取单独的信息 27 | fmt.Println(u.User) 28 | fmt.Println(u.User.Username()) 29 | p, _ := u.User.Password() 30 | fmt.Println(p) 31 | 32 | // Host包含了主机名和端口,如果需要可以 33 | // 手动分解主机名和端口 34 | fmt.Println(u.Host) 35 | h := strings.Split(u.Host, ":") 36 | fmt.Println(h[0]) 37 | fmt.Println(h[1]) 38 | 39 | // 这里我们解析出路径和`#`后面的片段 40 | fmt.Println(u.Path) 41 | fmt.Println(u.Fragment) 42 | 43 | // 为了得到`k=v`格式的查询参数,使用RawQuery。你可以将 44 | // 查询参数解析到一个map里面。这个map为字符串作为key, 45 | // 字符串切片作为value。 46 | fmt.Println(u.RawQuery) 47 | m, _ := url.ParseQuery(u.RawQuery) 48 | fmt.Println(m) 49 | fmt.Println(m["k"][0]) 50 | } 51 | ``` 52 | 运行结果 53 | ``` 54 | postgres 55 | user:pass 56 | user 57 | pass 58 | host.com:5432 59 | host.com 60 | 5432 61 | /path 62 | f 63 | k=v 64 | map[k:[v]] 65 | v 66 | ``` -------------------------------------------------------------------------------- /Go示例学/Go for循环.markdown: -------------------------------------------------------------------------------- 1 | # Go for循环 2 | 3 | for循环是Go语言唯一的循环结构。这里有三个基本的for循环类型。 4 | 5 | ```go 6 | package main 7 | 8 | import "fmt" 9 | 10 | func main() { 11 | 12 | // 最基本的一种,单一条件循环 13 | // 这个可以代替其他语言的while循环 14 | i := 1 15 | for i <= 3 { 16 | fmt.Println(i) 17 | i = i + 1 18 | } 19 | 20 | // 经典的循环条件初始化/条件判断/循环后条件变化 21 | for j := 7; j <= 9; j++ { 22 | fmt.Println(j) 23 | } 24 | 25 | // 无条件的for循环是死循环,除非你使用break跳出循环或者 26 | // 使用return从函数返回 27 | for { 28 | fmt.Println("loop") 29 | break 30 | } 31 | } 32 | ``` 33 | 输出结果 34 | ``` 35 | 1 36 | 2 37 | 3 38 | 7 39 | 8 40 | 9 41 | loop 42 | ``` 43 | 在后面的例子中,你将会看到其他的循环方式,比如使用range函数循环数组,切片和字典,或者用select函数循环channel通道。 -------------------------------------------------------------------------------- /Go示例学/Go if..else if..else 条件判断.markdown: -------------------------------------------------------------------------------- 1 | # Go if..else if..else 条件判断 2 | 3 | Go语言的条件判断结构也很简单。 4 | 5 | ```go 6 | package main 7 | 8 | import "fmt" 9 | 10 | func main() { 11 | 12 | // 基本的例子 13 | if 7%2 == 0 { 14 | fmt.Println("7 is even") 15 | } else { 16 | fmt.Println("7 is odd") 17 | } 18 | 19 | // 只有if条件的情况 20 | if 8%4 == 0 { 21 | fmt.Println("8 is divisible by 4") 22 | } 23 | 24 | // if条件可以包含一个初始化表达式,这个表达式中的变量 25 | // 是这个条件判断结构的局部变量 26 | if num := 9; num < 0 { 27 | fmt.Println(num, "is negative") 28 | } else if num < 10 { 29 | fmt.Println(num, "has 1 digit") 30 | } else { 31 | fmt.Println(num, "has multiple digits") 32 | } 33 | } 34 | ``` 35 | 36 | 条件判断结构中,条件两边的小括号()是可以省略的,但是条件执行语句块两边的大括号{}不可以。 37 | 38 | 输出结果为 39 | 40 | ``` 41 | 7 is odd 42 | 8 is divisible by 4 43 | 9 has 1 digit 44 | ``` 45 | 在Go里面没有三元表达式"?:",所以你只能使用条件判断语句。 -------------------------------------------------------------------------------- /Go示例学/Go range函数.markdown: -------------------------------------------------------------------------------- 1 | # Go range函数 2 | 3 | range函数是个神奇而有趣的内置函数,你可以使用它来遍历数组,切片和字典。 4 | 5 | 当用于遍历数组和切片的时候,range函数返回索引和元素; 6 | 7 | 当用于遍历字典的时候,range函数返回字典的键和值。 8 | 9 | ```go 10 | package main 11 | 12 | import "fmt" 13 | 14 | func main() { 15 | 16 | // 这里我们使用range来计算一个切片的所有元素和 17 | // 这种方法对数组也适用 18 | nums := []int{2, 3, 4} 19 | sum := 0 20 | for _, num := range nums { 21 | sum += num 22 | } 23 | fmt.Println("sum:", sum) 24 | 25 | // range 用来遍历数组和切片的时候返回索引和元素值 26 | // 如果我们不要关心索引可以使用一个下划线(_)来忽略这个返回值 27 | // 当然我们有的时候也需要这个索引 28 | for i, num := range nums { 29 | if num == 3 { 30 | fmt.Println("index:", i) 31 | } 32 | } 33 | 34 | // 使用range来遍历字典的时候,返回键值对。 35 | kvs := map[string]string{"a": "apple", "b": "banana"} 36 | for k, v := range kvs { 37 | fmt.Printf("%s -> %s\n", k, v) 38 | } 39 | 40 | // range函数用来遍历字符串时,返回Unicode代码点。 41 | // 第一个返回值是每个字符的起始字节的索引,第二个是字符代码点, 42 | // 因为Go的字符串是由字节组成的,多个字节组成一个rune类型字符。 43 | for i, c := range "go" { 44 | fmt.Println(i, c) 45 | } 46 | } 47 | ``` 48 | 49 | 输出结果为 50 | 51 | ``` 52 | sum: 9 53 | index: 1 54 | a -> apple 55 | b -> banana 56 | 0 103 57 | 1 111 58 | ``` -------------------------------------------------------------------------------- /Go示例学/Go switch语句.markdown: -------------------------------------------------------------------------------- 1 | # Go Switch语句 2 | 3 | 当条件判断分支太多的时候,我们会使用switch语句来优化逻辑。 4 | 5 | ```go 6 | package main 7 | 8 | import "fmt" 9 | import "time" 10 | 11 | func main() { 12 | 13 | // 基础的switch用法 14 | i := 2 15 | fmt.Print("write ", i, " as ") 16 | switch i { 17 | case 1: 18 | fmt.Println("one") 19 | case 2: 20 | fmt.Println("two") 21 | case 3: 22 | fmt.Println("three") 23 | } 24 | 25 | // 你可以使用逗号来在case中分开多个条件。还可以使用default语句, 26 | // 当上面的case都没有满足的时候执行default所指定的逻辑块。 27 | switch time.Now().Weekday() { 28 | case time.Saturday, time.Sunday: 29 | fmt.Println("it's the weekend") 30 | default: 31 | fmt.Println("it's a weekday") 32 | } 33 | 34 | // 当switch没有跟表达式的时候,功能和if/else相同,这里我们 35 | // 还可以看到case后面的表达式不一定是常量。 36 | t := time.Now() 37 | switch { 38 | case t.Hour() < 12: 39 | fmt.Println("it's before noon") 40 | default: 41 | fmt.Println("it's after noon") 42 | } 43 | } 44 | ``` 45 | 46 | 输出结果为 47 | 48 | ``` 49 | write 2 as two 50 | it's a weekday 51 | it's before noon 52 | ``` 53 | -------------------------------------------------------------------------------- /Go示例学/Go 互斥.markdown: -------------------------------------------------------------------------------- 1 | # Go 互斥 2 | 上面的例子中,我们看过了如何在多个协程之间原子地访问计数器,对于更复杂的例子,我们可以使用`Mutex`来在多个协程之间安全地访问数据。 3 | 4 | ```go 5 | package main 6 | 7 | import ( 8 | "fmt" 9 | "math/rand" 10 | "runtime" 11 | "sync" 12 | "sync/atomic" 13 | "time" 14 | ) 15 | 16 | func main() { 17 | 18 | // 这个例子的状态就是一个map 19 | var state = make(map[int]int) 20 | 21 | // 这个`mutex`将同步对状态的访问 22 | var mutex = &sync.Mutex{} 23 | 24 | // ops将对状态的操作进行计数 25 | var ops int64 = 0 26 | // 这里我们启动100个协程来不断地读取这个状态 27 | for r := 0; r < 100; r++ { 28 | go func() { 29 | total := 0 30 | for { 31 | 32 | // 对于每次读取,我们选取一个key来访问, 33 | // mutex的`Lock`函数用来保证对状态的 34 | // 唯一性访问,访问结束后,使用`Unlock` 35 | // 来解锁,然后增加ops计数器 36 | key := rand.Intn(5) 37 | mutex.Lock() 38 | total += state[key] 39 | mutex.Unlock() 40 | atomic.AddInt64(&ops, 1) 41 | 42 | // 为了保证这个协程不会让调度器出于饥饿状态, 43 | // 我们显式地使用`runtime.Gosched`释放了资源 44 | // 控制权,这种控制权会在通道操作结束或者 45 | // time.Sleep结束后自动释放。但是这里我们需要 46 | // 手动地释放资源控制权 47 | runtime.Gosched() 48 | } 49 | }() 50 | } 51 | // 同样我们使用10个协程来模拟写状态 52 | for w := 0; w < 10; w++ { 53 | go func() { 54 | for { 55 | key := rand.Intn(5) 56 | val := rand.Intn(100) 57 | mutex.Lock() 58 | state[key] = val 59 | mutex.Unlock() 60 | atomic.AddInt64(&ops, 1) 61 | runtime.Gosched() 62 | } 63 | }() 64 | } 65 | 66 | // 主协程Sleep,让那10个协程能够运行一段时间 67 | time.Sleep(time.Second) 68 | 69 | // 输出总操作次数 70 | opsFinal := atomic.LoadInt64(&ops) 71 | fmt.Println("ops:", opsFinal) 72 | 73 | // 最后锁定并输出状态 74 | mutex.Lock() 75 | fmt.Println("state:", state) 76 | mutex.Unlock() 77 | } 78 | ``` 79 | 运行结果 80 | ```go 81 | ops: 3931611 82 | state: map[0:84 2:20 3:18 1:65 4:31] 83 | ``` -------------------------------------------------------------------------------- /Go示例学/Go 信号处理.markdown: -------------------------------------------------------------------------------- 1 | # Go 信号处理 2 | 有的时候我们希望Go能够智能地处理Unix信号。例如我们希望一个server接收到一个SIGTERM的信号时,能够自动地停止;或者一个命令行工具接收到一个SIGINT信号时,能够停止接收输入。现在我们来看下如何使用channel来处理信号。 3 | ```go 4 | package main 5 | 6 | import "fmt" 7 | import "os" 8 | import "os/signal" 9 | import "syscall" 10 | 11 | func main() { 12 | 13 | // Go信号通知通过向一个channel发送``os.Signal`来实现。 14 | // 我们将创建一个channel来接受这些通知,同时我们还用 15 | // 一个channel来在程序可以退出的时候通知我们 16 | sigs := make(chan os.Signal, 1) 17 | done := make(chan bool, 1) 18 | 19 | // `signal.Notify`在给定的channel上面注册该channel 20 | // 可以接受的信号 21 | signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) 22 | 23 | // 这个goroutine阻塞等待信号的到来,当信号到来的时候, 24 | // 输出该信号,然后通知程序可以结束了 25 | go func() { 26 | sig := <-sigs 27 | fmt.Println() 28 | fmt.Println(sig) 29 | done <- true 30 | }() 31 | 32 | // 程序将等待接受信号,然后退出 33 | fmt.Println("awaiting signal") 34 | <-done 35 | fmt.Println("exiting") 36 | } 37 | ``` 38 | 当运行程序的时候,程序将阻塞等待信号的到来,我们可以使用`CTRL+C`来发送一个`SIGINT`信号,这样程序就会输出interrupt后退出。 39 | ``` 40 | awaiting signal 41 | 42 | interrupt 43 | exiting 44 | ``` -------------------------------------------------------------------------------- /Go示例学/Go 关闭通道.markdown: -------------------------------------------------------------------------------- 1 | # Go 关闭通道 2 | 关闭通道的意思是该通道将不再允许写入数据。这个方法可以让通道数据的接受端知道数据已经全部发送完成了。 3 | ```go 4 | package main 5 | 6 | import "fmt" 7 | 8 | // 在这个例子中,我们使用通道jobs在main函数所在的协程和一个数据 9 | // 接收端所在的协程通信。当我们数据发送完成后,我们关闭jobs通道 10 | func main() { 11 | jobs := make(chan int, 5) 12 | done := make(chan bool) 13 | 14 | // 这里是数据接收端协程,它重复使用`j, more := <-jobs`来从通道 15 | // jobs获取数据,这里的more在通道关闭且通道中不再有数据可以接收的 16 | // 时候为false,我们通过判断more来决定所有的数据是否已经接收完成。 17 | // 如果所有数据接收完成,那么向done通道写入true 18 | go func() { 19 | for { 20 | j, more := <-jobs 21 | if more { 22 | fmt.Println("received job", j) 23 | } else { 24 | fmt.Println("received all jobs") 25 | done <- true 26 | return 27 | } 28 | } 29 | }() 30 | 31 | // 这里向jobs通道写入三个数据,然后关闭通道 32 | for j := 1; j <= 3; j++ { 33 | jobs <- j 34 | fmt.Println("sent job", j) 35 | } 36 | close(jobs) 37 | fmt.Println("sent all jobs") 38 | 39 | // 我们知道done通道在接收数据的时候会阻塞,所以在所有的数据发送 40 | // 接收完成后,写入done的数据将在这里被接收,然后程序结束。 41 | <-done 42 | } 43 | ``` 44 | 运行结果 45 | ``` 46 | sent job 1 47 | received job 1 48 | sent job 2 49 | sent job 3 50 | sent all jobs 51 | received job 2 52 | received job 3 53 | received all jobs 54 | ``` -------------------------------------------------------------------------------- /Go示例学/Go 写入文件.markdown: -------------------------------------------------------------------------------- 1 | # Go 写入文件 2 | Go将数据写入文件的方法和上面介绍过的读取文件的方法很类似。 3 | ```go 4 | package main 5 | 6 | import ( 7 | "bufio" 8 | "fmt" 9 | "io/ioutil" 10 | "os" 11 | ) 12 | 13 | func check(e error) { 14 | if e != nil { 15 | panic(e) 16 | } 17 | } 18 | 19 | func main() { 20 | 21 | // 首先看一下如何将一个字符串写入文件 22 | d1 := []byte("hello\ngo\n") 23 | err := ioutil.WriteFile("/tmp/dat1", d1, 0644) 24 | check(err) 25 | 26 | // 为了实现细颗粒度的写入,打开文件后再写入 27 | f, err := os.Create("/tmp/dat2") 28 | check(err) 29 | 30 | // 在打开文件后通常应该立刻使用defer来调用 31 | // 打开文件的Close方法,以保证main函数结束 32 | // 后,文件关闭 33 | defer f.Close() 34 | 35 | // 你可以写入字节切片 36 | d2 := []byte{115, 111, 109, 101, 10} 37 | n2, err := f.Write(d2) 38 | check(err) 39 | fmt.Printf("wrote %d bytes\n", n2) 40 | 41 | // 也可以使用`WriteString`直接写入字符串 42 | n3, err := f.WriteString("writes\n") 43 | fmt.Printf("wrote %d bytes\n", n3) 44 | 45 | // 调用Sync方法来将缓冲区数据写入磁盘 46 | f.Sync() 47 | 48 | // `bufio`除了提供上面的缓冲读取数据外,还 49 | // 提供了缓冲写入数据的方法 50 | w := bufio.NewWriter(f) 51 | n4, err := w.WriteString("buffered\n") 52 | fmt.Printf("wrote %d bytes\n", n4) 53 | 54 | // 使用Flush方法确保所有缓冲区的数据写入底层writer 55 | w.Flush() 56 | } 57 | ``` 58 | 运行结果 59 | ``` 60 | wrote 5 bytes 61 | wrote 7 bytes 62 | wrote 9 bytes 63 | ``` -------------------------------------------------------------------------------- /Go示例学/Go 函数命名返回值.markdown: -------------------------------------------------------------------------------- 1 | # Go 函数命名返回值 2 | 3 | 函数接受参数。在 Go 中,函数可以返回多个“结果参数”,而不仅仅是一个值。它们可以像变量那样命名和使用。 4 | 5 | 如果命名了返回值参数,一个没有参数的`return`语句,会将当前的值作为返回值返回。注意,如果遇到if等代码块和返回值同名,还需要显示写出返回值。 6 | 7 | ```go 8 | package main 9 | 10 | import "fmt" 11 | 12 | func split(sum int) (x, y int) { 13 | x = sum * 4 / 9 14 | y = sum - x 15 | return 16 | } 17 | 18 | func main() { 19 | fmt.Println(split(17)) 20 | } 21 | ``` 22 | 23 | 运行结果 24 | ``` 25 | 7 10 26 | ``` 27 | -------------------------------------------------------------------------------- /Go示例学/Go 函数回调.markdown: -------------------------------------------------------------------------------- 1 | # Go 函数回调 2 | 3 | Go支持函数回调,你可以把函数名称作为参数传递给另外一个函数,然后在别的地方实现这个函数。 4 | 5 | ```go 6 | package main 7 | 8 | import "fmt" 9 | 10 | type Callback func(x, y int) int 11 | 12 | func main() { 13 | x, y := 1, 2 14 | fmt.Println(test(x, y, add)) 15 | } 16 | 17 | //提供一个接口,让外部去实现 18 | func test(x, y int, callback Callback) int { 19 | return callback(x, y) 20 | } 21 | 22 | func add(x, y int) int { 23 | return x + y 24 | } 25 | 26 | ``` 27 | 28 | 运行结果 29 | 30 | ``` 31 | 3 32 | ``` 33 | -------------------------------------------------------------------------------- /Go示例学/Go 函数多返回值.markdown: -------------------------------------------------------------------------------- 1 | # Go 函数多返回值 2 | 3 | Go语言内置支持多返回值,这个在Go语言中用的很多,比如一个函数同时返回结果和错误信息。 4 | 5 | ```go 6 | package main 7 | 8 | import "fmt" 9 | 10 | // 这个函数的返回值为两个int 11 | func vals() (int, int) { 12 | return 3, 7 13 | } 14 | 15 | func main() { 16 | 17 | // 获取函数的两个返回值 18 | a, b := vals() 19 | fmt.Println(a) 20 | fmt.Println(b) 21 | 22 | // 如果你只对多个返回值里面的几个感兴趣 23 | // 可以使用下划线(_)来忽略其他的返回值 24 | _, c := vals() 25 | fmt.Println(c) 26 | } 27 | ``` 28 | 输出结果为 29 | 30 | ``` 31 | 3 32 | 7 33 | 7 34 | ``` -------------------------------------------------------------------------------- /Go示例学/Go 函数定义.markdown: -------------------------------------------------------------------------------- 1 | # Go 函数定义 2 | 3 | 函数是Go语言的重要内容。 4 | 5 | ```go 6 | package main 7 | 8 | import "fmt" 9 | 10 | // 这个函数计算两个int型输入数据的和,并返回int型的和 11 | func plus(a int, b int) int { 12 | // Go需要使用return语句显式地返回值 13 | return a + b 14 | } 15 | 16 | func main() { 17 | // 函数的调用方式很简单 18 | // "名称(参数列表)" 19 | res := plus(1, 2) 20 | fmt.Println("1+2 =", res) 21 | } 22 | ``` 23 | 输出结果为 24 | ``` 25 | 1+2 = 3 26 | ``` 27 | 28 | Go的函数还有很多其他的特性,其中一个就是多返回值,我们下面会看到。 -------------------------------------------------------------------------------- /Go示例学/Go 切片.markdown: -------------------------------------------------------------------------------- 1 | # Go 切片 2 | 3 | 切片是Go语言的关键类型之一,它提供了比数组更多的功能。 4 | 5 | 示例1: 6 | ```go 7 | package main 8 | 9 | import "fmt" 10 | 11 | func main() { 12 | 13 | // 和数组不同的是,切片的长度是可变的。 14 | // 我们可以使用内置函数make来创建一个长度不为零的切片 15 | // 这里我们创建了一个长度为3,存储字符串的切片,切片元素 16 | // 默认为零值,对于字符串就是""。 17 | s := make([]string, 3) 18 | fmt.Println("emp:", s) 19 | 20 | // 可以使用和数组一样的方法来设置元素值或获取元素值 21 | s[0] = "a" 22 | s[1] = "b" 23 | s[2] = "c" 24 | fmt.Println("set:", s) 25 | fmt.Println("get:", s[2]) 26 | 27 | // 可以用内置函数len获取切片的长度 28 | fmt.Println("len:", len(s)) 29 | 30 | // 切片还拥有一些数组所没有的功能。 31 | // 例如我们可以使用内置函数append给切片追加值,然后 32 | // 返回一个拥有新切片元素的切片。 33 | // 注意append函数不会改变原切片,而是生成了一个新切片, 34 | // 我们需要用原来的切片来接收这个新切片 35 | s = append(s, "d") 36 | s = append(s, "e", "f") 37 | fmt.Println("apd:", s) 38 | 39 | // 另外我们还可以从一个切片拷贝元素到另一个切片 40 | // 下面的例子就是创建了一个和切片s长度相同的新切片 41 | // 然后使用内置的copy函数来拷贝s的元素到c中。 42 | c := make([]string, len(s)) 43 | copy(c, s) 44 | fmt.Println("cpy:", c) 45 | 46 | // 切片还支持一个取切片的操作 "slice[low:high]" 47 | // 获取的新切片包含元素"slice[low]",但是不包含"slice[high]" 48 | // 下面的例子就是取一个新切片,元素包括"s[2]","s[3]","s[4]"。 49 | l := s[2:5] 50 | fmt.Println("sl1:", l) 51 | 52 | // 如果省略low,默认从0开始,不包括"slice[high]"元素 53 | l = s[:5] 54 | fmt.Println("sl2:", l) 55 | 56 | // 如果省略high,默认为len(slice),包括"slice[low]"元素 57 | l = s[2:] 58 | fmt.Println("sl3:", l) 59 | 60 | // 我们可以同时声明和初始化一个切片 61 | t := []string{"g", "h", "i"} 62 | fmt.Println("dcl:", t) 63 | 64 | // 我们也可以创建多维切片,和数组不同的是,切片元素的长度也是可变的。 65 | twoD := make([][]int, 3) 66 | for i := 0; i < 3; i++ { 67 | innerLen := i + 1 68 | twoD[i] = make([]int, innerLen) 69 | for j := 0; j < innerLen; j++ { 70 | twoD[i][j] = i + j 71 | } 72 | } 73 | fmt.Println("2d: ", twoD) 74 | } 75 | ``` 76 | 77 | 输出结果为 78 | 79 | ``` 80 | emp: [ ] 81 | set: [a b c] 82 | get: c 83 | len: 3 84 | apd: [a b c d e f] 85 | cpy: [a b c d e f] 86 | sl1: [c d e] 87 | sl2: [a b c d e] 88 | sl3: [c d e f] 89 | dcl: [g h i] 90 | 2d: [[0] [1 2] [2 3 4]] 91 | ``` 92 | 93 | 数组和切片的定义方式的区别在于`[]`之中是否有`固定长度`或者推断长度标志符`...`。 94 | 95 | 示例2: 96 | ```go 97 | package main 98 | 99 | import "fmt" 100 | 101 | func main() { 102 | s1 := make([]int, 0) 103 | test(s1) 104 | fmt.Println(s1) 105 | } 106 | 107 | func test(s []int) { 108 | s = append(s, 3) 109 | //因为原来分配的空间不够,所以在另外一个地址又重新分配了空间,所以原始地址的数据没有变 110 | } 111 | 112 | ``` 113 | 输出结果为: 114 | ``` 115 | [] 116 | ``` 117 | 若改为: 118 | ```go 119 | 120 | package main 121 | 122 | import "fmt" 123 | 124 | func main() { 125 | s1 := make([]int, 0) 126 | s1 = test(s1) 127 | fmt.Println(s1) 128 | } 129 | 130 | func test(s []int) []int { 131 | s = append(s, 3) 132 | return s 133 | } 134 | ``` 135 | 136 | 输出结果为: 137 | ``` 138 | [3]//正确结果 139 | ``` 140 | 示例3: 141 | 142 | cap是slice的最大容量,append函数添加元素,如果超过原始slice的容量,会重新分配底层数组。 143 | ``` 144 | package main 145 | 146 | import "fmt" 147 | 148 | func main() { 149 | s1 := make([]int, 3, 6) 150 | fmt.Println("s1= ", s1, len(s1), cap(s1)) 151 | s2 := append(s1, 1, 2, 3) 152 | fmt.Println("s1= ", s1, len(s1), cap(s1)) 153 | fmt.Println("s2= ", s2, len(s2), cap(s2)) 154 | s3 := append(s2, 4, 5, 6) 155 | fmt.Println("s1= ", s1, len(s1), cap(s1)) 156 | fmt.Println("s2= ", s2, len(s2), cap(s2)) 157 | fmt.Println("s3= ", s3, len(s3), cap(s3)) 158 | 159 | } 160 | ``` 161 | 输出结果为: 162 | ``` 163 | s1= [0 0 0] 3 6 164 | s1= [0 0 0] 3 6 165 | s2= [0 0 0 1 2 3] 6 6 166 | s1= [0 0 0] 3 6 167 | s2= [0 0 0 1 2 3] 6 6 168 | s3= [0 0 0 1 2 3 4 5 6] 9 12 169 | ``` 170 | 示例4: 171 | 172 | 指向同一底层数组的slice之间copy时,允许存在重叠。copy数组时,受限于src和dst数组的长度最小值。 173 | ```go 174 | package main 175 | 176 | import "fmt" 177 | 178 | func main() { 179 | s1 := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} 180 | s2 := make([]int, 3, 20) 181 | var n int 182 | n = copy(s2, s1) 183 | fmt.Println(n, s2, len(s2), cap(s2)) 184 | 185 | s3 := s1[4:6] 186 | fmt.Println(n, s3, len(s3), cap(s3)) 187 | 188 | n = copy(s3, s1[1:5]) 189 | fmt.Println(n, s3, len(s3), cap(s3)) 190 | } 191 | 192 | ``` 193 | 输出结果: 194 | ``` 195 | 3 [0 1 2] 3 20 196 | 3 [4 5] 2 6 197 | 2 [1 2] 2 6 198 | ``` 199 | -------------------------------------------------------------------------------- /Go示例学/Go 原子计数器.markdown: -------------------------------------------------------------------------------- 1 | # Go 原子计数器 2 | Go里面的管理协程状态的主要机制就是通道通讯。这些我们上面的例子介绍过。这里还有一些管理状态的机制,下面我们看看多协程原子访问计数器的例子,这个功能是由sync/atomic包提供的函数来实现的。 3 | ```go 4 | package main 5 | 6 | import "fmt" 7 | import "time" 8 | import "sync/atomic" 9 | import "runtime" 10 | 11 | func main() { 12 | 13 | // 我们使用一个无符号整型来代表一个永远为正整数的counter 14 | var ops uint64 = 0 15 | 16 | // 为了模拟并行更新,我们使用50个协程来每隔1毫秒来 17 | // 增加一下counter值,注意这里的50协程里面的for循环, 18 | // 也就是说如果主协程不退出,这些协程将永远运行下去 19 | // 所以这个程序每次输出的值有可能不一样 20 | for i := 0; i < 50; i++ { 21 | go func() { 22 | for { 23 | // 为了能够保证counter值增加的原子性,我们使用 24 | // atomic包中的AddUint64方法,将counter的地址和 25 | // 需要增加的值传递给函数即可 26 | atomic.AddUint64(&ops, 1) 27 | 28 | // 允许其他的协程来处理 29 | runtime.Gosched() 30 | } 31 | }() 32 | } 33 | 34 | //等待1秒中,让协程有时间运行一段时间 35 | time.Sleep(time.Second) 36 | 37 | // 为了能够在counter仍被其他协程更新值的同时安全访问counter值, 38 | // 我们获取一个当前counter值的拷贝,这里就是opsFinal,需要把 39 | // ops的地址传递给函数`LoadUint64` 40 | opsFinal := atomic.LoadUint64(&ops) 41 | fmt.Println("ops:", opsFinal) 42 | } 43 | ``` 44 | 我们多运行几次,结果如下: 45 | ``` 46 | ops: 7499289 47 | ops: 7700843 48 | ops: 7342417 49 | ``` -------------------------------------------------------------------------------- /Go示例学/Go 变量.markdown: -------------------------------------------------------------------------------- 1 | # Go变量 2 | Go是静态类型语言,变量是有明确类型的。编译器会检查函数调用中,变量类型的正确性。 3 | 4 | 使用`var`关键字来定义变量。 5 | 6 | Go 的基本类型有: 7 | 8 | - bool 9 | - string 10 | - int int8 int16 int32 int64 11 | - uint uint8 uint16 uint32 uint64 uintptr 12 | - byte // uint8 的别名 13 | - rune // int32 的别名 代表一个Unicode码 14 | - float32 float64 15 | - complex64 complex128 16 | 17 | 看看下面的例子 18 | 19 | ```go 20 | package main 21 | 22 | import "fmt" 23 | 24 | func main() { 25 | // `var` 关键字用来定义一个或者多个变量 26 | var a string = "initial" 27 | fmt.Println(a) 28 | 29 | // 你一次可以定义多个变量 30 | var b, c int = 1, 2 31 | fmt.Println(b, c) 32 | 33 | // Go会推断出具有初始值的变量的类型 34 | var d = true 35 | fmt.Println(d) 36 | 37 | //定义变量时,没有给出初始值的变量被默认初始化为零值 38 | //整型的零值就是0 39 | var e int 40 | fmt.Println(e) 41 | 42 | //":=" 语法是同时定义和初始化变量的快捷方式 43 | f := "short" 44 | fmt.Println(f) 45 | } 46 | ``` 47 | 输出结果为 48 | ``` 49 | initial 50 | 1 2 51 | true 52 | 0 53 | short 54 | ``` -------------------------------------------------------------------------------- /Go示例学/Go 可变长参数列表.markdown: -------------------------------------------------------------------------------- 1 | # Go 可变长参数列表 2 | 3 | 支持可变长参数列表的函数可以支持任意个传入参数,比如fmt.Println函数就是一个支持可变长参数列表的函数。 4 | 5 | ```go 6 | package main 7 | 8 | import "fmt" 9 | 10 | // 这个函数可以传入任意数量的整型参数 11 | func sum(nums ...int) { 12 | fmt.Print(nums, " ") 13 | total := 0 14 | for _, num := range nums { 15 | total += num 16 | } 17 | fmt.Println(total) 18 | } 19 | 20 | func main() { 21 | 22 | // 支持可变长参数的函数调用方法和普通函数一样 23 | // 也支持只有一个参数的情况 24 | sum(1, 2) 25 | sum(1, 2, 3) 26 | 27 | // 如果你需要传入的参数在一个切片中,像下面一样 28 | // "func(slice...)"把切片打散传入 29 | nums := []int{1, 2, 3, 4} 30 | sum(nums...) 31 | } 32 | ``` 33 | 34 | 输出结果为 35 | 36 | ``` 37 | [1 2] 3 38 | [1 2 3] 6 39 | [1 2 3 4] 10 40 | ``` 41 | 42 | 需要注意的是,可变长参数应该是函数定义的最右边的参数,即最后一个参数。 43 | -------------------------------------------------------------------------------- /Go示例学/Go 命令行参数.markdown: -------------------------------------------------------------------------------- 1 | # Go 命令行参数 2 | 命令行参数是一种指定程序运行初始参数的常用方式。比如`go run hello.go`使用`run`和`hello.go`参数来执行程序。 3 | ```go 4 | package main 5 | 6 | import "os" 7 | import "fmt" 8 | 9 | func main() { 10 | 11 | // `os.Args`提供了对命令行参数的访问,注意该 12 | // 切片的第一个元素是该程序的运行路径,而 13 | // `os.Args[1:]`则包含了该程序的所有参数 14 | argsWithProg := os.Args 15 | argsWithoutProg := os.Args[1:] 16 | 17 | // 你可以使用索引的方式来获取单个参数 18 | arg := os.Args[3] 19 | 20 | fmt.Println(argsWithProg) 21 | fmt.Println(argsWithoutProg) 22 | fmt.Println(arg) 23 | } 24 | ``` 25 | 在运行该程序的时候,需要首先用`go build`将代码编译为可执行文件,然后提供足够数量的参数。例如 26 | ``` 27 | $ go build command-line-arguments.go 28 | $ ./command-line-arguments a b c d 29 | [./command-line-arguments a b c d] 30 | [a b c d] 31 | c 32 | ``` -------------------------------------------------------------------------------- /Go示例学/Go 命令行参数标记.markdown: -------------------------------------------------------------------------------- 1 | # Go 命令行参数标记 2 | 命令行参数标记是为命令行程序指定选项参数的常用方法。例如,在命令`wc -l`中,`-l`就是一个命令行参数标记。 3 | 4 | Go提供了`flag`包来支持基本的命令行标记解析。我们这里将要使用这个包提供的方法来实现带选项的命令行程序。 5 | 6 | ```go 7 | package main 8 | 9 | import "flag" 10 | import "fmt" 11 | 12 | func main() { 13 | 14 | // 基础的标记声明适用于string,integer和bool型选项。 15 | // 这里我们定义了一个标记`word`,默认值为`foo`和一 16 | // 个简短的描述。`flag.String`函数返回一个字符串指 17 | // 针(而不是一个字符串值),我们下面将演示如何使 18 | // 用这个指针 19 | wordPtr := flag.String("word", "foo", "a string") 20 | 21 | // 这里定义了两个标记,一个`numb`,另一个是`fork`, 22 | // 使用和上面定义`word`标记相似的方法 23 | numbPtr := flag.Int("numb", 42, "an int") 24 | boolPtr := flag.Bool("fork", false, "a bool") 25 | 26 | // 你也可以程序中任意地方定义的变量来定义选项,只 27 | // 需要把该变量的地址传递给flag声明函数即可 28 | var svar string 29 | flag.StringVar(&svar, "svar", "bar", "a string var") 30 | 31 | // 当所有的flag声明完成后,使用`flag.Parse()`来分 32 | // 解命令行选项 33 | flag.Parse() 34 | 35 | // 这里我们仅仅输出解析后的选项和任何紧跟着的位置 36 | // 参数,注意我们需要使用`*wordPtr`的方式来获取最 37 | // 后的选项值 38 | fmt.Println("word:", *wordPtr) 39 | fmt.Println("numb:", *numbPtr) 40 | fmt.Println("fork:", *boolPtr) 41 | fmt.Println("svar:", svar) 42 | fmt.Println("tail:", flag.Args()) 43 | } 44 | ``` 45 | 为了运行示例,你需要先将程序编译为可执行文件。 46 | ``` 47 | go build command-line-flags.go 48 | ``` 49 | 下面分别看看给予该命令行程序不同选项参数的例子: 50 | 51 | (1) 给所有的选项设置一个参数 52 | 53 | ``` 54 | $ ./command-line-flags -word=opt -numb=7 -fork -svar=flag 55 | word: opt 56 | numb: 7 57 | fork: true 58 | svar: flag 59 | tail: [] 60 | ``` 61 | (2) 如果你不设置flag,那么它们自动采用默认的值 62 | ``` 63 | $ ./command-line-flags -word=opt 64 | word: opt 65 | numb: 42 66 | fork: false 67 | svar: bar 68 | tail: [] 69 | ``` 70 | (3) 尾部的位置参数可以出现在任意一个flag后面 71 | ``` 72 | $ ./command-line-flags -word=opt a1 a2 a3 73 | word: opt 74 | numb: 42 75 | fork: false 76 | svar: bar 77 | tail: [a1 a2 a3] 78 | ``` 79 | (4) 注意flag包要求所有的flag都必须出现在尾部位置参数的前面,否则这些flag将被当作位置参数处理 80 | ``` 81 | $ ./command-line-flags -word=opt a1 a2 a3 -numb=7 82 | word: opt 83 | numb: 42 84 | fork: false 85 | svar: bar 86 | trailing: [a1 a2 a3 -numb=7] 87 | ``` 88 | (5) 使用`-h`或者`--help`这两个flag来自动地生成命令行程序的帮助信息 89 | ``` 90 | $ ./command-line-flags -h 91 | Usage of ./command-line-flags: 92 | -fork=false: a bool 93 | -numb=42: an int 94 | -svar="bar": a string var 95 | -word="foo": a string 96 | ``` 97 | (6) 如果你提供了一个程序不支持的flag,那么程序会打印一个错误信息和帮助信息 98 | ``` 99 | $ ./command-line-flags -wat 100 | flag provided but not defined: -wat 101 | Usage of ./go_cmd_flag: 102 | -fork=false: a bool 103 | -numb=42: an int 104 | -svar="bar": a string var 105 | -word="foo": a string 106 | ``` -------------------------------------------------------------------------------- /Go示例学/Go 字典.markdown: -------------------------------------------------------------------------------- 1 | # Go 字典 2 | 3 | 字典是Go语言内置的关联数据类型。因为数组是索引对应数组元素,而字典是键对应值。 4 | 5 | ```go 6 | package main 7 | 8 | import "fmt" 9 | 10 | func main() { 11 | 12 | // 创建一个字典可以使用内置函数make 13 | // "make(map[键类型]值类型)" 14 | m := make(map[string]int) 15 | 16 | // 使用经典的"name[key]=value"来为键设置值 17 | m["k1"] = 7 18 | m["k2"] = 13 19 | 20 | // 用Println输出字典,会输出所有的键值对 21 | fmt.Println("map:", m) 22 | 23 | // 获取一个键的值 "name[key]". 24 | v1 := m["k1"] 25 | fmt.Println("v1: ", v1) 26 | 27 | // 内置函数返回字典的元素个数 28 | fmt.Println("len:", len(m)) 29 | 30 | // 内置函数delete从字典删除一个键对应的值 31 | delete(m, "k2") 32 | fmt.Println("map:", m) 33 | 34 | // 根据键来获取值有一个可选的返回值,这个返回值表示字典中是否 35 | // 存在该键,如果存在为true,返回对应值,否则为false,返回零值 36 | // 有的时候需要根据这个返回值来区分返回结果到底是存在的值还是零值 37 | // 比如字典不存在键x对应的整型值,返回零值就是0,但是恰好字典中有 38 | // 键y对应的值为0,这个时候需要那个可选返回值来判断是否零值。 39 | _, ok := m["k2"] 40 | fmt.Println("ok:", ok) 41 | 42 | // 你可以用 ":=" 同时定义和初始化一个字典 43 | n := map[string]int{"foo": 1, "bar": 2} 44 | fmt.Println("map:", n) 45 | } 46 | ``` 47 | 48 | 输出结果为 49 | 50 | ``` 51 | map: map[k1:7 k2:13] 52 | v1: 7 53 | len: 2 54 | map: map[k1:7] 55 | ok: false 56 | map: map[foo:1 bar:2] 57 | ``` -------------------------------------------------------------------------------- /Go示例学/Go 字符串操作函数.markdown: -------------------------------------------------------------------------------- 1 | # Go 字符串操作函数 2 | strings 标准库提供了很多字符串操作相关的函数。这里提供的几个例子是让你先对这个包有个基本了解。 3 | 4 | ```go 5 | package main 6 | 7 | import s "strings" 8 | import "fmt" 9 | 10 | // 这里给fmt.Println起个别名,因为下面我们会多处使用。 11 | var p = fmt.Println 12 | 13 | func main() { 14 | 15 | // 下面是strings包里面提供的一些函数实例。注意这里的函数并不是 16 | // string对象所拥有的方法,这就是说使用这些字符串操作函数的时候 17 | // 你必须将字符串对象作为第一个参数传递进去。 18 | p("Contains: ", s.Contains("test", "es")) 19 | p("Count: ", s.Count("test", "t")) 20 | p("HasPrefix: ", s.HasPrefix("test", "te")) 21 | p("HasSuffix: ", s.HasSuffix("test", "st")) 22 | p("Index: ", s.Index("test", "e")) 23 | p("Join: ", s.Join([]string{"a", "b"}, "-")) 24 | p("Repeat: ", s.Repeat("a", 5)) 25 | p("Replace: ", s.Replace("foo", "o", "0", -1)) 26 | p("Replace: ", s.Replace("foo", "o", "0", 1)) 27 | p("Split: ", s.Split("a-b-c-d-e", "-")) 28 | p("ToLower: ", s.ToLower("TEST")) 29 | p("ToUpper: ", s.ToUpper("test")) 30 | p() 31 | 32 | // 你可以在strings包里面找到更多的函数 33 | 34 | // 这里还有两个字符串操作方法,它们虽然不是strings包里面的函数, 35 | // 但是还是值得提一下。一个是获取字符串长度,另外一个是从字符串中 36 | // 获取指定索引的字符 37 | p("Len: ", len("hello")) 38 | p("Char:", "hello"[1]) 39 | } 40 | ``` 41 | 运行结果 42 | ``` 43 | Contains: true 44 | Count: 2 45 | HasPrefix: true 46 | HasSuffix: true 47 | Index: 1 48 | Join: a-b 49 | Repeat: aaaaa 50 | Replace: f00 51 | Replace: f0o 52 | Split: [a b c d e] 53 | ToLower: test 54 | ToUpper: TEST 55 | 56 | Len: 5 57 | Char: 101 58 | ``` 59 | -------------------------------------------------------------------------------- /Go示例学/Go 字符串格式化.markdown: -------------------------------------------------------------------------------- 1 | # Go 字符串格式化 2 | Go对字符串格式化提供了良好的支持。下面我们看些常用的字符串格式化的例子。 3 | ```go 4 | package main 5 | 6 | import "fmt" 7 | import "os" 8 | 9 | type point struct { 10 | x, y int 11 | } 12 | 13 | func main() { 14 | 15 | // Go提供了几种打印格式,用来格式化一般的Go值,例如 16 | // 下面的%v打印了一个point结构体的对象的值 17 | p := point{1, 2} 18 | fmt.Printf("%v\n", p) 19 | 20 | // 如果所格式化的值是一个结构体对象,那么`%+v`的格式化输出 21 | // 将包括结构体的成员名称和值 22 | fmt.Printf("%+v\n", p) 23 | 24 | // `%#v`格式化输出将输出一个值的Go语法表示方式。 25 | fmt.Printf("%#v\n", p) 26 | 27 | // 使用`%T`来输出一个值的数据类型 28 | fmt.Printf("%T\n", p) 29 | 30 | // 格式化布尔型变量 31 | fmt.Printf("%t\n", true) 32 | 33 | // 有很多的方式可以格式化整型,使用`%d`是一种 34 | // 标准的以10进制来输出整型的方式 35 | fmt.Printf("%d\n", 123) 36 | 37 | // 这种方式输出整型的二进制表示方式 38 | fmt.Printf("%b\n", 14) 39 | 40 | // 这里打印出该整型数值所对应的字符 41 | fmt.Printf("%c\n", 33) 42 | 43 | // 使用`%x`输出一个值的16进制表示方式 44 | fmt.Printf("%x\n", 456) 45 | 46 | // 浮点型数值也有几种格式化方法。最基本的一种是`%f` 47 | fmt.Printf("%f\n", 78.9) 48 | 49 | // `%e`和`%E`使用科学计数法来输出整型 50 | fmt.Printf("%e\n", 123400000.0) 51 | fmt.Printf("%E\n", 123400000.0) 52 | 53 | // 使用`%s`输出基本的字符串 54 | fmt.Printf("%s\n", "\"string\"") 55 | 56 | // 输出像Go源码中那样带双引号的字符串,需使用`%q` 57 | fmt.Printf("%q\n", "\"string\"") 58 | 59 | // `%x`以16进制输出字符串,每个字符串的字节用两个字符输出 60 | fmt.Printf("%x\n", "hex this") 61 | 62 | // 使用`%p`输出一个指针的值 63 | fmt.Printf("%p\n", &p) 64 | 65 | // 当输出数字的时候,经常需要去控制输出的宽度和精度。 66 | // 可以使用一个位于%后面的数字来控制输出的宽度,默认 67 | // 情况下输出是右对齐的,左边加上空格 68 | fmt.Printf("|%6d|%6d|\n", 12, 345) 69 | 70 | // 你也可以指定浮点数的输出宽度,同时你还可以指定浮点数 71 | // 的输出精度 72 | fmt.Printf("|%6.2f|%6.2f|\n", 1.2, 3.45) 73 | 74 | // To left-justify, use the `-` flag. 75 | fmt.Printf("|%-6.2f|%-6.2f|\n", 1.2, 3.45) 76 | 77 | // 你也可以指定输出字符串的宽度来保证它们输出对齐。默认 78 | // 情况下,输出是右对齐的 79 | fmt.Printf("|%6s|%6s|\n", "foo", "b") 80 | 81 | // 为了使用左对齐你可以在宽度之前加上`-`号 82 | fmt.Printf("|%-6s|%-6s|\n", "foo", "b") 83 | 84 | // `Printf`函数的输出是输出到命令行`os.Stdout`的,你 85 | // 可以用`Sprintf`来将格式化后的字符串赋值给一个变量 86 | s := fmt.Sprintf("a %s", "string") 87 | fmt.Println(s) 88 | 89 | // 你也可以使用`Fprintf`来将格式化后的值输出到`io.Writers` 90 | fmt.Fprintf(os.Stderr, "an %s\n", "error") 91 | } 92 | ``` 93 | 运行结果 94 | ``` 95 | {1 2} 96 | {x:1 y:2} 97 | main.point{x:1, y:2} 98 | main.point 99 | true 100 | 123 101 | 1110 102 | ! 103 | 1c8 104 | 78.900000 105 | 1.234000e+08 106 | 1.234000E+08 107 | "string" 108 | "\"string\"" 109 | 6865782074686973 110 | 0x103a10c0 111 | | 12| 345| 112 | | 1.20| 3.45| 113 | |1.20 |3.45 | 114 | | foo| b| 115 | |foo |b | 116 | a string 117 | an error 118 | ``` -------------------------------------------------------------------------------- /Go示例学/Go 工作池.markdown: -------------------------------------------------------------------------------- 1 | # Go 工作池 2 | 在这个例子中,我们来看一下如何使用gorouotine和channel来实现工作池。 3 | 4 | ```go 5 | package main 6 | 7 | import "fmt" 8 | import "time" 9 | 10 | // 我们将在worker函数里面运行几个并行实例,这个函数从jobs通道 11 | // 里面接受任务,然后把运行结果发送到results通道。每个job我们 12 | // 都休眠一会儿,来模拟一个耗时任务。 13 | func worker(id int, jobs <-chan int, results chan<- int) { 14 | for j := range jobs { 15 | fmt.Println("worker", id, "processing job", j) 16 | time.Sleep(time.Second) 17 | results <- j * 2 18 | } 19 | } 20 | 21 | func main() { 22 | 23 | // 为了使用我们的工作池,我们需要发送工作和接受工作的结果, 24 | // 这里我们定义两个通道,一个jobs,一个results 25 | jobs := make(chan int, 100) 26 | results := make(chan int, 100) 27 | 28 | // 这里启动3个worker协程,一开始的时候worker阻塞执行,因为 29 | // jobs通道里面还没有工作任务 30 | for w := 1; w <= 3; w++ { 31 | go worker(w, jobs, results) 32 | } 33 | 34 | // 这里我们发送9个任务,然后关闭通道,告知任务发送完成 35 | for j := 1; j <= 9; j++ { 36 | jobs <- j 37 | } 38 | close(jobs) 39 | 40 | // 然后我们从results里面获得结果 41 | for a := 1; a <= 9; a++ { 42 | <-results 43 | } 44 | ``` 45 | 运行结果 46 | ``` 47 | worker 1 processing job 1 48 | worker 2 processing job 2 49 | worker 3 processing job 3 50 | worker 1 processing job 4 51 | worker 3 processing job 5 52 | worker 2 processing job 6 53 | worker 1 processing job 7 54 | worker 3 processing job 8 55 | worker 2 processing job 9 56 | ``` -------------------------------------------------------------------------------- /Go示例学/Go 常量.markdown: -------------------------------------------------------------------------------- 1 | # Go常量 2 | 3 | Go支持定义字符常量,字符串常量,布尔型常量和数值常量。 4 | 5 | 使用`const`关键字来定义常量。 6 | 7 | ```go 8 | package main 9 | 10 | import "fmt" 11 | import "math" 12 | 13 | // "const" 关键字用来定义常量 14 | const s string = "constant" 15 | 16 | func main() { 17 | fmt.Println(s) 18 | 19 | // "const"关键字可以出现在任何"var"关键字出现的地方 20 | // 区别是常量必须有初始值 21 | const n = 500000000 22 | 23 | // 常量表达式可以执行任意精度数学计算 24 | const d = 3e20 / n 25 | fmt.Println(d) 26 | 27 | // 数值型常量没有具体类型,除非指定一个类型 28 | // 比如显式类型转换 29 | fmt.Println(int64(d)) 30 | 31 | // 数值型常量可以在程序的逻辑上下文中获取类型 32 | // 比如变量赋值或者函数调用。 33 | // 例如,对于math包中的Sin函数,它需要一个float64类型的变量 34 | fmt.Println(math.Sin(n)) 35 | } 36 | 37 | ``` 38 | 输出结果为 39 | ``` 40 | constant 41 | 6e+11 42 | 600000000000 43 | -0.28470407323754404 44 | ``` -------------------------------------------------------------------------------- /Go示例学/Go 并行功能.markdown: -------------------------------------------------------------------------------- 1 | # Go 并行功能 2 | goroutine是一个轻量级的线程。 3 | ```go 4 | package main 5 | 6 | import "fmt" 7 | 8 | func f(from string) { 9 | for i := 0; i < 3; i++ { 10 | fmt.Println(from, ":", i) 11 | } 12 | } 13 | 14 | func main() { 15 | 16 | // 假设我们有一个函数叫做f(s) 17 | // 这里我们使用通常的同步调用来调用函数 18 | f("direct") 19 | 20 | // 为了能够让这个函数以协程(goroutine)方式 21 | // 运行使用go f(s) 22 | // 这个协程将和调用它的协程并行执行 23 | go f("goroutine") 24 | 25 | // 你也可以为匿名函数开启一个协程运行 26 | go func(msg string) { 27 | fmt.Println(msg) 28 | }("going") 29 | 30 | // 上面的协程在调用之后就异步执行了,所以程序不用等待它们执行完成 31 | // 就跳到这里来了,下面的Scanln用来从命令行获取一个输入,然后才 32 | // 让main函数结束 33 | // 如果没有下面的Scanln语句,程序到这里会直接退出,而上面的协程还 34 | // 没有来得及执行完,你将无法看到上面两个协程运行的结果 35 | var input string 36 | fmt.Scanln(&input) 37 | fmt.Println("done") 38 | } 39 | ``` 40 | 运行结果 41 | ``` 42 | direct : 0 43 | direct : 1 44 | direct : 2 45 | goroutine : 0 46 | goroutine : 1 47 | goroutine : 2 48 | going 49 | ok 50 | done 51 | ``` -------------------------------------------------------------------------------- /Go示例学/Go 并行通道Channel.markdown: -------------------------------------------------------------------------------- 1 | # Go 并行通道Channel 2 | Channel是连接并行协程(goroutine)的通道。你可以向一个通道写入数据然后从另外一个通道读取数据。 3 | ```go 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | 10 | // 使用`make(chan 数据类型)`来创建一个Channel 11 | // Channel的类型就是它们所传递的数据的类型 12 | messages := make(chan string) 13 | 14 | // 使用`channel <-`语法来向一个Channel写入数据 15 | // 这里我们从一个新的协程向messages通道写入数据ping 16 | go func() { messages <- "ping" }() 17 | 18 | // 使用`<-channel`语法来从Channel读取数据 19 | // 这里我们从main函数所在的协程来读取刚刚写入 20 | // messages通道的数据 21 | msg := <-messages 22 | fmt.Println(msg) 23 | } 24 | ``` 25 | 运行结果 26 | ``` 27 | ping 28 | ``` 29 | 当我们运行程序的时候,数据ping成功地从一个协程传递到了另外一个协程。 30 | 默认情况下,协程之间的通信是同步的,也就是说数据的发送端和接收端必须配对使用。Channel的这种特点使得我们可以不用在程序结尾添加额外的代码也能够获取协程发送端发来的信息。因为程序执行到`msg:=<-messages`的时候被阻塞了,直到获得发送端发来的信息才继续执行。 -------------------------------------------------------------------------------- /Go示例学/Go 打点器.markdown: -------------------------------------------------------------------------------- 1 | # Go 打点器 2 | Timer是让你等待一段时间然后去做一件事情,这件事情只会做一次。而Ticker是让你按照一定的时间间隔循环往复地做一件事情,除非你手动停止它。 3 | 4 | ```go 5 | package main 6 | 7 | import "time" 8 | import "fmt" 9 | 10 | func main() { 11 | 12 | // Ticker使用和Timer相似的机制,同样是使用一个通道来发送数据。 13 | // 这里我们使用range函数来遍历通道数据,这些数据每隔500毫秒被 14 | // 发送一次,这样我们就可以接收到 15 | ticker := time.NewTicker(time.Millisecond * 500) 16 | go func() { 17 | for t := range ticker.C { 18 | fmt.Println("Tick at", t) 19 | } 20 | }() 21 | 22 | // Ticker和Timer一样可以被停止。一旦Ticker停止后,通道将不再 23 | // 接收数据,这里我们将在1500毫秒之后停止 24 | time.Sleep(time.Millisecond * 1500) 25 | ticker.Stop() 26 | fmt.Println("Ticker stopped") 27 | } 28 | ``` 29 | 输出结果 30 | ``` 31 | Tick at 2014-02-18 05:42:50.363640783 +0800 CST 32 | Tick at 2014-02-18 05:42:50.863793985 +0800 CST 33 | Tick at 2014-02-18 05:42:51.363532887 +0800 CST 34 | Ticker stopped 35 | ``` 36 | 在这个例子中,我们让Ticker一个独立协程上每隔500毫秒执行一次,然后在main函数所在协程上等待1500毫秒,然后停止Ticker。所以只输出了3次`Ticker at`信息。 -------------------------------------------------------------------------------- /Go示例学/Go 指针.markdown: -------------------------------------------------------------------------------- 1 | # Go 指针 2 | 3 | Go支持指针,可以用来给函数传递变量的引用。 4 | 5 | ```go 6 | package main 7 | 8 | import "fmt" 9 | 10 | // 我们用两个不同的例子来演示指针的用法 11 | // zeroval函数有一个int类型参数,这个时候传递给函数的是变量的值 12 | func zeroval(ival int) { 13 | ival = 0 14 | } 15 | 16 | // zeroptr函数的参数是int类型指针,这个时候传递给函数的是变量的地址 17 | // 在函数内部对这个地址所指向的变量的任何修改都会反映到原来的变量上。 18 | func zeroptr(iptr *int) { 19 | *iptr = 0 20 | } 21 | 22 | func main() { 23 | i := 1 24 | fmt.Println("initial:", i) 25 | 26 | zeroval(i) 27 | fmt.Println("zeroval:", i) 28 | 29 | // &操作符用来取得i变量的地址 30 | zeroptr(&i) 31 | fmt.Println("zeroptr:", i) 32 | 33 | // 指针类型也可以输出 34 | fmt.Println("pointer:", &i) 35 | } 36 | ``` 37 | 输出结果为 38 | 39 | ``` 40 | initial: 1 41 | zeroval: 1 42 | zeroptr: 0 43 | pointer: 0xc084000038 44 | ``` 45 | -------------------------------------------------------------------------------- /Go示例学/Go 排序.markdown: -------------------------------------------------------------------------------- 1 | # Go 排序 2 | 3 | Go的sort包实现了内置数据类型和用户自定义数据类型的排序功能。我们先看看内置数据类型的排序。 4 | 5 | ```go 6 | package main 7 | 8 | import "fmt" 9 | import "sort" 10 | 11 | func main() { 12 | 13 | // 这些排序方法都是针对内置数据类型的。 14 | // 这里的排序方法都是就地排序,也就是说排序改变了 15 | // 切片内容,而不是返回一个新的切片 16 | strs := []string{"c", "a", "b"} 17 | sort.Strings(strs) 18 | fmt.Println("Strings:", strs) 19 | 20 | // 对于整型的排序 21 | ints := []int{7, 2, 4} 22 | sort.Ints(ints) 23 | fmt.Println("Ints: ", ints) 24 | 25 | // 我们还可以检测切片是否已经排序好 26 | s := sort.IntsAreSorted(ints) 27 | fmt.Println("Sorted: ", s) 28 | } 29 | ``` 30 | 输出结果 31 | ``` 32 | Strings: [a b c] 33 | Ints: [2 4 7] 34 | Sorted: true 35 | ``` -------------------------------------------------------------------------------- /Go示例学/Go 接口.markdown: -------------------------------------------------------------------------------- 1 | # Go 接口 2 | 3 | 接口是一个方法签名的集合。 4 | 所谓方法签名,就是指方法的声明,而不包括实现。 5 | 6 | ```go 7 | package main 8 | 9 | import "fmt" 10 | import "math" 11 | 12 | // 这里定义了一个最基本的表示几何形状的方法的接口 13 | type geometry interface { 14 | area() float64 15 | perim() float64 16 | } 17 | 18 | // 这里我们要让正方形square和圆形circle实现这个接口 19 | type square struct { 20 | width, height float64 21 | } 22 | type circle struct { 23 | radius float64 24 | } 25 | 26 | // 在Go中实现一个接口,只要实现该接口定义的所有方法即可 27 | // 下面是正方形实现的接口 28 | func (s square) area() float64 { 29 | return s.width * s.height 30 | } 31 | func (s square) perim() float64 { 32 | return 2*s.width + 2*s.height 33 | } 34 | 35 | // 圆形实现的接口 36 | func (c circle) area() float64 { 37 | return math.Pi * c.radius * c.radius 38 | } 39 | func (c circle) perim() float64 { 40 | return 2 * math.Pi * c.radius 41 | } 42 | 43 | // 如果一个函数的参数是接口类型,那么我们可以使用命名接口 44 | // 来调用这个函数 45 | // 比如这里的正方形square和圆形circle都实现了接口geometry, 46 | // 那么它们都可以作为这个参数为geometry类型的函数的参数。 47 | // 在measure函数内部,Go知道调用哪个结构体实现的接口方法。 48 | func measure(g geometry) { 49 | fmt.Println(g) 50 | fmt.Println(g.area()) 51 | fmt.Println(g.perim()) 52 | } 53 | 54 | func main() { 55 | s := square{width: 3, height: 4} 56 | c := circle{radius: 5} 57 | 58 | // 这里circle和square都实现了geometry接口,所以 59 | // circle类型变量和square类型变量都可以作为measure 60 | // 函数的参数 61 | measure(s) 62 | measure(c) 63 | } 64 | ``` 65 | 输出结果为 66 | 67 | ``` 68 | {3 4} 69 | 12 70 | 14 71 | {5} 72 | 78.53981633974483 73 | 31.41592653589793 74 | ``` 75 | 76 | 也就是说如果结构体A实现了接口B定义的所有方法,那么A也是B类型。 -------------------------------------------------------------------------------- /Go示例学/Go 数值.markdown: -------------------------------------------------------------------------------- 1 | #Go数值 2 | 3 | Go有很多种数据类型,包括字符串类型,整型,浮点型,布尔型等等,这里有几个基础的例子。 4 | ```go 5 | package main 6 | 7 | import "fmt" 8 | 9 | func main() { 10 | 11 | // 字符串可以使用"+"连接 12 | fmt.Println("go" + "lang") 13 | 14 | //整型和浮点型 15 | fmt.Println("1+1 =", 1+1) 16 | fmt.Println("7.0/3.0 =", 7.0/3.0) 17 | 18 | // 布尔型的几种操作符 19 | fmt.Println(true && false) 20 | fmt.Println(true || false) 21 | fmt.Println(!true) 22 | } 23 | ``` 24 | 输出结果为 25 | ``` 26 | golang 27 | 1+1 = 2 28 | 7.0/3.0 = 2.3333333333333335 29 | false 30 | true 31 | false 32 | ``` 33 | -------------------------------------------------------------------------------- /Go示例学/Go 数字解析.markdown: -------------------------------------------------------------------------------- 1 | # Go 数字解析 2 | 从字符串解析出数字是一个基本的而且很常见的任务。 3 | Go内置的`strconv`提供了数字解析功能。 4 | ```go 5 | package main 6 | 7 | import "strconv" 8 | import "fmt" 9 | 10 | func main() { 11 | // 使用ParseFloat解析浮点数,64是说明使用多少位 12 | // 精度来解析 13 | f, _ := strconv.ParseFloat("1.234", 64) 14 | fmt.Println(f) 15 | 16 | // 对于ParseInt函数,0 表示从字符串推断整型进制, 17 | // 则表示返回结果的位数 18 | i, _ := strconv.ParseInt("123", 0, 64) 19 | fmt.Println(i) 20 | 21 | // ParseInt能够解析出16进制的数字 22 | d, _ := strconv.ParseInt("0x1c8", 0, 64) 23 | fmt.Println(d) 24 | 25 | // 还可以使用ParseUint函数 26 | u, _ := strconv.ParseUint("789", 0, 64) 27 | fmt.Println(u) 28 | 29 | // Atoi是解析10进制整型的快捷方法 30 | k, _ := strconv.Atoi("135") 31 | fmt.Println(k) 32 | 33 | // 解析函数在遇到无法解析的输入时,会返回错误 34 | _, e := strconv.Atoi("wat") 35 | fmt.Println(e) 36 | } 37 | ``` 38 | 运行结果 39 | ``` 40 | 1.234 41 | 123 42 | 456 43 | 789 44 | 135 45 | strconv.ParseInt: parsing "wat": invalid syntax 46 | ``` 47 | -------------------------------------------------------------------------------- /Go示例学/Go 数组.markdown: -------------------------------------------------------------------------------- 1 | # Go 数组 2 | 3 | - 数组是一个具有`相同数据类型`的元素组成的`固定长度`的`有序集合`。 4 | - 在Go语言中,数组是值类型,长度是类型的组成部分,也就是说"`[10]int`"和“`[20]int`”是完全不同的两种数组类型。 5 | - 同类型的两个数组支持"=="和"!="比较,但是不能比较大小。 6 | - 数组作为参数时,函数内部不改变数组内部的值,除非是传入数组的指针。 7 | - 数组的指针:*[3]int 8 | - 指针数组:[2]*int 9 | 10 | 示例1: 11 | ```go 12 | package main 13 | 14 | import "fmt" 15 | 16 | func main() { 17 | 18 | // 这里我们创建了一个具有5个元素的整型数组 19 | // 元素的数据类型和数组长度都是数组的一部分 20 | // 默认情况下,数组元素都是零值 21 | // 对于整数,零值就是0 22 | var a [5]int 23 | fmt.Println("emp:", a) 24 | 25 | // 我们可以使用索引来设置数组元素的值,就像这样 26 | // "array[index] = value" 或者使用索引来获取元素值, 27 | // 就像这样"array[index]" 28 | a[4] = 100 29 | fmt.Println("set:", a) 30 | fmt.Println("get:", a[4]) 31 | 32 | // 内置的len函数返回数组的长度 33 | fmt.Println("len:", len(a)) 34 | 35 | // 这种方法可以同时定义和初始化一个数组 36 | b := [5]int{1, 2, 3, 4, 5} 37 | fmt.Println("dcl:", b) 38 | 39 | // 数组都是一维的,但是你可以把数组的元素定义为一个数组 40 | // 来获取多维数组结构 41 | var twoD [2][3]int 42 | for i := 0; i < 2; i++ { 43 | for j := 0; j < 3; j++ { 44 | twoD[i][j] = i + j 45 | } 46 | } 47 | fmt.Println("2d: ", twoD) 48 | } 49 | ``` 50 | 51 | 输出结果为 52 | 53 | ``` 54 | emp: [0 0 0 0 0] 55 | set: [0 0 0 0 100] 56 | get: 100 57 | len: 5 58 | dcl: [1 2 3 4 5] 59 | 2d: [[0 1 2] [1 2 3]] 60 | ``` 61 | 62 | `拥有固定长度`是数组的一个特点,但是这个特点有时候会带来很多不便,尤其在一个集合元素个数不固定的情况下。这个时候我们更多地使用`切片`。 63 | 64 | 示例2: 65 | 66 | 可以用new创建数组,并返回数组的指针 67 | ```go 68 | package main 69 | 70 | import "fmt" 71 | 72 | func main() { 73 | var a = new([5]int) 74 | test(a) 75 | fmt.Println(a, len(a)) 76 | } 77 | 78 | func test(a *[5]int) { 79 | a[1] = 5 80 | } 81 | 82 | ``` 83 | 84 | 输出结果: 85 | ``` 86 | &[0 5 0 0 0] 5 87 | ``` 88 | 示例3: 89 | ```go 90 | package main 91 | 92 | import "fmt" 93 | 94 | func main() { 95 | a := [...]User{ 96 | {0, "User0"}, 97 | {8, "User8"}, 98 | } 99 | b := [...]*User{ 100 | {0, "User0"}, 101 | {8, "User8"}, 102 | } 103 | fmt.Println(a, len(a)) 104 | fmt.Println(b, len(b)) 105 | 106 | } 107 | 108 | type User struct { 109 | Id int 110 | Name string 111 | } 112 | 113 | ``` 114 | 输出结果: 115 | ``` 116 | [{0 User0} {8 User8}] 2 117 | [0x1f216130 0x1f216140] 2 118 | ``` 119 | -------------------------------------------------------------------------------- /Go示例学/Go 方法.markdown: -------------------------------------------------------------------------------- 1 | # Go 方法 2 | 3 | 一般的函数定义叫做函数,定义在结构体上面的函数叫做该结构体的方法。 4 | 5 | 示例1: 6 | 7 | ```go 8 | package main 9 | 10 | import "fmt" 11 | 12 | type rect struct { 13 | width, height int 14 | } 15 | 16 | // 这个area方法有一个限定类型*rect, 17 | // 表示这个函数是定义在rect结构体上的方法 18 | func (r *rect) area() int { 19 | return r.width * r.height 20 | } 21 | 22 | // 方法的定义限定类型可以为结构体类型 23 | // 也可以是结构体指针类型 24 | // 区别在于如果限定类型是结构体指针类型 25 | // 那么在该方法内部可以修改结构体成员信息 26 | func (r rect) perim() int { 27 | return 2*r.width + 2*r.height 28 | } 29 | 30 | func main() { 31 | r := rect{width: 10, height: 5} 32 | 33 | // 调用方法 34 | fmt.Println("area: ", r.area()) 35 | fmt.Println("perim:", r.perim()) 36 | 37 | // Go语言会自动识别方法调用的参数是结构体变量还是 38 | // 结构体指针,如果你要修改结构体内部成员值,那么使用 39 | // 结构体指针作为函数限定类型,也就是说参数若是结构体 40 | //变量,仅仅会发生值拷贝。 41 | rp := &r 42 | fmt.Println("area: ", rp.area()) 43 | fmt.Println("perim:", rp.perim()) 44 | } 45 | ``` 46 | 47 | 输出结果为 48 | 49 | ``` 50 | area: 50 51 | perim: 30 52 | area: 50 53 | perim: 30 54 | ``` 55 | 示例2: 56 | 57 | 从某种意义上说,方法是函数的“语法糖”。当函数与某个特定的类型绑定,那么它就是一个方法。也证因为如此,我们可以将方法“还原”成函数。 58 | 59 | instance.method(args)->(type).func(instance,args) 60 | 61 | 为了区别这两种方式,官方文档中将左边的称为`Method Value`,右边则是`Method Expression`。Method Value是包装后的状态对象,总是与特定的对象实例关联在一起(类似闭包,拐带私奔),而Method Expression函数将Receiver作为第一个显式参数,调用时需额外传递。 62 | 63 | 注意:对于Method Expression,T仅拥有T Receiver方法,*T拥有(T+*T)所有方法。 64 | ```go 65 | package main 66 | 67 | import ( 68 | "fmt" 69 | ) 70 | 71 | func main() { 72 | p := Person{2, "张三"} 73 | 74 | p.test(1) 75 | var f1 func(int) = p.test 76 | f1(2) 77 | Person.test(p, 3) 78 | var f2 func(Person, int) = Person.test 79 | f2(p, 4) 80 | 81 | } 82 | 83 | type Person struct { 84 | Id int 85 | Name string 86 | } 87 | 88 | func (this Person) test(x int) { 89 | fmt.Println("Id:", this.Id, "Name", this.Name) 90 | fmt.Println("x=", x) 91 | } 92 | ``` 93 | 输出结果: 94 | ``` 95 | Id: 2 Name 张三 96 | x= 1 97 | Id: 2 Name 张三 98 | x= 2 99 | Id: 2 Name 张三 100 | x= 3 101 | Id: 2 Name 张三 102 | x= 4 103 | ``` 104 | 示例3: 105 | 106 | 使用匿名字段,实现模拟继承。即可直接访问匿名字段(匿名类型或匿名指针类型)的方法这种行为类似“继承”。访问匿名字段方法时,有隐藏规则,这样我们可以实现override效果。 107 | ```go 108 | package main 109 | 110 | import ( 111 | "fmt" 112 | ) 113 | 114 | func main() { 115 | p := Student{Person{2, "张三"}, 25} 116 | p.test() 117 | 118 | } 119 | 120 | type Person struct { 121 | Id int 122 | Name string 123 | } 124 | 125 | type Student struct { 126 | Person 127 | Score int 128 | } 129 | 130 | func (this Person) test() { 131 | fmt.Println("person test") 132 | } 133 | 134 | func (this Student) test() { 135 | fmt.Println("student test") 136 | } 137 | 138 | ``` 139 | 输出结果为: 140 | ``` 141 | student test 142 | ``` -------------------------------------------------------------------------------- /Go示例学/Go 时间.markdown: -------------------------------------------------------------------------------- 1 | # Go 时间 2 | Go提供了对时间和一段时间的支持。这里有一些例子。 3 | ```go 4 | package main 5 | 6 | import "fmt" 7 | import "time" 8 | 9 | func main() { 10 | p := fmt.Println 11 | 12 | // 从获取当前时间开始 13 | now := time.Now() 14 | p(now) 15 | 16 | // 你可以提供年,月,日等来创建一个时间。当然时间 17 | // 总是会和地区联系在一起,也就是时区 18 | then := time.Date(2009, 11, 17, 20, 34, 58, 651387237, time.UTC) 19 | p(then) 20 | 21 | // 你可以获取时间的各个组成部分 22 | p(then.Year()) 23 | p(then.Month()) 24 | p(then.Day()) 25 | p(then.Hour()) 26 | p(then.Minute()) 27 | p(then.Second()) 28 | p(then.Nanosecond()) 29 | p(then.Location()) 30 | 31 | // 输出当天是周几,Monday-Sunday中的一个 32 | p(then.Weekday()) 33 | 34 | // 下面的几个方法判断两个时间的顺序,精确到秒 35 | p(then.Before(now)) 36 | p(then.After(now)) 37 | p(then.Equal(now)) 38 | 39 | // Sub方法返回两个时间的间隔(Duration) 40 | diff := now.Sub(then) 41 | p(diff) 42 | 43 | // 可以以不同的单位来计算间隔的大小 44 | p(diff.Hours()) 45 | p(diff.Minutes()) 46 | p(diff.Seconds()) 47 | p(diff.Nanoseconds()) 48 | 49 | // 你可以使用Add方法来为时间增加一个间隔 50 | // 使用负号表示时间向前推移一个时间间隔 51 | p(then.Add(diff)) 52 | p(then.Add(-diff)) 53 | } 54 | ``` 55 | 运行结果 56 | ``` 57 | 2014-03-02 22:54:40.561698065 +0800 CST 58 | 2009-11-17 20:34:58.651387237 +0000 UTC 59 | 2009 60 | November 61 | 17 62 | 20 63 | 34 64 | 58 65 | 651387237 66 | UTC 67 | Tuesday 68 | true 69 | false 70 | false 71 | 37578h19m41.910310828s 72 | 37578.328308419674 73 | 2.2546996985051804e+06 74 | 1.3528198191031083e+08 75 | 135281981910310828 76 | 2014-03-02 14:54:40.561698065 +0000 UTC 77 | 2005-08-05 02:15:16.741076409 +0000 UTC 78 | ``` -------------------------------------------------------------------------------- /Go示例学/Go 时间戳.markdown: -------------------------------------------------------------------------------- 1 | # Go 时间戳 2 | 程序的一个通常需求是计算从Unix起始时间开始到某个时刻的秒数,毫秒数,微秒数等。 3 | 我们来看看Go里面是怎么做的。 4 | ```go 5 | package main 6 | 7 | import "fmt" 8 | import "time" 9 | 10 | func main() { 11 | 12 | // 使用Unix和UnixNano来分别获取从Unix起始时间 13 | // 到现在所经过的秒数和微秒数 14 | now := time.Now() 15 | secs := now.Unix() 16 | nanos := now.UnixNano() 17 | fmt.Println(now) 18 | 19 | // 注意这里没有UnixMillis方法,所以我们需要将 20 | // 微秒手动除以一个数值来获取毫秒 21 | millis := nanos / 1000000 22 | fmt.Println(secs) 23 | fmt.Println(millis) 24 | fmt.Println(nanos) 25 | 26 | // 反过来,你也可以将一个整数秒数或者微秒数转换 27 | // 为对应的时间 28 | fmt.Println(time.Unix(secs, 0)) 29 | fmt.Println(time.Unix(0, nanos)) 30 | } 31 | ``` 32 | 运行结果 33 | ``` 34 | 2014-03-02 23:11:31.118666918 +0800 CST 35 | 1393773091 36 | 1393773091118 37 | 1393773091118666918 38 | 2014-03-02 23:11:31 +0800 CST 39 | 2014-03-02 23:11:31.118666918 +0800 CST 40 | ``` -------------------------------------------------------------------------------- /Go示例学/Go 时间格式化和解析.markdown: -------------------------------------------------------------------------------- 1 | # Go 时间格式化和解析 2 | Go使用模式匹配的方式来支持日期格式化和解析。 3 | ```go 4 | package main 5 | 6 | import "fmt" 7 | import "time" 8 | 9 | func main() { 10 | p := fmt.Println 11 | 12 | // 这里有一个根据RFC3339来格式化日期的例子 13 | t := time.Now() 14 | p(t.Format("2006-01-02T15:04:05Z07:00")) 15 | 16 | // Format 函数使用一种基于示例的模式匹配方式, 17 | // 它使用已经格式化的时间模式来决定所给定参数 18 | // 的输出格式 19 | p(t.Format("3:04PM")) 20 | p(t.Format("Mon Jan _2 15:04:05 2006")) 21 | p(t.Format("2006-01-02T15:04:05.999999-07:00")) 22 | 23 | // 对于纯数字表示的时间来讲,你也可以使用标准 24 | // 的格式化字符串的方式来格式化时间 25 | fmt.Printf("%d-%02d-%02dT%02d:%02d:%02d-00:00\n", 26 | t.Year(), t.Month(), t.Day(), 27 | t.Hour(), t.Minute(), t.Second()) 28 | 29 | // 时间解析也是采用一样的基于示例的方式 30 | withNanos := "2006-01-02T15:04:05.999999999-07:00" 31 | t1, e := time.Parse( 32 | withNanos, 33 | "2012-11-01T22:08:41.117442+00:00") 34 | p(t1) 35 | kitchen := "3:04PM" 36 | t2, e := time.Parse(kitchen, "8:41PM") 37 | p(t2) 38 | 39 | // Parse将返回一个错误,如果所输入的时间格式不对的话 40 | ansic := "Mon Jan _2 15:04:05 2006" 41 | _, e = time.Parse(ansic, "8:41PM") 42 | p(e) 43 | 44 | // 你可以使用一些预定义的格式来格式化或解析时间 45 | p(t.Format(time.Kitchen)) 46 | } 47 | ``` 48 | 运行结果 49 | ``` 50 | 2014-03-03T22:39:31+08:00 51 | 10:39PM 52 | Mon Mar 3 22:39:31 2014 53 | 2014-03-03T22:39:31.647077+08:00 54 | 2014-03-03T22:39:31-00:00 55 | 2012-11-01 22:08:41.117442 +0000 +0000 56 | 0000-01-01 20:41:00 +0000 UTC 57 | parsing time "8:41PM" as "Mon Jan _2 15:04:05 2006": cannot parse "8:41PM" as "Mon" 58 | 10:39PM 59 | ``` -------------------------------------------------------------------------------- /Go示例学/Go 正则表达式.markdown: -------------------------------------------------------------------------------- 1 | # Go 正则表达式 2 | Go内置了对正则表达式的支持,这里是一般的正则表达式常规用法的例子。 3 | 4 | ```go 5 | package main 6 | 7 | import "bytes" 8 | import "fmt" 9 | import "regexp" 10 | 11 | func main() { 12 | 13 | // 测试模式是否匹配字符串,括号里面的意思是 14 | // 至少有一个a-z之间的字符存在 15 | match, _ := regexp.MatchString("p([a-z]+)ch", "peach") 16 | fmt.Println(match) 17 | 18 | // 上面我们直接使用了字符串匹配的正则表达式, 19 | // 但是对于其他的正则匹配任务,你需要使用 20 | // `Compile`来使用一个优化过的正则对象 21 | r, _ := regexp.Compile("p([a-z]+)ch") 22 | 23 | // 正则结构体对象有很多方法可以使用,比如上面的例子 24 | // 也可以像下面这么写 25 | fmt.Println(r.MatchString("peach")) 26 | 27 | // 这个方法检测字符串参数是否存在正则所约束的匹配 28 | fmt.Println(r.FindString("peach punch")) 29 | 30 | // 这个方法查找第一次匹配的索引,并返回匹配字符串 31 | // 的起始索引和结束索引,而不是匹配的字符串 32 | fmt.Println(r.FindStringIndex("peach punch")) 33 | 34 | // 这个方法返回全局匹配的字符串和局部匹配的字符,比如 35 | // 这里会返回匹配`p([a-z]+)ch`的字符串 36 | // 和匹配`([a-z]+)`的字符串 37 | fmt.Println(r.FindStringSubmatch("peach punch")) 38 | 39 | // 和上面的方法一样,不同的是返回全局匹配和局部匹配的 40 | // 起始索引和结束索引 41 | fmt.Println(r.FindStringSubmatchIndex("peach punch")) 42 | 43 | // 这个方法返回所有正则匹配的字符,不仅仅是第一个 44 | fmt.Println(r.FindAllString("peach punch pinch", -1)) 45 | 46 | // 这个方法返回所有全局匹配和局部匹配的字符串起始索引 47 | // 和结束索引 48 | fmt.Println(r.FindAllStringSubmatchIndex("peach punch pinch", -1)) 49 | 50 | // 为这个方法提供一个正整数参数来限制匹配数量 51 | fmt.Println(r.FindAllString("peach punch pinch", 2)) 52 | 53 | //上面我们都是用了诸如`MatchString`这样的方法,其实 54 | // 我们也可以使用`[]byte`作为参数,并且使用`Match` 55 | // 这样的方法名 56 | fmt.Println(r.Match([]byte("peach"))) 57 | 58 | // 当使用正则表达式来创建常量的时候,你可以使用`MustCompile` 59 | // 因为`Compile`返回两个值 60 | r = regexp.MustCompile("p([a-z]+)ch") 61 | fmt.Println(r) 62 | 63 | // regexp包也可以用来将字符串的一部分替换为其他的值 64 | fmt.Println(r.ReplaceAllString("a peach", "")) 65 | 66 | // `Func`变量可以让你将所有匹配的字符串都经过该函数处理 67 | // 转变为所需要的值 68 | in := []byte("a peach") 69 | out := r.ReplaceAllFunc(in, bytes.ToUpper) 70 | fmt.Println(string(out)) 71 | } 72 | ``` 73 | 运行结果 74 | ``` 75 | true 76 | true 77 | peach 78 | [0 5] 79 | [peach ea] 80 | [0 5 1 3] 81 | [peach punch pinch] 82 | [[0 5 1 3] [6 11 7 9] [12 17 13 15]] 83 | [peach punch] 84 | true 85 | p([a-z]+)ch 86 | a 87 | a PEACH 88 | ``` 89 | -------------------------------------------------------------------------------- /Go示例学/Go 状态协程.markdown: -------------------------------------------------------------------------------- 1 | # Go 状态协程 2 | 在上面的例子中,我们演示了如何通过使用mutex来在多个协程之间共享状态。另外一种方法是使用协程内置的同步机制来实现。这种基于通道的方法和Go的通过消息共享内存,保证每份数据为单独的协程所有的理念是一致的。 3 | 4 | ```go 5 | package main 6 | 7 | import ( 8 | "fmt" 9 | "math/rand" 10 | "sync/atomic" 11 | "time" 12 | ) 13 | 14 | // 在这个例子中,将有一个单独的协程拥有这个状态。这样可以 15 | // 保证这个数据不会被并行访问所破坏。为了读写这个状态,其 16 | // 他的协程将向这个协程发送信息并且相应地接受返回信息。 17 | // 这些`readOp`和`writeOp`结构体封装了这些请求和回复 18 | type readOp struct { 19 | key int 20 | resp chan int 21 | } 22 | type writeOp struct { 23 | key int 24 | val int 25 | resp chan bool 26 | } 27 | 28 | func main() { 29 | 30 | // 我们将计算我们执行了多少次操作 31 | var ops int64 = 0 32 | 33 | // reads和writes通道将被其他协程用来从中读取或写入数据 34 | reads := make(chan *readOp) 35 | writes := make(chan *writeOp) 36 | 37 | // 这个是拥有`state`的协程,`state`是一个协程的私有map 38 | // 变量。这个协程不断地`select`通道`reads`和`writes`, 39 | // 当有请求来临的时候进行回复。一旦有请求,首先执行所 40 | // 请求的操作,然后给`resp`通道发送一个表示请求成功的值。 41 | go func() { 42 | var state = make(map[int]int) 43 | for { 44 | select { 45 | case read := <-reads: 46 | read.resp <- state[read.key] 47 | case write := <-writes: 48 | state[write.key] = write.val 49 | write.resp <- true 50 | } 51 | } 52 | }() 53 | 54 | // 这里启动了100个协程来向拥有状态的协程请求读数据。 55 | // 每次读操作都需要创建一个`readOp`,然后发送到`reads` 56 | // 通道,然后等待接收请求回复 57 | for r := 0; r < 100; r++ { 58 | go func() { 59 | for { 60 | read := &readOp{ 61 | key: rand.Intn(5), 62 | resp: make(chan int)} 63 | reads <- read 64 | <-read.resp 65 | atomic.AddInt64(&ops, 1) 66 | } 67 | }() 68 | } 69 | 70 | // 我们开启10个写协程 71 | for w := 0; w < 10; w++ { 72 | go func() { 73 | for { 74 | write := &writeOp{ 75 | key: rand.Intn(5), 76 | val: rand.Intn(100), 77 | resp: make(chan bool)} 78 | writes <- write 79 | <-write.resp 80 | atomic.AddInt64(&ops, 1) 81 | } 82 | }() 83 | } 84 | 85 | // 让协程运行1秒钟 86 | time.Sleep(time.Second) 87 | 88 | // 最后输出操作数量ops的值 89 | opsFinal := atomic.LoadInt64(&ops) 90 | fmt.Println("ops:", opsFinal) 91 | } 92 | ``` 93 | 运行结果 94 | ``` 95 | ops: 880578 96 | ``` 97 | 运行这个程序,我们会看到基于协程的状态管理每秒可以处理800, 000个操作。对于这个例子来讲,基于协程的方法比基于mutex的方法更加复杂一点。当然在某些情况下还是很有用的。例如你有很多复杂的协程,而且管理多个mutex可能导致错误。 98 | 当然你可以选择使用任意一种方法,只要你保证这种方法让你觉得很舒服而且也能保证程序的正确性。 -------------------------------------------------------------------------------- /Go示例学/Go 环境变量.markdown: -------------------------------------------------------------------------------- 1 | # Go 环境变量 2 | 环境变量是一种很普遍的将配置信息传递给Unix程序的机制。 3 | ```go 4 | package main 5 | 6 | import "os" 7 | import "strings" 8 | import "fmt" 9 | func main() { 10 | // 为了设置一个key/value对,使用`os.Setenv` 11 | // 为了获取一个key的value,使用`os.Getenv` 12 | // 如果所提供的key在环境变量中没有对应的value, 13 | // 那么返回空字符串 14 | os.Setenv("FOO", "1") 15 | fmt.Println("FOO:", os.Getenv("FOO")) 16 | fmt.Println("BAR:", os.Getenv("BAR")) 17 | 18 | // 使用`os.Environ`来列出环境变量中所有的key/value对 19 | // 你可以使用`strings.Split`方法来将key和value分开 20 | // 这里我们打印所有的key 21 | fmt.Println() 22 | for _, e := range os.Environ() { 23 | pair := strings.Split(e, "=") 24 | fmt.Println(pair[0]) 25 | } 26 | } 27 | ``` 28 | 这里我们设置了FOO环境变量,所以我们取到了它的值,但是没有设置BAR环境变量,所以值为空。另外我们列出了系统的所有环境变量,当然这个输出根据不同的系统设置可能并不相同。 29 | 30 | 输出结果 31 | ``` 32 | FOO: 1 33 | BAR: 34 | 35 | TERM_PROGRAM 36 | TERM 37 | SHELL 38 | TMPDIR 39 | Apple_PubSub_Socket_Render 40 | OLDPWD 41 | USER 42 | SSH_AUTH_SOCK 43 | __CF_USER_TEXT_ENCODING 44 | __CHECKFIX1436934 45 | PATH 46 | PWD 47 | ITERM_PROFILE 48 | SHLVL 49 | COLORFGBG 50 | HOME 51 | ITERM_SESSION_ID 52 | LOGNAME 53 | LC_CTYPE 54 | GOPATH 55 | _ 56 | FOO 57 | ``` 58 | -------------------------------------------------------------------------------- /Go示例学/Go 经典hello world.md: -------------------------------------------------------------------------------- 1 | 我们的第一个例子是打印经典的“hello world”信息,我们先看下代码。 2 | 3 | ```go 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | fmt.Println("hello world") 10 | } 11 | ``` 12 | 输出结果为: 13 | ``` 14 | $ ls 15 | el_01_hello_world.go 16 | $ go build el_01_hello_world.go 17 | $ ls 18 | el_01_hello_world el_01_hello_world.go 19 | $ ./el_01_hello_world 20 | hello world 21 | ``` 22 | 为了使一个`go文件`能够`编译`为`可执行文件`,包名必须是`main`,然后我们导入提供格式化输出的`fmt`包,该程序的执行入口是`func main()`函数,在函数里面,我们使用`fmt`包提供的`Println`函数来输出"hello world"字符串。 23 | 24 | 为了运行这个程序,我们可以使用`go run el_01_hello_world.go`来运行这个例子,这样是直接输出运行结果而不会产生任何中间文件。但是有的时候我们希望能够将程序编译为二进制文件保存起来,我们可以像上面一样使用`go build el_01_hello_world.go`来将源代码编译为二进制可执行文件。然后我们可以直接运行这个二进制可执行文件。 25 | 26 | 好了,第一个例子就这样结束了。很简单。 27 | -------------------------------------------------------------------------------- /Go示例学/Go 结构体.markdown: -------------------------------------------------------------------------------- 1 | # Go 结构体 2 | 3 | Go语言结构体数据类是将各个类型的变量定义的集合,通常用来表示记录。 4 | 5 | ```go 6 | package main 7 | 8 | import "fmt" 9 | 10 | // 这个person结构体有name和age成员 11 | type person struct { 12 | name string 13 | age int 14 | } 15 | 16 | func main() { 17 | 18 | // 这个语法创建一个新结构体变量 19 | fmt.Println(person{"Bob", 20}) 20 | 21 | // 可以使用"成员:值"的方式来初始化结构体变量 22 | fmt.Println(person{name: "Alice", age: 30}) 23 | 24 | // 未显式赋值的成员初始值为零值 25 | fmt.Println(person{name: "Fred"}) 26 | 27 | // 可以使用&来获取结构体变量的地址 28 | fmt.Println(&person{name: "Ann", age: 40}) 29 | 30 | // 使用点号(.)来访问结构体成员 31 | s := person{name: "Sean", age: 50} 32 | fmt.Println(s.name) 33 | 34 | // 结构体指针也可以使用点号(.)来访问结构体成员 35 | // Go语言会自动识别出来 36 | sp := &s 37 | fmt.Println(sp.age) 38 | 39 | // 结构体成员变量的值是可以改变的 40 | sp.age = 51 41 | fmt.Println(sp.age) 42 | } 43 | ``` 44 | 45 | 输出结果为 46 | 47 | ``` 48 | {Bob 20} 49 | {Alice 30} 50 | {Fred 0} 51 | &{Ann 40} 52 | Sean 53 | 50 54 | 51 55 | ``` -------------------------------------------------------------------------------- /Go示例学/Go 自定义排序.markdown: -------------------------------------------------------------------------------- 1 | # Go 自定义排序 2 | 有的时候我们希望排序不是仅仅按照自然顺序排序。例如,我们希望按照字符串的长度来对一个字符串数组排序而不是按照字母顺序来排序。这里我们介绍一下Go的自定义排序。 3 | 4 | ```go 5 | package main 6 | 7 | import "sort" 8 | import "fmt" 9 | 10 | // 为了能够使用自定义函数来排序,我们需要一个 11 | // 对应的排序类型,比如这里我们为内置的字符串 12 | // 数组定义了一个别名ByLength 13 | type ByLength []string 14 | 15 | // 我们实现了sort接口的Len,Less和Swap方法 16 | // 这样我们就可以使用sort包的通用方法Sort 17 | // Len和Swap方法的实现在不同的类型之间大致 18 | // 都是相同的,只有Less方法包含了自定义的排序 19 | // 逻辑,这里我们希望以字符串长度升序排序 20 | func (s ByLength) Len() int { 21 | return len(s) 22 | } 23 | func (s ByLength) Swap(i, j int) { 24 | s[i], s[j] = s[j], s[i] 25 | } 26 | func (s ByLength) Less(i, j int) bool { 27 | return len(s[i]) < len(s[j]) 28 | } 29 | 30 | // 一切就绪之后,我们就可以把需要进行自定义排序 31 | // 的字符串类型fruits转换为ByLength类型,然后使用 32 | // sort包的Sort方法来排序 33 | func main() { 34 | fruits := []string{"peach", "banana", "kiwi"} 35 | sort.Sort(ByLength(fruits)) 36 | fmt.Println(fruits) 37 | } 38 | ``` 39 | 输出结果 40 | ``` 41 | [kiwi peach banana] 42 | ``` 43 | 同样的,对于其他的类型,使用这种方法,我们可以为Go的切片提供任意的排序方法。归纳一下就是: 44 | 45 | 1. 创建自定义排序类型 46 | 2. 实现sort包的接口方法Len,Swap和Less 47 | 3. 使用sort.Sort方法来排序 -------------------------------------------------------------------------------- /Go示例学/Go 计时器.markdown: -------------------------------------------------------------------------------- 1 | # Go 计时器 2 | 我们有的时候希望Go在未来的某个时刻执行或者是以一定的时间间隔重复执行。Go内置的timer和ticker功能使得这些任务变得简单了。我们先看看timer的功能,下一节再看看ticker的功能。 3 | 4 | ```go 5 | package main 6 | 7 | import "time" 8 | import "fmt" 9 | 10 | func main() { 11 | // Timer 代表了未来的一个事件,你告诉timer需要等待多久,然后 12 | // 计时器提供了一个通道,这个通道将在等待的时间结束后得到通知, 13 | // 这里的timer将等待2秒 14 | timer1 := time.NewTimer(time.Second * 2) 15 | 16 | // 这里`<-timer1.C`在timer的通道`C`上面阻塞等待,直到有个值发送给该 17 | // 通道,通知通道计时器已经等待完成。 18 | // timer.NewTimer方法获取的timer1的结构体定义为 19 | // type Ticket struct{ 20 | // C <-chan Time 21 | //} 22 | <-timer1.C 23 | fmt.Println("Timer 1 expired") 24 | 25 | // 如果你仅仅需要等待的话,你可以使用`time.Sleep`,而timer的 26 | // 独特之处在于你可以在timer等待完成之前取消等待。 27 | timer2 := time.NewTimer(time.Second) 28 | go func() { 29 | <-timer2.C 30 | fmt.Println("Timer 2 expired") 31 | }() 32 | stop2 := timer2.Stop() 33 | if stop2 { 34 | fmt.Println("Timer 2 stopped") 35 | } 36 | } 37 | ``` 38 | 运行结果 39 | ``` 40 | Timer 1 expired 41 | Timer 2 stopped 42 | ``` 43 | 在上面的例子中,第一个timer将在2秒后等待完成而第二个timer则在等待完成之前被取消了。 -------------------------------------------------------------------------------- /Go示例学/Go 请求处理频率控制.markdown: -------------------------------------------------------------------------------- 1 | # Go 请求处理频率控制 2 | 频率控制是控制资源利用和保证服务高质量的重要机制。Go可以使用goroutine,channel和ticker来以优雅的方式支持频率控制。 3 | ```go 4 | package main 5 | 6 | import "time" 7 | import "fmt" 8 | 9 | func main() { 10 | 11 | // 首先我们看下基本的频率限制。假设我们得控制请求频率, 12 | // 我们使用一个通道来处理所有的这些请求,这里向requests 13 | // 发送5个数据,然后关闭requests通道 14 | requests := make(chan int, 5) 15 | for i := 1; i <= 5; i++ { 16 | requests <- i 17 | } 18 | close(requests) 19 | 20 | // 这个limiter的Ticker每隔200毫秒结束通道阻塞 21 | // 这个limiter就是我们频率控制处理器 22 | limiter := time.Tick(time.Millisecond * 200) 23 | 24 | // 通过阻塞从limiter通道接受数据,我们将请求处理控制在每隔200毫秒 25 | // 处理一个请求,注意`<-limiter`的阻塞作用。 26 | for req := range requests { 27 | <-limiter 28 | fmt.Println("request", req, time.Now()) 29 | } 30 | 31 | // 我们可以保持正常的请求频率限制,但也允许请求短时间内爆发 32 | // 我们可以通过通道缓存来实现,比如下面的这个burstyLimiter 33 | // 就允许同时处理3个事件。 34 | burstyLimiter := make(chan time.Time, 3) 35 | 36 | // 填充burstyLimiter,先发送3个数据 37 | for i := 0; i < 3; i++ { 38 | burstyLimiter <- time.Now() 39 | } 40 | 41 | // 然后每隔200毫秒再向burstyLimiter发送一个数据,这里是不断地 42 | // 每隔200毫秒向burstyLimiter发送数据 43 | go func() { 44 | for t := range time.Tick(time.Millisecond * 200) { 45 | burstyLimiter <- t 46 | } 47 | }() 48 | 49 | // 这里模拟5个请求,burstyRequests的前面3个请求会连续被处理, 50 | // 因为burstyLimiter被先连续发送3个数据的的缘故,而后面两个 51 | // 则每隔200毫秒处理一次 52 | burstyRequests := make(chan int, 5) 53 | for i := 1; i <= 5; i++ { 54 | burstyRequests <- i 55 | } 56 | close(burstyRequests) 57 | for req := range burstyRequests { 58 | <-burstyLimiter 59 | fmt.Println("request", req, time.Now()) 60 | } 61 | } 62 | ``` 63 | 运行结果 64 | ``` 65 | request 1 2014-02-21 14:20:05.2696437 +0800 CST 66 | request 2 2014-02-21 14:20:05.4696637 +0800 CST 67 | request 3 2014-02-21 14:20:05.6696837 +0800 CST 68 | request 4 2014-02-21 14:20:05.8697037 +0800 CST 69 | request 5 2014-02-21 14:20:06.0697237 +0800 CST 70 | request 1 2014-02-21 14:20:06.0697237 +0800 CST 71 | request 2 2014-02-21 14:20:06.0697237 +0800 CST 72 | request 3 2014-02-21 14:20:06.0707238 +0800 CST 73 | request 4 2014-02-21 14:20:06.2707438 +0800 CST 74 | request 5 2014-02-21 14:20:06.4707638 +0800 CST 75 | ``` 76 | 我们从输出的结果上可以看出最后的5个输出结果中,前三个的时间是连续的,而后两个的时间是隔了200毫秒。 -------------------------------------------------------------------------------- /Go示例学/Go 读取文件.markdown: -------------------------------------------------------------------------------- 1 | # Go 读取文件 2 | 读写文件是很多程序的基本任务,下面我们看看Go里面的文件读取。 3 | ```go 4 | package main 5 | 6 | import ( 7 | "bufio" 8 | "fmt" 9 | "io" 10 | "io/ioutil" 11 | "os" 12 | ) 13 | 14 | // 读取文件的函数调用大多数都需要检查错误, 15 | // 使用下面这个错误检查方法可以方便一点 16 | func check(e error) { 17 | if e != nil { 18 | panic(e) 19 | } 20 | } 21 | 22 | func main() { 23 | 24 | // 最基本的文件读写任务就是把整个文件的内容读取到内存 25 | dat, err := ioutil.ReadFile("/tmp/dat") 26 | check(err) 27 | fmt.Print(string(dat)) 28 | 29 | // 有的时候你想更多地控制到底是读取文件的哪个部分,这个 30 | // 时候你可以使用`os.Open`打开一个文件获取一个`os.File` 31 | // 对象 32 | f, err := os.Open("/tmp/dat") 33 | 34 | // 从这个文件中读取一些字节,并且由于字节数组长度所限, 35 | // 最多读取5个字节,另外还需要注意实际能够读取的字节 36 | // 数量 37 | b1 := make([]byte, 5) 38 | n1, err := f.Read(b1) 39 | check(err) 40 | fmt.Printf("%d bytes: %s\n", n1, string(b1)) 41 | 42 | // 你也可以使用`Seek`来跳转到文件中的一个已知位置,并从 43 | // 那个位置开始读取数据 44 | o2, err := f.Seek(6, 0) 45 | check(err) 46 | b2 := make([]byte, 2) 47 | n2, err := f.Read(b2) 48 | check(err) 49 | fmt.Printf("%d bytes @ %d: %s\n", n2, o2, string(b2)) 50 | 51 | // `io`包提供了一些帮助文件读取的函数。例如上面的方法如果 52 | // 使用方法`ReadAtLeast`函数来实现,将使得程序更健壮 53 | o3, err := f.Seek(6, 0) 54 | check(err) 55 | b3 := make([]byte, 2) 56 | n3, err := io.ReadAtLeast(f, b3, 2) 57 | check(err) 58 | fmt.Printf("%d bytes @ %d: %s\n", n3, o3, string(b3)) 59 | 60 | // 没有内置的rewind方法,但是可以使用`Seek(0,0)`来实现 61 | _, err = f.Seek(0, 0) 62 | check(err) 63 | 64 | // `bufio`包提供了缓冲读取文件的方法,这将使得文件读取更加 65 | // 高效 66 | r4 := bufio.NewReader(f) 67 | b4, err := r4.Peek(5) 68 | check(err) 69 | fmt.Printf("5 bytes: %s\n", string(b4)) 70 | 71 | // 最后关闭打开的文件。一般来讲这个方法会在打开文件的时候, 72 | // 使用defer来延迟关闭 73 | f.Close() 74 | } 75 | ``` 76 | 在运行程序之前,你需要创建一个`/tmp/dat`文件,然后写入一些测试数据。 77 | 运行结果 78 | ``` 79 | hello world 80 | i am jemy 81 | who are you 82 | what do you like 83 | 5 bytes: hello 84 | 2 bytes @ 6: wo 85 | 2 bytes @ 6: wo 86 | 5 bytes: hello 87 | ``` 88 | -------------------------------------------------------------------------------- /Go示例学/Go 超时.markdown: -------------------------------------------------------------------------------- 1 | # Go 超时 2 | 超时对那些连接外部资源的程序来说是很重要的,否则就需要限定执行时间。在Go里面实现超时很简单。我们可以使用channel和select很容易地做到。 3 | 4 | ```go 5 | package main 6 | 7 | import "time" 8 | import "fmt" 9 | 10 | func main() { 11 | 12 | // 在这个例子中,假设我们执行了一个外部调用,2秒之后将结果写入c1 13 | c1 := make(chan string, 1) 14 | go func() { 15 | time.Sleep(time.Second * 2) 16 | c1 <- "result 1" 17 | }() 18 | 19 | // 这里使用select来实现超时,`res := <-c1`等待通道结果, 20 | // `<- Time.After`则在等待1秒后返回一个值,因为select首先 21 | // 执行那些不再阻塞的case,所以这里会执行超时程序,如果 22 | // `res := <-c1`超过1秒没有执行的话 23 | select { 24 | case res := <-c1: 25 | fmt.Println(res) 26 | case <-time.After(time.Second * 1): 27 | fmt.Println("timeout 1") 28 | } 29 | 30 | // 如果我们将超时时间设为3秒,这个时候`res := <-c2`将在 31 | // 超时case之前执行,从而能够输出写入通道c2的值 32 | c2 := make(chan string, 1) 33 | go func() { 34 | time.Sleep(time.Second * 2) 35 | c2 <- "result 2" 36 | }() 37 | select { 38 | case res := <-c2: 39 | fmt.Println(res) 40 | case <-time.After(time.Second * 3): 41 | fmt.Println("timeout 2") 42 | } 43 | } 44 | ``` 45 | 运行结果 46 | ``` 47 | timeout 1 48 | result 2 49 | ``` -------------------------------------------------------------------------------- /Go示例学/Go 进程执行.markdown: -------------------------------------------------------------------------------- 1 | # Go 进程执行 2 | 在上面的例子中,我们演示了一下如何去触发执行一个外部的进程。我们这样做的原因是我们希望从Go进程里面可以访问外部进程的信息。但有的时候,我们仅仅希望执行一个外部进程来替代当前的Go进程。这个时候,我们需要使用Go提供的`exec`函数。 3 | ```go 4 | package main 5 | 6 | import "syscall" 7 | import "os" 8 | import "os/exec" 9 | 10 | func main() { 11 | 12 | // 本例中,我们使用`ls`来演示。Go需要一个该命令 13 | // 的完整路径,所以我们使用`exec.LookPath`函数来 14 | // 找到它 15 | binary, lookErr := exec.LookPath("ls") 16 | if lookErr != nil { 17 | panic(lookErr) 18 | } 19 | // `Exec`函数需要一个切片参数,我们给ls命令一些 20 | // 常见的参数。注意,第一个参数必须是程序名称 21 | args := []string{"ls", "-a", "-l", "-h"} 22 | 23 | // `Exec`还需要一些环境变量,这里我们提供当前的 24 | // 系统环境 25 | env := os.Environ() 26 | 27 | // 这里是`os.Exec`调用。如果一切顺利,我们的原 28 | // 进程将终止,然后启动一个新的ls进程。如果有 29 | // 错误发生,我们将获得一个返回值 30 | execErr := syscall.Exec(binary, args, env) 31 | if execErr != nil { 32 | panic(execErr) 33 | } 34 | } 35 | ``` 36 | 运行结果 37 | ``` 38 | total 16 39 | drwxr-xr-x 4 mark 136B Oct 3 16:29 . 40 | drwxr-xr-x 91 mark 3.0K Oct 3 12:50 .. 41 | -rw-r--r-- 1 mark 1.3K Oct 3 16:28 execing-processes.go 42 | ``` 43 | 注意,Go没有提供Unix下面经典的fork函数。通常这也不是一个问题,因为进程触发和进程执行已经覆盖了fork的大多数功能。 -------------------------------------------------------------------------------- /Go示例学/Go 进程触发.markdown: -------------------------------------------------------------------------------- 1 | # Go 进程触发 2 | 有的时候,我们需要从Go程序里面触发一个其他的非Go进程来执行。 3 | 4 | ```go 5 | package main 6 | 7 | import "fmt" 8 | import "io/ioutil" 9 | import "os/exec" 10 | 11 | func main() { 12 | 13 | // 我们从一个简单的命令开始,这个命令不需要任何参数 14 | // 或者输入,仅仅向stdout输出一些信息。`exec.Command` 15 | // 函数创建了一个代表外部进程的对象 16 | dateCmd := exec.Command("date") 17 | 18 | // `Output`是另一个运行命令时用来处理信息的函数,这个 19 | // 函数等待命令结束,然后收集命令输出。如果没有错误发 20 | // 生的话,`dateOut`将保存date的信息 21 | dateOut, err := dateCmd.Output() 22 | if err != nil { 23 | panic(err) 24 | } 25 | fmt.Println("> date") 26 | fmt.Println(string(dateOut)) 27 | 28 | // 下面我们看一个需要从stdin输入数据的命令,我们将 29 | // 数据输入传给外部进程的stdin,然后从它输出到stdout 30 | // 的运行结果收集信息 31 | grepCmd := exec.Command("grep", "hello") 32 | 33 | // 这里我们显式地获取input/output管道,启动进程, 34 | // 向进程写入数据,然后读取输出结果,最后等待进程结束 35 | grepIn, _ := grepCmd.StdinPipe() 36 | grepOut, _ := grepCmd.StdoutPipe() 37 | grepCmd.Start() 38 | grepIn.Write([]byte("hello grep\ngoodbye grep")) 39 | grepIn.Close() 40 | grepBytes, _ := ioutil.ReadAll(grepOut) 41 | grepCmd.Wait() 42 | 43 | // 在上面的例子中,我们忽略了错误检测,但是你一样可以 44 | // 使用`if err!=nil`模式来进行处理。另外我们仅仅收集了 45 | // `StdoutPipe`的结果,同时你也可以用一样的方法来收集 46 | // `StderrPipe`的结果 47 | fmt.Println("> grep hello") 48 | fmt.Println(string(grepBytes)) 49 | 50 | // 注意,我们在触发外部命令的时候,需要显式地提供 51 | // 命令和参数信息。另外如果你想用一个命令行字符串 52 | // 触发一个完整的命令,你可以使用bash的-c选项 53 | lsCmd := exec.Command("bash", "-c", "ls -a -l -h") 54 | lsOut, err := lsCmd.Output() 55 | if err != nil { 56 | panic(err) 57 | } 58 | fmt.Println("> ls -a -l -h") 59 | fmt.Println(string(lsOut)) 60 | } 61 | ``` 62 | 所触发的程序的执行结果和我们直接执行这些程序的结果是一样的。 63 | 运行结果 64 | ``` 65 | > date 66 | Wed Oct 10 09:53:11 PDT 2012 67 | > grep hello 68 | hello grep 69 | > ls -a -l -h 70 | drwxr-xr-x 4 mark 136B Oct 3 16:29 . 71 | drwxr-xr-x 91 mark 3.0K Oct 3 12:50 .. 72 | -rw-r--r-- 1 mark 1.3K Oct 3 16:28 spawning-processes.go 73 | ``` -------------------------------------------------------------------------------- /Go示例学/Go 递归函数.markdown: -------------------------------------------------------------------------------- 1 | # Go 递归函数 2 | 3 | Go语言支持递归函数,这里是一个经典的斐波拉切数列的列子。 4 | 5 | ```go 6 | package main 7 | 8 | import "fmt" 9 | 10 | // fact函数不断地调用自身,直到达到基本状态fact(0) 11 | func fact(n int) int { 12 | if n == 0 { 13 | return 1 14 | } 15 | return n * fact(n-1) 16 | } 17 | 18 | func main() { 19 | fmt.Println(fact(7)) 20 | } 21 | ``` 22 | 23 | 输出结果为 24 | 25 | ``` 26 | 5040 27 | ``` -------------------------------------------------------------------------------- /Go示例学/Go 通道方向.markdown: -------------------------------------------------------------------------------- 1 | # Go通道方向 2 | 当使用通道作为函数的参数时,你可以指定该通道是只读的还是只写的。这种设置有时候会提高程序的参数类型安全。 3 | 4 | ```go 5 | package main 6 | 7 | import "fmt" 8 | 9 | // 这个ping函数只接收能够发送数据的通道作为参数,试图从这个通道接收数据 10 | // 会导致编译错误,这里只写的定义方式为`chan<- string`表示这个类型为 11 | // 字符串的通道为只写通道 12 | func ping(pings chan<- string, msg string) { 13 | pings <- msg 14 | } 15 | 16 | // pong函数接收两个通道参数,一个是只读的pings,使用`<-chan string`定义 17 | // 另外一个是只写的pongs,使用`chan<- string`来定义 18 | func pong(pings <-chan string, pongs chan<- string) { 19 | msg := <-pings 20 | pongs <- msg 21 | } 22 | 23 | func main() { 24 | pings := make(chan string, 1) 25 | pongs := make(chan string, 1) 26 | ping(pings, "passed message") 27 | pong(pings, pongs) 28 | fmt.Println(<-pongs) 29 | } 30 | ``` 31 | 运行结果 32 | ``` 33 | passed message 34 | ``` 35 | 其实这个例子就是把信息首先写入pings通道里面,然后在pong函数里面再把信息从pings通道里面读出来再写入pongs通道里面,最后在main函数里面将信息从pongs通道里面读出来。 36 | 在这里,pings和pongs事实上是可读且可写的,不过作为参数传递的时候,函数参数限定了通道的方向。不过pings和pongs在ping和pong函数里面还是可读且可写的。只是ping和pong函数调用的时候把它们当作了只读或者只写。 -------------------------------------------------------------------------------- /Go示例学/Go 通道的同步功能.markdown: -------------------------------------------------------------------------------- 1 | # Go通道的同步功能 2 | 我们使用通道来同步协程之间的执行。 3 | 下面的例子是通过获取同步通道数据来阻塞程序执行的方法来等待另一个协程运行结束的。 4 | 也就是说main函数所在的协程在运行到`<-done`语句的时候将一直等待worker函数所在的协程执行完成,向通道写入数据才会(从通道获得数据)继续执行。 5 | 6 | ```go 7 | package main 8 | 9 | import "fmt" 10 | import "time" 11 | 12 | // 这个worker函数将以协程的方式运行 13 | // 通道`done`被用来通知另外一个协程这个worker函数已经执行完成 14 | func worker(done chan bool) { 15 | fmt.Print("working...") 16 | time.Sleep(time.Second) 17 | fmt.Println("done") 18 | 19 | // 向通道发送一个数据,表示worker函数已经执行完成 20 | done <- true 21 | } 22 | 23 | func main() { 24 | 25 | // 使用协程来调用worker函数,同时将通道`done`传递给协程 26 | // 以使得协程可以通知别的协程自己已经执行完成 27 | done := make(chan bool, 1) 28 | go worker(done) 29 | 30 | // 一直阻塞,直到从worker所在协程获得一个worker执行完成的数据 31 | <-done 32 | } 33 | ``` 34 | 运行结果 35 | ``` 36 | working...done 37 | ``` 38 | 如果我们从main函数里面移除`<-done`语句,那么main函数在worker协程开始运行之前就结束了。 -------------------------------------------------------------------------------- /Go示例学/Go 通道缓冲.markdown: -------------------------------------------------------------------------------- 1 | # Go通道缓冲 2 | 默认情况下,通道是不带缓冲区的。 3 | 发送端发送数据,同时必须又接收端相应的接收数据。 4 | 而带缓冲区的通道则允许发送端的数据发送和接收端的数据获取处于异步状态,就是说发送端发送的数据可以放在缓冲区里面,可以等待接收端去获取数据,而不是立刻需要接收端去获取数据。 5 | 不过由于缓冲区的大小是有限的,所以还是必须有接收端来接收数据的,否则缓冲区一满,数据发送端就无法再发送数据了。 6 | 7 | ```go 8 | package main 9 | 10 | import "fmt" 11 | 12 | func main() { 13 | 14 | // 这里我们定义了一个可以存储字符串类型的带缓冲通道 15 | // 缓冲区大小为2 16 | messages := make(chan string, 2) 17 | 18 | // 因为messages是带缓冲的通道,我们可以同时发送两个数据 19 | // 而不用立刻需要去同步读取数据 20 | messages <- "buffered" 21 | messages <- "channel" 22 | 23 | // 然后我们和上面例子一样获取这两个数据 24 | fmt.Println(<-messages) 25 | fmt.Println(<-messages) 26 | } 27 | ``` 28 | 运行结果 29 | ``` 30 | buffered 31 | channel 32 | ``` 33 | -------------------------------------------------------------------------------- /Go示例学/Go 通道选择Select.markdown: -------------------------------------------------------------------------------- 1 | # Go 通道选择Select 2 | Go的select关键字可以让你同时等待多个通道操作,将协程(goroutine),通道(channel)和select结合起来构成了Go的一个强大特性。 3 | 4 | ```go 5 | package main 6 | 7 | import "time" 8 | import "fmt" 9 | 10 | func main() { 11 | 12 | // 本例中,我们从两个通道中选择 13 | c1 := make(chan string) 14 | c2 := make(chan string) 15 | 16 | // 为了模拟并行协程的阻塞操作,我们让每个通道在一段时间后再写入一个值 17 | go func() { 18 | time.Sleep(time.Second * 1) 19 | c1 <- "one" 20 | }() 21 | go func() { 22 | time.Sleep(time.Second * 2) 23 | c2 <- "two" 24 | }() 25 | 26 | // 我们使用select来等待这两个通道的值,然后输出 27 | for i := 0; i < 2; i++ { 28 | select { 29 | case msg1 := <-c1: 30 | fmt.Println("received", msg1) 31 | case msg2 := <-c2: 32 | fmt.Println("received", msg2) 33 | } 34 | } 35 | } 36 | ``` 37 | 输出结果 38 | ``` 39 | received one 40 | received two 41 | ``` 42 | 如我们所期望的,程序输出了正确的值。对于select语句而言,它不断地检测通道是否有值过来,一旦发现有值过来,立刻获取输出。 -------------------------------------------------------------------------------- /Go示例学/Go 遍历通道.markdown: -------------------------------------------------------------------------------- 1 | # Go 遍历通道 2 | 我们知道range函数可以遍历数组,切片,字典等。这里我们还可以使用range函数来遍历通道以接收通道数据。 3 | ```go 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | 10 | // 我们遍历queue通道里面的两个数据 11 | queue := make(chan string, 2) 12 | queue <- "one" 13 | queue <- "two" 14 | close(queue) 15 | 16 | // range函数遍历每个从通道接收到的数据,因为queue再发送完两个 17 | // 数据之后就关闭了通道,所以这里我们range函数在接收到两个数据 18 | // 之后就结束了。如果上面的queue通道不关闭,那么range函数就不 19 | // 会结束,从而在接收第三个数据的时候就阻塞了。 20 | for elem := range queue { 21 | fmt.Println(elem) 22 | } 23 | } 24 | ``` 25 | 运行结果 26 | ``` 27 | one 28 | two 29 | ``` 30 | 这个例子同时说明了,即使关闭了一个非空通道,我们仍然可以从通道里面接收到值。 -------------------------------------------------------------------------------- /Go示例学/Go 错误处理.markdown: -------------------------------------------------------------------------------- 1 | # Go 错误处理 2 | 在Go里面通常采用显式返回错误代码的方式来进行错误处理。这个和Java或者Ruby里面使用异常或者是C里面运行正常返回结果,发生错误返回错误代码的方式不同。Go的这种错误处理的方式使得我们能够很容易看出哪些函数可能返回错误,并且能够像调用那些没有错误返回的函数一样调用。 3 | 4 | ```go 5 | package main 6 | 7 | import "errors" 8 | import "fmt" 9 | 10 | // Go语言里面约定错误代码是函数的最后一个返回值, 11 | // 并且类型是error,这是一个内置的接口 12 | 13 | func f1(arg int) (int, error) { 14 | if arg == 42 { 15 | 16 | // errors.New使用错误信息作为参数,构建一个基本的错误 17 | return -1, errors.New("can't work with 42") 18 | 19 | } 20 | 21 | // 返回错误为nil表示没有错误 22 | return arg + 3, nil 23 | } 24 | 25 | // 你可以通过实现error接口的方法Error()来自定义错误 26 | // 下面我们自定义一个错误类型来表示上面例子中的参数错误 27 | type argError struct { 28 | arg int 29 | prob string 30 | } 31 | 32 | func (e *argError) Error() string { 33 | return fmt.Sprintf("%d - %s", e.arg, e.prob) 34 | } 35 | 36 | func f2(arg int) (int, error) { 37 | if arg == 42 { 38 | 39 | // 这里我们使用&argError语法来创建一个新的结构体对象, 40 | // 并且给它的成员赋值 41 | return -1, &argError{arg, "can't work with it"} 42 | } 43 | return arg + 3, nil 44 | } 45 | 46 | func main() { 47 | 48 | // 下面的两个循环例子用来测试我们的带有错误返回值的函数 49 | // 在for循环语句里面,使用了if来判断函数返回值是否为nil是 50 | // Go语言里面的一种约定做法。 51 | for _, i := range []int{7, 42} { 52 | if r, e := f1(i); e != nil { 53 | fmt.Println("f1 failed:", e) 54 | } else { 55 | fmt.Println("f1 worked:", r) 56 | } 57 | } 58 | for _, i := range []int{7, 42} { 59 | if r, e := f2(i); e != nil { 60 | fmt.Println("f2 failed:", e) 61 | } else { 62 | fmt.Println("f2 worked:", r) 63 | } 64 | } 65 | 66 | // 如果你需要使用自定义错误类型返回的错误数据,你需要使用类型断言 67 | // 来获得一个自定义错误类型的实例才行。 68 | _, e := f2(42) 69 | if ae, ok := e.(*argError); ok { 70 | fmt.Println(ae.arg) 71 | fmt.Println(ae.prob) 72 | } 73 | } 74 | ``` 75 | 运行结果为 76 | ``` 77 | f1 worked: 10 78 | f1 failed: can't work with 42 79 | f2 worked: 10 80 | f2 failed: 42 - can't work with it 81 | 42 82 | can't work with it 83 | ``` 84 | -------------------------------------------------------------------------------- /Go示例学/Go 闭包函数.markdown: -------------------------------------------------------------------------------- 1 | # Go 闭包函数 2 | 3 | Go支持匿名函数,匿名函数可以形成闭包。闭包函数可以访问定义闭包的函数定义的内部变量。 4 | 5 | 示例1: 6 | 7 | ```go 8 | package main 9 | 10 | import "fmt" 11 | 12 | // 这个"intSeq"函数返回另外一个在intSeq内部定义的匿名函数, 13 | // 这个返回的匿名函数包住了变量i,从而形成了一个闭包 14 | func intSeq() func() int { 15 | i := 0 16 | return func() int { 17 | i += 1 18 | return i 19 | } 20 | } 21 | 22 | func main() { 23 | // 我们调用intSeq函数,并且把结果赋值给一个函数nextInt, 24 | // 这个nextInt函数拥有自己的i变量,这个变量每次调用都被更新。 25 | // 这里i的初始值是由intSeq调用的时候决定的。 26 | nextInt := intSeq() 27 | 28 | // 调用几次nextInt,看看闭包的效果 29 | fmt.Println(nextInt()) 30 | fmt.Println(nextInt()) 31 | fmt.Println(nextInt()) 32 | 33 | // 为了确认闭包的状态是独立于intSeq函数的,再创建一个。 34 | newInts := intSeq() 35 | fmt.Println(newInts()) 36 | } 37 | ``` 38 | 39 | 输出结果为 40 | 41 | ``` 42 | 1 43 | 2 44 | 3 45 | 1 46 | ``` 47 | 示例2: 48 | 49 | ```go 50 | 51 | package main 52 | 53 | import "fmt" 54 | 55 | func main() { 56 | add10 := closure(10)//其实是构造了一个加10函数 57 | fmt.Println(add10(5)) 58 | fmt.Println(add10(6)) 59 | add20 := closure(20) 60 | fmt.Println(add20(5)) 61 | } 62 | 63 | func closure(x int) func(y int) int { 64 | return func(y int) int { 65 | return x + y 66 | } 67 | 68 | } 69 | 70 | ``` 71 | 72 | 输出结果为: 73 | ``` 74 | 15 75 | 16 76 | 25 77 | ``` 78 | 示例3: 79 | ```go 80 | 81 | package main 82 | 83 | import "fmt" 84 | 85 | func main() { 86 | 87 | var fs []func() int 88 | 89 | for i := 0; i < 3; i++ { 90 | 91 | fs = append(fs, func() int { 92 | 93 | return i 94 | }) 95 | } 96 | for _, f := range fs { 97 | fmt.Printf("%p = %v\n", f, f()) 98 | } 99 | } 100 | 101 | ``` 102 | 103 | 输出结果: 104 | ``` 105 | 0x401200 = 3 106 | 0x401200 = 3 107 | 0x401200 = 3 108 | ``` 109 | 示例4: 110 | ```go 111 | package main 112 | 113 | import "fmt" 114 | 115 | func adder() func(int) int { 116 | sum := 0 117 | return func(x int) int { 118 | sum += x 119 | return sum 120 | } 121 | } 122 | 123 | func main() { 124 | result := adder() 125 | for i := 0; i < 10; i++ { 126 | fmt.Println(result(i)) 127 | } 128 | } 129 | ``` 130 | 131 | 输出结果为: 132 | ``` 133 | 0 134 | 1 135 | 3 136 | 6 137 | 10 138 | 15 139 | 21 140 | 28 141 | 36 142 | 45 143 | ``` -------------------------------------------------------------------------------- /Go示例学/Go 随机数.markdown: -------------------------------------------------------------------------------- 1 | # Go 随机数 2 | Go的`math/rand`包提供了伪随机数的生成。 3 | ```go 4 | package main 5 | 6 | import "fmt" 7 | import "math/rand" 8 | 9 | func main() { 10 | 11 | // 例如`rand.Intn`返回一个整型随机数n,0<=n<100 12 | fmt.Print(rand.Intn(100), ",") 13 | fmt.Print(rand.Intn(100)) 14 | fmt.Println() 15 | 16 | // `rand.Float64` 返回一个`float64` `f`, 17 | // `0.0 <= f < 1.0` 18 | fmt.Println(rand.Float64()) 19 | 20 | // 这个方法可以用来生成其他数值范围内的随机数, 21 | // 例如`5.0 <= f < 10.0` 22 | fmt.Print((rand.Float64()*5)+5, ",") 23 | fmt.Print((rand.Float64() * 5) + 5) 24 | fmt.Println() 25 | 26 | // 为了使随机数生成器具有确定性,可以给它一个seed 27 | s1 := rand.NewSource(42) 28 | r1 := rand.New(s1) 29 | 30 | fmt.Print(r1.Intn(100), ",") 31 | fmt.Print(r1.Intn(100)) 32 | fmt.Println() 33 | 34 | // 如果源使用一个和上面相同的seed,将生成一样的随机数 35 | s2 := rand.NewSource(42) 36 | r2 := rand.New(s2) 37 | fmt.Print(r2.Intn(100), ",") 38 | fmt.Print(r2.Intn(100)) 39 | fmt.Println() 40 | } 41 | ``` 42 | 运行结果 43 | ``` 44 | 81,87 45 | 0.6645600532184904 46 | 7.1885709359349015,7.123187485356329 47 | 5,87 48 | 5,87 49 | ``` -------------------------------------------------------------------------------- /Go示例学/Go 集合功能.markdown: -------------------------------------------------------------------------------- 1 | # Go 集合功能 2 | 我们经常需要程序去处理一些集合数据,比如选出所有符合条件的数据或者使用一个自定义函数将一个集合元素拷贝到另外一个集合。 3 | 4 | 在一些语言里面,通常是使用泛化数据结构或者算法。但是Go不支持泛化类型,在Go里面如果你的程序或者数据类型需要操作集合,那么通常是为集合提供一些操作函数。 5 | 6 | 这里演示了一些操作strings切片的集合函数,你可以使用这些例子来构建你自己的函数。注意在有些情况下,使用内联集合操作代码会更清晰,而不是去创建新的帮助函数。 7 | 8 | ```go 9 | package main 10 | 11 | import "strings" 12 | import "fmt" 13 | 14 | // 返回t在vs中第一次出现的索引,如果没有找到t,返回-1 15 | func Index(vs []string, t string) int { 16 | for i, v := range vs { 17 | if v == t { 18 | return i 19 | } 20 | } 21 | return -1 22 | } 23 | 24 | // 如果t存在于vs中,那么返回true,否则false 25 | func Include(vs []string, t string) bool { 26 | return Index(vs, t) >= 0 27 | } 28 | 29 | // 如果使用vs中的任何一个字符串作为函数f的参数可以让f返回true, 30 | // 那么返回true,否则false 31 | func Any(vs []string, f func(string) bool) bool { 32 | for _, v := range vs { 33 | if f(v) { 34 | return true 35 | } 36 | } 37 | return false 38 | } 39 | 40 | // 如果分别使用vs中所有的字符串作为f的参数都能让f返回true, 41 | // 那么返回true,否则返回false 42 | func All(vs []string, f func(string) bool) bool { 43 | for _, v := range vs { 44 | if !f(v) { 45 | return false 46 | } 47 | } 48 | return true 49 | } 50 | 51 | // 返回一个新的字符串切片,切片的元素为vs中所有能够让函数f 52 | // 返回true的元素 53 | func Filter(vs []string, f func(string) bool) []string { 54 | vsf := make([]string, 0) 55 | for _, v := range vs { 56 | if f(v) { 57 | vsf = append(vsf, v) 58 | } 59 | } 60 | return vsf 61 | } 62 | 63 | // 返回一个bool类型切片,切片的元素为vs中所有字符串作为f函数 64 | // 参数所返回的结果 65 | func Map(vs []string, f func(string) string) []string { 66 | vsm := make([]string, len(vs)) 67 | for i, v := range vs { 68 | vsm[i] = f(v) 69 | } 70 | return vsm 71 | } 72 | 73 | func main() { 74 | 75 | // 来,试试我们的字符串切片操作函数 76 | var strs = []string{"peach", "apple", "pear", "plum"} 77 | 78 | fmt.Println(Index(strs, "pear")) 79 | 80 | fmt.Println(Include(strs, "grape")) 81 | 82 | fmt.Println(Any(strs, func(v string) bool { 83 | return strings.HasPrefix(v, "p") 84 | })) 85 | 86 | fmt.Println(All(strs, func(v string) bool { 87 | return strings.HasPrefix(v, "p") 88 | })) 89 | 90 | fmt.Println(Filter(strs, func(v string) bool { 91 | return strings.Contains(v, "e") 92 | })) 93 | 94 | // 上面的例子都使用匿名函数,你也可以使用命名函数 95 | fmt.Println(Map(strs, strings.ToUpper)) 96 | } 97 | ``` 98 | 运行结果 99 | ``` 100 | 2 101 | false 102 | true 103 | false 104 | [peach apple pear] 105 | [PEACH APPLE PEAR PLUM] 106 | ``` -------------------------------------------------------------------------------- /Go示例学/Go 非阻塞通道.markdown: -------------------------------------------------------------------------------- 1 | # Go 非阻塞通道 2 | 默认情况下,通道发送和接收数据是阻塞的。然而我们可以使用select的一个default的选项来实现无阻塞发送或接收数据,甚至可以将多个select的case选项和default选项结合起来使用。 3 | ```go 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | messages := make(chan string) 10 | signals := make(chan bool) 11 | 12 | // 这里是一个非阻塞的从通道接收数据,如果messages通道有数据 13 | // 可以接收,那么select将运行`<-messages`这个case,否则的话 14 | // 程序立刻执行default选项后面的语句 15 | select { 16 | case msg := <-messages: 17 | fmt.Println("received message", msg) 18 | default: 19 | fmt.Println("no message received") 20 | } 21 | 22 | // 非阻塞通道发送数据也是一样的 23 | msg := "hi" 24 | select { 25 | case messages <- msg: 26 | fmt.Println("sent message", msg) 27 | default: 28 | fmt.Println("no message sent") 29 | } 30 | 31 | // 在default前面,我们可以有多个case选项,从而实现多通道 32 | // 非阻塞的选择,这里我们尝试从messages和signals接收数据 33 | // 如果有数据可以接收,那么执行对应case后面的逻辑,否则立刻 34 | // 执行default选项后面的逻辑 35 | select { 36 | case msg := <-messages: 37 | fmt.Println("received message", msg) 38 | case sig := <-signals: 39 | fmt.Println("received signal", sig) 40 | default: 41 | fmt.Println("no activity") 42 | } 43 | } 44 | ``` 45 | 运行结果 46 | ``` 47 | no message received 48 | no message sent 49 | no activity 50 | ``` 51 | 这个例子中,由于我们使用了default来实现非阻塞的通道,所以开始的时候messages里面没有数据可以接收,直接输出`no message received`,而第二次由于messages通道没有相应的数据接收方,所以也不会写入数据,直接转到default,输出`no message sent`,至于第三个就很好理解了,什么也没有,输出`no activity`。 52 | 其实,我们可以把这个例子修改一下,让messages通道带缓冲区,这样例子或许更好理解一点。定义messages的时候使用`messages := make(chan string, 1)`。 53 | ```go 54 | package main 55 | 56 | import "fmt" 57 | 58 | func main() { 59 | messages := make(chan string, 1) 60 | signals := make(chan bool) 61 | 62 | // 这里是一个非阻塞的从通道接收数据,如果messages通道有数据 63 | // 可以接收,那么select将运行`<-messages`这个case,否则的话 64 | // 程序立刻执行default选项后面的语句 65 | select { 66 | case msg := <-messages: 67 | fmt.Println("received message", msg) 68 | default: 69 | fmt.Println("no message received") 70 | } 71 | 72 | // 非阻塞通道发送数据也是一样的,但是由于messages带了缓冲区, 73 | // 即使没有数据接受端也可以发送数据,所以这里的`messages<-msg` 74 | // 会被执行,从而不再跳到default去了。 75 | msg := "hi" 76 | select { 77 | case messages <- msg: 78 | fmt.Println("sent message", msg) 79 | default: 80 | fmt.Println("no message sent") 81 | } 82 | 83 | // 在default前面,我们可以有多个case选项,从而实现多通道 84 | // 非阻塞的选择,这里我们尝试从messages和signals接收数据 85 | // 如果有数据可以接收,那么执行对应case后面的逻辑,否则立刻 86 | // 执行default选项后面的逻辑 87 | select { 88 | case msg := <-messages: 89 | fmt.Println("received message", msg) 90 | case sig := <-signals: 91 | fmt.Println("received signal", sig) 92 | default: 93 | fmt.Println("no activity") 94 | } 95 | } 96 | ``` 97 | 运行结果 98 | ``` 99 | no message received 100 | sent message hi 101 | received message hi 102 | ``` -------------------------------------------------------------------------------- /Go示例学/README.md: -------------------------------------------------------------------------------- 1 | 本例子源于 http://gobyexample.com 的英文例子。 2 | 其中部分例子来自 @itfanr 同学的贡献。@golangfan 同学修改了一些错误。 -------------------------------------------------------------------------------- /Go轻松学/go_tutorial_0_what_to_learn.md: -------------------------------------------------------------------------------- 1 | 2 | ![Go轻松学](golang_500x500.png "Go轻松学") 3 | 4 | # 多科学堂出品 5 | 6 | ## 微信公众号 7 | ###### duokr_school 8 | 9 | ![多科学堂](qrcode_duokr.jpg "多科学堂") 10 | ## QQ群 11 | ###### 332558747 12 | ## 邮箱 13 | ###### [duokr.school@gmail.com](mailto:duokr.school@gmail.com) 14 | ## Go友团 15 | ###### [http://baoz.me/#!/Go友团](http://baoz.me/#!/Go友团) 16 | ## 赞助链接 17 | ######[https://me.alipay.com/jemygraw](https://me.alipay.com/jemygraw) 18 | 19 | 20 | --- 21 | 22 | 23 | # 学习目录 24 | 25 | ## 第一节 Go语言安装与测试 26 | 轻松友好的安装方式,多平台支持。 27 | ## 第二节 内置基础数据类型 28 | 认识Go提供的清晰的数据类型,很清晰,不骗你。 29 | ## 第三节 变量与常量定义 30 | 学语言绕不开的变量,当然Go是静态语言,变量都是有固定类型的,程序运行过程中无法改变变量类型。 31 | ## 第四节 控制流程 32 | 很简单,只有if,for,switch三种流程,连while都没有。 33 | ## 第五节 数组,切片和字典 34 | 内置高级数据类型。如果我们需要频繁使用一些功能,与其提供包支持,不如作为内置功能提供。 35 | ## 第六节 使用函数 36 | 代码的功能要精简,那么使用函数吧! 37 | ## 第七节 清楚的指针 38 | 不要怕,Go的指针不是C的指针,不可怕,但很有用。 39 | ## 第八节 结构体和接口 40 | 非常Cool的结构体功能,完全和C不同,很方便;另外Go提供的接口设计独特,值得研究。 41 | ## 第九节 并行计算 42 | 让你一见钟情的超酷功能! 43 | ## 第十节 使用包和测试 44 | 良好的项目管理支持也是一门语言的独特之处。标准的测试用例是控制项目Bug的有效手段。 45 | -------------------------------------------------------------------------------- /Go轻松学/go_tutorial_1_how_to_install_go.md: -------------------------------------------------------------------------------- 1 | # Go语言环境安装与测试 2 | ## 安装 3 | 现在来谈谈Go语言的安装,要使用Go来编写程序首先得把环境搭建起来。 4 | Go的语言环境搭建还是比较简单的👌。Google提供了Windows和Mac的安装包,所以去下载一下安装就可以了。 5 | 对于Linux的系统,可以使用系统提供的包安装工具来安装。 6 | 7 | **Go的下载地址** 8 | 9 | [https://code.google.com/p/go/downloads/list](https://code.google.com/p/go/downloads/list) 10 | 11 | **Windows** 12 | 13 | 对于Windows系统,Go提供了两种不同的安装包,分别对应32位的系统和64位的系统,安装的时候根据自己的系统实际情况选择下载包。Windows下面提供的是msi格式的安装包,这种包是可执行文件,直接双击安装就可以了。安装完成之后,安装程序会自动地将安装完的Go的根目录下的bin目录加入系统的PATH环境变量里面。所以直接打开命令行,输入go,就可以看到一些提示信息了。 14 | 15 | **Mac** 16 | 17 | 如果是新买的Mac,里面可能自带了一个go的可执行文件,在路径`/etc/paths.d/`下面,就是一个go可执行文件。如果我们需要安装从官网下载的dmg安装包,先要把这个文件删除掉。可以用`sudo rm /etc/paths.d/go`来删除。然后自动安装dmg之后,要使用`export PATH`的方法将安装好的Go目录下面的bin目录加入PATH中。一般安装完之后路径为`/usr/local/go`,所以你可以用下面的方法: 18 | 首先切换到自己的用户目录 19 | 20 | cd ~ 21 | 22 | 然后 23 | 24 | vim .profile 25 | 26 | 加入一行 27 | 28 | export PATH=/usr/local/go/bin:$PATH 29 | 30 | 就可以了。 31 | 32 | 33 | **Linux** 34 | 35 | Linux的发行版有很多,可以根据不同系统提供的包管理工具来安装Go,不过可能系统包管理工具提供的不是最新的Go版本。在这种情况下,你可以去下载最新的tar包。 36 | 然后使用下面的方法 37 | 38 | sudo tar -C /usr/local -xzf go1.2.linux-386.tar.gz 39 | 40 | 如果是64位的系统,用下面的方法 41 | 42 | sudo tar -C /usr/local -xzf go1.2.linux-amd64.tar.gz 43 | 44 | 当然,这样的方式只是将安装包解压拷贝到`/usr/local/`下面。你还需要使用`export PATH`的方式将Go的bin目录加入PATH。 45 | 方法和上面Mac介绍的一样。 46 | 另外如果你不是将Go安装到`/usr/local`目录下面,你还需要设置一个GOROOT环境变量。比如你安装到你自己的文件夹下面,比如叫jemy的用户的路径是`/home/jemy`,那么你安装到这个目录的Go路径为`/home/jemy/go`,那么在`export PATH`之前,你还需要使用下面的命令。 47 | 48 | export GOROOT=/home/jemy/go 49 | 50 | 总结一下,如果你默认安装路径为`/usr/local/go`,那么只需要用 51 | 52 | export PATH=$PATH:/usr/local/go/bin 53 | 54 | 就可以了。 55 | 如果不是默认路径则需要这样 56 | 57 | export GOROOT=/home/jemy/go 58 | export PATH=$PATH:/$GROOT/bin 59 | 60 | 上面的`/home/jemy`是根据实际安装的路径情况来确定。 61 | 62 | 最后说一下go的最基本的三个命令 63 | 64 | 1.查看版本号 65 | 66 | go version 67 | 68 | 结果为 69 | 70 | duokr:~ jemy$ go version 71 | go version go1.2 darwin/386 72 | 73 | 2.格式化go代码文件 74 | 75 | go fmt file_name.go 76 | 77 | 3.运行单个go代码文件 78 | 79 | go run file_name.go 80 | 81 | ## 测试 82 | 83 | `生` `死` `hello world` 84 | 85 | 学习计算机的, 绕不开的三件事。 86 | 87 | 有谁安装好语言环境,不试一下hello world的? 88 | 89 | 90 | //main包, 凡是标注为main包的go文件都会被编译为可执行文件 91 | package main 92 | 93 | //导入需要使用的包 94 | import ( 95 | "fmt" //支持格式化输出的包,就是format的简写 96 | ) 97 | 98 | //主函数,程序执行入口 99 | func main() { 100 | /* 101 | 输出一行hello world 102 | Println函数就是print line的意思 103 | */ 104 | fmt.Println("hello world") 105 | } 106 | 然后使用`go run helloworld.go`来运行这个例子。如果安装成功,那么会输出一行`hello world`。 107 | 108 | *PS* 109 | 110 | `Windows7可以在文件所在目录下面使用Shift+右键,快速打开已定位到所在目录的命令行窗口。直接输入上面命令即可。` 111 | -------------------------------------------------------------------------------- /Go轻松学/go_tutorial_2_data_type.md: -------------------------------------------------------------------------------- 1 | # Go语言内置基础数据类型 2 | 3 | 在自然界里面,有猫,有狗,有猪。有各种动物。每种动物都是不同的。 4 | 比如猫会喵喵叫,狗会旺旺叫,猪会哼哼叫。。。 5 | Stop!!! 6 | 好了,大家毕竟不是幼儿园的小朋友。介绍到这里就可以了。 7 | 8 | 论点就是每个东西都有自己归属的类别(Type)。 9 | 那么在Go语言里面,每个变量也都是有类别的,这种类别叫做`数据类型(Data Type)`。 10 | Go的数据类型有两种:一种是`语言内置的数据类型`,另外一种是`通过语言提供的自定义数据类型方法自己定义的自定义数据类型`。 11 | 12 | 先看看语言`内置的基础数据类型` 13 | 14 | **数值型(Number)** 15 | 16 | 数值型有`三种`,一种是`整数类型`,另外一种是`带小数的类型`(一般计算机里面叫做`浮点数类型`),还有一种`虚数类型`。 17 | 18 | 整数类型不用说了,和数学里面的是一样的。和数学里面不同的地方在于计算机里面`正整数和零`统称为`无符号整型`,而`负整数`则称为`有符号整型`。 19 | 20 | Go的内置整型有`uint8`, `uint16`, `uint32`, `uint64`, `int8`, `int16`, `int32`和`int64`。其中`u`开头的类型就是`无符号整型`。无符号类型能够表示正整数和零。而有符号类型除了能够表示正整数和零外,还可以表示负整数。 21 | 另外还有一些别名类型,比如`byte`类型,这个类型和`uint8`是一样的,表示`字节类型`。另外一个是`rune类型`,这个类型和`int32`是一样的,用来表示`unicode的代码点`,就是unicode字符所对应的整数。 22 | 23 | Go还定义了三个`依赖系统`的类型,`uint`,`int`和`uintptr`。因为在32位系统和64位系统上用来表示这些类型的位数是不一样的。 24 | 25 | *对于32位系统* 26 | 27 | uint=uint32 28 | int=int32 29 | uintptr为32位的指针 30 | 31 | *对于64位系统* 32 | 33 | uint=uint64 34 | int=int64 35 | uintptr为64位的指针 36 | 37 | 至于类型后面跟的数字8,16,32或是64则表示用来表示这个类型的位不同,`位越多,能表示的整数范围越大`。 38 | 比如对于用N位来表示的整数,如果是`有符号的整数`,能够表示的整数范围为`-2^(N-1) ~ 2^(N-1)-1`;如果是`无符号的整数`,则能表示的整数范围为`0 ~ 2^N`。 39 | 40 | Go的浮点数类型有两种,`float32`和`float64`。float32又叫`单精度浮点型`,float64又叫做`双精度浮点型`。其`最主要的区别就是小数点后面能跟的小数位数不同`。 41 | 42 | 另外Go还有两个其他语言所没有的类型,`虚数类型`。`complex64`和`complex128`。 43 | 44 | 对于数值类型,其所共有的操作为`加法(+)`,`减法(-)`,`乘法(*)`和`除法(/)`。另外对于`整数类型`,还定义了`求余运算(%)` 45 | 46 | 求余运算为整型所独有。如果对浮点数使用求余,比如这样 47 | 48 | package main 49 | 50 | import ( 51 | "fmt" 52 | ) 53 | 54 | func main() { 55 | var a float64 = 12 56 | var b float64 = 3 57 | 58 | fmt.Println(a % b) 59 | } 60 | 61 | 62 | 编译时候会报错 63 | 64 | invalid operation: a % b (operator % not defined on float64) 65 | 66 | 所以,这里我们可以知道所谓的`数据类型有两层意思`,一个是定义了`该类型所能表示的数`,另一个是定义了`该类型所能进行的操作`。 67 | 简单地说,对于一只小狗,你能想到的一定是狗的面貌和它会汪汪叫,而不是猫的面容和喵喵叫。 68 | 69 | 70 | **字符串类型(String)** 71 | 72 | 字符串就是一串固定长度的字符连接起来的字符序列。Go的字符串是由`单个字节`连接起来的。(对于汉字,通常由多个字节组成)。这就是说,传统的字符串是由字符组成的,而`Go的字符串不同`,是`由字节组成`的。这一点需要注意。 73 | 74 | 字符串的表示很简单。用(双引号"")或者(``号)来描述。 75 | 76 | "hello world" 77 | 78 | 或者 79 | 80 | `hello world` 81 | 82 | 唯一的区别是,**双引号之间的转义字符会被转义,而``号之间的转义字符保持原样不变**。 83 | 84 | package main 85 | 86 | import ( 87 | "fmt" 88 | ) 89 | 90 | func main() { 91 | var a = "hello \n world" 92 | var b = `hello \n world` 93 | 94 | fmt.Println(a) 95 | fmt.Println("----------") 96 | fmt.Println(b) 97 | } 98 | 99 | 输出结果为 100 | 101 | hello 102 | world 103 | ---------- 104 | hello \n world 105 | 106 | 字符串所能进行的一些基本操作包括: 107 | (1)`获取字符长度` 108 | (2)`获取字符串中单个字节` 109 | (3)`字符串连接` 110 | 111 | package main 112 | 113 | import ( 114 | "fmt" 115 | ) 116 | 117 | func main() { 118 | var a string = "hello" 119 | var b string = "world" 120 | 121 | fmt.Println(len(a)) 122 | fmt.Println(a[1]) 123 | fmt.Println(a + b) 124 | } 125 | 126 | 输出如下 127 | 128 | 5 129 | 101 130 | helloworld 131 | 132 | 这里我们看到a[1]得到的是一个整数,这就证明了上面`"Go的字符串是由字节组成的这句话"`。我们还可以再验证一下。 133 | 134 | package main 135 | 136 | import ( 137 | "fmt" 138 | ) 139 | 140 | func main() { 141 | var a string = "你" 142 | var b string = "好" 143 | fmt.Println(len(a)) 144 | fmt.Println(len(b)) 145 | fmt.Println(len(a + b)) 146 | fmt.Println(a[0]) 147 | fmt.Println(a[1]) 148 | fmt.Println(a[2]) 149 | } 150 | 151 | 输出 152 | 153 | 3 154 | 3 155 | 6 156 | 228 157 | 189 158 | 160 159 | 160 | 我们开始的时候,从上面的三行输出知道,"你"和"好"分别是用三个字节组成的。我们依次获取a的三个字节,输出,得到结果。 161 | 162 | 163 | **布尔型(Bool)** 164 | 165 | 布尔型是表示`真值`和`假值`的类型。可选值为`true`和`false`。 166 | 167 | 所能进行的操作如下: 168 | `&& and 与` 169 | `|| or 或` 170 | `! not 非` 171 | 172 | Go的布尔型取值`就是true`或`false`。`任何空值(nil)或者零值(0, 0.0, "")都不能作为布尔型来直接判断`。 173 | 174 | package main 175 | 176 | import ( 177 | "fmt" 178 | ) 179 | 180 | func main() { 181 | var equal bool 182 | var a int = 10 183 | var b int = 20 184 | equal = (a == b) 185 | fmt.Println(equal) 186 | } 187 | 输出结果 188 | 189 | false 190 | 191 | 下面是错误的用法 192 | 193 | package main 194 | 195 | import ( 196 | "fmt" 197 | ) 198 | 199 | func main() { 200 | if 0 { 201 | fmt.Println("hello world") 202 | } 203 | if nil { 204 | fmt.Println("hello world") 205 | } 206 | if "" { 207 | fmt.Println("hello world") 208 | } 209 | } 210 | 211 | 编译错误 212 | 213 | ./t.go:8: non-bool 0 (type untyped number) used as if condition 214 | ./t.go:11: non-bool nil used as if condition 215 | ./t.go:14: non-bool "" (type untyped string) used as if condition 216 | 217 | 218 | 上面介绍的是Go语言内置的基础数据类型。 -------------------------------------------------------------------------------- /Go轻松学/go_tutorial_7_pointer.md: -------------------------------------------------------------------------------- 1 | # Go指针 2 | 不要害怕,Go的指针是好指针。 3 | 4 | **定义** 5 | 6 | 所谓`指针其实你可以把它想像成一个箭头,这个箭头指向(存储)一个变量的地址`。 7 | 8 | 因为这个箭头本身也需要变量来存储,所以也叫做指针变量。 9 | 10 | Go的指针`不支持那些乱七八糟的指针移位`。`它就表示一个变量的地址`。看看这个例子: 11 | 12 | package main 13 | 14 | import ( 15 | "fmt" 16 | ) 17 | 18 | func main() { 19 | var x int 20 | var x_ptr *int 21 | 22 | x = 10 23 | x_ptr = &x 24 | 25 | fmt.Println(x) 26 | fmt.Println(x_ptr) 27 | fmt.Println(*x_ptr) 28 | } 29 | 30 | 31 | 上面例子输出`x的值`,`x的地址`和`通过指针变量输出x的值`,而`x_ptr就是一个指针变量`。 32 | 33 | 10 34 | 0xc084000038 35 | 10 36 | 37 | 认真理清楚这两个符号的意思。 38 | 39 | **&** `取一个变量的地址` 40 | 41 | **\*** `取一个指针变量所指向的地址的值` 42 | 43 | 44 | 考你一下,上面的例子中,如何输出x_ptr的地址呢? 45 | 46 | package main 47 | 48 | import ( 49 | "fmt" 50 | ) 51 | 52 | func main() { 53 | var x int 54 | var x_ptr *int 55 | 56 | x = 10 57 | x_ptr = &x 58 | 59 | fmt.Println(&x_ptr) 60 | } 61 | 62 | 此例看懂,指针就懂了。 63 | 64 | 永远记住一句话,`所谓指针就是一个指向(存储)特定变量地址的变量`。没有其他的特别之处。 65 | 66 | 再变态一下,看看这个: 67 | 68 | package main 69 | 70 | import ( 71 | "fmt" 72 | ) 73 | 74 | func main() { 75 | var x int 76 | var x_ptr *int 77 | 78 | x = 10 79 | x_ptr = &x 80 | 81 | fmt.Println(*&x_ptr) 82 | } 83 | 84 | 1. x_ptr 是一个`指针变量`,它`指向(存储)x的地址`; 85 | 2. &x_ptr 是`取这个指针变量x_ptr的地址`,这里可以设想`有另一个指针变量x_ptr_ptr(指向)存储`这个`x_ptr指针的地址`; 86 | 3. *&x_ptr 等价于`*x_ptr_ptr`就是`取这个x_ptr_ptr指针变量`所`指向(存储)`的`地址所对应的变量的值` ,也就是`x_ptr的值`,也就是`指针变量x_ptr指向(存储)的地址`,也就是`x的地址`。 这里可以看到,其实`*&`这两个运算符在一起就相互抵消作用了。 87 | 88 | **用途** 89 | 90 | `指针的一大用途就是可以将变量的指针作为实参传递给函数,从而在函数内部能够直接修改实参所指向的变量值。` 91 | 92 | Go的变量传递都是值传递。 93 | 94 | package main 95 | 96 | import ( 97 | "fmt" 98 | ) 99 | 100 | func change(x int) { 101 | x = 200 102 | } 103 | func main() { 104 | var x int = 100 105 | fmt.Println(x) 106 | change(x) 107 | fmt.Println(x) 108 | } 109 | 110 | 111 | 上面的例子输出结果为 112 | 113 | 100 114 | 100 115 | 116 | 很显然,change函数`改变的`仅仅是`内部变量x`的`值`,而`不会改变`传递进去的`实参`。其实,也就是说Go的函数一般关心的是输出结果,而输入参数就相当于信使跑到函数门口大叫,你们这个参数是什么值,那个是什么值,然后就跑了。你函数根本就不能修改它的值。不过如果是传递的实参是指针变量,那么函数一看,小子这次你地址我都知道了,哪里跑。那么就是下面的例子: 117 | 118 | package main 119 | 120 | import ( 121 | "fmt" 122 | ) 123 | 124 | func change(x *int) { 125 | *x = 200 126 | } 127 | func main() { 128 | var x int = 100 129 | fmt.Println(x) 130 | change(&x) 131 | fmt.Println(x) 132 | } 133 | 134 | 135 | 上面的例子中,change函数的虚参为`整型指针变量`,所以在main中调用的时候`传递的是x的地址`。然后在change里面使用`*x=200`修改了这个x的地址的值。所以`x的值就变了`。这个输出是: 136 | 137 | 100 138 | 200 139 | 140 | 141 | **new** 142 | 143 | new这个函数挺神奇,因为它的用处太多了。这里还可以通过new来`初始化一个指针`。上面说过指针指向(存储)的是一个变量的地址,但是指针本身也需要地址存储。先看个例子: 144 | 145 | package main 146 | 147 | import ( 148 | "fmt" 149 | ) 150 | 151 | func set_value(x_ptr *int) { 152 | *x_ptr = 100 153 | } 154 | func main() { 155 | x_ptr := new(int) 156 | set_value(x_ptr) 157 | //x_ptr指向的地址 158 | fmt.Println(x_ptr) 159 | //x_ptr本身的地址 160 | fmt.Println(&x_ptr) 161 | //x_ptr指向的地址值 162 | fmt.Println(*x_ptr) 163 | } 164 | 165 | 166 | 上面我们定义了一个x_ptr变量,然后用`new申请`了一个`存储整型数据的内存地址`,然后将这个`地址赋值`给`x_ptr指针变量`,也就是说`x_ptr指向(存储)的是一个可以存储整型数据的地址`,然后用set_value函数将`这个地址中存储的值`赋值为100。所以第一个输出是`x_ptr指向的地址`,第二个则是`x_ptr本身的地址`,而`*x_ptr`则是`x_ptr指向的地址中存储的整型数据的值`。 167 | 168 | 169 | 0xc084000040 170 | 0xc084000038 171 | 100 172 | 173 | **小结** 174 | 175 | 好了,现在用个例子再来回顾一下指针。 176 | 177 | 交换两个变量的值。 178 | 179 | package main 180 | 181 | import ( 182 | "fmt" 183 | ) 184 | 185 | func swap(x, y *int) { 186 | *x, *y = *y, *x 187 | } 188 | func main() { 189 | x_val := 100 190 | y_val := 200 191 | swap(&x_val, &y_val) 192 | fmt.Println(x_val) 193 | fmt.Println(y_val) 194 | } 195 | 196 | 197 | 很简单吧,这里利用了Go提供的`交叉赋值`的功能,另外由于是使用了指针作为参数,所以在swap函数内,x_val和y_val的值就被交换了。 198 | 199 | 200 | -------------------------------------------------------------------------------- /Go轻松学/golang_500x500.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jemygraw/TechDoc/38794deee04bafa5f1510c634f23c4f992762b39/Go轻松学/golang_500x500.png -------------------------------------------------------------------------------- /Go轻松学/qrcode_duokr.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jemygraw/TechDoc/38794deee04bafa5f1510c634f23c4f992762b39/Go轻松学/qrcode_duokr.jpg -------------------------------------------------------------------------------- /Jenkins/2018-10-09 Jenkins常用API汇总.md: -------------------------------------------------------------------------------- 1 | 最近在研究如何将 Jenkins 集成到发布环境中,重点研究了一下 Jenkins 中相关的 API。 2 | 3 | 对于 Jenkins 中的任务,Jenkins 提供了一些 RESTFUL 的API来获取这些任务的信息,或者是来触发新的任务构建等。 4 | 5 | ## 鉴权 6 | 7 | Jenkins 的 API 使用的是 Basic 的鉴权方式,也就是在 Jenkins 中,我们可以使用登陆ID和相应的 API Token 来做鉴权。 8 | 9 | 假设我们的登陆ID是 `jinxinxin` ,相应的 API Token 为 `611e3be83c538f9bf8b25be0218f0832`。那么如果使用 curl 来模拟请求的话,请求的头部如下所示,就是把登陆ID和 API Token 用冒号 `:` 拼接起来,然后做 Base64 编码,前面再拼接上 `Basic `,构建了 Authorization 头部的值。 10 | 11 | ## API 12 | 13 | ### 启动一个新的 Jenkins Build 14 | 15 | ``` 16 | POST {JenkinsHost}/job/{JenkinsJobName}/build 17 | 18 | Authorization: Basic 19 | 20 | --- 21 | 22 | 200 OK 23 | ``` 24 | 25 | 例如: 26 | 27 | ``` 28 | $ curl --user jinxinxin:11e42a69ae0872b71c013ec3f825f9df43 http://localhost:8080/job/test-echo-java/build -X POST -v 29 | * Trying ::1... 30 | * TCP_NODELAY set 31 | * Connected to localhost (::1) port 8080 (#0) 32 | * Server auth using Basic with user 'jinxinxin' 33 | > POST /job/test-echo-java/build HTTP/1.1 34 | > Host: localhost:8080 35 | > Authorization: Basic amlueGlueGluOjExZTQyYTY5YWUwODcyYjcxYzAxM2VjM2Y4MjVmOWRmNDM= 36 | > User-Agent: curl/7.54.0 37 | > Accept: */* 38 | > 39 | < HTTP/1.1 201 Created 40 | < Date: Wed, 10 Oct 2018 04:17:13 GMT 41 | < X-Content-Type-Options: nosniff 42 | < Location: http://localhost:8080/queue/item/12/ 43 | < Content-Length: 0 44 | < Server: Jetty(9.4.z-SNAPSHOT) 45 | < 46 | * Connection #0 to host localhost left intact 47 | ``` 48 | 49 | 该请求会触发一个新的构建,但是这个请求并不返回一个Body,而是通过状态码判断请求是否成功,成功为200,失败的可能是404表示job不存在或者其他错误等。 50 | 或许你会好奇,这个请求为什么不返回一个新的构建的 BuildNumber,因为这个请求本身触发的是将新的构建请求加入到队列中,这个时候还没有分配给这个构建任务一个 Build Number。 51 | 52 | ### 获取启动的 Jenkins Build 的 BuildNumber 53 | 54 | 我们为什么那么执着地一定要获取这个任务的 BuildNumber 呢?因为我们想要获取这个任务的具体执行情况,就必须要有这个 BuildNumber。我们使用下面的 API 来获取这个 BuildNumber。 55 | 56 | ``` 57 | GET {JenkinsHost}/job/{JenkinsJobName}/lastBuild/api/json 58 | 59 | Authorization: Basic 60 | 61 | --- 62 | 63 | 200 OK 64 | 65 | { 66 | ... 67 | } 68 | ``` 69 | 70 | 例如: 71 | 72 | ``` 73 | $ curl --user 'jinxinxin:611e3be83c538f9bf8b25be0218f0832' http://ci.example.com/job/test-echo-java/lastBuild/api/json 74 | ``` 75 | 76 | 这个请求用来获取刚刚触发的构建任务的信息,我们从返回的JSON格式的Body里面,可以解析得到 Build 的 Number。 77 | 78 | ### -------------------------------------------------------------------------------- /Jenkins/images/jenkins-add-git-repo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jemygraw/TechDoc/38794deee04bafa5f1510c634f23c4f992762b39/Jenkins/images/jenkins-add-git-repo.png -------------------------------------------------------------------------------- /Jenkins/images/jenkins-create-a-freestyle-job.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jemygraw/TechDoc/38794deee04bafa5f1510c634f23c4f992762b39/Jenkins/images/jenkins-create-a-freestyle-job.png -------------------------------------------------------------------------------- /Jenkins/images/private-key-in-jenkins.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jemygraw/TechDoc/38794deee04bafa5f1510c634f23c4f992762b39/Jenkins/images/private-key-in-jenkins.png -------------------------------------------------------------------------------- /Jenkins/images/public-key-in-gitlab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jemygraw/TechDoc/38794deee04bafa5f1510c634f23c4f992762b39/Jenkins/images/public-key-in-gitlab.png -------------------------------------------------------------------------------- /Kubernetes/2018-09-11 kubelet使用imagePullSecret来拉取镜像.md: -------------------------------------------------------------------------------- 1 | 最近在做部署工具的时候,将应用(Deployment)发布到集群后,发现Pod启动不起来,使用 kubectl describe pod xxx 发现错误如下: 2 | 3 | ``` 4 | ---- ------ ---- ---- ------- 5 | Normal Scheduled 1m default-scheduler Successfully assigned default/echo-java-5bb694886c-7wkkv to 10.8.1.76 6 | Normal Pulling 25s (x3 over 1m) kubelet, 10.8.1.76 pulling image "reg.example.com/java-apps/echo-java:1.0.0" 7 | Warning Failed 25s (x3 over 1m) kubelet, 10.8.1.76 Failed to pull image "reg.example.com/java-apps/echo-java:1.0.0": rpc error: code = Unknown desc = Error response from daemon: Get https://reg.example.com/v2/apps/echo-java/manifests/1.0.0: unauthorized: authentication required 8 | Warning Failed 25s (x3 over 1m) kubelet, 10.8.1.76 Error: ErrImagePull 9 | Normal BackOff 11s (x4 over 1m) kubelet, 10.8.1.76 Back-off pulling image "reg.example.com/java-apps/echo-java:1.0.0" 10 | Warning Failed 11s (x4 over 1m) kubelet, 10.8.1.76 Error: ImagePullBackOff 11 | ``` 12 | 13 | 从错误描述上看,说的是镜像拉取失败了。提示的错误是:`authentication required`,从这里我们判断出是因为集群里面没有配置拉取镜像用的 Secret 导致的。在Kubernetes中我们可以把镜像中心的登录信息写入到 Secret,然后通过 Deployment 的 YAML 文件来引用,从而实现让 kubelet 取拉取镜像的功能。 14 | 15 | 我们使用如下的命令创建一个保存镜像中心登录信息的 Secret: 16 | 17 | ``` 18 | $ kubectl create secret docker-registry myregistrykey --docker-server https://reg.example.com/v2/ --docker-username jemygraw --docker-password jemypasword --docker-email jemygraw@example.com 19 | ``` 20 | 21 | 其中 `--docker-username` , `--docker-password` ,`--docker-email` 分别填写登录的账号信息。而 `--docker-server` 填写注册中心的API服务地址。注意上面例子中的最后的部分为 `v2/` ,这个根据各自的注册中心不同可能不同,注意不要把最后的斜杠忘了。 22 | 23 | 在创建完成 Secret 之后,我们可以用命令查看下: 24 | 25 | ``` 26 | $ kubectl get secret myregistrykey 27 | ``` 28 | 29 | 然后我们就可以在 Deployment 的 YAML 配置文件中使用这个名为 myregistrykey 的Secret。 30 | 31 | ``` 32 | kind: Deployment 33 | ... 34 | spec: 35 | ... 36 | template: 37 | ... 38 | spec: 39 | ... 40 | imagePullSecrets: 41 | - name: myregistrykey 42 | ``` 43 | 44 | 然后使用 kubectl delete pod xxx 删除那个Pod,让kubelet重建一个Pod就好了。 -------------------------------------------------------------------------------- /Kubernetes/2018-09-13 kubelet报错 broken pipe for writing log.md: -------------------------------------------------------------------------------- 1 | 最近发现从kubelet的日志中发现一些 Error 级别的错误。主要的错误信息为 `write: broken pipe when writing log for log file` 和 `write: connection reset by peer when writing log for log file` ,详细信息如下: 2 | 3 | ``` 4 | 2018-09-13 13:36 Test Cluster 5 | 1-132.example.net kubelet -- E0913 13:36:00.011285 5203 kuberuntime_logs.go:201] Failed with err write tcp 10.8.1.132:10250->10.8.1.55:62786: write: broken pipe when writing log for log file /var/log/pods/d63e3f59-b715-11e8-811e-427775cbe8d8/shop-pay-server_0.log: &{timestamp:{sec:63672413760 nsec:10835564 loc:} stream:stdout log:[9 97 116 32 111 114 103 46 115 112 114 105 110 103 102 114 97 109 101 119 111 114 107 46 97 111 112 46 102 114 97 109 101 119 111 114 107 46 82 101 102 108 101 99 116 105 118 101 77 101 116 104 111 100 73 110 118 111 99 97 116 105 111 110 46 112 114 111 99 101 101 100 40 82 101 102 108 101 99 116 105 118 101 77 101 116 104 111 100 73 110 118 111 99 97 116 105 111 110 46 106 97 118 97 58 49 55 57 41 10]} 6 | ``` 7 | 8 | ``` 9 | 2018-09-13 13:59 Test Cluster 10 | 1-132.example.net kubelet -- E0913 13:59:00.003444 5203 kuberuntime_logs.go:201] Failed with err write tcp 10.8.1.132:10250->10.8.1.55:63848: write: connection reset by peer when writing log for log file /var/log/pods/d63e3f59-b715-11e8-811e-427775cbe8d8/shop-pay-server_0.log: &{timestamp:{sec:63672415140 nsec:3320231 loc:} stream:stdout log:[50 48 49 56 45 48 57 45 49 51 32 49 51 58 53 57 58 48 48 46 48 48 51 32 91 115 104 97 110 100 105 97 110 45 112 97 121 45 115 101 114 118 101 114 45 55 98 52 102 53 56 100 56 56 53 45 99 119 110 99 52 32 124 32 115 104 97 110 100 105 97 110 45 112 97 121 45 115 101 114 118 101 114 32 124 32 45 32 124 32 112 111 111 108 45 55 45 116 104 114 101 97 100 45 49 48 93 32 68 69 66 85 71 32 111 46 115 46 106 100 98 99 46 100 97 116 97 115 111 117 114 99 101 46 68 97 116 97 83 111 117 114 99 101 85 116 105 108 115 32 45 32 70 101 116 99 104 105 110 103 32 74 68 66 67 32 67 111 110 110 101 99 116 105 111 110 32 102 114 111 109 32 68 97 116 97 83 111 117 114 99 101 10]} 11 | ``` 12 | 13 | 其中,`10.8.1.132` 是一个 Node 节点,然后 `10.8.1.55` 是 Master 节点,所以这个错误的信息就是 Node 节点上面的 kubelet 在和Master进行通信,尝试往 Master 上面写入数据的时候报错了。而且这个错误看上去是 Master 那边主动断掉了链接(从 connection reset by peer判断)。所以,我们在错误的日志里面找到了一个关键字,就是 Pod 的名字 shop-pay-server ,然后我们在 master上面查 `journalctl |grep 'Sep 13 13:59' |grep 'shop-pay-server'` 的日志得到如下内容: 14 | 15 | ``` 16 | Sep 13 13:59:00 1-55-k8s-master.example.net kube-apiserver[90382]: I0913 13:59:00.002017 90382 trace.go:76] Trace[1375012443]: "Get /api/v1/namespaces/default/pods/shop-pay-server-7b4f58d885-cwnc4/log" (started: 2018-09-13 13:48:08.166214535 +0800 CST) (total time: 10m51.835742077s): 17 | Sep 13 13:59:00 1-55-k8s-master.example.net kube-apiserver[90382]: Trace[1375012443]: [10m51.835742077s] [10m51.832181896s] END 18 | Sep 13 13:59:00 1-55-k8s-master.example.net kube-apiserver[90382]: I0913 13:59:00.002103 90382 wrap.go:42] GET /api/v1/namespaces/default/pods/shop-pay-server-7b4f58d885-cwnc4/log?follow=true: (10m51.836399608s) 200 [[kubectl/v1.8.9 (linux/amd64) kubernetes/3fb1aaf] 10.8.1.59:31962] 19 | ``` 20 | 21 | 从调用的API上面,有一个关键信息 `kubectl/v1.8.9 (linux/amd64)`,据此分析是有人使用kubectl进行日志查询。从源码层面分析的话,也会了解到请求 22 | 23 | ``` 24 | GET /api/v1/namespaces/default/pods/shop-pay-server-7b4f58d885-cwnc4/log?follow=true 25 | ``` 26 | 27 | 其实是一个 kubelet 命令触发的。然后我们从 master 的 history 调查执行过的命令发现触发这个操作的命令: 28 | 29 | ``` 30 | 1027 2018-09-13 13:48:07 root kubectl logs -f shop-pay-server-7b4f58d885-cwnc4 31 | ``` 32 | 33 | 该问题复现方法:命令行输入上面的logs命令,然后等一会儿有日志输出的时候,ctrl+c掉,即会输出上面的报警信息。 -------------------------------------------------------------------------------- /Kubernetes/2018-09-20 Kubernetes API访问鉴权之Basic模式.md: -------------------------------------------------------------------------------- 1 | Kubernetes 支持多种模式的API访问鉴权方式。包括私钥+证书模式,Basic 用户名密码模式,Bearer Token 模式等。其中最常用的是基于ServiceAccount的私钥+证书模式。不过另外两种模式也在支持范畴,所以我们也了解一下,方便特殊场景下的使用。 2 | 3 | # Basic用户鉴权 4 | 5 | 首先,我们在API服务端的 `/etc/kubernetes/` 目录下新建一个 `users.csv` 文件,内容如下: 6 | 7 | ``` 8 | [root@ksnode1 kubernetes]# cat users.csv 9 | pass123,jemy,1007,"developer" 10 | ``` 11 | 12 | 然后在 APIServer 的启动命令行选项中加入选项 13 | 14 | ``` 15 | --basic-auth-file=/etc/kubernetes/users.csv 16 | ``` 17 | 18 | 我们在集群外的一个客户端设置下访问集群的Basic账号信息,主要需要用户名,密码,集群ca证书以及集群的 API Server 服务地址。 19 | 20 | ``` 21 | $ kubectl config set-credentials jemy --username jemy --password pass123 22 | User "jemy" set. 23 | $ kubectl config set-cluster k8s-learning --server https://10.8.1.72:6444 --certificate-authority /etc/kubernetes/pki/env-jxx/ca.pem 24 | $ kubectl config set-context k8s-learning-ctx --cluster k8s-learning --user jemy 25 | $ kubectl config use-context k8s-learning-ctx 26 | ``` 27 | 28 | 然后,我们尝试直接访问nodes命令,会发现报错如下: 29 | 30 | ``` 31 | $ kubectl get nodes 32 | Error from server (Forbidden): nodes is forbidden: User "jemy" cannot list nodes at the cluster scope 33 | ``` 34 | 35 | 这种错误表示权限验证通过了但是没有任何的权限。如果是权限验证不通过,报错如下: 36 | 37 | ``` 38 | $ kubectl get nodes 39 | error: You must be logged in to the server (Unauthorized) 40 | ``` 41 | 42 | 上面的这个问题主要是由K8S的访问权限管理模式决定的。K8S的资源访问权限的基本处理流程分为三步: 43 | 44 | 1. Authentication 权限验证,简单来说就是检查所使用的API的用户权限是否存在,比如用户名密码是否正确,就是一个登陆权限而已 45 | 2. Authorization 授权验证,简单来说就是检查这个用户权限是否拥有操作K8S资源的权限,对哪些资源有操作权限,只要存在一个资源的操作权限正确,就允许通过 46 | 3. Admission Control 准入控制,简单来说就是检查这个用户对这些资源的具体操作权限是否合法,存在一个不合法则全部拒绝操作 47 | 48 | 为了让这个用户能够访问集群资源,我们给这个普通的用户jemy授权一个系统内置的ClusterRole:`cluster-admin` 49 | 50 | ``` 51 | $ kubectl create clusterrolebinding cluster-admin-for-jemy --clusterrole cluster-admin --user jemy 52 | clusterrolebinding.rbac.authorization.k8s.io/cluster-admin-for-jemy created 53 | ``` 54 | 55 | 然后再运行上面的 `kubectl get nodes` 即可发现节点。 56 | 57 | ``` 58 | $ kubectl get nodes 59 | NAME STATUS ROLES AGE VERSION 60 | 10.8.1.75 Ready 23d v1.11.0 61 | 10.8.1.76 Ready 23d v1.11.0 62 | ``` 63 | 64 | 我们使用 `kubectl get clusterrole cluster-admin -o yaml` 看下这个内置的 cluster-admin 定义: 65 | 66 | ``` 67 | apiVersion: rbac.authorization.k8s.io/v1 68 | kind: ClusterRole 69 | metadata: 70 | annotations: 71 | rbac.authorization.kubernetes.io/autoupdate: "true" 72 | creationTimestamp: 2018-08-27T01:31:58Z 73 | labels: 74 | kubernetes.io/bootstrapping: rbac-defaults 75 | name: cluster-admin 76 | resourceVersion: "43" 77 | selfLink: /apis/rbac.authorization.k8s.io/v1/clusterroles/cluster-admin 78 | uid: fd9b31a4-a998-11e8-811b-569fbff0044d 79 | rules: 80 | - apiGroups: 81 | - '*' 82 | resources: 83 | - '*' 84 | verbs: 85 | - '*' 86 | - nonResourceURLs: 87 | - '*' 88 | verbs: 89 | - '*' 90 | ``` 91 | 92 | 这个 ClusterRole 里面定义了所有资源的访问,配置等操作权限。 93 | 94 | 为了了解权限控制,我们可以创建一个自己的ClusterRole,只允许读取节点和列举节点信息。 95 | 96 | 先把刚刚的ClusterRoleBinding删除 97 | 98 | ``` 99 | $ kubectl delete clusterrolebinding cluster-admin-for-jemy 100 | ``` 101 | 102 | 然后先创建我们自己的 ClusterRole 103 | 104 | ``` 105 | $ kubectl create clusterrole my-cluster-admin --resource nodes --verb 'get,list' 106 | clusterrole.rbac.authorization.k8s.io/my-cluster-admin created 107 | ``` 108 | 109 | 重新创建绑定 110 | 111 | ``` 112 | $ kubectl create clusterrolebinding cluster-admin-for-jemy --clusterrole my-cluster-admin --user jemy 113 | clusterrolebinding.rbac.authorization.k8s.io/cluster-admin-for-jemy created 114 | ``` 115 | 116 | 读取某个节点信息,对应get操作 117 | 118 | ``` 119 | $ kubectl get nodes 10.8.1.75 120 | NAME STATUS ROLES AGE VERSION 121 | 10.8.1.75 Ready 23d v1.11.0 122 | ``` 123 | 124 | 获取节点的列表,对应list操作 125 | 126 | ``` 127 | $ kubectl get nodes 128 | NAME STATUS ROLES AGE VERSION 129 | 10.8.1.75 Ready 23d v1.11.0 130 | 10.8.1.76 Ready 23d v1.11.0 131 | ``` 132 | 133 | 如果获取其他的资源比如 Pods,就会失败 134 | 135 | ``` 136 | $ kubectl get pods 137 | Error from server (Forbidden): pods is forbidden: User "jemy" cannot list pods in the namespace "default" 138 | ``` 139 | 140 | 在上面的例子中,基本介绍了普通用户在K8S中的使用方法,这里有个小的问题,如果我们改变这个用户名字,需要同步集群和客户端的哪些信息呢? 141 | 142 | 1. 改变 /etc/kubernetes/users.csv 中的用户名 jemy 为 jemygraw,并scp到各个API Server的节点 143 | 2. 重启APIServer服务器 144 | 3. 修改客户端~/.kube/config 里面定义users 的地方,把里面的 username 改为 jemygraw 145 | 4. 在集群中使用 kubectl edit clusterrolebinding cluster-admin-for-jemy,把里面定义subjects的地方kind:User的节点里面的name改为 jemygraw 即可 146 | 5. 然后客户端使用kubectl get nodes 就可以了 147 | 148 | 这个过程可以自行验证下,顺便再熟悉下上面的操作流程。 149 | 150 | 参考文档:https://kubernetes.io/docs/reference/access-authn-authz/authentication/ -------------------------------------------------------------------------------- /Kubernetes/2019-06-12 Kubernetes 中如何开发一个kubectl的插件命令.md: -------------------------------------------------------------------------------- 1 | # Kubernetes 中如何开发一个 kubectl 的插件命令 2 | 3 | ## 背景 4 | 5 | 在日常使用中,Kubectl 作为和 Kubernetes 集群进行交互的工具,提供了丰富的功能。但是偶尔也有时候,你想做一些 Kubectl 暂时还不支持的功能。那么在这种情况下,如何不改变 Kubectl 的代码并且重新编译就能引入新的功能呢? 这个问题的答案就是采用 Kubectl 的 Plugin 机制。 6 | 7 | Kubectl 的 Plugin 机制在 v1.8.0 版本的时候就引入了,并且在 v1.12.0 版本中进行了大规模的重构以适应更加复杂多样的场景,并且最终在 v1.14.0 版本中稳定下来。所以你必须使用 Kubectl v1.12.0 及以上版本才可以支持当前的插件命令。 8 | 9 | ## 插件命令 10 | 11 | 所谓的插件命令其实很简单,只要符合以下几个特点即可: 12 | 13 | (1) 该命令是一个可执行的文件; 14 | (2) 该命令能够通过 `$PATH` 搜索到,也就是说如果需要,你必须把这个命令加入到 `$PATH` 中; 15 | (3) 该命令必须以 `kubectl-` 开头,例如 `kubectl-echo` 就是一个合法的插件命令名称。 16 | 17 | 基于以上的要求,我们可以用任何语言去编写这个命令,比如我们最简单的用 C 语言写一个 `kubectl-hello` 的插件命令尝试下。 18 | 19 | ```c 20 | #include 21 | int 22 | main(int argc, char *argv[]) 23 | { 24 | printf("hello, i am a kubelet plugin command\n"); 25 | } 26 | ``` 27 | 28 | 然后我们编译一下: 29 | 30 | ``` 31 | $ gcc -o kubectl-hello kubectl-hello.c 32 | ``` 33 | 34 | 然后我们把这个命令所在的目录放到系统的 $PATH 变量中,最后通过 kubectl 命令尝试下。 35 | 36 | ``` 37 | $ kubectl hello 38 | hello, i am a kubelet plugin command 39 | ``` 40 | 41 | 通过上面的输出我们可以看到,这个插件命令已经成功完成了,那么剩下来就是利用你熟悉的语言来编写二进制工具来满足你的需求了。 42 | 43 | ## 发现插件 44 | 45 | Kubectl 提供了一个 plugin 的命令,该命令可以使用子命令 list 来列举当前系统中的插件命令。具体的搜索方法如下: 46 | 47 | (1) 搜索系统的 $PATH 中指定的所有的目录,查找所有以 `kubectl-` 开头的文件; 48 | (2) 如果搜索到的匹配以 `kubectl-` 开头的文件是可执行文件,那么会按照顺序作为插件命令输出;如果不是可执行文件,也会输出,但是同时会输出一个 `Warning` 的信息; 49 | 50 | ## 当前限制 51 | 52 | 虽然我们可以自定义插件命令,但是有个限制就是**你无法定义一个 kubectl 已经存在的命令去试图覆盖原命令的行为**。例如 `kubectl-version` 这样的命令永远不会被执行,因为 kubectl 会优先执行内置的 version 命令。基于这样的原因,你也无法给已有的命令增加额外的子命令。 53 | 54 | ## 使用插件 55 | 56 | 插件命令不需要安装,也不需要预加载任何东西。它继承 kubectl 命令的执行环境。kubectl 通过插件命令的名称来执行它。例如对于上面的名为 `kubectl-hello` 的命令,kubectl 就通过 `$ kubectl hello` 来执行它。 57 | 58 | 对于插件命令来讲,它接收到的第一个参数总是它文件所在的全路径。对于上面的 `kubectl-hello` 命令,我们稍作修改,用来打印所有的参数。 59 | 60 | ```c 61 | #include 62 | int 63 | main(int argc, char *argv[]) 64 | { 65 | int i = 0; 66 | printf("hello, i am a kubelet plugin command\n"); 67 | printf("\n"); 68 | for (; i < argc; i++) { 69 | printf("%s\n", argv[i]); 70 | } 71 | } 72 | ``` 73 | 74 | 输出如下: 75 | 76 | ``` 77 | $ kubectl hello kubernetes 78 | hello, i am a kubelet plugin command 79 | 80 | /Users/jemy/Bin/k8s-plugins/kubectl-hello 81 | kubernetes 82 | ``` 83 | 84 | ## 插件命名 85 | 86 | 对于插件的命令,必须了解的两点如下: 87 | 88 | (1) 插件命令支持子命令,其格式必须为 `kubectl-cmd-cmd1-cmd11` ,也就是每个命令通过 `-` 分隔。这样在调用的时候可以使用 `$ kubectl cmd cmd1 cmd11` 这样的方式来调用。 89 | (2) 如果要在插件命令中使用多个单词构成一个命令,那么多个单词必须用 `_` 进行分隔,例如对于 `kubectl-hello_world` 命令,可以通过 `$ kubectl hello_world` 这样的方式来调用。 90 | (3) 插件命令必须自行解析所有传给该命令的选项参数,并进行相应的处理。 91 | 92 | ## 插件管理 93 | 94 | 鉴于 kubernetes 本身并没有提供插件命令的包管理器用来安装和更新插件命令,我们可以使用 Kubernetes-sigs 项目中的 krew 来完成相关工作。 95 | 96 | 97 | 参考文档:[https://kubernetes.io/docs/tasks/extend-kubectl/kubectl-plugins/](https://kubernetes.io/docs/tasks/extend-kubectl/kubectl-plugins/) 98 | 99 | -------------------------------------------------------------------------------- /Kubernetes/2019-06-15 自定义Ingress的默认404页面.md: -------------------------------------------------------------------------------- 1 | # 背景 2 | 3 | 在企业的 Kubernetes 实践中,存在和虚拟机系统共存的情况。有的时候应用部署在虚拟机,有的时候部署到 Kubernetes 集群。在配置完成前端的域名解析和转发之后,如果后端的服务还没有部署,那么很容易出现 404 的情况。Ingress Nginx Controller 的默认的 404 的页面过于简洁,而且当有多副本集群的时候,也没有办法判断是否域名配置已转发到多个集群上面。在这种情况下,我们需要一个内容更加丰富的 404 页面。 4 | 5 | # 方案 6 | 7 | Ingress Nginx Controller 的默认 404 页面响应的后端 Pod 在空间 `ingress-nginx` 下面。有个名称叫做 `default-http-backend` 的 Deploymen,这个是全局的404响应服务。默认情况下,这个服务返回的内容是 `default backend - 404`。有的时候这个响应不太能够帮助我们快速定位问题。 8 | 9 | 所以我们可以自定义这个服务。我们额外写一个http的服务,监听8080的端口,当请求路径为 `/healthz` 健康检查时返回200;其他的情况下返回404,并且给一个较为详细的回复内容。在这个内容里面我们可以给出更加详细的信息,包括命中的集群信息等。 10 | 11 | ## 实施 12 | 13 | 在部署的时候,我们只需要覆盖掉同名的 Deployment 即可,不需要再去创建新的 Service,直接使用原来的 Service 即可。原来的 Service 里面把暴露的 80 端口映射给了我们的后端服务的 8080 端口。 14 | 15 | ``` 16 | ➜ ~ kubectl get svc default-http-backend -n ingress-nginx -o yaml 17 | apiVersion: v1 18 | kind: Service 19 | metadata: 20 | creationTimestamp: 2018-04-06T15:26:59Z 21 | labels: 22 | app: default-http-backend 23 | name: default-http-backend 24 | namespace: ingress-nginx 25 | resourceVersion: "1727455" 26 | selfLink: /api/v1/namespaces/ingress-nginx/services/default-http-backend 27 | uid: f3076744-39ae-11e8-90f4-427775cbe8d8 28 | spec: 29 | clusterIP: 10.1.187.249 30 | ports: 31 | - port: 80 32 | protocol: TCP 33 | targetPort: 8080 34 | selector: 35 | app: default-http-backend 36 | sessionAffinity: None 37 | type: ClusterIP 38 | status: 39 | loadBalancer: {} 40 | ``` 41 | 42 | ## 效果 43 | 44 | ![images/default-backend-404.png](images/default-backend-404.png) 45 | -------------------------------------------------------------------------------- /Kubernetes/2020-12-04 无头(Headless)Service的使用方法.md: -------------------------------------------------------------------------------- 1 | 2 | 一般来讲,Kubernetes里面的Service都有一个同名的Endpoints对象来定义具体的访问端点,这些端点是通过Service中定义的Selector根据Pod的标签选择器来查找指定的一组Pod作为端点的。 3 | 但是有些情况下,我们不要能够在Kubernetes的体系之内访问到外部的服务,这种情况下可以定义个静态的Endpoints对象,把相关的实际地址作为端点,然后还是通过Kubernetes的Ingress来访问。 4 | 5 | 比如后端有一个服务的Nginx配置如下: 6 | 7 | ``` 8 | server { 9 | listen 80; 10 | server_name bbs-static.duokexuetang.com; 11 | root /data0/www/bbs-go/; 12 | } 13 | 14 | server { 15 | listen 80; 16 | server_name bbs.duokexuetang.com; 17 | location / { 18 | proxy_set_header Host $http_host; 19 | proxy_set_header X-Real-IP $remote_addr; 20 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 21 | proxy_pass http://localhost:8081; 22 | } 23 | } 24 | 25 | server { 26 | listen 80; 27 | server_name bbs-api.duokexuetang.com; 28 | location / { 29 | proxy_set_header Host $http_host; 30 | proxy_set_header X-Real-IP $remote_addr; 31 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 32 | proxy_pass http://localhost:8082; 33 | } 34 | } 35 | ``` 36 | 37 | 上面定义了三个服务,一个静态文件,一个前端服务,一个后端接口服务,我们可以在一个Ingress里面把这三个服务都转发到一个Service中。 38 | 39 | ``` 40 | apiVersion: networking.k8s.io/v1beta1 41 | kind: Ingress 42 | metadata: 43 | annotations: 44 | kubernetes.io/ingress.class: kong 45 | labels: 46 | app: cloudbbs 47 | name: dev-cloudbbs 48 | namespace: devops 49 | spec: 50 | rules: 51 | - host: bbs.duokexuetang.com 52 | http: 53 | paths: 54 | - backend: 55 | serviceName: cloudbbs-exp 56 | servicePort: 8080 57 | path: / 58 | - host: bbs-api.duokexuetang.com 59 | http: 60 | paths: 61 | - backend: 62 | serviceName: cloudbbs-exp 63 | servicePort: 8080 64 | path: / 65 | - host: bbs-static.duokexuetang.com 66 | http: 67 | paths: 68 | - backend: 69 | serviceName: cloudbbs-exp 70 | servicePort: 8080 71 | path: / 72 | status: 73 | loadBalancer: 74 | ingress: 75 | - ip: 10.23.0.24 76 | ``` 77 | 78 | 这里我们再定义一个Service对象: 79 | 80 | ``` 81 | apiVersion: v1 82 | kind: Service 83 | metadata: 84 | labels: 85 | app: cloudbbs-exp 86 | name: cloudbbs-exp 87 | namespace: devops 88 | spec: 89 | ports: 90 | - name: http 91 | port: 8080 92 | protocol: TCP 93 | targetPort: 80 94 | type: ClusterIP 95 | ``` 96 | 97 | 需要注意的是,这里的 targetPort 必须是目标服务的实际监听端口,因为要在Endpoints中使用。最后我们再定一个Endpoints应用就可以了。 98 | 99 | ``` 100 | apiVersion: v1 101 | kind: Endpoints 102 | metadata: 103 | labels: 104 | app: cloudbbs 105 | name: cloudbbs-exp 106 | namespace: devops 107 | subsets: 108 | - addresses: 109 | - ip: 10.8.1.127 110 | ports: 111 | - name: http 112 | port: 80 113 | protocol: TCP 114 | ``` 115 | 116 | 以上就是无头(Headless)Service的使用方法。 117 | -------------------------------------------------------------------------------- /Kubernetes/images/default-backend-404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jemygraw/TechDoc/38794deee04bafa5f1510c634f23c4f992762b39/Kubernetes/images/default-backend-404.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TechDoc 2 | 3 | 本项目的所有内容都拥有自主版权。 -------------------------------------------------------------------------------- /ScalaTutorial/README.md: -------------------------------------------------------------------------------- 1 | # 来源 2 | 3 | 教程 http://docs.scala-lang.org/tutorials/ 的中文翻译版本 4 | -------------------------------------------------------------------------------- /ScalaTutorial/default_parameter_values.md: -------------------------------------------------------------------------------- 1 | # 参数默认值 2 | 3 | Scala提供了一个指定参数默认值的功能,可以允许调用方忽略这些拥有默认值的参数。 4 | 5 | ``` 6 | object DefaultParameterValue { 7 | def log(message: String, level: String = "INFO") = println(s"$level: $message") 8 | 9 | def main(args: Array[String]): Unit = { 10 | log("System starting") // 输出 INFO: System starting 11 | log("User not found", "WARNING") //输出 WARNING: User not found 12 | } 13 | } 14 | ``` 15 | 16 | 参数 `level` 有一个默认的值,所以它是可选的。在上面代码的最后一行,参数 `"WARNING"` 覆盖了默认的值 `"INFO"`。在Java中你或许需要使用方法重载来实现这种功能,而在Scala中,你可以使用可选参数来实现相同的效果。然而,当调用方忽略一个参数的时候,剩余的参数必须采用命名参数的方式。 17 | 18 | ``` 19 | class Point(val x: Double = 0, val y: Double = 0) 20 | 21 | val point1 = new Point(y = 1) 22 | ``` 23 | 24 | 这里,我们这样来使用,`y = 1`。 25 | 26 | 注意,当从Java调用Scala方法时,Scala方法中的默认参数不是可选的。 27 | 28 | ``` 29 | // Point.scala 30 | class Point(val x: Double = 0, val y: Double = 0) 31 | 32 | // Main.java 33 | public class Main { 34 | public static void main(String[] args) { 35 | Point point = new Point(1); // 无法编译 36 | } 37 | } 38 | ``` -------------------------------------------------------------------------------- /ScalaTutorial/named_arguments.md: -------------------------------------------------------------------------------- 1 | # 命名参数 2 | 3 | 当调用方法的时候,你可以使用变量名称来标记参数,就像这样: 4 | 5 | ``` 6 | object NamedParameter { 7 | def printName(first: String, last: String): Unit = { 8 | println(first + " " + last) 9 | } 10 | 11 | def main(args: Array[String]): Unit = { 12 | printName("John", "Smith") 13 | printName(first = "John", last = "Smith") 14 | printName(last = "Smith", first = "John") 15 | printName("John", last = "Smith") 16 | } 17 | } 18 | ``` 19 | 20 | 注意命名参数的顺序是可以重组的。不过,如果其中有一些参数是采用了命名的方式,另外一些没有,那么没有命名的参数必须出现在命名参数的前面,而且必须遵循它们在方法声明时的顺序。否则像下面的代码是无法编译的: 21 | 22 | ``` 23 | printName(last = "Smith", "John) 24 | ``` -------------------------------------------------------------------------------- /ScalaTutorial/scala-tutorial/NamedParameter$.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jemygraw/TechDoc/38794deee04bafa5f1510c634f23c4f992762b39/ScalaTutorial/scala-tutorial/NamedParameter$.class -------------------------------------------------------------------------------- /ScalaTutorial/scala-tutorial/NamedParameter.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jemygraw/TechDoc/38794deee04bafa5f1510c634f23c4f992762b39/ScalaTutorial/scala-tutorial/NamedParameter.class -------------------------------------------------------------------------------- /ScalaTutorial/scala-tutorial/default_parameter_value.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by jemy on 31/05/2017. 3 | */ 4 | 5 | class Point(val x: Double = 0, val y: Double = 0) 6 | 7 | val point1 = new Point(y = 1) 8 | 9 | object DefaultParameterValue { 10 | def log(message: String, level: String = "INFO") = println(s"$level: $message") 11 | 12 | def main(args: Array[String]): Unit = { 13 | log("System starting") // 输出 INFO: System starting 14 | log("User not found", "WARNING") //输出 WARNING: User not found 15 | } 16 | } -------------------------------------------------------------------------------- /ScalaTutorial/scala-tutorial/named_parameter.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by jemy on 31/05/2017. 3 | */ 4 | 5 | object NamedParameter { 6 | def printName(first: String, last: String): Unit = { 7 | println(first + " " + last) 8 | } 9 | 10 | def main(args: Array[String]): Unit = { 11 | printName("John", "Smith") 12 | printName(first = "John", last = "Smith") 13 | printName(last = "Smith", first = "John") 14 | printName("John", last = "Smith") 15 | //无法编译 16 | //printName(last = "Smith", "John") 17 | } 18 | } -------------------------------------------------------------------------------- /Translations/2018-09-17 Go并发模型: context.md: -------------------------------------------------------------------------------- 1 | 本文翻译自:https://blog.golang.org/context 2 | 永久地址: 3 | 4 | # 介绍 5 | 6 | 在 Go 服务器中,每个进来的请求都在单独的 goroutine 中处理。请求处理方法一般会启动额外的协程来访问后端服务,例如数据库或者RPC服务。处理请求的这组 goroutine 一般需要能够访问到请求相关的数据,例如终端用户的身份信息,授权凭证,以及请求的超时时间。当一个请求被取消或者超时的时候,所有处理请求的 goroutine 都应该快速退出,这样系统才能够收回它们所使用的资源。 7 | 8 | 在Google,我们开发了一个 context 的包,用来方便传递请求相关的数据,取消信号和截止时间给所有参与到请求处理过程中的 goroutine 。这个库目前在标准库中,名称叫context。本篇文章介绍如何使用这个包并且提供一个完整可运行的示例。 9 | 10 | # Context 11 | 12 | context 包的核心内容是类型 Context: 13 | 14 | ``` 15 | // A Context carries a deadline, cancelation signal, and request-scoped values 16 | // across API boundaries. Its methods are safe for simultaneous use by multiple 17 | // goroutines. 18 | // Context 对象跨 API 携带一个超时时间,取消信号以及请求相关的数据。Context 的方法可以在多个 19 | // goroutine 中同时使用,是并发安全的。 20 | type Context interface { 21 | // Done returns a channel that is closed when this Context is canceled 22 | // or times out. 23 | // Done 返回一个被关闭的 channel,当 Context 取消或者超时的时候 24 | Done() <-chan struct{} 25 | 26 | // Err indicates why this context was canceled, after the Done channel 27 | // is closed. 28 | // Err 描述了 context 取消的原因,在 Done() 返回的 channel 关闭之后 29 | Err() error 30 | 31 | // Deadline returns the time when this Context will be canceled, if any. 32 | // Deadline 返回 Context 将被取消的时间,如果 Context 存在的情况下 33 | Deadline() (deadline time.Time, ok bool) 34 | 35 | // Value returns the value associated with key or nil if none. 36 | // Value 返回这个key所关联的值,或者当key不存在时,返回nil 37 | Value(key interface{}) interface{} 38 | } 39 | ``` 40 | 41 | (上面的内容是精简过的,以 godoc 为准) 42 | 43 | Done() 方法返回一个 channel,用作 Context上 面调用的方法取消的信号。当这个 channel 关闭的时候,这些函数需要放弃执行并且返回。Err() 方法返回一个 error 对象,说明 Context 为什么被取消。 44 | 45 | 在文章 [Pipelines and Cancelation](https://blog.golang.org/pipelines) 中,有详细介绍利用 Done() channel 的模式。 46 | 47 | Context 本身并没有 Cancel 方法,原因和 Done() 返回的 channel 是只读的一样。接收到取消信号的函数一般不是发送信号的那个。尤其是,当一个父操作启动一组 goroutine 来执行子操作的时候,这些子操作不应该能够取消父操作。于是,下面描述的 WithCancel 函数提供了一种取消新的 Context 的方法。 48 | 49 | Context 可以在多个 goroutine 中安全使用。代码中,我们可以将一个 Context 传递给任意多的 goroutine,并且通过取消这个 Context 来取消所有的 goroutine 操作。 50 | 51 | Deadline() 方法允许函数决定它们是否需要执行,如果只剩下很短的时间的话,函数都不值得去执行。代码中也可以使用 Deadline() 返回的值来设置 I/O 操作的超时时间。 52 | 53 | Value() 方法允许 Context 传递请求相关的数据,这个数据必须要能够在所有的 goroutine 中安全地并发使用。 54 | 55 | # 派生Context 56 | 57 | context 包提供了几个函数用来从已有的 Context 对象派生出新的 Context。这些值构成一个Context树,当一个 Context 被取消的时候,所有派生的 Context 都会被取消。 58 | 59 | Background() 返回的 Context 是所有的 Context 树的根,它永远不会被取消。 60 | 61 | ``` 62 | // Background returns a non-nil, empty Context. It is never canceled, has no 63 | // values, and has no deadline. It is typically used by the main function, 64 | // initialization, and tests, and as the top-level Context for incoming 65 | // requests. 66 | // Background 返回一个非nil的,空Context对象。它永远不会被取消,没有值,也没有超时时间。 67 | // 主要在main函数,初始化和测试中使用,作为所有进来的请求的顶层 Context。 68 | func Background() Context { 69 | return background 70 | } 71 | ``` 72 | 73 | WithCancel 和 WithTimeout 返回的 Context 对象可以在父 Context 之前取消。关联到进来的请求的 Context 一般被取消的原因是请求处理方法返回了。WithCancel 在有多副本处理请求的情况下,用来取消多余的请求也很有用。 WithTimeout 在给到后端服务的请求设置超时时间也很有用。 74 | 75 | ``` 76 | // WithCancel returns a copy of parent whose Done channel is closed as soon as 77 | // parent.Done is closed or cancel is called. 78 | // WithCancel 返回一个父 Context 的拷贝,这个新的 Context 的 Done() 返回的 channel 79 | // 在 parent.Done() 关闭或者 cancel 函数被调用的时候被关闭 80 | func WithCancel(parent Context) (ctx Context, cancel CancelFunc) 81 | 82 | // A CancelFunc cancels a Context. 83 | // CancelFunc 取消一个 Context 84 | type CancelFunc func() 85 | 86 | // WithTimeout returns a copy of parent whose Done channel is closed as soon as 87 | // parent.Done is closed, cancel is called, or timeout elapses. The new 88 | // Context's Deadline is the sooner of now+timeout and the parent's deadline, if 89 | // any. If the timer is still running, the cancel function releases its 90 | // resources. 91 | // 92 | func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) 93 | ``` 94 | 95 | WithValue 提供了一种将请求相关数据关联到 Context 的方法: 96 | 97 | ``` 98 | // WithValue returns a copy of parent whose Value method returns val for key. 99 | func WithValue(parent Context, key interface{}, val interface{}) Context 100 | ``` 101 | 102 | # 示例:Google Web 搜索 103 | 104 | # 服务端程序 105 | 106 | -------------------------------------------------------------------------------- /Translations/closer-look-at-go-type-system/snippet-figure1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jemygraw/TechDoc/38794deee04bafa5f1510c634f23c4f992762b39/Translations/closer-look-at-go-type-system/snippet-figure1.png -------------------------------------------------------------------------------- /Translations/closer-look-at-go-type-system/snippet-figure2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jemygraw/TechDoc/38794deee04bafa5f1510c634f23c4f992762b39/Translations/closer-look-at-go-type-system/snippet-figure2.png -------------------------------------------------------------------------------- /Translations/closer-look-at-go-type-system/snippet-figure3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jemygraw/TechDoc/38794deee04bafa5f1510c634f23c4f992762b39/Translations/closer-look-at-go-type-system/snippet-figure3.png --------------------------------------------------------------------------------