forked from snowdreams1006/snowdreams1006.github.io
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsearch_plus_index.json
1 lines (1 loc) · 844 KB
/
search_plus_index.json
1
{"./":{"url":"./","title":"简介","keywords":"","body":"简介 教程分享均是笔者亲身学习经验总结,涉及到的知识点均亲身试验,但有时为了知识完整性,可能并未亲自确认,对于这部分内容会明确指出,到时由读者确认试验. 如果读者实际运行效果和教程演示效果有出入,很可能是版本问题,系统环境等原因,希望能及时反馈,避免更多人踩坑,谢谢! 特点 面向初学者,适合零基础入门; 面向常用操作,技能实用性强; 情景教学,理清事情来龙去脉; 章节重点知识小结,精华集锦; 要求 保持哲学三问,是什么,为什么,怎么样?希望最终能有你自己的答案; 好记性不如烂笔头,亲自动手操作一遍,你会发现你的理解更上一层楼; 授人以鱼不如授人以渔,希望带给你不仅仅是知识更多的是学习的方法; 知识重在分享才有价值,鼓励知识传播与分享,创造收益更有价值; 因本人能力有限,如有出入,敬请指正,请联系我 snowdreams1006 私信 微信公众号 名称 : 雪之梦技术驿站,微信号 : snowdreams1006 关注理由: 开源的不开源的都会发布到微信公众号,不再局限于系列教程而是随性而为,展示真实的技术人生. 个人微信号 名称 : 雪之梦技术驿站,微信号 : snowdreams1109 适用场景: 如果三言两语很难阐释你遇到的问题,如果你是人见人爱的妹子,那么欢迎加我私人微信一起畅谈人生. 说明 本教程源码托管在 snowdreams1006.github.io ,在线访问地址 https://snowdreams1006.github.io/ 或者 https://snowdreams1006.gitbook.io/index/ 如果你觉得本教程对你有所帮助,请不吝 Star. 如果你想贡献一份力量,欢迎提交 Pull Request. © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-05-22 16:48:12 "},"markdown/":{"url":"markdown/","title":"markdown 入门教程","keywords":"","body":"markdown 入门教程 markdown 不止是 HTML 的简化版,更重要的是 txt 的升级版,word 的轻量版,是笔记的最佳载体. markdown 作为一种简单的格式标记语言,不同于 txt 的无格式,不同于 HTML 的复杂标记,也不同于 word 的鼠标调整样式. markdown 通过简单的几个字符键入,就可以快捷的定义文档的样式. 掌握 markdown,你可以完全抛弃 txt 和笔记软件的编辑器,并且在大多数场景下替代掉复杂臃肿的 word.享受简洁之美、享受效率提升. 下面列举了 markdown 语法及对应的示例: 标题 在标题文字前面加#,并且加上空格分割. 一个#是一级标题,两个#是两级标题,以此类推,最多支持六级标题. 示例: # 标题1 ## 标题2 ### 标题3 #### 标题4 ##### 标题5 ###### 标题6 效果: 标题1 标题2 标题3 标题4 标题5 标题6 列表 包括有序列表和无序列表,支持列表嵌套. 有序列表 有序列表就是有顺序的列表,依靠行前的数字加.标记顺序,序号和内容之间以空格 分开. 示例: 1. 有序列表1 2. 有序列表2 3. 有序列表3 效果: 有序列表1 有序列表2 有序列表3 无序列表 无序列表就是列表不排序,支持- * + 3种前缀,可用于表示1级列表,2级列表,3级列表. 示例: - 无序列表1 * 无序列表2 + 无序列表3 效果: 无序列表1 无序列表2 无序列表3 列表嵌套 上一级和下一级列表之间空两个空格 即可表示列表嵌套. 示例: - 无序列表1 * 无序列表11 * 无序列表12 * 无序列表13 + 无序列表131 + 无序列表132 + 无序列表133 - 无序列表2 - 无序列表3 效果: 无序列表1 无序列表11 无序列表12 无序列表13 无序列表131 无序列表132 无序列表133 无序列表2 无序列表3 引用 在引用文字前加一个>即可,支持引用嵌套. 示例: > 引用1 >> 引用11 >>> 引用111 效果: 引用1 引用11 引用111 字体 粗体 要加粗的文字左右两边分别用两个 * 号或者 _ 号包围起来 斜体 要倾斜的文字左右两边分别用一个 * 号或者 _ 号包围起来 粗体+斜体 要加粗并倾斜的文字左右两边分别用三个 * 号或者 _ 号包围起来 删除线 要删除的文字左右两边分别用两个 ~ 号包围起来 示例: **粗体1** __粗体2__ *斜体1* _斜体2_ ***粗体+斜体1*** ___粗体+斜体2___ ~~删除线~~ 效果: 粗体1 粗体2 斜体1 斜体2 粗体+斜体1 粗体+斜体2 删除线 分割线 三个或三个以上的- *即可表示分割线 示例: --- *** 效果: 图片 其中,图片alt表示图片的解释文字,图片src是图片地址,支持本地路径和网络路径,图片title是图片的标题,可选. 示例:  效果: 超链接 超链接text 其中,超链接text表示超链接的解释文字,超链接url支持本地路径和网络路径,超链接title是超链接的标题,可选. 示例: [https://snowdreams1006.github.io](https://snowdreams1006.github.io \"snowdreams1006\") 效果: https://snowdreams1006.github.io 表格 第一行定义表头,单元格内定义标题; 第二行定义样式,单元格内部至少一个-,文字默认居左对齐,单元格内部-两侧均加:表示居中,只有右侧加:表示居右对齐; 第三行定义数据; 示例: |默认居左|文字居中|文字居右| |-|:-:|-:| |居左对齐1|居中对齐1|居右对齐1| |居左对齐2|居中对齐2|居右对齐2| |居左对齐3|居中对齐3|居右对齐3| 效果: 默认居左 文字居中 文字居右 居左对齐1 居中对齐1 居右对齐1 居左对齐2 居中对齐2 居右对齐2 居左对齐3 居中对齐3 居右对齐3 代码 单行代码 代码两侧分别用一个反引号包围起来 示例: `code` 效果: code 多行代码 代码块首尾分别用三个反引号包围起来,且两边的反引号独占一行 示例: (```) function fun(){ echo \"这是一句非常牛逼的代码\"; } fun(); (```) 注:为了防止转译,前后三个反引号处加了小括号,实际是没有的. 效果: function fun(){ echo \"这是一句非常牛逼的代码\"; } fun(); 注释 示例: 效果: 看不到注释就对了! 都学会了吗? 那考考你,你猜当前文档是如何书写的,看看你的答案和我实际书写规则是否一致呢! 答案请参考snowdreams1006.github.io 参考文献 https://daringfireball.net/projects/markdown/syntax http://www.markdown.cn/ https://www.appinn.com/markdown/index.html © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:09:57 "},"markdown/juejin.html":{"url":"markdown/juejin.html","title":"掘金入门指南","keywords":"","body":"掘金入门指南 用掘金-Markdown 编辑器写文章 欢迎使用 掘金-Markdown 编辑器撰写技术文章,只专注于内容和技术,不再费心排版的问题。这是一份简要的 Markdown 引导指南,希望可以帮助您顺利的开始使用 Markdown 编辑器。 丰富的快捷键 本 Markdown 编辑器支持丰富的格式快捷键,可以非常便捷、轻松的使用 Markdown 语言,形成优美的排版和内容格式。 支持的快捷键有: 加粗: Ctrl/Cmd + B 标题: Ctrl/Cmd + H 插入链接: Ctrl/Cmd + K 插入代码: Ctrl/Cmd + Shift + C 行内代码: Ctrl/Cmd + Shift + K 插入图片: Ctrl/Cmd + Shift + I 无序列表: Ctrl/Cmd + Shift + L 撤销: Ctrl/Cmd + Z 常用语法 标题 语法格式: '#'+'空格'+'文本' 一级标题 二级标题 三级标题 四级标题 五级标题 六级标题 列表 无序列表语法格式: '-' + '空格' + '文本' 文本一 文本二 文本三 有序列表语法格式: '数字' + '.' + '空格' + '文本' 文本一 文本二 文本三 任务列表语法格式: '-' + '空格' + '[ ]' + '文本' [x] 文本一 [ ] 文本二 [ ] 文本三 链接和图片 在 Markdown 中插入链接不需要其他按钮,你只需要使用[显示文本](链接地址)这样的格式语法即可。例如: 稀土掘金 插入图片的语法与插入链接的语法很像,只是前面多了一个 !.语法如下:  引用 语法: '>'+'空格'+'文本' 例如: Markdown 是一种轻量级标记语言,它允许人们使用易读易写的纯文本格式编写文档,然后转换成格式丰富的HTML页面。 代码 如下是代码段的语法: ```编程语言 这是代码段 ``` 例如: def bubbleSort(alist): for passnum in range(len(alist)-1,0,-1): #print alist,passnum for i in range(passnum): if alist[i]>alist[i+1]: temp = alist[i] alist[i] = alist[i+1] alist[i+1] = temp return alist 表格 Markdown Extra 表格语法: 项目 价格 iPhone $560 iPad $780 iMac $1000 可以使用冒号来定义对齐方式: 项目 价格 数量 iPhone 6000 元 5 iPad 3800 元 12 iMac 10000 元 234 结语 以上是最常见的 Markdown 的语法和格式,如果你还希望深入的学习 Markdown,可以参考这里Markdown语法,非常感谢使用 掘金-Markdown 编辑器,希望为您提供舒适的写作体验。 © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:09:58 "},"markdown/imooc.html":{"url":"markdown/imooc.html","title":"慕课网语法演示","keywords":"","body":"慕课网语法演示 欢迎使用慕课网 - Markdown 编辑器 Markdown 编辑器使用一套简单实用的标记语言来实现简单的文本排版,可以让你专注于键盘码字而非排版,化繁为简,回归写作本质,带来前所未有的书写体验! 我们在工具栏提供了丰富的快捷键,可以使用它们标记不同的标题,将一些文字标记为粗体或者斜体,也可以创建一个链接或者插入一张很有气质的图片。如需了解更多语法请使用快捷键“Ctrl + /”查看帮助。 常用语法使用说明 标题 规范的语法格式:“#+空格+文本” 举例如下: 一级标题 二级标题 三级标题 四级标题 五级标题 六级标题 引用 规范的语法格式:“>+空格+文本” 举例如下: 我是一段被引用的文本,请熟记它的语法格式 图片 规范的语法格式:“” 我们推荐你是用快捷工具来添加图片 超链接 规范的语法格式:“[链接描述](链接地址)” 举例如下: 欢迎使用慕课网手记 无序列表 规范的语法格式:“- + 空格 + 文本” 文本一 文本二 文本三 有序列表 规范的语法格式:“数字 + 空格 + 文本” 文本一 文本二 文本三 代码 规范的语法格式: ```代码语言(填写了代码语言才会显示代码高亮) 在这里输入代码 ``` 举例如下: def quick_sort(qlist): if qlist == []: return [] else: qfirst = qlist[0] qless = quick_sort([l for l in qlist[1:] if l = qfirst]) return qless + [qfirst] + qmore qlist = quick_sort([4,5,6,7,3,2,6,9,8]) print qlist 表格 规范的语法格式如下: 姓名 身高 体重 小明 175CM 75KG 小李 168CM 70KG 小张 185CM 80KG 使用冒号设置表格内容对齐方式: 品牌 价格 售出 奥迪Q7 900000 元 5000辆 大众GOLF6 150000 元 70000辆 哈佛M6 70000 元 200000辆 感谢阅读慕课网 - Markdown 编辑器使用说明。马上开始前所未有的编辑体验吧。 © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:09:58 "},"markdown/HBuilderX.html":{"url":"markdown/HBuilderX.html","title":"HBuilderX语法帮助","keywords":"","body":"HBuilderX语法帮助 markdown - 更简洁、更高效 ================================================= 强烈建议开发者认真阅读本文档,掌握md及HBuilderX对md的强大支持。 如果没有点右键设置自动换行,可按Alt+滚轮横向滚动查看。 很多人只把markdown用于网络文章发表,这糟蹋了markdown。 markdown不止是HTML的简化版,更重要的是txt的升级版、word的轻量版、笔记的最佳载体。 作为一种简单的格式标记语言,不同于txt的无格式,不同于HTML的复杂标记,也不同于word的鼠标调整样式。markdown通过简单的几个字符键入,就可以快捷的定义文档的样式。 比如在行首敲一个“#”,就把这行定义为了1级标题,并且在HBuilderX里有直观完善的着色,这样无需发布为web页面,可直接当word用。 掌握markdown,你可以完全抛弃txt和笔记软件的编辑器,并且在大多数场景下替代掉复杂臃肿的word。享受简洁之美、享受效率提升。 而HBuilderX,可以被称为最强大的markdown书写工具了。 下面的示例列举了markdown语法及对应的HBuilderX使用技巧: 开始前,可以先按下文档结构图的快捷键Alt+w(Mac是Ctrl+w),浏览本文的大纲。 标题语法 markdown的标题是行首以#号开头,空格分割的,不同级别的标题,在HX里着色也不同。如下: 标题1 标题2 标题3 标题4 标题5 标题6 标题使用技巧: Emmet快速输入:敲h2+Tab即可生成二级标题【同HTML里的emmet写法,不止标题,HX里所有可对应tag的markdown语法均支持emmet写法】。仅行首生效 智能双击:双击#号可选中整个标题段落 智能回车:行尾回车或行中Ctrl+Enter强制换行后会自动在下一行补#。而连续2次回车后将自动补的#去掉。(体验同word) 回车后再次按Tab可递进一层标题,再按Tab切换列表符 在# 后回车,可上插一个空标题行【同word】,或任意位置按Ctrl+Shift+Enter也可以上插空标题行 折叠: 点标题前的-号可折叠该标题段落,快捷键是Alt+-(展开折叠是Alt+=) 多层折叠时折叠或展开子节点,快捷键是Alt+Shift+-或= 全文全部折叠或展开,快捷键是Ctrl+Alt+Shift+-或= 折叠其他区域,快捷键是Alt+Shift+o。这对长文档管理非常有用,可以专注于当前章节 可以在菜单-跳转-折叠中随时找到这些功能 列表 markdown的列表支持有序列表、无序列表以及特殊的任务列表。 同样也是在行前加一个特殊符号,并空格后再跟列表文字内容。 有序列表 有序列表就是有顺序的列表,依靠行前的数字标记顺序。 有序列表1 【设置或取消有序列表符的快捷键:Ctrl+Alt+1,可选中多行批量设置序号;支持多光标批量设置列表符,即按Ctrl+鼠标左键添加多光标】 有序列表2 【列表后回车会自动补序号】 有序列表3 【智能双击:双击前面的数字,可重新对数字排序,修正序号错误,并选中有序列表段落(左边的4是故意写错让你体验的)】 无序列表 无序列表就是列表不排序,无序列表因书写随意而被更广泛的使用。 无序列表有3种前缀,HX里分别用于表示1级列表、2级列表、3级列表。 无序列表1 【快捷键:Ctrl+Alt+-;智能双击:双击-号可选中整段无序列表;再次按Tab会更换二级列表符】 无序列表2 Emmet:li后敲Tab可生成*号列表符,行首生效 快捷键:Ctrl+Alt+8【8即*对应的数字】,支持多光标批量设置列表符,即按Ctrl+鼠标左键添加多光标 智能双击:双击*号可选中整段无序列表 智能回车:行尾回车或行中Ctrl+Enter强制换行后会自动续列表;连续按回车会清除列表符;再次按Tab会更换列表符;在列表符后回车或行尾Shift+回车,上一行留出列表符 *号常用于二级列表,列表符后继续Tab,可切换列表符 无序列表3 【快捷键:Ctrl+Alt+=;常用于三级列表;其他同上】 任务列表 任务列表非常实用,管理待办已办非常便利。 [ ] 任务列表-未完成任务 【快捷键:Ctrl+Alt+[】 [x] 任务列表-已完成任务 【快捷键:Ctrl+Alt+]】 1. 智能双击:双击方括号内可切换勾选状态,把任务标记为完成或未完成;双击方括号右侧可选中任务列表段落 2. 智能回车:回车后自动补任务列表前缀符号;连续按回车清除前缀符号;在列表符后回车或行尾Shift+回车,上一行留出列表符 以上三种列表,均支持批量修改列表符,有如下方式建议依次学习尝试: 选中多行,按快捷键Ctrl+Alt+“1”或“-”或“[”或“]”,批量设置列表符 如果需要跳行设置有序或无序列表,通过Ctrl+鼠标左键点中目标多行(可不连续),产生多光标,然后按快捷键Ctrl+Alt+“1”或“-”或“[”或“]”,可跳行设置列表符,尤其是有序列表,数字也会跳行加1 按Alt+鼠标选中行首那列(列选择),这样每行行首都有光标,然后再键入或删除列表符即可批量操作 选中多行,按快捷键Ctrl+Shift+\\(其实就是Ctrl+|),可以在每行行首添加一个光标 引用列表 引用1 引用2 快捷键:Ctrl+Alt+Shift+. 智能双击:双击>号可选中整段引用列表 智能回车:行尾回车或行中Ctrl+Enter强制换行后会自动续列表;连续按回车会清除列表符;在列表符后回车或行尾Shift+回车,上一行留出列表符 文字样式语法 加粗 【快捷键:Ctrl+B,支持多光标;Emmet:b后敲Tab】 加粗2 倾斜【Emmet:i后敲Tab;前后包围:选中文字按Ctrl+\\是在选区两侧添加光标,可以继续输入】 倾斜 删除线 单行代码 包围插入:先选中文字内容,然后按*~`等符号,会自动在2侧加包围 智能双击:双击语法区前面的定义符号,选中包含定义符的整段文字 去包围:选中整段文字后,按Ctrl+Shift+],可去除2侧包围符号 引号括号虽然不属于markdown语法,但也支持相同的包围、选择、去包围操作。 引号括号智能双击选择时略特殊的是:双击引号括号内侧,选中引号括号里的内容(不含引号括号);按下Alt+双击引号括号内侧,则选中包含符号的整段文字 HBuilderX还支持以下对2侧文本高效处理的手段 选中文字按Ctrl+\\是在选区两侧添加光标,可以继续输入~~,会在2侧同时输入 向2侧扩大选择:【Win:Alt+Shit+→ 、Mac:Ctrl++Shit+→】;由2侧向内减少选择:【Win:Alt+Shit+← 、Mac:Ctrl++Shit+←】 链接文字 Emmet:a后敲Tab 打开链接:Alt+鼠标单击;如果是本地文件,可通过Shift+Alt+单击,在另一分栏打开文件 智能粘贴:粘贴URL会自动变成超链接格式;粘贴本地文件进来也会自动创建引用链接 智能双击:双击语法区开头,即[左侧,选中包含定义符的整段文字 Emmet:img后敲Tab 智能粘贴:粘贴剪切板里的图形时会自动保存为本md文档的附件;删除文档中的图片语法,保存md文档时会自动删除对应的图片附件;粘贴图片文件时自动变成链接引用格式; 悬浮预览:鼠标移到图片语法上,本地图片会自动显示出来 智能双击:双击语法区开头,即!左侧,选中包含定义符的整段文字 表格 Emmet:table3*3后敲Tab,表示生成3行3列的表格,行首生效 md表格对齐是传统md的痛点,HBuilderX按下Ctrl+K可以自动整理表格格式(暂未兼容不同缩放模式和字体的情况) 支持从excel、wps、word、number的表格中复制粘贴表格进来(不支持合并单元格和单元格换行) 分割线 ------------- 【Emmet:hr后敲Tab】 * 代码区 var a = document Emmet:code后敲Tab,行首生效 智能双击:双击语法区开头,即!左侧,选中包含定义符的整段文字 注释 快捷键:Ctrl+/ 智能双击:双击注释首尾的定义符,选中整段注释 其他emmet快捷输入 day后敲Tab,当前日期。注意day需在行首或前面有空格 time后敲Tab,当前时间。注意time需在行首或前面有空格 文档结构图 文章很长时,word里有文档结构图,HBuilderX也有。 菜单视图-文档结构图,快捷键Alt+W(mac是ctrl+W),轻松管理长文档 运行、预览和打印PDF 对md文件点工具栏或菜单里的浏览器运行,可以使用外部浏览器预览此md文件,会自动渲染为HTML。 点右上角的预览【快捷键Alt+p】,可在HBuilderX右侧预览该md文档的HTML渲染结果。 在浏览器中点打印,选择打印到PDF,可将md输出为PDF格式。(注意在打印选项里去掉页眉页脚) 其他常用但你可能不知道的快捷操作技巧 Ctrl+鼠标左键添加多光标,然后敲字或粘贴,可批量处理。Ctrl+鼠标左键拖选,可选中多个选区。 Ctrl+鼠标右键删除多光标 不选内容按Ctrl+C或X可复制或剪切整行 选中2个选区后,按Ctrl+Shift+X,可互换选区内容。如无选区,只是2个光标,则互换2行 Ctrl+上下键可上下移动行 Ctrl+Insert可重复插入当前行,如果有选中内容,可重复插入选中内容 Ctrl+Shift+K可合并多行(是格式化Ctrl+K的反操作) 删除 按Ctrl+D可删除选中行,支持多光标 Shift+Del删除到行尾 Shift+Backspace删除到行首 选择 Ctrl+E选相同词(mac是Command+D),连续按可选中多词进一步操作,比替换更方便 Ctrl+L可连选多行,Ctrl+Shift+L也是选择行,但不选行首尾的空白字符 Ctrl+=可逐级放大选区 双击标题、列表符可选中相应段落 双击英文引号、括号内侧,可选中内部内容 双击缩进符,可选中同缩进段落 双击连字符比如-或_,可选中相连的词,比如双击这里试试,uni-app 查找 Ctrl+P查找文件 Ctrl+Alt+F可在当前目录的所有文档中搜索指定关键字(mac是Command+Shift+f) 选中文字按F3,查找下一个,Shift+F3找上一个 云同步:HBuilderX+markdown用于云同步笔记的技巧,请参考http://ask.dcloud.net.cn/article/13097 都学会了吗? markdown语法其实很简单,认真学半小时就能掌握。 HBuilderX的极客操作则需要不停反复练习,熟练掌握这些技巧,你将成为高效极客! © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:09:57 "},"markdown/csdn.html":{"url":"markdown/csdn.html","title":"csdn帮助文档","keywords":"","body":"csdn帮助文档 快捷键 撤销:Ctrl/Command + Z 重做:Ctrl/Command + Y 加粗:Ctrl/Command + B 斜体:Ctrl/Command + I 标题:Ctrl/Command + Shift + H 无序列表:Ctrl/Command + Shift + U 有序列表:Ctrl/Command + Shift + O 检查列表:Ctrl/Command + Shift + C 插入代码:Ctrl/Command + Shift + K 插入链接:Ctrl/Command + Shift + L 插入图片:Ctrl/Command + Shift + G 标题 1级标题 2级标题 3级标题 四级标题 五级标题 六级标题 文本样式 强调文本 强调文本 加粗文本 加粗文本 ==标记文本== 删除文本 引用文本 H~2~O is是液体。 2^10^ 运算结果是 1024。 列表 项目 项目 项目 项目1 项目2 项目3 [ ] 计划任务 [x] 完成任务 链接 链接: https://snowdreams1006.github.io. 图片: 代码片 下面展示一些 内联代码片。 // A code block var foo = 'bar'; // An highlighted block var foo = 'bar'; 表格 项目 Value 电脑 $1600 手机 $12 导管 $1 Column 1 Column 2 centered 文本居中 right-aligned 文本居右 自定义列表 Markdown : Text-to-HTML conversion tool Authors : John : Luke 注脚 一个具有注脚的文本。1 1. 注脚的解释 ↩ 注释 Markdown将文本转换为 HTML。 *[HTML]: 超文本标记语言 LaTeX 数学公式 Gamma公式展示 $\\Gamma(n) = (n-1)!\\quad\\forall n\\in\\mathbb N$ 是通过 Euler integral $$ \\Gamma(z) = \\int_0^\\infty t^{z-1}e^{-t}dt\\,. $$ 插入甘特图 gantt dateFormat YYYY-MM-DD title Adding GANTT diagram functionality to mermaid section 现有任务 已完成 :done, des1, 2014-01-06,2014-01-08 进行中 :active, des2, 2014-01-09, 3d 计划中 : des3, after des2, 5d 插入UML图 sequenceDiagram 张三 ->> 李四: 你好!李四, 最近怎么样? 李四-->>王五: 你最近怎么样,王五? 李四--x 张三: 我很好,谢谢! 李四-x 王五: 我很好,谢谢! Note right of 王五: 李四想了很长时间, 文字太长了不适合放在一行. 李四-->>张三: 打量着王五... 张三->>王五: 很好... 王五, 你怎么样? 插入Mermaid流程图 graph LR A[长方形] -- 链接 --> B((圆)) A --> C(圆角长方形) B --> D{菱形} C --> D 插入Flowchart流程图 flowchat st=>start: 开始 e=>end: 结束 op=>operation: 我的操作 cond=>condition: 确认? st->op->cond cond(yes)->e cond(no)->op © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:09:58 "},"git/":{"url":"git/","title":"git 入门教程","keywords":"","body":"git 入门教程 git 是分布式版本控制系统,是文本文档管理的利器,是帮助你管理文件动态的好帮手. 如果你曾经手动管理过文档,一定有这样的经历,比如你正在编辑文档,想删除某段落,又担心不久后可能会恢复,此时你可能会先备份然后再删除,或者想要修改某段落,几经修改后发现还是最初的比较好,这是就哭笑不得了... 从最初的新建文档,经过反反复复的修改,最终定稿文档的过程极其繁琐冗长,这就是手动式管理文档的痛点. 如果有这么一种工具,能帮我自动记录每次文档的改动,想要查看文档变更详情只需要打开软件就能一目了然告诉我发生了哪些改变?岂不美哉! 版本 文件 用户 说明 时间 1 README.md snowdreams1006 初始化简介文档 2019-03-01 08:00 2 README.md snowdreams1006 增加特点说明 2019-03-01 10:00 3 README.md snowdreams1006 增加要求说明 2019-03-01 12:00 事实上,还真有这样的软件,专业术语称为版本控制系统,而git就是最先进的分布式版本控制系统; 特点: 文件的变更从此有迹可循,再也不怕丢失文件; 有网无网均可工作,数据交换不需再相互拷贝; 人人平等的开放环境,有机会贡献自己的智慧; 本书发表在 https://snowdreams1006.github.io/git/ © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:08:04 "},"git/base/about.html":{"url":"git/base/about.html","title":"初识 git","keywords":"","body":"初识 git git 是一个开源的分布式版本控制系统,用于敏捷高效地处理任何或小或大的项目. 背景 我们都知道,Linus 在1991年创建了开源的linux系统,随着不断发展壮大,目前已发展成为最大的服务器系统软件. Linus 虽然创建了 linux,但 linux 的发展壮大是靠全世界热心的志愿者参与贡献的,这么多人在世界各地为linux系统编写代码,那么linux的代码是如何管理呢? 事实上,在2002年以前,世界各地的志愿者直接将源代码通过 diff 的方式发送给Linus,然后由Linus本人通过手动方式合并代码! ... Linus花了两周时间自己用 C语言 写了一个分布式版本控制系统,这就是Git! 一个月之内,Linux系统的源码已经由Git管理了!牛是怎么定义的呢?大家可以体会一下. 分布式 和 集中式 先说集中式版本控制系统,版本库是集中存放在专门的中央服务器中,而平时使用过程中需要时刻处于联网状态才能和中央服务器保持联系.日常工作流程是这样的,上班前先从中央服务器拉取最新工作内容,本地修改完毕后推送到中央服务器,第二天上班再拉取最新内容,修改后再推送给中央服务器... 集中式版本控制系统的特点就是必须要有一个专门的中央服务器,工作中必须联网才能进行版本控制,试想一下如果正在在外地出差或者没有网络条件下,还怎么进行版本控制,岂不是又重新回到原始时代了吗? 那再说说分布式版本控制系统,版本库是存放在各自使用者的电脑的,不需要专门的中央服务器,每个人电脑中就是一份完整的版本库,因此不需要联网也能工作,工作流程和其他的版本控制系统大致相同. 由此可见,集中式的版本控制系统依赖于中央服务器,要求使用者一直保持通信,而分布式的版本控制系统并不依赖中央服务器,不必强制联网. 万一出现意外,集中式版本控制系统中充当中央服务器的电脑宕机了,那么所有人就没法工作了,再也不能享受版本控制带来的便利了! 同样的情况发生在分布式版本控制系统身上会如何呢?一台电脑宕机没关系,所有人的电脑不可能同时都宕机吧,因为每个人电脑中都是一份完整的版本控制,那么找到其中一个人的版本手动复制到宕机电脑中瞬间不久恢复运行了么?所以说分布式比集中式更安全! 可能会有疑问了,既然分布式版本控制系统中每个人都拥有完整的版本库,那么两个人到底如何交流以谁的版本为准呢?一个版本,两个版本还好,假设有100个版本库呢? 实际上,这并不重要,假设有100个人在合作开发一个项目,而你作为项目负责人,你可能并不关心100人的全部工作细节,在乎的只是最终成果,而这些成果是由10个项目组长提交维护的,所以你关心的只是10个版本,假设没有集中式的中央服务器角色,那么你需要手动合并10个版本库,最终完成项目. 这样看起来中央服务器确实还是有存在的必要,为了方便不同版本库之间进行交流,通常分布式版本控制系统也有一台充当中央服务器角色的电脑,需要理解的是,此时中央服务器的作用仅仅是方便大家交换各自的修改而已,没有它,大家还是可以照常工作的,只是彼此间交换修改不太方便而已! 不论是分布式还是集中式,存在即合理,如何取舍有着各自应用场景,分别代表民主和专制. git 和 svn git 是分布式版本控制系统的代表,除此之外还有BitKeeper,Mercurial,Bazaar 等分布式控制系统,每种分布式控制系统均有自身特点,毋容置疑的是git是最简单最流行! svn 是集中式版本控制系统的代表,是目前使用最广泛的集中式版本控制系统,cvs ClearCase等均属于集中式. 不论是分布式还是集中式,不论是免费还是收费,不一昧追求最好的,只需要最适合自己的即可. git 是分布式控制系统,svn 是集中式版本控制系统 git 将内容按元数据方式存储,svn 是按文件方式存储 git 的内容完整性优于svn,因为 git 内容存储基于sha-1哈希算法,确保内容的完整性. 小结 git 是Linus为了帮助管理 Linux 内核开发而开发的一个开放源码的版本控制软件. © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:08:04 "},"git/base/install.html":{"url":"git/base/install.html","title":"安装 git","keywords":"","body":"安装 git git 目前支持 Linux/Unix、Solaris、Mac和 Windows 平台上运行,根据自身环境选择安装. Linux 系统 linux 系统安装软件大致有两种途径,一种是利用安装包管理工具安装,另一种采用源码包安装方式. 安装前先确认下是否之前已安装过,在命令行窗口输入git --version ,如果打印出版本号则表示已安装,否则参考一下内容进行安装. 查看 git 版本 git --version Debian/Ubuntu # 安装 git 依赖 apt-get install libcurl4-gnutls-dev libexpat1-dev gettext \\ libz-dev libssl-dev # 安装 git apt-get install git # 查看 git 版本 git --version Centos/RedHat # 安装 git 依赖 yum install curl-devel expat-devel gettext-devel \\ openssl-devel zlib-devel # 安装 git yum -y install git # 查看 git 版本 git --version git-core 和 git 历史渊源: 以前有个软件也叫GIT(GNU Interactive Tools),所以git只能叫git-core了,后来由于git名气实在太大以至于GNU Interactive Tools改名成gnuit,而git-core正式改为git. 源码安装 先从git 官网下载指定版本源码,然后解压,依次输入:./config,make, sudo make install 这几个命令安装到指定目录即可. Debian/Ubuntu # 安装 git 相关依赖 apt-get install libcurl4-gnutls-dev libexpat1-dev gettext \\ libz-dev libssl-dev # 下载指定版本源码包 wget https://github.com/git/git/archive/v2.21.0.tar.gz # 解压 tar -zxf v2.21.0.tar.gz # 切换到 git目录 cd git-2.21.0 # 安装 make prefix=/usr/local all # 安装 sudo make prefix=/usr/local install Centos/RedHat # 安装 git 相关依赖 yum install curl-devel expat-devel gettext-devel \\ openssl-devel zlib-devel # 解压 tar -zxf v2.21.0.tar.gz # 切换到 git目录 cd git-2.21.0 # 安装 make prefix=/usr/local all # 安装 sudo make prefix=/usr/local install Windows 系统 直接从git 官网下载安装程序,然后按默认选项安装即可. 安装完成后,在开始菜单里找到Git->Git Bash,弹出命令行窗口,则说明安装成功! Mac 系统 一般有两种安装方式,一种是利用 mac 的homebrew管理工具安装git,具体安装方法参考homebrew官方文档 另一种方法安装xcode默认集成git,首先从 App Store下载 xcode ,下载完成后运行Xcode,选择菜单Xcode->Preferences,在弹出窗口中找到Downloads,选择Command Line Tools,点Install就可以完成安装了 © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:08:04 "},"git/base/config.html":{"url":"git/base/config.html","title":"配置 git","keywords":"","body":"配置 git 安装完成后,还需要最后一步配置就可以愉快使用了,在命令行输入: git config --global user.name \"your username\" git config --global user.email \"[email protected]\" 因为Git是分布式版本控制系统,所以每个机器都必须自报家门:你的名字和Email地址. 配置文件 git 提供git config工具,专门用来配置相应的工作环境变量,支持三种不同的位置. /etc/gitconfig 配置文件 (优先级最低) 系统中对所有用户都生效的配置,效果等同于git config --system ~/.gitconfig 配置文件 (优先级其次) 系统中仅仅对当前登录用户生效的配置,效果等同于git config --global $(pwd)/.git/config 配置文件 (优先级最高) 仅仅对当前项目生效,效果等同于git config 每一级别的配置都会自动覆盖上级相同配置,当前项目配置优先于其余配置 查看配置 如果要查看已有的配置信息,可以输入 git config --list 命令,如果看到重复变量名,表示来自不同配置文件(比如/etc/gitconfig 和 ~/.gitconfig),实际上git会采用最后一个! # 查看已有配置信息 git config --list # 查看当前用户配置信息 cat ~/.gitconfig # 查看系统级别配置信息 cat /etc/gitconfig 也可以直接查看某项环境变量值,比如 # 查看用户名称变量 git config user.name © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:08:04 "},"git/usage/about.html":{"url":"git/usage/about.html","title":"实战 git","keywords":"","body":"实战 git git 是一款分布式版本控制系统,可以简单概括: 不要把鸡蛋放在一个篮子里,你的一举一动都在监视中. 实战场景 你作为某项目的其中一员或者负责人,和小伙伴们一起开发,大家既有着各自分工互不干扰,也有着相互合作,最终每个人的劳动成果汇聚成最后的项目,愉快完成项目! 要求 理解 git 的工作流程,懂得实际工作中如何交流合作 掌握 git 常用操作,工具为我所有,进而提高工作效率 独当一面,最好能够独自解决使用git 过程中遇到的问题 主动分享经验,能够教会别人如何使用 git 更上一层楼 推荐 最好的教程在官网 git 官网 在线练习常用操作 Learning Git Branching 廖雪峰的官方网站 git教程 © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:07:59 "},"git/usage/local-repository.html":{"url":"git/usage/local-repository.html","title":"本地仓库","keywords":"","body":"本地仓库 背景 创建工作目录 平时工作时我们习惯对文档分门别类进行管理,.doc .txt 等文本类型的文件习惯存在 doc文件下,开发java js 等源代码文件存在在 src 目录下,这一点很好理解,那么讲解 git的项目我们也要创建一个文件夹,姑且新建一个demo的文件夹吧! # 在工作空间创建指定目录 mkdir demo # 切换至工作目录 cd demo 创建本地仓库 既然已经创建了工作文件夹,那么我们自然是希望该文件下的所有文件都能被 git 管理,也就是说在当前文件下的创建新文件,修改原文件内容或者删除文件等操作都能纳入版本控制中,不然为什么要用git 呢? 下面这个命令就是告诉git 这个 demo 目录要纳入版本控制了. # 初始化本地仓库 git init 一旦运行git init 命令,细心的读者可能会发现在原来的 demo 目录下多了.git隐藏文件,正因如此,原来被我们称为工作目录的 demo 才能纳入版本控制,我们将.git目录称之为版本库. 由于当前项目 demo 只在我们自己电脑上,其他人无法访问,所以我们称这种形式的版本库为本地仓库. 添加文件到版本库 首先明确的是,所有的版本控制系统只能追踪文本文件的改动,文本文件就是平常熟悉的.txt .html .js .css .java .xml等等文件,非文本文件的其他格式有哪些? 例如二进制文件,像我们平时听音乐的.mp3,看视频的.mp4,浏览图片的.png等这些都是二进制文件,需要专门的软件才能正常打开,不信的话,你用记事本看看能不能打开视频? 了解文本文件和二进制文件的区别,那是不是说二进制文件没法进行版本控制了,刚才你不是还说demo 目录下的所有文件吗?这不是自相矛盾吗! 非也非也,git 当然也能够管理二进制文件,对于文本文件的追踪,可以细粒度到哪个文件在哪一行发生了哪些变化,而二进制文件只能粗粒度知道哪个文件变化了,并不知道具体变化. 不幸的是,Microsoft 的Word格式是二进制格式,因此,版本控制系统是没法跟踪Word文件的改动的,前面我们举的例子只是为了演示,如果要真正使用版本控制系统,就要以纯文本方式编写文件. 因为文本是有编码的,比如中文有常用的GBK编码,日文有Shift_JIS编码,如果没有历史遗留问题,强烈建议使用标准的UTF-8编码,所有语言使用同一种编码,既没有冲突,又被所有平台所支持. 言归正传,现在我们在demo 目录下创建一个test.txt 演示文件,内容如下git test # 创建新文件 touch test.txt # 编辑新文件,输入 git test echo \"git test\" > test.txt 接下来我们还需要两步操作才能将test.txt纳入git管理: 第一步,使用git add 命令将文件添加到本地仓库: # 添加到本地仓库: 第一步指定要添加的文件 git add test.txt 第二步,使用git commit -m 命令将文件提交到本地仓库: # 添加到本地仓库: 第二步指定添加文件备注 git commit -m \"add test.txt\" 经过上述两步操作,test.txt 文件已经纳入到版本控制中了,这里你可能会有疑问了为什么需要add commit两步呢? 因为commit 可以一次性提交很多文件,所以你可以多次add不同的文件,比如: # 创建三个文件file1.txt file2.txt file3.txt touch file1.txt file2.txt file3.txt # 添加一个文件file1.txt git add file1.txt # 添加两个文件file2.txt file3.txt git add file2.txt file3.txt # 一次性提交全部文件 git commit -m \"add 3 files.\" 小结 初始化本地仓库 git init 添加文件到本地仓库分两步 git add 和 git commit -m 实际工作中,大致以下流程 # 在工作空间创建指定目录 mkdir demo # 切换至工作目录 cd demo # 初始化本地仓库 git init # 创建新文件 touch test.txt # 编辑新文件,输入 git test echo \"git test\" > test.txt # 添加到本地仓库: 第一步指定要添加的文件 git add test.txt # 添加到本地仓库: 第二步指定添加文件备注 git commit -m \"add test.txt\" ... # 继续编辑目标文件,追加 git init echo \"git init\" >> test.txt # 将目标文件添加到本地仓库 git add test.txt # 添加本次新增文件的备注 git commit -m \"add git init\" © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:08:01 "},"git/usage/version-manage.html":{"url":"git/usage/version-manage.html","title":"版本管理","keywords":"","body":"版本管理 背景 在上一节中我们已经成功创建版本库并且已经添加test.txt等文件,这一节我们继续讲解如何进行版本控制. 首先我们先查看test.txt 文件有什么内容吧! # 查看文件内容 $ cat test.txt git test git init git diff $ 接下来模拟正常工作,接着输入一下内容: # 追加新内容到 test.txt 文件 echo \"understand how git control version\" >> test.txt # 查看当前文件内容 $ cat test.txt git test git init git diff understand how git control version $ 紧接着运行 git status 看一下输出结果: # 查看文件状态 $ git status On branch master Changes to be committed: (use \"git reset HEAD ...\" to unstage) modified: test.txt Changes not staged for commit: (use \"git add ...\" to update what will be committed) (use \"git checkout -- ...\" to discard changes in working directory) modified: test.txt $ 从上述 git status 命令输出的结果可以看出,test.txt 已经被修改但还没提交,但是具体发生了什么变化却没能告诉我们,如果能够告诉我们具体修改细节那就好了! 运行git diff命令可以实现上述需求 $ git diff diff --git a/test.txt b/test.txt index 729112f..989ce33 100644 --- a/test.txt +++ b/test.txt @@ -1,3 +1,4 @@ git test git init git diff +understand how git control version $ git diff 命令即查看差异(difference),从输出结果可以看出我们在最后一行新增了understand how git control version 文字. 通过git status 知道文件发生了改动,git diff 让我们看到了改动的细节,现在我们提交到版本库就放心多了,还记得上节课如何添加版本库的命令吗? 分两步操作: git add 和 git commit -m 第一步: git add $ git add test.txt $ 等一下,在执行 git commit 命令之前,我们再运行 git status 命令查看一下当前仓库状态: $ git status On branch master Changes to be committed: (use \"git reset HEAD ...\" to unstage) modified: test.txt $ 此时 git status 命令告诉我们 test.txt 文件已被修改等待提交,好了,那么接着第二步的commit吧! 第二步: git commit -m # 提交到版本库并添加备注 $ git commit -m \"add understand how git control version\" [master 36f234a] add understand how git control version 1 file changed, 2 insertions(+) $ 提交后,我们此时再次运行git status 命令查看当前仓库状态: $ git status On branch master nothing to commit, working tree clean $ 输出结果显示没有需要提价的改动,工作目录是干净的. 小结 查看工作区状态 git status 比较修改差异 git diff © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:08:03 "},"git/usage/reset.html":{"url":"git/usage/reset.html","title":"回到过去","keywords":"","body":"回到过去 背景 现在你已经掌握git的基本操作了,文件发生更改首先使用 git add 添加更改,然后 git commit 提交全部更改,当本地文件再次发生更改时,仍然需要git add 和 git commit 两步操作,中途如何想查看文件是否发生更改,使用git status 查看版本库状态,git diff 命令帮助我们查看更改详情. 像这样重复的操作其实每次都会产生一个快照,用于保存文件状态,只不过这个快照不是完整的文件,被称为提交或者版本commit .一旦发生意外,假如文件修改乱了或者误删了文件,我们可以从最近的一个 commit 中进行恢复,然后继续工作,这就是git 管理的好处之一. 每一次重大更新或者你认为比较重要的时刻,我们总会留作纪念,添加些什么特殊标记来区分平时的提交,还记得我们每次提交都会添加备注吗?git commit -m 这条命令现在就可以大显身手了,我们现在要做的就是找到我们提交的历史记录,而历史记录中有我们提交的详情,这样即使过了一个月或者更长时间,我们也能清楚知道当时的情景! 查看提交历史记录 git log,接下来我们赶紧试一下吧 $ git log commit 36f234a60d858871f040cb0d7ca3e78251df82f7 (HEAD -> master) Author: snowdreams1006 Date: Thu Mar 7 22:19:00 2019 +0800 add understand how git control version commit 2006f72ffe2ce2278b5974313b8598847cf445e4 Author: snowdreams1006 Date: Tue Mar 5 13:27:46 2019 +0800 add 3 files. commit eaa4850070354ae987dc5108a9fd57fda9d64730 Author: snowdreams1006 Date: Tue Mar 5 12:18:57 2019 +0800 add git init commit 6ad8956bc09a6a62c731711eabe796690aa6471c Author: snowdreams1006 Date: Tue Mar 5 12:17:51 2019 +0800 add test.txt git log 命令默认显示最近到最远的提交历史,这一点也很好理解,毕竟我们是在命令行操作,输入git log 完毕后自然先要定位到命令处,看到最新提交记录方便我们确认是否符合我们预期,还有一点就是如果提交历史过多,从头开始到最新提交记录岂不是眼花缭乱,简直不敢想象啊! 下面以最新的一次提交 commit 为例,简单解释一下输出内容: # 提交唯一标示id: 36f234a60d858871f040cb0d7ca3e78251df82f7 commit 36f234a60d858871f040cb0d7ca3e78251df82f7 (HEAD -> master) # 作者: snowdreams1006 邮箱: Author: snowdreams1006 # 日期: Thu Mar 7 22:19:00 2019 +0800 Date: Thu Mar 7 22:19:00 2019 +0800 # 提交备注: add understand how git control version add understand how git control version 默认输出内容有点多,不仅有提交 id ,提交备注还有作者时间之类的,由于每个 commit 都如此,这样一来,满屏都展示不下,那能不能简化些呢? 一行显示提交日志 --pretty=oneline ,即git log --pretty=oneline $ git log --pretty=oneline 36f234a60d858871f040cb0d7ca3e78251df82f7 (HEAD -> master) add understand how git control version 2006f72ffe2ce2278b5974313b8598847cf445e4 add 3 files. eaa4850070354ae987dc5108a9fd57fda9d64730 add git init 6ad8956bc09a6a62c731711eabe796690aa6471c add test.txt $ 相比无参数git log,是不是简短了一些呢? 和之前日志相比少了作者和时间等信息,仍然保留提交 id 和提交备注. 因为提交 commit 是 git 的基础,当然不能省略,而提交备注能够帮助我们理解commit 的含义,毕竟提交备注使我们自定义的内容,这也是我们为什么提交时要写提交备注的原因! 现在我们已经了解到版本库存放了我们的提交,接下来让我们验证一下是否能够回到过去吧! 回到上一个提交,上一个提交自然是相对当前提交而言,只有知道当前提交才能知道上一个提交以及上一个提交的上一个提交. 提交id 36f234a60d858871f040cb0d7ca3e78251df82f7,那么上一个提交HEAD^,上上一个提交是HEAD^^.如果此时我想回到往上数100个版本,那么是不是可以这么写? HEAD^^^^...^^^ 其中^ 有100个,如果需要手动打出100个^的话,那么绝对是疯了! 既然有这种相对定位方式,自然也有绝对定位方式,用绝对定位方式解决就是这样: HEAD~100 $ git log commit 36f234a60d858871f040cb0d7ca3e78251df82f7 (HEAD -> master) Author: snowdreams1006 Date: Thu Mar 7 22:19:00 2019 +0800 add understand how git control version 回到上一个版本 git reset --hard HEAD^ 在操作之前我们先看一下当前文件 test.txt 的内容: $ cat test.txt git test git init git diff understand how git control version 现在让我们开始回到过去,运行 git reset --hard HEAD^ 命令: $ git reset --hard HEAD^ HEAD is now at 2006f72 add 3 files. $ 现在让我们再看一下,test.txt 的内容有没有被还原: $ cat test.txt git test git init 果然被还原了!这就是git的神奇之处,说明我们已经能够回到过去了! 现在我们先用git log 查看下提交历史: $ git log commit 2006f72ffe2ce2278b5974313b8598847cf445e4 (HEAD -> master) Author: snowdreams1006 Date: Tue Mar 5 13:27:46 2019 +0800 add 3 files. commit eaa4850070354ae987dc5108a9fd57fda9d64730 Author: snowdreams1006 Date: Tue Mar 5 12:18:57 2019 +0800 add git init commit 6ad8956bc09a6a62c731711eabe796690aa6471c Author: snowdreams1006 Date: Tue Mar 5 12:17:51 2019 +0800 add test.txt $ 和上次相比,少了一条提交记录: commit 36f234a60d858871f040cb0d7ca3e78251df82f7 (HEAD -> master) Author: snowdreams1006 Date: Thu Mar 7 22:19:00 2019 +0800 add understand how git control version 这样是正常的,毕竟你已经处于 过去 了,当然看不到 未来 的提交记录. 正如影视穿越剧那样,主人公意外穿越过去,总是想要回到未来,怎么办,没有法器没有未来的确切目标怎么行?! git 的穿越剧也需要这样一种法器,能准确告诉时光机把我们带到具体的那个时间点,当然这个时间点不一定是未来时刻,过去时刻也行,反正就是一个准确的坐标. 聪明的你肯定已经猜测到这个任务是由commit 担任的,所有我们现在要找到未来的时间点,也就是commit id,就是那一长串 hash 字符串. 只要当前命令行窗口还没有关闭,慢慢往上翻,总是能找到当初我们的穿越点commit的,即36f234a60d858871f040cb0d7ca3e78251df82f7 回到当初提交 git reset --hard 万事俱备只欠东风,已经成功定位到未来坐标,等待穿越到未来! $ git reset --hard 36f234a60d858871f040cb0d7ca3e78251df82f7 HEAD is now at 36f234a add understand how git control version $ 现在我们再次查看 test.txt 内容: $ cat test.txt git test git init git diff understand how git control versi 果然成功穿越回到未来! 上述穿越回到未来的场景是我们知道目标 commit ,也就是在当前命令行窗口没有关闭的情况下,手动查找穿越点 commit.那如果命令行窗口已关闭或者没办法通过查阅历史命令来定位穿越点 commit 情况下怎么办呢? 这种情况下也是有补救措施的,git 提供了命令历史 git reflog,记录了我们操作的命令历史. 翻阅历史命令 git reflog $ git reflog 36f234a (HEAD -> master) HEAD@{0}: reset: moving to 36f234a60d858871f040cb0d7ca3e78251df82f7 2006f72 HEAD@{1}: reset: moving to HEAD^ 36f234a (HEAD -> master) HEAD@{2}: commit: add understand how git control version 2006f72 HEAD@{3}: commit: add 3 files. eaa4850 HEAD@{4}: commit: add git init 6ad8956 HEAD@{5}: commit (initial): add test.txt 确实记录了我们操作的关键命令,从上述输出结果可以看出,穿越点 commit 正是36f234a60d858871f040cb0d7ca3e78251df82f7,剩下的事情应该不必多说了吧! 小结 HEAD 是当前提交的指针,指向的提交就是当前提交,上一个提交是 HEAD^,上上个提交是 HEAD^^,前100个提交是HEAD~100. git log 查看提交历史,git log --pretty=oneline 简短化输出提交历史. git reflog 查看命令历史,以便我们重拾关键步骤信息. git reset --hard 穿越到指定提交,比如上一个提交就是 git reset --hard HEAD^ . © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:08:01 "},"git/usage/concept.html":{"url":"git/usage/concept.html","title":"基本概念","keywords":"","body":"基本概念 了解工作区,暂存区和版本库的区别和联系有助于我们更好理解 git 的工作流程,了解命令的操作意图. git 和其他版本控制系统如 svn 的不同之处就是有暂存区的概念. 基本概念 工作区 | Working Directory 正常情况下能看到的目录(不包括隐藏文件),也就是用户主动创建的目录 暂存区 | Stage 工作区下的隐藏.git目录下的.index文件,因此也称为索引. 版本库 | Repository 工作区下的隐藏目录.git目录 通过前几节我们知道,将文件纳入版本控制,需要分两步操作: 第一步 git add 添加文件,实际上是将文件更改添加到暂存区. 第二步 git commit 提交更改,实际上是将暂存区所有内容提交到当前分支. 我们使用 git init 命令初始化创建 git 仓库时,git 会自动创建唯一一个 master 分支,默认所有操作是在 master 分支上进行的,所以 git commit 就是徃 master 分支上提交更改的. 通俗地讲,文件更改可以多次添加到暂存区,即允许多次执行 git add 命令,然后一次性提交暂存区的全部更改到版本库,即只需要执行一次 git commit 命令即可. 说说个人理解 git 为何分成三部分进行版本控制操作,二部分行不行? 答案是肯定的,没有暂存区概念的 svn 同样可以进行版本控制,所以 git 增加暂存区必然是有存在的意外也就是所谓的好处的. 第一,暂存区的概念允许将本地文件的更改添加进来,也就是说本地文件的更改只有添加到暂存区才能进行下一步的提交更改,所以说那些更改添加到暂存区是由开发者本人决定的,这其实有了一定灵活性,并不是所有的更改都需要被记录! 第二,暂存区作为中间过程,暂存区的内容是打算提交更改的内容,也就是说暂存区可以视为一种临时缓存,用来记录预提交更改.实际工作中,新功能的开发并不是一蹴而就的,是由一系列的更改一起组成的,如果将这些更改分散开来单独提交,那势必会产生很多commit,如果等待全部工作完成再提交的话,解决了过多commit的问题,但是又遇到新问题就是你可能很长时间才能提交一次更改,失去了版本控制的意义.综上所述,暂存区的出现一种很好的解决方案,它允许将相关性代码添加在一起,方便后续提交更改时提交的都是相关性代码! 第三,作为分布式版本控制系统,不像集中式控制系统那样,对网络强相关,失去网络的 svn 是没办法再进行版本控制的,但失去网络的 git 仍然可以进行版本控制,只不过不能远程操作了而已,不过这部分也是无可厚非的,正所谓\"巧妇难为无米之炊\",你总不能要求断网下继续访问百度吧! 好了,我们继续回到 git 常用操作上,看一下工作区,暂存区和版本库三者如何协同工作的. 首先,先修改test.txt文件. # 查看 test.txt 文件内容 $ cat test.txt git test git init git diff understand how git control version # 追加 how git work 到 test.txt 文件 $ echo \"how git work\" >> test.txt # 再次查看 test.txt 文件内容 $ cat test.txt git test git init git diff understand how git control version how git work $ 紧接着新建newFile.txt 并随便输入内容: # 查看当前文件夹下全部文件 $ ls . file1.txt file2.txt file3.txt test.txt # 创建新文件 newFile.txt $ touch newFile.txt # 再次查看当前文件夹下全部文件 $ ls file1.txt file2.txt file3.txt newFile.txt test.txt # 输入 add newFile.txt 文件内容 到 newFile.txt 文件 $ echo \"add newFile.txt\" > newFile.txt # 查看 newFile.txt 文件内容 $ cat newFile.txt add newFile.txt $ 现在运行git status 命令查看当前文件状态: $ git status On branch master Changes not staged for commit: (use \"git add ...\" to update what will be committed) (use \"git checkout -- ...\" to discard changes in working directory) modified: test.txt Untracked files: (use \"git add ...\" to include in what will be committed) .DS_Store newFile.txt no changes added to commit (use \"git add\" and/or \"git commit -a\") $ 从输出结果中得知,test.txt 文件已修改(modified),还没添加到暂存区,而newFile.txt 文件还没被跟踪(Untracked). 现在我们使用git add 命令将 test.txt 和 newFile.txt 都添加到暂存区,再用 git status 查看文件状态: # 添加 test.txt 文件 git add test.txt # 添加 newFile.txt 文件 git add newFile.txt # 查看文件状态 git status On branch master Changes to be committed: (use \"git reset HEAD ...\" to unstage) new file: newFile.txt modified: test.txt Untracked files: (use \"git add ...\" to include in what will be committed) .DS_Store $ 现在输出结果和上次就不一样了,显示的是即将被提交文件,其中newFile.txt 是新文件(new file),test.txt 是修改文件(modified). 所以,git add 命令作用是将需要提交的更改文件临时放到暂存区中,然后执行git commit 命令就可以一次性将暂存区的所有内容提交到当前分支. $ git commit -m \"understand how stage works\" [master a5cd3fb] understand how stage works 2 files changed, 2 insertions(+) create mode 100644 newFile.txt $ git status On branch master Untracked files: (use \"git add ...\" to include in what will be committed) .DS_Store nothing added to commit but untracked files present (use \"git add\" to track) $ 暂存区的所有内容提交到版本库,所以运行git status 时,工作区是干净的,即此时暂存区没有内容了! .DS_Store 是 mac 电脑自动生成的文件,可以暂不理会,等到后面的.gitignore 文件时再处理. 图解 下图展示了工作区,暂存区,版本库之间的关系: 图中左侧是工作区,右侧是版本库,版本库中标记index 的区域是暂存区,标记 master 的是 master 分支所代表的目录树. HEAD 是指向 master 分支的指针,标记 objects 的区域是 git 的对象库,真实路径位于.git/objects目录下,用于表示创建的对象和内容. 意图说明 git add 添加文件 工作区的修改或者新增的文件执行git add 命令后,暂存区(index)的目录树会自动更新,同时引发这次变化的文件内容会被记录下来,即生成对象库(objects)中的新对象,而对象的 id会被记录到暂存区的文件索引(index)中. git commit 提交文件 暂存区的目录树写入到对象库(objects),master 分支的目录树自动更新. git reset HEAD 撤销文件 暂存区的目录树被重写,被master 分支的目录树所替换,但是工作区不受影响. git rm --cached 删除缓存文件 删除暂存区文件,工作区不受影响. git checkout . 检出文件 暂存区的文件替换工作区文件,注意:当前尚未添加到暂存区的改动会全部丢失! git checkout HEAD . 检出文件 HEAD 指针指向的 master 分支中的文件替换暂存区以及工作区文件,注意:不仅清除工作区未提交的改动,连暂存区未提交的改动也会被清除! 小结 以上就是常用命令的背后意图,主要是工作区,暂存区和版本库之间文件同步策略的关系. git add 是工作区更新到暂存区 git commit 是暂存区更新到版本库 git reset HEAD 是版本库更新到暂存区 git checkout -- 是暂存区更新到工作区 git checkout HEAD 是版本库同时更新暂存区和工作区 git rm --cached 清空暂存区 © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:08:03 "},"git/usage/version-control.html":{"url":"git/usage/version-control.html","title":"版本控制","keywords":"","body":"版本控制 我们知道 git 是分布式版本控制系统,所以称被控制对象是版本本身没错,但是从git 命令中发现,并没有版本这个名词,有的只是commit,所以前几节我一直称其为提交. 为了避免后续教程引发歧义,特意说明,无论是版本也好,提交也罢,都是中文翻译而已,不必太过较真,直接原汁原味称commit也可以啊! 假设你已掌握暂存区的相关概念,简单来说,暂存区就是更改文件的缓存集合,等待一次性全部提交到版本库,正因如此,方便我们批量操作相关性文件,打包提交到版本库,这正是暂存区的独特魅力. 我们反复在说 git 是分布式版本控制系统,分布式的概念已经粗略讲过多次了,下面我们讲一下版本控制,谈谈 git 的版本控制和其他系统的版本控制有什么不同,为什么 git 这么优秀,如此流行? git 跟踪并管理的是更改,而非文件本身.正如linux 一切皆文件,java 一切皆对象一样,git 一切皆更改.新增文件是一个更改,新增文件内容是一个更改,修改文件内容是一个更改,删除文件内容也是一个更改,换言之,git 管理的正是这一个个的更改,并不是文件本身. 下面我们用事实说话,证明 git 管理的是更改而不是文件本身: 第一步,追加 git tracks changes 到 test.txt 文件 # 查看 test.txt 文件内容 $ cat test.txt git test git init git diff understand how git control version how git work # 追加 git tracks changes 文件内容到 test.txt 文件 $ echo \"git tracks changes\" >> test.txt # 再次查看 test.txt 文件内容 $ cat test.txt git test git init git diff understand how git control version how git work git tracks changes $ 第二步,添加test.txt 文件到暂存区并查看文件状态 $ git add test.txt sunpodeMacBook-Pro:demo sunpo$ git status On branch master Changes to be committed: (use \"git reset HEAD ...\" to unstage) modified: test.txt Untracked files: (use \"git add ...\" to include in what will be committed) .DS_Store $ 对于上述内容应该不必再解释了吧,无外乎说test.txt 文件已修改(modified),即将被提交(to be committed). 但是,此时偏偏不提交,继续修改 test.txt 文件:(这种情况实际工作中也有可能出现,比如你正在研发某功能,本以为已经开发完毕,满心欢喜添加到暂存区,然后意外发现一个小bug,分分钟就修复了,时间间隔很短以至于你根本不记得还需要再次添加到暂存区.) 第三步,继续修改文件内容,忘记再次添加到暂存区 # 编辑 test.txt 文件,将 git tracks changes 更改为 git tracks changes of files vim test.txt # 查看 test.txt 文件内容 $ cat test.txt git test git init git diff understand how git control version how git work git tracks changes of files $ 第四步,正常提交暂存区的全部更改到版本库 $ git commit -m \"git tracks changes\" [master 2daa74a] git tracks changes 1 file changed, 1 insertion(+) 此次提交后,我们再看一下文件状态: $ git status On branch master Changes not staged for commit: (use \"git add ...\" to update what will be committed) (use \"git checkout -- ...\" to discard changes in working directory) modified: test.txt Untracked files: (use \"git add ...\" to include in what will be committed) .DS_Store no changes added to commit (use \"git add\" and/or \"git commit -a\") $ 发现有什么不同吗?以往提交后再次查看文件状态,工作区都是干净的,这次居然提示我们 test.txt 文件已经修改但未添加到暂存区?! 等一下,我们先回忆一下我们的操作流程: 第一次修改(git tracks changes) -> git add -> 第二次修改(git tracks changes of files) -> git commit 这样就很好理解了,git 管理的是更改而不是文件本身,如果是文件本身的话,应该将文件的内容全部提交才对,所以管理的是更改. 第一次修改过后使用 git add 命令将工作区的第一次修改内容放到暂存区准备提交,但是此时工作区发生了第二次修改,注意,这次修改并没有放到暂存区,所以下一步的git commit 命令提交的暂存区内容中自然也就没有第二次修改的内容了!所以git commit 完毕后运行git status命令才会发现此时工作区和暂存区还存在版本差异,即此时工作区不是干净的! 这一次的实验很好理解,工作区的修改需要主动告诉暂存区,暂存区的全部更改再提交到版本库.所以版本库的提交取决于暂存区,而暂存区又取决工作区是否主动将更改添加进去了吗! 理论再多不如亲身体验,让我们直接比较一下工作区和版本库的差异吧! # 比较 test.txt 文件在工作区和版本库的差异 $ git diff HEAD -- test.txt diff --git a/test.txt b/test.txt index d31bdd2..56c76b7 100644 --- a/test.txt +++ b/test.txt @@ -3,4 +3,4 @@ git init git diff understand how git control version how git work -git tracks changes +git tracks changes of files $ 由此可见,工作区比版本库多了git tracks changes of files,少了git tracks changes,所以说第二次修改内容 git tracks changes of files 并没有被提交. 现在我们再解释一下-git tracks changes 和 +git tracks changes of files 的问题: 首先查看工作区 test.txt 文件内容 $ cat test.txt git test git init git diff understand how git control version how git work git tracks changes of files $ 根据上述分析,我们知道第一次的修改git tracks changes 已被提交到版本库,第二次的修改git tracks changes of files 没有被提交而是继续留在工作区. 因此,可以推断出目前版本库的文件应该是这样的: git test git init git diff understand how git control version how git work git tracks changes 既然如何,工作区和版本库相比岂不刚好是少了一个git tracks changes,多了git tracks changes of files,其余文件内容完全相同! 透过现象看本质,已经分析了现象也解释了产生现象的原因,是时候分析一下本质了. 抛出问题:因为git tracks changes of fiels 和 git tracks changes 被视为不同的更改,所以才会造成上述现象.如果git tracks changes of fiels 被认为是git tracks changes + of fiels 两者叠加产生的更改,还会产生上述现象吗? 答案是否定的,如果两个更改可以叠加的话,按照版本控制的思路,第二次的修改即便没有提交也只是 of fiels 没有加入到版本库而已,如此一来,工作区和版本库的差异将不再是少了一个git tracks changes,多了git tracks changes of files,而仅仅是多了of files! 由此可见,git 版本控制系统其实是全量更新的思维模式,并不是差量更新模式. 小结 工作区的更改需要git add 添加到暂存区,git commit 将暂存区的全部更改提交到版本库. 工作区,暂存区,版本库三者既相关独立又密切关联,三者是传递性依赖的关系. git 版本控制的是文件的更改,而不是文件本身,是全量更新模式,而不是差量更新模式. © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:08:02 "},"git/usage/checkout-reset.html":{"url":"git/usage/checkout-reset.html","title":"撤销更改","keywords":"","body":"撤销更改 相信你已经了解了 git 的基本概念,也清楚了工作区,暂存区和版本库的关系,现在让我们用所学的知识继解决实际问题吧! 背景 正常看得见的目录是我们最为熟悉的工作区,在工作中不可能总是100%的精力,难免会犯错,尤其是下午犯困,晚上加班更是如此.下面列举了常见的一些场景 场景一: 工作区出现意外更改且尚未添加到暂存区 北京时间现在是晚上10点钟,你正在赶制一份工作报告,尽管心中一万个不愿意,还是不得不做. 开始模拟意外更改前,先查看一下 test.txt 文件相关信息: # 列出当前目录的文件 $ ls file1.txt file2.txt file3.txt newFile.txt test.txt # 查看 `test.txt` 文件内容 $ cat test.txt git test git init git diff understand how git control version how git work git tracks changes of files # 查看 `test.txt` 文件状态 $ git status On branch master Changes not staged for commit: (use \"git add ...\" to update what will be committed) (use \"git checkout -- ...\" to discard changes in working directory) modified: test.txt Untracked files: (use \"git add ...\" to include in what will be committed) .DS_Store no changes added to commit (use \"git add\" and/or \"git commit -a\") # 查看 `test.txt` 文件差异 $ git diff diff --git a/test.txt b/test.txt index d31bdd2..56c76b7 100644 --- a/test.txt +++ b/test.txt @@ -3,4 +3,4 @@ git init git diff understand how git control version how git work -git tracks changes +git tracks changes of files $ 还记得在上一节中我们讲解 git 版本控制的到底是什么,为了证明 git 管理的是更改而不是文件本身,我们特意在第二次更改时没有添加到暂存区,现在我们先把这个遗留问题解决掉. # 工作区更改添加到暂存区 $ git add test.txt # 暂存区内容提交到版本没哭 $ git commit -m \"git tracks changes of files\" [master b7bda05] git tracks changes of files 1 file changed, 1 insertion(+), 1 deletion(-) # 查看文件状态 $ git status On branch master Untracked files: (use \"git add ...\" to include in what will be committed) .DS_Store nothing added to commit but untracked files present (use \"git add\" to track) $ 现在正在加班加点干活,一不小心将心中的不满表露出来了,于是有了下面的内容: # 意外更改正是这么犯傻的一句话 $ echo \"My stupid boss still prefers svn\" >> test.txt # 当前文件内容 $ cat test.txt git test git init git diff understand how git control version how git work git tracks changes of files My stupid boss still prefers svn $ 虽然强打精神,可还是很困,于是打算喝杯咖啡提提神,猛然发现 stupid boss 可能会让你丢掉这个月的奖金! 暗自庆幸,咖啡果然是个好东西,既然发现了问题,那就事不宜迟赶紧修复,因为不适宜的话正是 stupid boss ,所以你完全可以手动删除,但是假如你说了一大堆不合适的话,或者复制粘贴时弄错了,这就不是删除一两行那么简单了! 既然手动解决比较麻烦,那git 有没有什么好方法来解决这类问题呢?在寻求git 帮助前,首先再看一下当前文件状态(git status).正所谓\"知己知彼方能百战百胜\",还是看一眼吧! # 查看文件状态 $ git status On branch master Changes not staged for commit: (use \"git add ...\" to update what will be committed) (use \"git checkout -- ...\" to discard changes in working directory) modified: test.txt Untracked files: (use \"git add ...\" to include in what will be committed) .DS_Store no changes added to commit (use \"git add\" and/or \"git commit -a\") $ git 不负众望,果然给了我们希望,(use \"git checkout -- ...\" to discard changes in working directory) 这句话的告诉我们可以丢弃工作区的更改! 脑海中在快速回忆一下工作区,暂存区,版本库三者之间的关系,其实git checkout -- 命令的意思是用暂存区的内容替换掉工作区内容,因此也就是丢弃掉工作区的更改了. 事不宜迟,运行 git checkout -- 命令试试看吧: # 丢弃工作区的更改 $ git checkout -- test.txt # 查看文件内容: My stupid boss still prefers svn 终于不见了 $ cat test.txt git test git init git diff understand how git control version how git work git tracks changes of files # 查看文件状态 $ git status On branch master Untracked files: (use \"git add ...\" to include in what will be committed) .DS_Store nothing added to commit but untracked files present (use \"git add\" to track) $ 一顿操作猛如虎,撤销掉意外更改,回到上一次版本控制状态,世界如此美好... 注意: git checkout -- 中的 -- 至关重要,没有它就是切换分支了! 场景二: 工作区出现意外更改且已经添加到暂存区,但尚未提交到版本库 时间一分一秒过去了,转眼间已经11点了,假设你不但写了一些胡话,还添加到暂存区了(git add).可想而知,这次意外比场景一要糟糕. # 模拟正常提交(不然岂不是从场景一到场景二你什么都没做,那还能叫做赶制工作报告吗?!) $ echo \"someone prefers svn,but i don't care it\" >> test.txt $ cat test.txt git test git init git diff understand how git control version how git work git tracks changes of files someone prefers svn,but i don't care it $ git add test.txt $ git commit -m \"normal commit\" [master ab1cbd2] normal commit 1 file changed, 1 insertion(+) # 意外更改的前夕 $ cat test.txt git test git init git diff understand how git control version how git work git tracks changes of files someone prefers svn,but i don't care it # 意外更改内容: my teammate is stupid too. $ echo \"my teammate is stupid too.\" >> test.txt $ cat test.txt git test git init git diff understand how git control version how git work git tracks changes of files someone prefers svn,but i don't care it my teammate is stupid too. # 意外操作: 将意外更改内容提交到暂存区 $ git add test.txt 不过庆幸的是,在提交到版本库(git commit)之前及时发现问题,还是看一下现在的文件状态(git status)吧! # 查看文件状态: 救命稻草 (use \"git reset HEAD ...\" to unstage) $ git status On branch master Changes to be committed: (use \"git reset HEAD ...\" to unstage) modified: test.txt Untracked files: (use \"git add ...\" to include in what will be committed) .DS_Store $ git 同样告诉我们,可以使用 git reset HEAD 命令撤销暂存区更改. 其实 git reset HEAD 命令是用版本库的内容替换掉暂存区的内容,也就是说原来暂存区的内容已被丢弃了! 所以说这个命令并不会影响工作区内容,不如我们现在再看一眼工作区内容,方便执行 git reset HEAD 命令后证实我们的结论. # 查看文件内容: my teammate is stupid too. $ cat test.txt git test git init git diff understand how git control version how git work git tracks changes of files someone prefers svn,but i don't care it my teammate is stupid too. $ 迫不及待执行 git reset HEAD 命令,先睹为快! # 救命稻草: 版本库内容替换掉暂存区内容 $ git reset HEAD test.txt Unstaged changes after reset: M test.txt # 效果: 目标文件已修改但未添加到暂存区 $ git status On branch master Changes not staged for commit: (use \"git add ...\" to update what will be committed) (use \"git checkout -- ...\" to discard changes in working directory) modified: test.txt Untracked files: (use \"git add ...\" to include in what will be committed) .DS_Store no changes added to commit (use \"git add\" and/or \"git commit -a\") # 目标文件内容: 仍然保持不变 $ cat test.txt git test git init git diff understand how git control version how git work git tracks changes of files someone prefers svn,but i don't care it my teammate is stupid too. $ 现在场景二已经退化成场景一了,目标文件发生意外更改但还没添加到暂存区,如何撤销工作区更改,请参考场景一方法. 提示: git checkout -- test.txt 场景三: 工作区出现意外更改不仅已添加到暂存区,还已提交到版本库,但尚未推送到远程仓库 时间不紧不慢地已经到凌晨了,困意越来越浓,洋洋洒洒写下几千字的工作报告,总算是写完了,添加到暂存区(git add),提交到版本库(git commit)一气呵成,等等,好像有什么不对劲,难免会犯糊涂,这不又发生意外了! # 衔接场景二 $ cat test.txt git test git init git diff understand how git control version how git work git tracks changes of files someone prefers svn,but i don't care it # 正常提交一 $ echo \"i love working,work makes me happy\" >> test.txt $ git add test.txt $ git commit -m \"encourage myself\" [master a44cf7a] encourage myself 1 file changed, 1 insertion(+) # 正常提交二 $ echo \"fix 110 bugs,so happy\" >> test.txt $ git add test.txt $ git commit -m \"fix bugs\" [master c66399d] fix bugs 1 file changed, 1 insertion(+) sunpodeMacBook-Pro:demo sunpo$ git status On branch master Untracked files: (use \"git add ...\" to include in what will be committed) .DS_Store nothing added to commit but untracked files present (use \"git add\" to track) # 意外更改: hate to work overtime $ echo \"hate to work overtime\" >> test.txt $ git add test.txt $ git commit -m \"test.txt\" [master c965724] test.txt 1 file changed, 1 insertion(+) $ 天妒英才,加班加点做事情,本想赢得老板的赏识,没想到最后一句话\"hate to work overtime\"让所有的努力都付之一炬,怎么办? 死马当活马医,还是照例看看git status 能提供什么建议吧! $ git status On branch master Untracked files: (use \"git add ...\" to include in what will be committed) .DS_Store nothing added to commit but untracked files present (use \"git add\" to track) $ 没有提供任何意见能帮助我们撤销意外更改,先别慌,容我深思三秒钟... 既然意外更改已经提交到版本库,那么应该用什么内容替换版本库内容呢?有了,既然最新版本库不可用,那上一个版本库内容可用的啊,完全可以用上一个版本库内容替换最新版本库内容,真乃\"天生我材必有用\"! # 当前文件内容: 闯祸的\"hate to work overtime\" $ cat test.txt git test git init git diff understand how git control version how git work git tracks changes of files someone prefers svn,but i don't care it i love working,work makes me happy fix 110 bugs,so happy hate to work overtime # 版本回退: 回到过去假装什么都没发生过 $ git reset --hard HEAD^ HEAD is now at c66399d fix bugs sunpodeMacBook-Pro:demo sunpo$ git status On branch master Untracked files: (use \"git add ...\" to include in what will be committed) .DS_Store nothing added to commit but untracked files present (use \"git add\" to track) # 岁月静好,一切似乎都没发生过 $ cat test.txt git test git init git diff understand how git control version how git work git tracks changes of files someone prefers svn,but i don't care it i love working,work makes me happy fix 110 bugs,so happy # 当前文件状态 $ git status On branch master Untracked files: (use \"git add ...\" to include in what will be committed) .DS_Store nothing added to commit but untracked files present (use \"git add\" to track) $ 详情请参考回到过去,时空穿越之旅就是这么方便哈! 提示: git reset --hard HEAD^ 场景四: 工作区出现意外更改不仅已添加到暂存区,还提交到版本库,还已推送到远程仓库 场景一到场景三都是本地仓库,所有的文件更改只能本机访问,小伙伴也好,上级领导也罢都无法查看到你本地更改,但是一旦你推送到远程仓库了,那么其他人就能查看你的更改了! 正常的提交更改还好,怕就怕这种\"stupid boss\"被领导看到就不好了,那应该怎么办?暂时还是自求多福吧! 小结 丢弃工作区更改: git checkout -- 丢弃暂存区更改: git reset HEAD 丢弃本地版本库更改: git reset --hard HEAD^ 丢弃远程版本库更改: 自求多福 © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:08:00 "},"git/usage/delete.html":{"url":"git/usage/delete.html","title":"删除文件","keywords":"","body":"删除文件 回忆一下文件的常见操作,新增文件,修改文件,删除文件等,新增和修改文件都单独讨论过,现在我们来研究一下如何删除文件. 你可能会说删除文件还不简单啊,直接 rm -rf 即可,但是这仅仅是本地文件被删除了,对于 git 来说,文件并没有被删除. 还记得我们开篇介绍git 时就说过,一切操作皆版本 ,对于新增是一个版本,修改也是一个版本,就连删除都是一个版本. 下面让我们看一下 git 中如何删除文件吧! 背景 # 查看当前文件列表 $ ls file1.txt file2.txt file3.txt newFile.txt test.txt # 新建待删除文件 $ touch delete.txt # 再次查看当前文件列表,确保新建文件成功 $ ls delete.txt file2.txt newFile.txt file1.txt file3.txt test.txt # 查看当前文件状态: 新文件 `delete.txt` 还没被跟踪 $ git status On branch master Untracked files: (use \"git add ...\" to include in what will be committed) .DS_Store delete.txt nothing added to commit but untracked files present (use \"git add\" to track) # 添加新文件 `delete.txt` $ git add delete.txt # 查看文件状态: 已添加到暂存区,待提交到版本库 $ git status On branch master Changes to be committed: (use \"git reset HEAD ...\" to unstage) new file: delete.txt Untracked files: (use \"git add ...\" to include in what will be committed) .DS_Store # 提交新文件 `delete.txt` $ git commit -m \"add delete.txt\" [master 7df386a] add delete.txt 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 delete.txt # 再次查看文件状态: 已经没有新文件 `delete.txt` 的更改信息 $ git status On branch master Untracked files: (use \"git add ...\" to include in what will be committed) .DS_Store nothing added to commit but untracked files present (use \"git add\" to track) $ 以上操作,我们简单创建 delete.txt 文件,添加(git add)并提交(git commit) 该文件,完成准备工作后,开始删除文件! # 删除前文件列表 $ ls delete.txt file2.txt newFile.txt file1.txt file3.txt test.txt # 删除刚刚创建的文件 `delete.txt` $ rm delete.txt # 删除后文件列表 $ ls file1.txt file2.txt file3.txt newFile.txt test.txt # 当前文件状态: `delete.txt` 文件已被删除,且未添加到暂存区 $ git status On branch master Changes not staged for commit: (use \"git add/rm ...\" to update what will be committed) (use \"git checkout -- ...\" to discard changes in working directory) deleted: delete.txt Untracked files: (use \"git add ...\" to include in what will be committed) .DS_Store no changes added to commit (use \"git add\" and/or \"git commit -a\") $ 本地删除 delete.txt 文件后,再次查看文件状态 git status 发现 git 给了我们两条建议,其中一条 git checkout -- 我们很熟悉,就是丢弃工作区的更改,此时此景下如果丢弃删除操作,相当于撤销删除,难怪说删除也是一个版本呢! 现在我们重点来看第一条建议 git add/rm ,rm 是 remove 单词的缩写,即删除文件. # 删除文件 $ git rm delete.txt rm 'delete.txt' # 查看文件状态: `delete.txt` 文件待提交 $ git status On branch master Changes to be committed: (use \"git reset HEAD ...\" to unstage) deleted: delete.txt Untracked files: (use \"git add ...\" to include in what will be committed) .DS_Store # 提交文件 $ git commit -m \"remove delete.txt\" [master 6298070] remove delete.txt 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 delete.txt # 再次查看文件状态 $ git status On branch master Untracked files: (use \"git add ...\" to include in what will be committed) .DS_Store nothing added to commit but untracked files present (use \"git add\" to track) $ 删除文件和添加文件类似,都是一次commit ,本地文件的任何更改都要添加到暂存区,然后提交到版本库. 小结 删除文件和新增文件类似逻辑,git rm 删除文件后,依然需要 git commit 提交版本. © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:08:01 "},"git/usage/remote-repository.html":{"url":"git/usage/remote-repository.html","title":"远程仓库","keywords":"","body":"远程仓库 如果说本地仓库已经足够个人进行版本控制了,那么远程仓库则使多人合作开发成为可能. 如果你只是打算自己使用git,你的工作内容不需要发布给其他人看,那就用不到远程仓库的概念. git 是分布式版本控制系统,分布式意味着同一个git 仓库 可以部署在不同的机器上,正如\"鸡生蛋蛋生鸡\"问题一样,不论如何,先要有一个原始仓库,然后才能分布到其他机器上去. 充当原始仓库的机器要有一个特点那就是24h 开机且大家都能访问到,这个概念类似于\"中央服务器\".这样一来大家都可以从\"中央服务器\"下载最新代码,克隆到本地,本地发生更改后再推送给\"中央服务器\".如此一来,大家交流方便很多,轻松实现文件内容的共享. 这种\"中央服务器\"比较有名的是国外的网站 github,当然国内也有不少类似服务.像这种\"中央服务器\"也可以自己搭建,现阶段搭建的话简直就是\"杀鸡焉用牛刀\"! 背景 关于如何注册配置相关请参考 github 教程 为了和上述教程保持一致,项目名git-demo,先看一下当前工作区状态: # 查看文件列表 $ ls LICENSE README.md test.txt # 查看文件内容 $ cat test.txt add test.txt 现在测试一下本地更改能否推送到远程仓库,先在本地文件 test.txt 随便写点东西,然后添加(git add),提交(git commit),最后推送到远程仓库(git push origin master). # 写入新的内容并提交到本地仓库 $ echo \"see https://snowdreams1006.github.io/git/usage/remote-repository.html\" >> test.txt $ git add test.txt $ git commit -m \"see https://snowdreams1006.github.io/git/usage/remote-repository.html\" [master b3d8193] see https://snowdreams1006.github.io/git/usage/remote-repository.html 1 file changed, 1 insertion(+) # 推送到远程仓库 $ git push origin master Counting objects: 3, done. Delta compression using up to 4 threads. Compressing objects: 100% (3/3), done. Writing objects: 100% (3/3), 359 bytes | 359.00 KiB/s, done. Total 3 (delta 1), reused 0 (delta 0) remote: Resolving deltas: 100% (1/1), completed with 1 local object. To github.com:snowdreams1006/git-demo.git 8e62564..b3d8193 master -> master $ 命令行没有报错证明我们已经成功推送到 github,现在登录 github 看一下有没有刚才我们提交的新内容. 现在本地版本库和远程版本库已经能够正常建立关联了,此刻起将不再是独自一人在战斗! 小结 创建已有本地仓库和远程仓库的关联 # 添加远程仓库关联 git remote add origin [email protected]:username/repos.git # 首次推送 master 分支的全部内容 git push -u origin master # 后续推送 master 分支的最新更改 git push origin master 从已有远程仓库克隆到本地仓库 # 克隆远程仓库到本地仓库 git clone [email protected]:username/repos.git # 推送 master 分支的最新更改 git push origin master © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:08:01 "},"git/usage/branch-manage.html":{"url":"git/usage/branch-manage.html","title":"分支管理","keywords":"","body":"分支管理 背景 什么是分支?简单地说,分支就是两个相对独立的时间线,正常情况下,独立的时间线永远不会有交集,彼此不知道对方的存在,只有特定情况下,两条时间线才会相遇,因为相遇,所以相知,因为相知,所以改变! 正如分支对于科幻电影来说是一个很好的卖点,关于分支的话题完全可以开启新的题材,对于这点相信不少科幻迷都深有体会,不必赘述. 回归正题,分支对于版本控制系统又意味着什么呢?实际工作中,我们大多作为一个团队一起合作开发项目,如果是独立开发者,只有一个人的话,其实用不到分支的概念,甚至远程仓库也用不到.所以下述情况针对的都是团队开发情况. 作为团队中的一员不论是项目领导还是项目成员,都需要了解并掌握分支的一般概念和常用操作.如果你刚好是实际开发的程序猿,上级领导分派一个新功能,预期两个星期内才能完成,其他同事也是如此,每个人都有自己的任务.接收任务就要开始干活,第一天工作开了一个头,还留下一大堆的 TODO 标记,此时你照例运行 git add ,git commit 等命令,学会上节的git push origin master 你知道了本地仓库和远程仓库的概念,你想将你的工作成果分享给其他人就要推送到远程仓库,这样其他人才能可见,等一等,别急! 首先明确的是,这个完整功能至少需要2个星期才能基本完成啊,你现在刚刚起了个头还没完成呢!你要是真的推送到远程仓库了,那其他人是不是有理由认为你这部分功能已完成?那你可能会反驳说,我可以在工作群吼一声,说这个功能还没完成,大家别着急使用哈!这样确实可以,很长一段时间内其他人必须无视你的代码,只有等你的功能基本可用时,等你再吼一声,别人才会去使用你的代码.粗略一看,好像并没有什么问题?! 实际上这种情况是存在很大风险的,因为未完成未经过测试的代码可能会产生大量意外 bug,严重的话,甚至影响整个系统,到时候由于你的未完成代码导致别人项目都无法运行,那别人还怎么工作,这个责任是谁负责? 所以,为了不给其他人造成麻烦,最好不要把未完成工作直接暴露到别人面前,那长时间提交又可能会造成丢失更改的风险,此时此景,平行时间线应用而生! 从接手新功能的时间点开始,创建一条新的时间线,于是新功能的开发完全在新的时间线上进行,至于其他人是否开启新的时间线那就不是我们能控制得了,我们能做到的就是不给其他人制造麻烦,如果其他人给我们制造麻烦的话,那我们就去上级领导那告他一状,哈哈! 等功能开发差不多时,你再想办法切换到原来的时间线上并将开发时间线的更改顺便都带过来,这样一来,别人虽然看不到你的开发时间线,但是看到了你离开的这段时间原来做了这么多的更改啊! 现在用git的专业术语再解释一遍上述场景: 接手新功能的时刻开始,创建一个开发分支(既可以是本地分支也可以是远程分支),以后新功能的开发全部在开发分支上完成,处于开发分支上你可以照常运行 git add ,git commit 等命令,不用担心丢失更改.等工作一段时间后,终于完成了新功能,是时候让新功能展示给其他同事了.此时再切换到原来的主干分支,在主干分支上合并开发分支,现在主干分支上已经有新功能了,这样一来,其他同事突然发现你已经偷偷地完成了新功能的开发! 不仅 git 有分支概念,其他版本控制系统比如 svn 也有分支概念,基本概念和常用操作类似,只不过 git 更强大,创建分支,切换分支,合并分支等功能十分强大,效率太高! (svn 创建分支,切换分支等操作简直慢到可以喝一杯茶了,分支管理都快成摆设了!) 建议 开发新功能时尽量创建自己的分支,不要给其他人造成麻烦 分配任务时要求项目成员创建各自分支,等时机成熟时再合并到主干分支 © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:07:59 "},"git/usage/branch-overview.html":{"url":"git/usage/branch-overview.html","title":"分支总览","keywords":"","body":"分支总览 分支就是一条独立的时间线,既有分支,必有主干,正如一棵树谈到树枝,必有树干一样的道理.我们先前对git 的全部操作默认都是在主干上进行的,这个主干也是一种特殊的分支,名为 master 分支. 无论是穿越历史还是撤销更改,我们都或多或少接触过时间线,git 管理的版本串在一起就组成了这个时间线,其中master 分支是当前分支,HEAD 指向master ,因此HEAD 相当于指向了最新的版本. 基于分支上的操作,每一次 commit 都会提交一个新版本,并且新的 commit 指向原来的 commit,这来最新的 commit 就可以往前找,直到找到最初的commit.这就是 git 的时间线. 当我们打算开辟新的时间线时,git 在当前 HEAD 指向的 master 分支的 commit 处新建一个 dev 分支.如果主角没有主动进入时间线的话,那么仍然处于 master 分支,进入的方法就是 HEAD指向新建的 dev 分支. 不考虑孙悟空的分身特效,主角不能同时处于不同的时空下,git 也是如何,HEAD 只能指向某一个 commit ,既然刚刚已经指向了 dev 分支,所以原来的 master 分支就没有 HEAD 了,因为相当于master 分支静止了. 当主角在 dev 分支独自闯荡干出一番事业时,决定回到故乡 master 分支,并将出门在外所学的本领带回家乡,建设美好家园.master 分支因为合并了 dev 分支,所以一下子增添了很多内容,家乡焕然一新! 主角这次携带 dev 分支归来,HEAD 分支自然又回到了 master 分支上,年轻的心向往外面的世间,相信不久后还会有同样的故事发生... 下面详解分支相关命令 创建分支 创建 dev 分支,列出分支已验证是否创建成功 # 创建分支 $git branch dev # 列出分支 $ git branch dev * master $ * master 前面的 * 标记表明当前仍然处于 master 分支 切换分支 切换到新分支以便在分支上开展工作 # 切换分支 $ git checkout dev Switched to branch 'dev' # 列出分支 $ git branch * dev master $ 现在,我们在 dev 分支上奋笔疾书,先后提交两个版本后完成分支开发工作: # 查看当前文件列表 $ ls LICENSE README.md test.txt # 查看目标文件内容 $ cat test.txt add test.txt see https://snowdreams1006.github.io/git/usage/remote-repository.html # 第一个版本: learn git branch $ echo \"learn git branch\" >> test.txt $ git add test.txt $ git commit -m \"learn git branch\" [dev 9c30e50] learn git branch 1 file changed, 1 insertion(+) # 第二个版本: see https://snowdreams1006.github.io/git/usage/branch-overview.html $ echo \"see https://snowdreams1006.github.io/git/usage/branch-overview.html\" >> test.txt $ git add test.txt sunpodeMacBook-Pro:git-demo sunpo$ git status On branch dev Changes to be committed: (use \"git reset HEAD ...\" to unstage) modified: test.txt $ git commit -m \"see https://snowdreams1006.github.io/git/usage/branch-overview.html\" [dev 413a4d1] see https://snowdreams1006.github.io/git/usage/branch-overview.html 1 file changed, 1 insertion(+) 此时,再从 dev 分支切换回 master 分支,合并dev分支前看一下当前文件内容: # 切换回 master 分支 $ git checkout master Switched to branch 'master' Your branch is up to date with 'origin/master'. sunpodeMacBook-Pro:git-demo sunpo$ git status On branch master Your branch is up to date with 'origin/master'. nothing to commit, working tree clean # 查看当前文件列表 $ ls LICENSE README.md test.txt # 查看文件内容: 无 dev 分支更改 $ cat test.txt add test.txt see https://snowdreams1006.github.io/git/usage/remote-repository.html $ 合并分支 切换回 master 分支并没有我们在 dev 分支的更改,因为两条时间线是独立的,现在合并 dev 分支,再看一下当前文件内容: # 合并 dev 分支 $ git merge dev Updating b3d8193..413a4d1 Fast-forward test.txt | 2 ++ 1 file changed, 2 insertions(+) # 查看文件内容: 已经存在 dev 分支的更改! $ cat test.txt add test.txt see https://snowdreams1006.github.io/git/usage/remote-repository.html learn git branch see https://snowdreams1006.github.io/git/ 删除分支 合并分支后,dev 分支的历史使命已经完成,应该及时清空不必要分支. # 删除 dev 分支 $ git branch -d dev Deleted branch dev (was 413a4d1). # 列出当前分支: 只剩下 master 分支 $ git branch * master $ 以上场景包括了分支的常用操作,创建分支(git branch ),切换分支(git checkout ),删除分支(git branch -d )一系列操作十分流畅,因此 git 鼓励我们大量使用分支! 小结 列出分支 git branch 创建分支 git branch 切换分支 git checkout 创建并切换分支 git checkout -b 合并指定分支到当前分支 git merge 删除分支 git branch -d © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:08:00 "},"git/usage/branch-merge-with-conflict.html":{"url":"git/usage/branch-merge-with-conflict.html","title":"冲突合并","keywords":"","body":"冲突合并 如果足够幸运的话,团队成员互不影响,彼此相安无事,大家各自基于 master 分支的某个 commit 创建自己的分支,平时在分支上独立工作,等到一段时间后再合并 merge 到 master 分支,这样一样 master 作为各个功能的集大成者,最终完成项目. 然而事情总不是一帆风顺的,团队协作时由于意见不同,遇到冲突简直是家常便饭,既然无法回避冲突,当冲突发生时如何应该呢? 背景 基于 master 分支上的某个 commit ,新功能由此继续开发: echo \"git commit c1\" >> test.txt $ git add test.txt $ git commit -m \"git commit c1\" 新功能分支命名为 feature ,使用git checkout -b 创建分支并切换: $git checkout -b feature Switched to a new branch 'feature' $ 在新功能 feature 分支上开发新功能,并提交: $ echo \"git commit c2\" >> test.txt $ git add test.txt $ git commit -m \"git commit c2\" [feature 0fe95f8] git commit c2 1 file changed, 1 insertion(+) $ 无论新功能 feature 是否开发完毕,团队的其他成员均有可能处于 master 分支并做相应更改: $ git checkout master Switched to branch 'master' Your branch is ahead of 'origin/master' by 3 commits. (use \"git push\" to publish your local commits) 其他成员对新功能有着自己的看法,于是也提交了版本,由于我们之前提交的是 git commit c2,而此时master 分支提交的是git commit c3,显然我们两个人的意见不一致! $ echo \"git commit c3\" >> test.txt $ git add test.txt $ git commit -m \"git commit c3\" [master 0949cc3] git commit c3 1 file changed, 1 insertion(+) $ 正在此时,feature 分支的新功能已开发完毕并主动切换回 master 分支,准备合并 feature 分支. # 合并 feature 分支 $ git merge feature Auto-merging test.txt CONFLICT (content): Merge conflict in test.txt Automatic merge failed; fix conflicts and then commit the result. $ 由于项目成员沟通不畅或者意见不一致,导致了代码冲突,git 作为版本控制系统,自然无法解决这类问题,总不能擅自做主抛弃后来的更改吧或者抛弃分支更改?所以 git 只负责抛出问题,等待我们程序员去解决问题. 既然是人的问题,那我们看一下我们到底是哪里不一致,为什么会产生冲突? # 查看状态 $ git status On branch master Your branch is ahead of 'origin/master' by 4 commits. (use \"git push\" to publish your local commits) You have unmerged paths. (fix conflicts and run \"git commit\") (use \"git merge --abort\" to abort the merge) Unmerged paths: (use \"git add ...\" to mark resolution) both modified: test.txt no changes added to commit (use \"git add\" and/or \"git commit -a\") # 比较差异 $ git diff diff --cc test.txt index 6e00f87,0f95fd7..0000000 --- a/test.txt +++ b/test.txt @@@ -3,4 -3,4 +3,8 @@@ see https://snowdreams1006.github.io/gi learn git branch see https://snowdreams1006.github.io/git/usage/branch-overview.html git commit c1 ++>>>>>> feature 和我们预期一样,test.txt 文件产生了冲突,当前 HEAD 指向的提交即 master 分支是 git commit c3 ,而 feature 分支是 git commit c2,对于同一个文件的同一行内容发生不同的更改,git 不知道也不应该知道如何处理. # 查看内容 $ cat test.txt add test.txt see https://snowdreams1006.github.io/git/usage/remote-repository.html learn git branch see https://snowdreams1006.github.io/git/usage/branch-overview.html git commit c1 >>>>>> feature git 用 标记一个分支冲突开始,======= 标记分支分割线,>>>>>>> 标记另一个分支结束. 经过冲突双方的讨论后,彼此间达成妥协,决定修改成git commit c2 and c3 ,修改后继续提交: # 编辑冲突文件,按照协商一致的内容修改文件 $ vim test.txt # 将冲突内容更改为 git commit c2 and c3 $ cat test.txt add test.txt see https://snowdreams1006.github.io/git/usage/remote-repository.html learn git branch see https://snowdreams1006.github.io/git/usage/branch-overview.html git commit c1 git commit c2 and c3 $ git add test.txt $ git commit -m \"fix conflict\" [master 3b8f434] fix conflict 冲突已经解决,现在回顾一下提交历史,使用git log --graph 图形化展示提交历史: # 查看提交日志 $ git log --pretty=oneline --graph * 3b8f434013caa8c27fade4c59d7aa2ee2c079636 (HEAD -> master) fix conflict |\\ | * 0fe95f871b371834d30ea17faa82f84b7d67672b (feature) git commit c2 * | 0949cc319e099d554795d03c69ee38923af00d6c git commit c3 |/ * 5c482cd9965b9dfd4f273b43b240ed7db66167a8 git commit c1 * 413a4d1d2aab5ab85b6097d4b9f81cb5601c3b26 see https://snowdreams1006.github.io/git/usage/branch-overview.html * 9c30e50248b773e38b032477a859e87abe7c1bb0 learn git branch * b3d8193bbcb9f76c47e831e3e212f2405ae09f93 (origin/master, origin/HEAD) see https://snowdreams1006.github.io/git/usage/remote-repository.html * 8e625640348a47ac922409a1ecb4c844385582aa add test.txt * 9b196aab5bc87eeb11709c9eef35fca283e05c61 Initial commit $ 最后,删除新功能分支 feature ,不用的分支及时清理干净,需要时再创建分支. $ git branch -d feature 小结 无法杜绝冲突的发生,代码上的冲突本质上是人为因素造成的冲突. 解决冲突需要有关双方协商解决,不可能独自解决冲突,除非你抛弃自我,完全以对方为准. 使用 git log --graph 命令可以图表化查看提交历史,抑或 git log --pretty=oneline --graph © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:07:59 "},"git/usage/branch-strategy.html":{"url":"git/usage/branch-strategy.html","title":"分支策略","keywords":"","body":"分支策略 默认情况下合并分支常常直接使用 git merge 命令,是最方便快速的合并方法.其实这种情况下 git 采用的是 fast forward 模式,特点是删除分支后,会丢失分支信息,好像从来没存在该分支一样,而我们推荐的是recursive 模式,能够保留分支的版本记录. 递归模式(recursive) 创建并切换 dev 分支,提交版本后切换回 master 分支,然后再合并 dev 分支,这不过这一次不再使用 git merge dev 命令: # 创建并切换 dev 分支 $ git checkout -b dev Switched to a new branch 'dev' # 提交版本 $ echo \"git checkout -b dev\" >> test.txt $ git add test.txt $ git commit -m \"git checkout -b dev\" [dev 44d68f6] git checkout -b dev 1 file changed, 1 insertion(+) # 切换回 master 分支 $ git checkout master Switched to branch 'master' Your branch is ahead of 'origin/master' by 6 commits. (use \"git push\" to publish your local commits) $ 现在添加 --no-ff 参数禁用 fast forward 模式,即git merge --no-ff: $ git merge --no-ff -m \"git merge --no-ff dev\" dev Merge made by the 'recursive' strategy. test.txt | 1 + 1 file changed, 1 insertion(+) $ 上述内容显示,这次使用的不再是 fast forward 模式,而是 recursive 模式,那让我们看一下提交历史有什么不同吧! $ git log --pretty=oneline --graph * 22fbef71b7575cd7eb7911079551618667f9f38f (HEAD -> master) git merge --no-ff dev |\\ | * 44d68f674bc85bc972426c572b78915e850e476c (dev) git checkout -b dev |/ * 3b8f434013caa8c27fade4c59d7aa2ee2c079636 fix conflict |\\ | * 0fe95f871b371834d30ea17faa82f84b7d67672b git commit c2 * | 0949cc319e099d554795d03c69ee38923af00d6c git commit c3 |/ * 5c482cd9965b9dfd4f273b43b240ed7db66167a8 git commit c1 * 413a4d1d2aab5ab85b6097d4b9f81cb5601c3b26 see https://snowdreams1006.github.io/git/usage/branch-overview.html * 9c30e50248b773e38b032477a859e87abe7c1bb0 learn git branch * b3d8193bbcb9f76c47e831e3e212f2405ae09f93 (origin/master, origin/HEAD) see https://snowdreams1006.github.io/git/usage/remote-repository.html * 8e625640348a47ac922409a1ecb4c844385582aa add test.txt * 9b196aab5bc87eeb11709c9eef35fca283e05c61 Initial commit $ 这种递归模式(recursive) 有一个明显的特点就是会产生一个新的 commit ,并不会像之前快速前进模式(fast forward)那样单纯更改 HEAD 的指向. 秉承着阅后即焚的习惯,分支一旦合并后就立即删除,现在删除 dev 分支,看一下会发生什么: # 删除 dev 分支 $ git branch -d dev Deleted branch dev (was 44d68f6). # 查看提交历史 $ git log --pretty=oneline --graph * 22fbef71b7575cd7eb7911079551618667f9f38f (HEAD -> master) git merge --no-ff dev |\\ | * 44d68f674bc85bc972426c572b78915e850e476c git checkout -b dev |/ * 3b8f434013caa8c27fade4c59d7aa2ee2c079636 fix conflict |\\ | * 0fe95f871b371834d30ea17faa82f84b7d67672b git commit c2 * | 0949cc319e099d554795d03c69ee38923af00d6c git commit c3 |/ * 5c482cd9965b9dfd4f273b43b240ed7db66167a8 git commit c1 * 413a4d1d2aab5ab85b6097d4b9f81cb5601c3b26 see https://snowdreams1006.github.io/git/usage/branch-overview.html * 9c30e50248b773e38b032477a859e87abe7c1bb0 learn git branch * b3d8193bbcb9f76c47e831e3e212f2405ae09f93 (origin/master, origin/HEAD) see https://snowdreams1006.github.io/git/usage/remote-repository.html * 8e625640348a47ac922409a1ecb4c844385582aa add test.txt * 9b196aab5bc87eeb11709c9eef35fca283e05c61 Initial commit $ 由此可见,删除 dev 分支后仅仅少了 dev 的引用而已,原来 dev 分支所做的更改全部保留下来了! 快速前进模式(fast forward) 创建并切换 dev 分支,提交版本后切换回 master 分支,然后再合并 dev 分支,使用 git merge dev 命令: # 创建并切换 dev 分支 $ git checkout -b dev Switched to a new branch 'dev' # 提交版本 $ echo \"fast forward\" >> test.txt $ git add test.txt $ git commit -m \"fast forward\" [dev 3fe94c0] fast forward 1 file changed, 1 insertion(+) $ 现在切换回 master 分支,采用默认的git merge 命令合并 dev 分支: $ git checkout master Switched to branch 'master' Your branch is ahead of 'origin/master' by 8 commits. (use \"git push\" to publish your local commits) sunpodeMacBook-Pro:git-demo sunpo$ git merge dev Updating 22fbef7..3fe94c0 Fast-forward test.txt | 1 + 1 file changed, 1 insertion(+) $ 上述内容显示这次合并采用的是快速前进模式(fast forward),让我们看一下提交历史: $ git log --pretty=oneline --graph * 3fe94c0088cae526eda1fb2ffa303001b1eb42ba (HEAD -> master, dev) fast forward * 22fbef71b7575cd7eb7911079551618667f9f38f git merge --no-ff dev |\\ | * 44d68f674bc85bc972426c572b78915e850e476c git checkout -b dev |/ * 3b8f434013caa8c27fade4c59d7aa2ee2c079636 fix conflict |\\ | * 0fe95f871b371834d30ea17faa82f84b7d67672b git commit c2 * | 0949cc319e099d554795d03c69ee38923af00d6c git commit c3 |/ * 5c482cd9965b9dfd4f273b43b240ed7db66167a8 git commit c1 * 413a4d1d2aab5ab85b6097d4b9f81cb5601c3b26 see https://snowdreams1006.github.io/git/usage/branch-overview.html * 9c30e50248b773e38b032477a859e87abe7c1bb0 learn git branch * b3d8193bbcb9f76c47e831e3e212f2405ae09f93 (origin/master, origin/HEAD) see https://snowdreams1006.github.io/git/usage/remote-repository.html * 8e625640348a47ac922409a1ecb4c844385582aa add test.txt * 9b196aab5bc87eeb11709c9eef35fca283e05c61 Initial commit $ 上述内容表明,此次合并并没有产生新的 commit ,只是更改下 HEAD 指向而已(HEAD -> master, dev). 同样,现在删除 dev 分支,再看一下提交历史: # 删除 dev 分支 $ git branch -d dev Deleted branch dev (was 3fe94c0). # 查看提交历史 $ git log --pretty=oneline --graph * 3fe94c0088cae526eda1fb2ffa303001b1eb42ba (HEAD -> master) fast forward * 22fbef71b7575cd7eb7911079551618667f9f38f git merge --no-ff dev |\\ | * 44d68f674bc85bc972426c572b78915e850e476c git checkout -b dev |/ * 3b8f434013caa8c27fade4c59d7aa2ee2c079636 fix conflict |\\ | * 0fe95f871b371834d30ea17faa82f84b7d67672b git commit c2 * | 0949cc319e099d554795d03c69ee38923af00d6c git commit c3 |/ * 5c482cd9965b9dfd4f273b43b240ed7db66167a8 git commit c1 * 413a4d1d2aab5ab85b6097d4b9f81cb5601c3b26 see https://snowdreams1006.github.io/git/usage/branch-overview.html * 9c30e50248b773e38b032477a859e87abe7c1bb0 learn git branch * b3d8193bbcb9f76c47e831e3e212f2405ae09f93 (origin/master, origin/HEAD) see https://snowdreams1006.github.io/git/usage/remote-repository.html * 8e625640348a47ac922409a1ecb4c844385582aa add test.txt * 9b196aab5bc87eeb11709c9eef35fca283e05c61 Initial commit $ 由此可见,快速前进模式一旦删除分支后就彻底丢失了分支的信息,即便是从提交历史中也找不到曾经存在的痕迹! 分支策略 git 是分布式版本控制系统,同时鼓励大量使用分支,如此一来大量的分支该如何管理? 实际开发中,建议准从以下原则进行分支管理: master 分支作为主干分支,负责对外提供服务,要求稳定可靠,因为应该专人负责更新维护. dev 分支作为开发分支,取代 master 分支的开发地位,积累到一定产出时再合并到 master 分支. feature 分支作为新功能分支,根据实际情况动态创建,删除分支,并适时合并到 dev 分支. bugFixed 分支作为修复特定 bug 分支,可能由 master 分支衍生而来,也可能由 dev 分支衍生等等,修复后及时合并到原分支. custom 自定义分支,项目成员私有分支,由上级领导分配任务后各开发人员自行选择创建自己的分支,并根据实际情况决定合并到 dev 分支或 feature 等分支. 小结 快速前进模式(git merge )不保留分支合并历史,递归模式(git merge --no-ff -m )保留分支合并历史. 制定大家都认同的分支管理原则,并严格准守规则. © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:08:00 "},"git/usage/branch-emergency-fixbug.html":{"url":"git/usage/branch-emergency-fixbug.html","title":"紧急修复","keywords":"","body":"紧急修复 和往常一样,每个人团队开发者都在自己的本地分支上进行日常工作,相互独立又相互联系,一直以来相安无事,可是某天下午,上级领导突然急冲冲的打电话告诉你线上出bug了,需要你紧急修复,下班之前必须解决! 我们天生就是创造 bug 的特殊群体,每天都在和各种各样的 bug 打交道,早已经习惯了这样的工作节奏,再也没有当初刚刚遇到紧急问题的手足无措,先喝杯茶,冷静一下,然后汇报领导说:放心吧!保证30min 内解决问题! 背景 学习了分支操作的相关知识,团队内部就基本的开发流程达成一致: 假设线上是主干 master 分支,开发是 dev 分支,团队成员是自定义 custom 分支,平时开发时在大家在各自 custom 分支上工作,完成分配任务后再合并到开发 dev 分支,等到开发分支功能稳定后,由项目领导负责合并到主干分支 master . 上述流程只是开发流程的简化版,实际情况更加复杂,后续再介绍 gitflow 工作流相关知识. 由于是线上出现 bug,理所当然是基于 master 分支检出临时分支,修复分支代号为 issue-110,然后定位 bug 并提交,最后再合并到 master 分支,如此一来成功修复 bug,完成既定任务,心安理得准备下班回家! 如果真的向上述步骤那样操作,显然还不够冷静,刚才那一杯茶算是白喝了!因为这样操作可能会丢失现场数据,那很多工作岂不是白做了,下面简单演示一下: 错误示例 (一). 事发前正在自定义的 snow 分支上愉快编码中... # 线上分支 `master`,开发分支 `dev`,自定义分支 `snow`,当前正处于自定义分支 $ git branch dev master * snow # 接到领导电话前正在自定义 `snow` 分支上进行愉快编码中... $ echo \"Happy coding\" >> test.txt $ git add test.txt $ git commit -m \"Happy coding\" (二). 事发时直接检出主分 master 分支,并紧急修复 bug . (2.1) 基于 master 分支检出 issue-110 分支,并修复提交. # 注意: 事发时正在思考人生,此时更改尚未添加到暂存区! $ echo \"who am i\" >> test.txt # 当前情况下,默认不允许直接切换到其他分支,因为工作区更改会被重写,这里为了演示错误示例,强制切换! $ git checkout -f master # 基于主干 `master` 分支检出修复 `issue-110`分支 $ git checkout -b issue-110 Switched to a new branch 'issue-110' # 定位线上 `bug`并修复,假设将 `fast forward` 更改为 `fast forward not recommend`,瞬间修复 `bug`有没有! $ cat test.txt add test.txt see https://snowdreams1006.github.io/git/usage/remote-repository.html learn git branch see https://snowdreams1006.github.io/git/usage/branch-overview.html git commit c1 git commit c2 and c3 git checkout -b dev fast forward $ vim test.txt $ cat test.txt add test.txt see https://snowdreams1006.github.io/git/usage/remote-repository.html learn git branch see https://snowdreams1006.github.io/git/usage/branch-overview.html git commit c1 git commit c2 and c3 git checkout -b dev fast forward not recommend # 修复 `bug` 后,提交更改并备注已修复 $ git add test.txt $ git commit -m \"fix bug about issue-110\" [issue-110 e60c8ad] fix bug about issue-110 1 file changed, 1 insertion(+), 1 deletion(-) sunpodeMacBook-Pro:git-demo sunpo$ git status On branch issue-110 nothing to commit, working tree clean $ (2.1) 切换到主干 master 分支,并合并修复 issue-110 分支 # 切换回 `master` 分支,合并修复 `issue-110` 分支 $ git checkout master Switched to branch 'master' Your branch is up to date with 'origin/master'. $ git merge issue-110 Updating 3fe94c0..e60c8ad Fast-forward test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) # 验证 `bug` 已修复: 更改为 `fast forward not recommend` $ cat test.txt add test.txt see https://snowdreams1006.github.io/git/usage/remote-repository.html learn git branch see https://snowdreams1006.github.io/git/usage/branch-overview.html git commit c1 git commit c2 and c3 git checkout -b dev fast forward not recommend $ (三). 事发后切换回自定义 snow 分支,打算下班回家. # 切换回 `snow` 分支,发现丢失了事发前的未保存更改:`who am i` $ git checkout snow Switched to branch 'snow' $ cat test.txt add test.txt see https://snowdreams1006.github.io/git/usage/remote-repository.html learn git branch see https://snowdreams1006.github.io/git/usage/branch-overview.html git commit c1 git commit c2 and c3 git checkout -b dev fast forward Happy coding $ 现在还打算下班吗?你所做的更改因为没有提交或者不能提交造成全部丢失! 结果 因为手头工作进行到一半无法提交或者忘记提交等原因,为了临时修复紧急 bug 而直接切换到目标分支再回来时发现更改全部丢失,相当于那部分工作白忙活了! 正确示例 经过上述错误示例的惨痛教训后,再也不敢轻易切换分支了,原因在于工作区更改并没有被提交,或者说不能提交,如果能够有一种机制来保护案发现场,这样我们就能放心切换到其他分支工作,回来时一切如初,那该多好? 幸运的是,git 确实提供这么一种机制,git stash 命令临时存储工作区,类似\"草稿箱\"作用. (一). 恢复工作区丢失更改,并使用 git stash 命令保存现场. # 修复工作区丢失更改: 同样未添加到暂存区 $ echo \"learn git stash\" >> test.txt $ cat test.txt add test.txt see https://snowdreams1006.github.io/git/usage/remote-repository.html learn git branch see https://snowdreams1006.github.io/git/usage/branch-overview.html git commit c1 git commit c2 and c3 git checkout -b dev fast forward Happy coding learn git stash # 保护现场: 存储到\"草稿箱\" $ git stash Saved working directory and index state WIP on snow: 93227ba Happy coding (二). 切换到开发 dev 分支并合并修复 issue-110 分支. # 切换到开发 `dev` 分支 $ git checkout dev Switched to branch 'dev' sunpodeMacBook-Pro:git-demo sunpo$ git status On branch dev nothing to commit, working tree clean # 合并修复 `issue-110` 分支 $ git merge issue-110 Updating 3fe94c0..e60c8ad Fast-forward test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) sunpodeMacBook-Pro:git-demo sunpo$ git status On branch dev nothing to commit, working tree clean $ (三). 切换回自定义 snow 分支,并恢复工作现场. # 切换回自定义 `snow` 分支 $ git checkout snow Switched to branch 'snow' sunpodeMacBook-Pro:git-demo sunpo$ git status On branch snow nothing to commit, working tree clean $ git status 命令返回结果怎么显示工作区是干净的,好不容易才将丢失的更改找回来怎么又不见了?!逗我玩? 冷静,冷静,不要慌,既然工作现场已经保存到\"草稿箱\",那我们想要找回总要去\"草稿箱\"才能取出来吧?现在让我们看一下\"草稿箱\"有没有我们的工作现场? # 查看存储的\"草稿箱\"列表 $ git stash list stash@{0}: WIP on snow: 93227ba Happy coding $ 这里的 stash@{0} 是草稿 id,因为\"草稿箱\"允许保存多条草稿! 现在放心了吧,保存的\"草稿\"安然无恙躺在未知的某个地方,现在我们想办法恢复回工作区即可! git stash apply 恢复草稿,然后 git stash drop 删除草稿 git stash pop 恢复并删除草稿 # 恢复工作现场 $ git stash pop On branch snow Changes not staged for commit: (use \"git add ...\" to update what will be committed) (use \"git checkout -- ...\" to discard changes in working directory) modified: test.txt no changes added to commit (use \"git add\" and/or \"git commit -a\") Dropped refs/stash@{0} (b0c8ddc034d21f31204c82e9838fc5d4c01a49a8) # 工作现场已恢复,更改未添加到暂存区,`learn git stash` 又恢复了! $ git status On branch snow Changes not staged for commit: (use \"git add ...\" to update what will be committed) (use \"git checkout -- ...\" to discard changes in working directory) modified: test.txt no changes added to commit (use \"git add\" and/or \"git commit -a\") $ cat test.txt add test.txt see https://snowdreams1006.github.io/git/usage/remote-repository.html learn git branch see https://snowdreams1006.github.io/git/usage/branch-overview.html git commit c1 git commit c2 and c3 git checkout -b dev fast forward Happy coding learn git stash 结果 不论手头工作有没有提交,一旦工作区保存到\"草稿箱\"后,就放心大胆切换分支进行工作,回来时岁月静好,一切如初! 小结 紧急修复 bug 时,可以通过 git stash 保护工作现场,然后再切换到目标分支,检出修复分支,完成修复后切换到目标分支,合并修复分支,最后删除修复分支,此时再切换回本地分支后一切如初! 工作区更改添加到\"草稿箱\" : git stash,支持多次添加到\"草稿箱\" 列出\"草稿箱\"内容 : git stash list 恢复\"草稿箱\"内容 : git stash apply 删除\"草稿箱\"内容 : git stash drop 恢复并删除\"草稿箱\"内容 : git stash pop 恢复|删除指定\"草稿箱\"内容 : git stash ,例如 git stash apply stash@{0} © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:08:03 "},"git/usage/branch-rebase.html":{"url":"git/usage/branch-rebase.html","title":"变基合并","keywords":"","body":"变基合并 git 鼓励大量使用分支---\"早建分支!多用分支!\",这是因为即便创建再多的分支也不会造成存储或内存开销,并且分支的作用有助于我们分解逻辑工作,这样一样其实比维护单一臃肿分支要简单得多! 正因如此,每个新功能会创建合并分支,修复 bug 会创建合并分支等等,一段时间后再次回顾整个版本库的提交历史就会发现分支错综复杂,难以理清! 虽然\"条条大路通罗马\",但错综复杂的道路容易让人迷失方向,如果不使用分支,当然就不存在\"分叉问题\",所以在某些情况下我们希望寻求一种替代方案来解决分支合并带来的\"分叉问题\"! 回顾提交历史 查看提交历史: git log --pretty=oneline --graph --abbrev-commit # 查看提交历史 $ git log --pretty=oneline --graph --abbrev-commit * e60c8ad (HEAD -> dev, origin/master, origin/HEAD, master) fix bug about issue-110 * 3fe94c0 fast forward * 22fbef7 git merge --no-ff dev |\\ | * 44d68f6 git checkout -b dev |/ * 3b8f434 fix conflict |\\ | * 0fe95f8 git commit c2 * | 0949cc3 git commit c3 |/ * 5c482cd git commit c1 * 413a4d1 see https://snowdreams1006.github.io/git/usage/branch-overview.html * 9c30e50 learn git branch * b3d8193 see https://snowdreams1006.github.io/git/usage/remote-repository.html * 8e62564 add test.txt * 9b196aa Initial commit 仅仅是简单的演示项目的提交历史都已经出现\"分叉问题\",更何况真实的企业级开发项目呢?如果真的是多分支多人合作开发的话,\"分叉现象\"将更加明显,模拟效果图大概长这样: 整理提交历史 如果想要一条直路直达罗马,那我们必须规划好路径,摒弃小道,坚持主干道.git 的各种 dev,feature等分支就是需要治理的一条条分叉小道,而 master 主分支就是我们的大道. 演示项目有三个分支,主干master,开发dev,自定义snow,目标是将自定义 snow 分支的工作成功整理合并到主干分支,从而解决\"分叉问题\",dev 分支与项目演示无关,无需更改. (1). 切换到 snow 分支并提交一个版本(learn git rebase) # 切换到 `snow` 分支 $ git checkout snow Switched to branch 'snow' # 追加新内容到 `test.txt` 文件 $ echo \"learn git rebase\" >> test.txt # 提交到版本库 $ git commit -am \"learn git rebase\" [snow 7d21e80] learn git rebase 1 file changed, 1 insertion(+) $ (2). 切换到 master 分支也提交一个版本(modify README) # 切换回 `master` 分支 $ git checkout master Switched to branch 'master' Your branch is up to date with 'origin/master'. # 追加新内容到 `README.md` 文件 $ echo \"learn git ,share git\" >> README.md # 提交到版本库 $ git add README.md $ git commit -m \"modify README\" [master 3931d48] modify README 1 file changed, 1 insertion(+) $ (3). 切换回 snow 分支,整理提交历史(git rebase)到 master 分支 # 切换到 `snow` 分支 $ git checkout snow Switched to branch 'snow' # 改变基础版本(父版本),简称\"变基\" $ git rebase master HEAD is up to date. # 当前提交历史线 $ git log --pretty=oneline --graph --abbrev-commit * e92f068 (HEAD) rebase * 72f4c01 fix confict about happy coding * 3931d48 (master) modify README * e60c8ad (origin/master, origin/HEAD, dev) fix bug about issue-110 * 3fe94c0 fast forward * 22fbef7 git merge --no-ff dev |\\ | * 44d68f6 git checkout -b dev |/ * 3b8f434 fix conflict |\\ | * 0fe95f8 git commit c2 * | 0949cc3 git commit c3 |/ * 5c482cd git commit c1 * 413a4d1 see https://snowdreams1006.github.io/git/usage/branch-overview.html * 9c30e50 learn git branch * b3d8193 see https://snowdreams1006.github.io/git/usage/remote-repository.html * 8e62564 add test.txt * 9b196aa Initial commit $ (4). 切换回 master 主干分支再次变基合并 snow 分支 # 切换回 `master` 分支 $ git checkout master Warning: you are leaving 2 commits behind, not connected to any of your branches: e92f068 rebase 72f4c01 fix confict about happy coding If you want to keep them by creating a new branch, this may be a good time to do so with: git branch e92f068 Switched to branch 'master' Your branch is ahead of 'origin/master' by 1 commit. (use \"git push\" to publish your local commits) # 改变父版本为 `snow` 分支指向的版本 $ git rebase snow First, rewinding head to replay your work on top of it... Applying: modify README $ (5). 整理分支完成,最终主干分支是一条直线 # 查看提交历史线 $ git log --pretty=oneline --graph --abbrev-commit # `modify README` 是 `master` 分支提交的版本 * dcce09c (HEAD -> master) modify README # `learn git rebase` 是 `snow` 分支提交的版本 * 7d21e80 (snow) learn git rebase * a06a866 fix conflict |\\ | * e60c8ad (origin/master, origin/HEAD, dev) fix bug about issue-110 * | ab846f9 learn git stash * | 93227ba Happy coding |/ * 3fe94c0 fast forward * 22fbef7 git merge --no-ff dev |\\ | * 44d68f6 git checkout -b dev |/ * 3b8f434 fix conflict |\\ | * 0fe95f8 git commit c2 * | 0949cc3 git commit c3 |/ * 5c482cd git commit c1 * 413a4d1 see https://snowdreams1006.github.io/git/usage/branch-overview.html * 9c30e50 learn git branch * b3d8193 see https://snowdreams1006.github.io/git/usage/remote-repository.html * 8e62564 add test.txt 这一次我们没有使用 git merge 而是采用 git rebase 方式完成了分支的合并,优点是提交历史更清晰,缺点是丢失了分支信息. 小结 git rebase 变基合并分支,实际上就是取出一系列的提交版本并“复制”到目标版本,从而形成一条新的提交历史线. 比如我们想要把 bugFix 分支里的工作直接移到 master 分支上,移动以后会使得两个分支的功能看起来像是按顺序开发,但实际上它们是并行开发的,这就是 git rebase 的作用. git rebase 的优势是创造更线性的提交历史,使得代码库的提交历史变得异常清晰,劣势是缺失了分支信息,好像从没存在过该分支一样. 将目标分支上的工作成果转移到到主干分支 : git rebase master 主干分支接收已转移好的目标分支工作成果 : git rebase © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:08:00 "},"git/usage/branch-remote.html":{"url":"git/usage/branch-remote.html","title":"协同开发","keywords":"","body":"协同开发 前面我们已经介绍过远程仓库的相关概念,不过那时并没有深入探讨,只是讲解了如何创建远程仓库以及推送最新工作成果到远程仓库,实际上远程仓库对于团队协同开发很重要,不仅仅是团队协同开发的基础,也是代码备份的保障手段,现在我们先简单回忆下相关概念,以便为接下来的协同开发做好铺垫! 远程仓库和远程分支 远程仓库 远程仓库其实并不复杂,实际上只是本地电脑上的本地仓库在另一台远程电脑的备份而已. 相对本地仓库来说远程电脑上的版本库自然就是远程仓库,远程仓库使得我们的版本库更加安全,毕竟远程电脑可不是一般的电脑,出错的概率比我们平时工作所使用的电脑概率要小得多,这样一来即使不小心丢失了本地仓库的全部数据,只要远程仓库没有丢失,那我们就可以通过远程仓库重新取回最新数据! 还有一点,远程仓库让代码社交化,因为大家有了一致途径来访问远程仓库,团队也好或者陌生人也罢,只有你愿意,他们就可以获取远程仓库的最新代码并参与开发,这也是 github 的一大亮点! 远程分支 回顾好远程仓库的概念后,我们再来讲一下本地仓库的远程分支是什么意思? 当前你正在工作的电脑上存储的是本地仓库,如果没有远程仓库的支持,只能一个人鼓捣,别人无法共享你的工作成果,现在加入了团队开发流程,自然不再一个人独自开发,需要和团队其他人协同开发,共享开发成果. 所以本地仓库必然保存着远程仓库的基本信息,只有区分好自己的工作成果和公共成果,才能不乱套,又能做到信息及时共享. 实际上,在项目初期刚刚拷贝远程仓库(git clone)时,git 已经默认在本地仓库创建一个远程分支(origin/master),本地修改提交首先都是在本地仓库完成的,比如 git add,git commit 等命令,如果需要发布你的工作成果,那么就需要使用 git push origin 命令推送到远程仓库,这里的 origin 指的就是远程仓库名称(因为最初大家都是先从远程仓库克隆下来的,所以远程仓库存储的项目相当于原始项目,故而叫origin). git clone 命令帮助本地仓库的 master 分支和远程仓库的 master 分支建立了关联,一般称远程仓库名称为 origin. 查看远程仓库信息 : git remote 或 git remote -v # 查看远程仓库名称 $ git remote origin # 查看远程仓库详情 : 拉取和推送链接 $ git remote -v origin [email protected]:snowdreams1006/git-demo.git (fetch) origin [email protected]:snowdreams1006/git-demo.git (push) $ 本地分支推送到远程仓库 : git push origin 本地仓库和远程仓库的分支理论上应该一一对应,本地仓库的主干分支叫做 master ,而远程仓库也有相应的分支叫做 master ,这种映射关系是使用 git clone 命令时默认生成的,也是推荐的做法. 一般来说,本地仓库的分支推送到远程仓库指的就是推送到远程仓库同名的分支上,例如 git push origin master 意思是: 推将本地仓库的 master 分支推送到远程仓库的 master分支,当然你也可以推送其他分支到相应的远程分支上. 按照之前约定的分支管理策略来说,master 分支用于生产环境部署,dev 分支用于收集开发成果,feature 分支用于开发具体功能分支,既然如此,那这些本地分支哪些需要同步推送到远程仓库就比较清晰了! 推送本地 master 分支到远程仓库的 master 分支 : git push origin master 推送本地 dev 分支到元层仓库的 dev 分支 : git push origin dev # 查看当前分支 : `master` 主分支 $ git branch dev * master snow # 推送本地 `master` 分支到远程仓库 `origin` 上相应的 `master` 分支 $ git push origin master Counting objects: 15, done. Delta compression using up to 4 threads. Compressing objects: 100% (15/15), done. Writing objects: 100% (15/15), 1.31 KiB | 1.31 MiB/s, done. Total 15 (delta 9), reused 0 (delta 0) remote: Resolving deltas: 100% (9/9), completed with 3 local objects. To github.com:snowdreams1006/git-demo.git e60c8ad..dcce09c master -> master $ 正常来说,本地仓库的 master 分支应该领先远程仓库 origin 上的 master 分支若干个版本. 一旦我们已经将本地分支上的工作成果推送到远程仓库上相应分支时,本地仓库和远程仓库这时候就保持一致了. $ git status On branch master Your branch is up to date with 'origin/master'. nothing to commit, working tree clean $ 远程仓库下载到本地分支 : git fetch 远程仓库的操作可以简单归纳为两部分: 上传和下载. 本地仓库推送到远程仓库是上传,而远程仓库拉取到本地仓库就是下载. 团队多人协作开发时,大家都会定期或不定期往 master 或 dev 等分支上推送各自的更改,相应的我们就需要下载别人的最新工作成果. 现在模拟其他伙伴正在往 master 分支上推送更改,最好在另一个电脑另一个账户,当然模拟的话也可以是同一个电脑下其他目录,或者最简单的方式,直接登录 github 更改 master 分支上某个文件内容,简单起见,我们采用最后一种方式. 其他伙伴已往远程仓库上的 master 分支提交了新的版本: 创建 git-remote.txt 文件 现在我们想要下载其他人的最新工作成果,接下来让我们看看本地仓库的 master 还能和远程仓库的 master 分支保持一致吗? # 下载远程仓库的 `master` 分支 $ git fetch origin master remote: Enumerating objects: 4, done. remote: Counting objects: 100% (4/4), done. remote: Compressing objects: 100% (2/2), done. remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 Unpacking objects: 100% (3/3), done. From github.com:snowdreams1006/git-demo * branch master -> FETCH_HEAD dcce09c..10942ff master -> origin/master $ 执行 git fetch 命令后,远程仓库上的最新提交记录已经下载到本地仓库,同时更新了本地仓库的远程分支origin/master ,值得注意的是本地仓库的 master 分支并没有更新! 那你可能会有疑问了,我想要的结果是下载其他人的最新工作成果,怎么我本地仓库的 master 分支并没有更新呢? # 查看工作区 $ ls LICENSE README.md test.txt # 查看版本库状态 $ git status On branch master Your branch is behind 'origin/master' by 1 commit, and can be fast-forwarded. (use \"git pull\" to update your local branch) nothing to commit, working tree clean $ 既然 git fetch 并没有更新本地仓库的 master 分支,那它到底做了哪些工作呢? git fetch 会做的事情 实际上, git fetch 完成了仅有的但是很重要的两步操作: 从远程仓库下载本地仓库中缺失的提交记录 更新本地仓库的远程分支(比如origin/master) 通过上述两步操作完成的效果是: 将本地仓库中的远程分支更新成了远程仓库相应分支最新的状态. 远程分支实际上是反映了远程仓库在你最后一次与它通信时的状态,而git fetch 就是你与远程仓库通信的方式了! git fetch 不会做的事情 git fetch 并不会改变你本地仓库的状态,所以也就不会更新你的 master分支,自然也不会修改你磁盘上的文件. 理解这一点很重要,因为许多开发人员误以为执行了 git fetch 以后,他们本地仓库就与远程仓库同步了. 实际上它可能已经将进行这一操作所需的所有数据都下载了下来,但是并没有修改你本地的文件. 既然本地仓库的远程分支已更新,那么想要更新本地仓库的 master 分支该如何做呢?很简单,可以 git merge 啊! 远程仓库更新到本地分支 : git pull 其实通过 git fetch 命令我们已经下载了远程仓库的最新版本,只不过还没有合并到本地仓库而已,如何合并分支相信大家已经轻车熟路了,有很多方法: git merge origin/master git rebase origin/master git cherry-pick origin/master 实际上,先抓取更新(git fetch)再合并(git merge)这个流程很常用,因此 git 是有专门的命令来完成这两步操作的,这就是拉取更新git pull --- 刚好与推送更新 git push 相反! # 拉取最新版本 $ git pull Updating dcce09c..10942ff Fast-forward git-remote.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 git-remote.txt # 查看版本库状态 $ git status On branch master Your branch is up to date with 'origin/master'. nothing to commit, working tree clean # 查看工作区内容: 文件已更新 $ ls LICENSE README.md git-remote.txt test.txt $ 团队协作 掌握了远程仓库和远程分支的相关概念后,现在开始真正模拟团队协作开发了,为了简单起见,仍然以直接操作 github 上的 master 分支为例说明如何协同开发. (1). 其他人已往远程仓库推送2个版本 (2). 你正在本地仓库提交1个版本 $ echo \"learn teamwork\" >> test.txt $ git commit -am \"learn teamwork\" [master f971647] learn teamwork 1 file changed, 1 insertion(+) $ (3). 你推送到远程仓库前先拉取最新版本 # 拉取最新版本,并尝试合并 $ git pull remote: Enumerating objects: 8, done. remote: Counting objects: 100% (8/8), done. remote: Compressing objects: 100% (5/5), done. remote: Total 6 (delta 0), reused 0 (delta 0), pack-reused 0 Unpacking objects: 100% (6/6), done. From github.com:snowdreams1006/git-demo 10942ff..612e08a master -> origin/master Merge made by the 'recursive' strategy. git-remote.txt | 2 ++ 1 file changed, 2 insertions(+) # 查看版本库状态 $ git status On branch master Your branch is ahead of 'origin/master' by 2 commits. (use \"git push\" to publish your local commits) nothing to commit, working tree clean # 查看其他人工作成果 $ cat git-remote.txt git remote git clone git commit -am \"fake second teamwork\" # 查看自己即将推送的工作成果 $ cat test.txt add test.txt see https://snowdreams1006.github.io/git/usage/remote-repository.html learn git branch see https://snowdreams1006.github.io/git/usage/branch-overview.html git commit c1 git commit c2 and c3 git checkout -b dev fast forward not recommend Happy coding learn git stash learn git rebase learn teamwork $ (4). 你将本地仓库更改内容推送到远程仓库 # 推送到远程仓库 $ git push origin master Counting objects: 5, done. Delta compression using up to 4 threads. Compressing objects: 100% (5/5), done. Writing objects: 100% (5/5), 564 bytes | 564.00 KiB/s, done. Total 5 (delta 3), reused 0 (delta 0) remote: Resolving deltas: 100% (3/3), completed with 3 local objects. To github.com:snowdreams1006/git-demo.git 612e08a..8fe5aba master -> master $ 现在前往 github 网站确认我们已经推送成功,我们的工作成果和其他人的工作成果同时存在于远程仓库中,这样就完成了一次团队协同开发的案例. 现在简单回顾一下整个协同开发流程: 其他人先于我们提交2个版本 我们本地提交1个版本 本地版本推送前拉取远程仓库 本地仓库推送到远程仓库 小结 查看远程仓库信息: git remote -v 本地仓库推送到远程仓库: git push origin 远程仓库抓取到本地仓库: git fetch 远程仓库拉取到本地仓库: git pull 相当于 git fetch 和 git merge 本地创建和远程仓库一致的分支: git checkout -b origin/,本地和远程分支名称最好一直,比如本地 master 和 远程 origin/master,本地 dev 和远程 origin/dev 本地分支和远程分支建立关联: git branch --set-upstream origin/ ,足够任性的话,本地 dev 可以关联远程 remote-dev 等,不过建议名称最好一致. 团队协同开发时,不仅平时要定期拉取(git pull),推送到远程仓库前更应先拉取(git pull)再推送(git push),如出现冲突,解决冲突后再推送. © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:08:00 "},"git/usage/local-remote-repository.html":{"url":"git/usage/local-remote-repository.html","title":"本地和远程仓库的本质","keywords":"","body":"本地和远程仓库的本质 本地仓库和远程仓库在本质上没有太大区别,只不过一个是本地电脑,一个是远程电脑. 远程仓库不一定非得是 github 那种专门的\"中央服务器\",甚至局域网的另外一台电脑也可以充当\"中央服务器\"的角色,因为它存在的最初目的只是方便大家交换彼此的提交记录而已! 所以本地仓库和远程仓库的基本行为应该是一致的,约定俗成的规定是远程仓库一般不直接参与日常开发工作,主要作为项目托管中心. 某些自动化持续集成环境中也可能会直接操作远程仓库,这时远程仓库就真的和本地仓库没什么区别了! 个人开发常用命令 个人开发看重的是效率,同时兼顾下版本控制的话算是是锦上添花,git 的本地仓库是本地备份,而远程仓库则是网盘备份. git init : 初始化本地项目 将本地项目初始化 git 项目,直观表现是在该项目同级目录下多了 .git 隐藏目录,其存储着 git 版本库相关信息. 此后当前项目便具备了本地管理的能力,可以与 git 进行交互. git clone : 克隆远程项目 同 git init 一样的作用,也是创建本地仓库,只不过 git init 是直接将本地项目作为本地仓库,而git clone 是将远程项目克隆到本地并作为本地仓库. 由此可见,git clone 比 git init 多了一层远程仓库的概念. git add : 添加文件 将工作区的提交记录添加到暂存区,暂存区是工作区和版本库交互的桥梁,暂存区积累到一定量的提交记录时可以批量提交到版本库,这一点暂存区有点像缓存. git commit : 提交文件 将暂存区的版本提交到版本库,从而形成工作区->暂存区->版本库的基本链路,本地工作区的版本控制流程大致如此. git push : 推送文件 如果是使用 git clone 命令克隆的本地项目,当工作到一定程度时可能需要将这部分工作成果推送到远程仓库,这时候使用 git push 命令完成本地版本的推送流程. 如果是使用 git init 命令初始化的本地项目,可能没有远程仓库,自然也就不需要推送.如果后来创建了远程仓库,那么你自然是想要将本地仓库推送到远程仓库的,因此你需要准确告诉 git 你要推送到哪个远程仓库. 使用 git remote add origin [email protected]:username/repos.git 命令添加远程仓库信息,这样就建立了本地仓库和远程仓库的关联,以后就可以正常推送到远程仓库了. 团队开发常用命令 团队开发注重的不仅是个人效率还有团队的整体进度,随着企业级开发的日趋复杂化,不再是一个人能够独立完成的,更何况时间也不允许慢慢完成,大多数公司采用的是人力换时间的方式,团队并行开发来缩短整个项目周期,这种复杂需求下正是 git 大展拳脚的好机会. 项目整体采用并行开发模式,拆解成不同的功能模块,每个人负责各自模块,模块之间相对独立但也不排除存在交集的可能性.对于每一个个体开发者来说,既需要版本控制又需要团队交流.这时候分支的作用就凸显出来了. 根据项目的业务特点将其拆解成不同的功能模块,这些功能模块分别代表不同的分支,而这些功能模块又组成了完整的项目,这就是主干和分支的关系. 初始时项目是一个整体,中间拆解成不同功能模块,最后再合并成一个整---\"分久必分合久必分\". git branch : 创建分支 每一个独立的功能模块被定义成一个单独分支,创建分支的过程其实是拆解项目的过程,创建本地分支后就在分支上开发特有功能,不再关心其他功能分支. git checkout : 切换分支 模块拆解完成并创建了相应的分支后,需要切换到既定分支上才能开展自己的工作. git merge : 合并分支 没有绝对的独立,项目再怎么拆分也是整体的一部分,肯定需要和其他功能模块发生关系,某些情况下需要其他分支的工作成果合并到自己的本地仓库中,这样才能完成一次小规模的组装. 可以预期的是,当这种组装足够多的时候,最终便会演变成项目的终极形态,形成一个整体. git fetch : 抓取远程分支 合并目标分支首先需要能够获取到目标分支的提交记录,既然每个功能模块都是不同的项目成员负责开发的,也就不在我们电脑上,所以我们先要将目标分支下载到我们本地电脑,然后才能合并该分支到本地分支. git pull : 拉取远程分支 \"先下载目标分支再合并到本地分支,从而小规模组成更复杂更强大的功能\",每一次的组装过程都需要两步操作者显然不符合懒人思维啊,git pull 就是这两步操作的简化命令,先下载再合并就是这么简单! 本地和远程仓库的碰撞 不论是个人开发还是团队开发,我们几乎习惯惯站在主动方的角度来思考问题,有没有想过当远程仓库接收到我们的git push 或 git pull 请求时,远程仓库发什么了什么改变,这种改变对本地仓库又有什么影响? 远程仓库(远程电脑上的本地仓库)只是众多分布式电脑上本地仓库中的一员,说它特殊也很特殊,充当着\"中央服务器\"作用,其余人统一从这里下载或推送;说它普通也很普通,和本地电脑上的本地仓库没有什么不同,因为它随时可被任意电脑上的本地仓库所取代! 揭开远程仓库的神秘面纱后,现在我们只需要将其视为普通的本地仓库一样对待即可,然而我们本地电脑上已经有了本地仓库,故而需要将远程仓库做一下简单标识区分(origin)称之为远程分支. 先说说 git push 命令做了什么? 对于本地来说,git 将本地仓库的指定分支推送到远程仓库的相应分支,同时更新了本地仓库的远程分支. 对于远程来说,git 接收到本地仓库的推送请求时应该在相应分支上合并本地分支,同时更新远程仓库的相应分支. 只要本地的指定分支成功推送到远程的相应分支时,对于本地来说,不论是指定分支还是远程分支(origin/master)都应该是最新状态,因为已经与服务器同步了. 而远程接收到此次推送请求时,应该尝试合并此次推送请求,再更新自己的相应分支,远程合并完成后再通知本地此次推送结果,如此一来,三端同步,皆大欢喜! 再讲讲 git pull 命令发生了什么? 对于远程来说,接收到本地的拉取请求时,因为没有新版本需要处理,所以无需任何操作. 对于本地来说,当远程仓库的相应分支下载到本地时应该更新远程分支状态,再尝试合并到本地的相应分支. git pull 命令或者说是 git fetch 命令是本地和远程通信的方式,所以 origin/master 会自动更新! 小结 本地仓库和远程仓库本质上没有太大区别, git fetch 是本地仓库和远程仓库之间的通信途径,本地仓库中的远程分支(origin/master)保存着它们之间最后一次的通信状态. © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:08:01 "},"git/usage/tag.html":{"url":"git/usage/tag.html","title":"里程碑式标签","keywords":"","body":"里程碑式标签 \"春风得意马蹄疾,一日看尽长安花\",对于项目也是如此,最值得期待的恐怕就要数新版本发布的时刻了吧?每当发布新版本时要么是版本号命名(比如v0.0.1)或者代号命名(比如Chelsea),不管怎么说这种里程碑阶段总是要留下些许纪念意义. 既然想要纪念这种特殊的历史时刻,自然是希望它能够固定下来,不要发生随意移动,产生不可预期后果. 这种需求其实和我们前面说的分支概念很相似,均是源于特殊的版本号,逐渐收集起一系列版本,最终形成一条相对独立的历史线,但分支并不是实现里程碑概念的最佳选择,为什么? 分支适合多人协作开发时互不影响,适当时机主动合并他人工作成果这种模式,而这种模式是由不同的功能模块进行驱动的,正所谓\"天下大势分久必合,合久必分\",当功能模块开发完毕后自然也就没有分支存在的必要性,更何况分支在收集版本的过程中会一直移动,并没有特殊的固定版本,显然分支不是最佳选择! 但是,分支确定一定程度上和里程碑概念很相似,源于特定版本,自主命名,收集版本等,那么何必重头再来,为何不复用已有概念呢? 实际上,git 中的标签(tag) 就是实现里程碑概念的方式,它可以永久性指向特定的提交并将命名,然后就可以将其理解成分支一样引用了! 但标签(tag)不是分支(branch),标签是一个点的话,分支就是若干点连接而成的线,标签是静态的,分支是动态的,标签是只读的,分只是可读可写的. 创建标签 git tag # 方式一: 默认 `HEAD` 指向的版本 git tag v0.0.1 # 方式二: 指定 `commit_id` 表示的版本 git tag v0.0.2 f971647 # 方式三: 指定 `commit_id` 表示的版本,同时创建标签说明信息 git tag -a v0.0.3 -m \"v0.0.3\" f971647 列出标签 git tag git tag 显示标签 git show git show v0.0.1 删除标签 git tag -d git tag -d v0.0.1 推送标签 git push origin git push origin v0.0.1 推送全部标签 git push origin --tags git push origin --tags 删除远程标签 git tag -d git push origin :refs/tags/ # 删除本地标签 git tag -d v0.0.1 # 推送删除标签(删除也是推送) git push origin :refs/tags/v0.0.1 © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:08:02 "},"git/custom/about.html":{"url":"git/custom/about.html","title":"私人定制","keywords":"","body":"个性化 git 前情概要 初识 git 时,我们就已经接触过 git 的基本配置,使用 git config 命令配置用户名和邮箱: # 配置当前项目(`local`)的用户名(`snowdreams1006`) git config --local user.name \"snowdreams1006\" # 配置当前项目(`local`)的邮箱(`[email protected]`) git config --local user.email \"[email protected]\" 快速回忆一下配置的相关语法: # 查看默认全部配置: `local>global>system` git config --list # 查看当前项目配置,等同于 `.git/config` 文件 git config --local --list # 查看当前用户配置,等同于 `~/.gitconfig` 文件 或 `~/.config/git/config` 文件 git config --global --list # 查看当前系统配置,等同于 `/etc/gitconfig` 文件 git config --system --list man git-config 查看帮助文档,git 的配置文件是普通文本,也可以直接编辑. 高频配置 总体来说,git 的配置项基本分为两类: 客户端和服务端.其中大部分属于客户端配置, 除非使用自己搭建私服,否则没机会手动配置服务端(第三方服务器基本都支持可视化配置,比如禁止强制推送等配置). alias 别名 熟悉 linux 操作的小伙伴对 ll 这个命令可能再熟悉不过了,是 ls -l 的缩写,称之为别名. git 也支持别名,有个别名我们可以将常用的命令都缩短,大大降低出概率,提高工作效率. # `git checkout` 缩写成 `git co` git config --global alias.co checkout # `git commit` 缩写成 `git ci` git config --global alias.ci commit # `git branch` 缩写成 `git br` git config --global alias.br branch 如此一来,以后再也不用担心打错字了,简化命令,懒人至上! core.editor 编辑器 默认情况下,git 使用的是 $VISUAL 或 $EDITOR 配置的文本编辑器,如果没有设置,则调用 vi 编辑器创建和编辑文本信息. 查看当前编辑器配置项: # 查看编辑器配置项: 若没配置过,则无内容输出,已配置过的话,会输出相应编辑器信息 git config core.editor 假设使用 sublime 作为默认编辑器,那么便可如下设置: # `Mac` 系统如下设置: 设置成自己的 `Sublime` 的安装路径 git config --local core.editor \"'/Applications/Sublime Text.app/Contents/SharedSupport/bin/subl' -n -w\" # `Windows` 系统如下设置: 设置成自己的 `Sublime` 的安装路径 git config --local core.editor \"'F:\\Sublime Text 3 sublime text.exe' -n -w\" 此时再次查看编辑器配置项应该会输出刚才配置信息,接下来我们验证下编辑器的效果: 查看提交历史,已经提交成功(之前备注信息是在命令行中直接输入的,而现在是在编辑器中编辑) $ git log --pretty=oneline --abbrev-commit 43fa8aa (HEAD -> master) validate sublime successfully 00e16d7 ok 2400f11 git config --local core.editor \"'/Applications/Sublime Text.app/Contents/SharedSupport/bin/subl' -n -w\" 0d60cb8 ok 8fe5aba (origin/master, origin/HEAD) Merge branch 'master' of github.com:snowdreams1006/git-demo $ 如果只是输入简单备注,根本用不到编辑器,若提交备注有格式化要求时再手动输入就显得力不从心了! core.template 提交模板 如果你需要格式化提交备注,那么这种情况下模板文件最好不过了,和自定义的编辑器一起搭配,这样就能约束自己和他人按照既定格式规范填写提交备注,方便以后统一管理. 查看当前提交模板配置: git config commit.template 假设你在当前项目下创建 commit-template.txt 模板文件,内容如下: # This is commit template # snowdreams1006 # git-demo 将编辑好的模板文件设置成提交默认信息,需要如下设置: git config --local commit.template commiit-template.txt 此时再次运行 git config commit.template 查看已配置提交模板,现在看一下实际效果: 查看提交历史,当然也提交成功啦,可根据实际需求定制适合自己的提交模板. $ git log --abbrev-commit commit a2ca3f0 (HEAD -> master) Author: snowdreams1006 Date: Wed Mar 27 16:22:18 2019 +0800 ok myself yes commit 43fa8aa Author: snowdreams1006 Date: Wed Mar 27 14:58:36 2019 +0800 validate sublime successfully commit 00e16d7 Author: snowdreams1006 Date: Wed Mar 27 14:56:20 2019 +0800 ok commit 2400f11 git 还支持其他配置,暂时不一一介绍了,详情请参考在线帮助文档: man git-config © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:08:04 "},"git/custom/ignore.html":{"url":"git/custom/ignore.html","title":"忽略文件","keywords":"","body":"忽略文件 \"并不是所有的牛奶都叫特仑苏\",在版本控制系统中也有相似的表达,那就是\"并不是所有的文件都需要提交\". 有的是因为没必要提交,比如日志文件,系统缓存文件等,有的是因为不能提交,比如个人隐私文件,付费文档等. 正常来说,这些文件都是不应该被提交到版本库,倘若一不留神提交到版本库,要么泄露机密信息,要是造成经济损失,要么对团队其他人工作造成不便. 有鉴于此,我们应该寻求一种机制来规避事故的发生,在 git 版本控制系统中一般有三种不同的解决方案. 最常用也是最简单的当属 .gitignore 文件,不过先不要着急,我们先了解一下忽略原则和配置规则. 忽略文件的基本原则 忽略操作系统自动生成的文件,保持不同操作系统的纯粹性和整洁度. 忽略工具软件自动生成的文件,避免因个性化配置而产生的工作障碍. 忽略个人隐私配置文件,除非你愿意承担公开隐私所带来的潜在风险. 目标: 只提交必要文件,忽略无用文件,尽可能考虑多种情况,不给他人制造麻烦. 忽略文件的配置规则 一行记录代表一条规则,配置规则仅针对尚未被跟踪的文件清单. # 忽略 `*.a` 文件 *.a # 忽略 `*.A` 文件,但 `somefile.A` 除外. *.A !somefile.A # 忽略 `*.b` 和 `*.B` 文件 *.[bB] # 忽略 `*.c` 和 `*.C` 文件,但 `somefile.C` 除外. *.[cC] !somefile.C # 只忽略 `somepath/` 目录(包括该目录下所有文件),但不忽略 `somepath` 文件 somepath/ # 只忽略 `somepath/` 一级子目录下 `*.txt`,但不忽略 `somepath/sub/*.txt` 文件 somepath/*.txt # 忽略 `somepath` 文件和 `somepath` 目录 somepath # 只忽略 `somepath` 文件,但不忽略 `somepath/` 目录 somepath !somepath/ # 只忽略当前目录下的 `somepath` 文件和目录,但不忽略子目录的 `somepath` /somepath 说明: # 开头表示注释,! 紧跟某规则之后表示增加例外情况 在线示例和帮助文档 提供两个不错的在线示例,可以参考下在什么场景应该忽略哪些文件以及如何编写忽略规则. https://www.gitignore.io/ https://github.com/github/gitignore 运行 git help ignore 命令查看帮助文档 三种设置方式 git 设置忽略文件有三种方式,如下: 全局配置文件(~/.gitignore),执行 git config --global core.excludesfile ~/.gitignore 命令后适用于所有的版本库. 远程配置文件($PWD/.gitignore),编辑 .gitignore 文件后适用于远程和本地版本库. 本地配置文件($PWD/.git/info/exlude),编辑 $PWD/.git/info/exlude 文件后适用于本地版本库. 最常用方式 三种设置方式中,第二种最为常见,另外两种大致一样,重点在于配置文件如何编写. 创建 .gitignore 文件 参考在线示例以及基本语法编写自定义忽略规则 # General .DS_Store .AppleDouble .LSOverride # Windows thumbnail cache files Thumbs.db ehthumbs.db ehthumbs_vista.db 提交 .gitignore 文件 忽略文件规则配置完毕后,需要将该文件提交到版本库,这样在其他电脑上也能应用相同的忽略规则. # 添加 `.gitignore` git add .gitignore # 提交 `.gitignore` git commit -m \"add .gitignore\" # 上传 `.gitignore` git push origin master 验证忽略效果 新建 .gitignore 文件中已忽略的文件,运行 git status 命令,如果提示 working directory clean,那么说明忽略文件的配置已经生效,如果工作区不干净,很遗憾,忽略文件配置可能并未生效,需要检查下哪里配置错了. 运行 git check-ignore 命令检查是哪个配置规则写错了,从而我们能够更正相应的配置规则. © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:08:04 "},"git/server/private.html":{"url":"git/server/private.html","title":"搭建私服","keywords":"","body":"git 私服搭建教程 前几节我们的远程仓库使用的是 github 网站,托管项目大多是公开的,如果不想让任何人都能看到就需要收费,而且 github 网站毕竟在国外,访问速度太慢,基于上述两点原因,我们有必要搭建自己的 git 服务器. 虽然我们能搭建基本的 git 服务器,但是想要做到 github 网站那种规模还不是目前能够探讨的,本节的主要目标是使用我们私有服务器对我提供类似于github的远程仓库托管服务,以下示例以centos 服务器为例说明: 安装 git 服务 运行以下命令安装 git 服务 # 安装 git 相关依赖 yum install curl-devel expat-devel gettext-devel openssl-devel zlib-devel perl-devel # 安装 git yum install git # 查看 git 版本 git --version 详情请参考安装 git 配置 git 用户 创建 git 用户组和 git 用户,以便对外提供 git 服务 # 新增 git 用户组 groupadd git # 新增 git 用户并归属于 git 用户组 useradd git -g git # 禁用 git 用户登录 shell 编辑 /etc/passwd git:x:1001:1001:,,,:/home/git:/bin/bash 更改为 git:x:1001:1001:,,,:/home/git:/usr/bin/git-shell 收集 git 公钥 回忆一下,在我们使用 github 网站时,我们是不是曾经将本地电脑生成的公钥~/.ssh/id_rsa.pub 复制到 Account -> Settings -> SSH and GPG keys -> New SSH key,而我们现在搭建的git 服务还是简单,但是这步骤必不可少,因此只能手动收集素有需要访问我们服务器的公钥文件. 我们知道需要登录我们服务器的用户公钥一般是存放在~/.ssh/id_rsa.pub ,那当前服务器作为远程服务器将这些公钥存放到哪里呢?还记得上一步我们创建了 git 用户吗? 因为 linux 系统支持多用户操作,而 git 用户就用于专门运行 git 服务,负责所有和 git 有关的事宜.因此,导入公钥文件的目录就是/home/git/.ssh/authorized_keys文件.一个用户公钥占用一行,几个用户就有几行. # 切换到 git 用户主目录 cd /home/git/ # 创建.ssh 目录 mkdir .ssh # 赋予标准目录权限 chmod 755 .ssh # 创建authorized_keys文件 touch .ssh/authorized_keys # 赋予标签文件权限 chmod 744 .ssh/authorized_keys 如果团队规模不大,那么上述方案完全可行,如果团队规模几百上千人,通过手动收集每个人的公钥再上传到服务器统一管理就有点力不从心了,这时候推荐 gitosis 决这一问题. 初始化 git 仓库 同样我们和github 网站类比,在 github 创建仓库时都会在当前账号下创建项目,完整的访问路径大概是这样的: [email protected]:snowdreams1006/git-demo.git,从中我们可以看出项目仓库都有一个前缀即命名空间,这和上一步操作是不是很类似,上一步收集 git 公钥时我们也有统一的目录,这次也不例外. 假设 git 仓库存放目录在 /home/git/repos/,同样的先创建该目录并赋予响应权限. # 切换到 git 用户主目录 cd /home/git/ # 创建 repos 目录 mkdir repos # 更改 repos 目录属主 chown git:git repos/ # 切换到 repos 目录 cd repos # 初始化 git 裸仓库 git init --bare git-demo.git # 更改 git-demo.git 仓库属主 chown -R git:git git-demo.git 这里搭建git服务器仅为了共享,不考虑用户直接登录该服务器上使用 git 将其作为工作区这一情况 经过上述操作,我们成功在远程服务器部署了 git 服务,并且创建了 git-demo 测试项目,实际访问路径大概是这样的 [email protected]:/home/git/repos/git-demo.git 访问授权 总是存在一些公司不仅视源代码为生命,还视员工为窃贼,抑或是深受svn毒害,要求在版本控制系统中设置一套完善的权限控制体系,具体到每个账号对每个项目的每个目录是否有读写权限. 然而 git 天生并不支持权限控制,这一点和其出身有关,本来就是为了开源而生,并不关心所有人的提交. 不过这并不意味着 git 无法实现权限控制功能,因为 git 支持钩子函数(hook) ,所以在服务器端编写一系列的脚本控制提交行为,从而实现权限控制.详情请参考 gitolite 本地克隆远程仓库 身份回到本地电脑,假设本地已搭建好 git 环境,并且生成的ssh 公钥上传到远程服务器,那么我们接下来就可以和之前远程服务器是 github 网站那样的方式开发我们的项目了,唯一不同的是,接下来我们推送的远程服务器均是我们刚搭建好的主机. 需要做好心里准备,我们搭建的服务器还很简单,没有 github 网站那样可以直观操作远程仓库,但是这并不影响我们的 pull push merge 等操作哟! git clone [email protected]:/home/git/repos/git-demo.git git-指的是 git 用户,snowdreams1006.cn-指的是远程主机域名或ip,/home/git/repos-指的是 git 仓库的目录,git-demo.git-指的是项目名称 现在我们已经成功搭建好自己的 git私服了,是不是很简单呢?有没有对 git 和 github 进一步理解?欢迎大家一起探讨! 小结 git 私服就是无 web 界面的简化版 github 小团队人工收集用户公钥,大团队使用 gitosis 实现类似 svn 那样的权限控制请使用 gitolite © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:08:03 "},"git/tools/about.html":{"url":"git/tools/about.html","title":"扩展工具","keywords":"","body":"扩展工具 © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:07:57 "},"git/tools/git-bash-command.html":{"url":"git/tools/git-bash-command.html","title":"git bash 常见命令","keywords":"","body":"git bash 常见命令 已投稿给脚本之家公众号,如需查看请访问: 从 git bash 命令行中窥探人生 https://mp.weixin.qq.com/s/5bSogfIMqmhgMcZ5NoYNlA © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:07:57 "},"git/tools/git-bash-relationship.html":{"url":"git/tools/git-bash-relationship.html","title":"git bash 朋友圈","keywords":"","body":"git bash 朋友圈 已投稿给脚本之家公众号,如需查看请访问: 看过git bash的朋友圈才知道cmd为啥会呵呵一笑 https://mp.weixin.qq.com/s/4t2OPNtlVL12AQjrqAjuHg © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:07:58 "},"git/tools/git-bash-tree.html":{"url":"git/tools/git-bash-tree.html","title":"git bash 竟然没有tree命令","keywords":"","body":"git bash 没有tree命令? 开门见山 git bash 是 Windows 用户安装 git 时默认安装的命令行工具,不仅界面漂亮功能也不错,大多数情况下可以替代 Windows 原生的 cmd 命令行. 然而,git bash 命令行不是万金油,并不能完全替代 cmd ,详情请参考 mintty 官网的相关说明. mintty is not a full replacement for the Windows Console window git bash 命令行默认使用 mintty 作为终端模拟器,而 mintty 官宣表示自己不能完全替代 cmd,也就是说 git bash 可能不具备某些 cmd 命令. 举个简单的例子,如果想要查看当前目录的文件结构,最好是以目录树的形式展现,聪明的你获取已经猜到了tree 命令. git bash 命令行中输入 tree 命令发现并无此命令. snowdreams1006@home MINGW64 /g/sublime/test $ tree bash: tree: command not found 为了验证,确实没有 tree 命令,我们直接打开 git bash 支持的命令文件目录,查看到底有没有 tree.exe 文件. 在 git bash 桌面快捷方式右键,选择打开文件位置,当前正处于 git 的安装目录,进入.\\usr\\bin 文件夹. 经过验证,git bash 支持的命令文件确实没有发现 tree.exe 文件,因此真的不支持 tree 命令. 然而,cmd 自带的命令行中输入 tree 中竟然发现能够输出目录树,原来 cmd 支持 tree 命令. G:\\sublime\\test>tree 卷 工作 的文件夹 PATH 列表 卷序列号为 00000081 CC3C:50D0 G:. ├─cmd └─git tree 命令其实调用的是 tree.com 并不是常见的 tree.exe 格式. 科普时间 现在我们已经知道 git bash 提供的终端模拟器不支持某些 linux 命令,但是为什么不支持以及如何才能支持这些命令呢? 这些问题必须等我们弄清楚 mintty 的朋友圈关系才能更好地解决上述问题,接下来简单科普下 mintty 的朋友圈. 关于科普知识的来源,请参考上一篇文章: > mintty 是什么 Mintty 是 Cygwin,MSYS 或 Msys2 的终端模拟器,派生项目和 WSL. mintty 开源终端模拟器,基于 putty 的终端仿真和 Windows 前端页面. mintty 作为一款优秀的终端模拟器,不仅是其他系统上默认的终端,也是 git bash 的默认终端. $ mintty --help Usage: mintty [OPTION]... [ PROGRAM [ARG]... | - ] Start a new terminal session running the specified program or the user's shell. If a dash is given instead of a program, invoke the shell as a login shell. Options: -c, --config FILE Load specified config file -e, --exec Treat remaining arguments as the command to execute -h, --hold never|start|error|always Keep window open after command finishes -i, --icon FILE[,IX] Load window icon from file, optionally with index -l, --log FILE|- Log output to file or stdout -o, --option OPT=VAL Override config file option with given value -p, --position X,Y Open window at specified coordinates -s, --size COLS,ROWS Set screen size in characters -t, --title TITLE Set window title (default: the invoked command) -u, --utmp Create a utmp entry -w, --window normal|min|max|full|hide Set initial window state --class CLASS Set window class name (default: mintty) -H, --help Display help and exit -V, --version Print version information and exit 如果想要自定义 mintty 终端,在 git bash 命令行界面右键选择选项设置即可打开设置页面. mingw 是什么 mingw 是 Minimalist GNU for Windows 的缩写,是 Microsoft Windows 应用程序的极简主义开发环境. msys 是什么 MSYS 是 Minimal SYStem 的缩写,是 Bourne Shell 命令行解释器系统.作为 Microsoft 的 cmd.exe 的替代品,它提供了一个通用的命令行环境,特别适合与 MinGW 一起使用,用于将许多开源应用程序移植到 MS-Windows平台; 它是 Cygwin-1.3 的轻量级分支,它包含一小部分 Unix 工具,可以帮助实现这一目标. cygwin 是什么 大量 GNU 和开源工具,提供类似于 Windows 上的 Linux 发行版的功能. gnu 是什么 GNU 是 GNU's Not Unix 的递归缩写,是自由软件操作系统. 朋友圈关系梳理 git bash 采用 mintty 作为终端模拟器,而 mintty 终端是 mingw ,msys2 和 cygwin的默认终端,这些\"操作系统\"或多或少都是 GNU 的一部分. GNU 是自由软件运动的成果,提出自由软件以及自由软件操作系统概念,源码开源发布. 正因如此,江山代有才人出,前人栽树后人乘凉,Cygwin 作为 GNU 一员,提出了要在 Windows 打造出 Linux 的感觉,开发出一套完整的解决方案. 或许由于这套方案太完整,功能齐全因而软件包体积庞大,因此 MinGW 和 MSYS 分别在其基于上进行精简重构,保留最简功能,发展出\"极简主义的GNU\". 开源的力量是可持续的,慢慢的,这些操作系统的内置终端功能也被单独提取出来,mintty 作为他们的默认终端也逐渐独立提供终端模拟器服务,轻松和各个系统进行安装集成. 说到开源,自然是少不了 git 的身影,分布式版本控制系统这种优秀工具应该造福全人类,然而 git 本身仅支持类 Unix 系统,并不提供 WIndows 系统的支持. Git For Windows 组织出手增加了 git 对 Windows 系统的支持,背后的技术多半离不开上述介绍的 GNU 操作系统. Git Bash Here 则是 Git For Windows 的命令行工具,使用的终端模拟器就是明星模拟器 mintty. 回到正题 弄清楚事情的来龙去脉后,对我们解决问题有什么帮助呢? 帮助可大了去了,刨根问题找到了源头,问题自然迎刃而解! 我劝少年放弃吧 你确定不是在逗我?让我直接放弃? 亲,真的抱歉呢,放弃是解决问题的最快途径! ... 谈一谈为什么要放弃? git bash 命令行使用的是 mintty 终端,而 mintty 终端并不能完全替代 cmd ,也没有提供包管理工具供我们扩展第三方命令. 所以默认情况下,如果没有提供某些命令,那我们只好放弃在 git bash 命令行中使用这些命令. 此路不通,自然会寻求其他解决途径,比如可以借助 cmd 命令行或者其他第三方软件等,没必要非要坚持使用git bash! 既然本文是 tree 命令引发的讨论,那就谈点和主题有关的技能点,不然岂不是跑题了? tree 命令虽然 git bash 不提供,但是 cmd 却已经内置了. 值得注意的是,cmd 提供的 tree 命令比较特殊,并不是常见的 .exe 结尾文件,而是 .com 结尾的文件. 所以 git bash 中输入 tree 命令时,自身 /usr/bin 中没有 tree.exe 文件,而系统中也没有 tree.exe 命令,那么就会提示找不到命令了啊! 手动补全命令 那么第一种调用方法便是补全后缀名,这样自然能够调用 cmd 的 tree.com 命令. git bash 并不识别 .com 后缀的命令,输入 tree 命令时以为是 tree.exe ,实际上 tree 命令应该是 tree.com 的简写. 我擦,竟然出现乱码,根据我多年的开发经验来看,乱码问题多半是编码问题导致的,那么修改下终端的编码设置应该就能解决问题. 在命令行窗口内右键弹出 mintty 终端的设置页面,选择文本(Text)中本地化(Locale)配置,选择中文简体(zh_CN),字符集(Character set)设置为UTF-8. 瞬间被打脸,无论是原来的git bash 命令行窗口还是新开的命令行窗口,仍然还是乱码! 然而,我是不会承认被打脸的,世人皆醉我独醒,肯定是 bug,哈哈! 其实,中文乱码真的是 bug ,还是官方认可的 bug 哟,我并没有被打脸呢,后续会介绍. 调用 cmd 程序 既然git bash 没有提供 tree 命令,而恰巧 cmd 提供了 tree 命令,那为何不假借他人之手实现自己的目的呢? 在 cmd 中使用 tree 命令直接输入即可得到目录树结构,但是现在需要在 git bash 中调用 cmd 中的 tree 命令. # 直接输入 `tree` 命令,正确响应并且无中文乱码. G:\\sublime\\test>tree 卷 工作 的文件夹 PATH 列表 卷序列号为 00000093 CC3C:50D0 G:. ├─cmd └─git # 输入 `cmd tree` 命令,虽无报错,但也没有正确响应. G:\\sublime\\test>cmd tree Microsoft Windows [版本 6.3.9600] (c) 2013 Microsoft Corporation。保留所有权利。 cmd 中直接输入 tree 命令即可,无需通过 cmd tree 这种方式,还以为你要调用 cmd 命令呢! 因此,我们需要告诉git bash 要通过 cmd 去调用下 tree 命令. # 注意看前缀是 `snowdreams1006@home MINGW64` 表明当前处于 `git bash` 环境 snowdreams1006@home MINGW64 /g/sublime/test # 输入 `cmd tree` 命令,虽无报错,但也没有正确响应,并且仍然有中文乱码. $ cmd tree Microsoft Windows [▒汾 6.3.9600] (c) 2013 Microsoft Corporation▒▒▒▒▒▒▒▒▒▒Ȩ▒▒▒▒ # 输出 `tree` 命令,正确响应但有中文乱码.此时命令行前缀已经更改为 `G:\\sublime\\test>` 表明当前不再处于`git bash` 环境! G:\\sublime\\test>tree tree ▒▒ ▒▒▒▒ ▒▒▒ļ▒▒▒ PATH ▒б▒ ▒▒▒▒▒к▒Ϊ 0000006B CC3C:50D0 G:. ▒▒▒▒cmd ▒▒▒▒git 通过上述操作结果来看,不难发现以下问题. cmd tree 命令切换到 cmd 环境,并且tree 命令并没有执行. 换句话说,cmd tree 和 cmd dir 或者 cmd 的作用相同,都是切换了当前 bash 环境. 进入 cmd 命令行运行 tree 能够得到正确响应,但存在中文乱码. 想要退出 cmd 环境,Ctrl + C 组合即可重新回到 git bash 环境. 由此可见,不加任何参数冒昧进入到 cmd 环境还是比较麻烦的,因此下面提供带参数的命令帮助我们阅后即焚. cmd //c tree 命令,阅后即焚,表示执行完立即退出. snowdreams1006@home MINGW64 /g/sublime/test $ cmd //c tree ▒▒ ▒▒▒▒ ▒▒▒ļ▒▒▒ PATH ▒б▒ ▒▒▒▒▒к▒Ϊ 00000008 CC3C:50D0 G:. ▒▒▒▒cmd ▒▒▒▒git snowdreams1006@home MINGW64 /g/sublime/test $ 执行命令前后我们都在 git bash 环境并且在 cmd 中得到正确响应结果,唯一的区别就是多加了 //c 参数,表示执行完命令立即退出 cmd 环境. 优雅调用 cmd 简单总结下,如何在 git bash 中借助 cmd 实现 tree 命令. tree.com : 补全调用命令后缀名,直接调用系统命令. cmd //c tree : 借助 cmd 运行 tree 命令,从而实现调用 tree 的目的. 这两种方式都存在中文乱码问题,即使设置了终端的编码方式也没有解决乱码. 调用 tree 命令的目的已经达到,没有解决的问题是中文乱码. 解决问题最快速的方式是百度一下或者从官网寻求帮助,这次我选择后者,因为百度一下人人都会,不用我再讲了吧! 简单解释下这段话的意思: 如果在 mintty 终端调用原生 cmd 程序,简单的输出指令没有什么问题,交互指令可能存在问题. 因此建议使用 winpty 进行包装再调用原生 cmd 程序. winpty 是一种提供与cmd 通信的软件包,详情请参考https://github.com/rprichard/winpty 有什么神奇之处?不妨加上 winpty 试试看! snowdreams1006@home MINGW64 /g/sublime/test # `winpty` + `tree.com` : 正常输出且无中文乱码 $ winpty tree.com 卷 工作 的文件夹 PATH 列表 卷序列号为 00000074 CC3C:50D0 G:. ├─cmd └─git snowdreams1006@home MINGW64 /g/sublime/test # `winpty` + `cmd //c tree` : 正常输出且无中文乱码 $ winpty cmd //c tree 卷 工作 的文件夹 PATH 列表 卷序列号为 000000B3 CC3C:50D0 G:. ├─cmd └─git snowdreams1006@home MINGW64 /g/sublime/test $ 果然是神药,一下子就治好了我多年的老寒腿啊! winpty tree.com : 不叫小名而叫全称,直接调用系统命令 winpty cmd //c tree : 假借他人之手,变相调用系统命令 上文中说设置文件编码应该能够解决中文乱码问题结果仍然有乱码,猜测是 bug ,现在没有打脸吧? 既然已经提供了解决方案,那文章是不是应该到此为止了呢? 不不不,远远还没结束,这只是开胃小菜,好戏还在后头呢. 更何况这命令也忒长了,记不住啊! 很简单,可以设置别名啊,把常用命令设置成别名,这样就记住啦! 输入 alias 命令没有报错,说明目前环境是支持设置别名的. snowdreams1006@home MINGW64 /g/sublime/test $ alias alias ll='ls -l' alias ls='ls -F --color=auto --show-control-chars' alias node='winpty node.exe' 按照 linux 的操作习惯,命令行设置的一般都是临时性的,想要永久生效,都要写入到文件中,别名这种当然要一劳永逸设置成永久文件. snowdreams1006@home MINGW64 /g/sublime/test $ cat /etc/bashrc cat: /etc/bashrc: No such file or directory snowdreams1006@home MINGW64 /g/sublime/test $ cat ~/.bashrc cat: /c/Users/snowdreams1006/.bashrc: No such file or directory 竟然配置文件都不存在? 当然不存在了啊!快醒醒,你是在 Windows 系统上并不是 Linux 系统,上哪给你弄这些配置文件去? 脑海中迅速闪现哲学基本问题: 我是谁,我在那,我在干什么? 我是 Windows 系统用户,正在 git bash 命令行中试图设置别名,没有找到类似于 linux 配置文件. 既然你明白你何出来,那你去那里看看有没有什么发现? snowdreams1006@home MINGW64 /g/sublime/test $ cd /e/git snowdreams1006@home MINGW64 /e/git $ winpty tree.com 卷 软件 的文件夹 PATH 列表 卷序列号为 00000063 223E:7300 E:. ├─bin ├─cmd ├─dev │ ├─mqueue │ └─shm ├─etc │ ├─pkcs11 │ ├─pki │ │ └─ca-trust │ │ ├─extracted │ │ │ ├─java │ │ │ ├─openssl │ │ │ └─pem │ │ └─source │ │ └─anchors │ ├─profile.d │ └─ssh ├─mingw64 │ ├─bin │ ├─doc │ │ └─git-credential-manager │ ├─etc │ │ ├─pkcs11 │ │ └─pki │ │ └─ca-trust │ │ └─extracted │ │ ├─java │ │ ├─openssl │ │ └─pem │ ├─lib │ │ ├─dde1.4 │ │ ├─engines │ │ ├─itcl4.0.4 │ │ ├─p11-kit │ │ ├─pkcs11 │ │ ├─reg1.3 │ │ ├─sqlite3.11.0 │ │ ├─tcl8 │ │ │ ├─8.4 │ │ │ │ └─platform │ │ │ ├─8.5 │ │ │ └─8.6 │ │ │ └─tdbc │ │ ├─tcl8.6 │ │ │ ├─encoding │ │ │ ├─http1.0 │ │ │ ├─msgs │ │ │ ├─opt0.4 │ │ │ └─tzdata │ │ │ ├─Africa │ │ │ ├─America │ │ │ │ ├─Argentina │ │ │ │ ├─Indiana │ │ │ │ ├─Kentucky │ │ │ │ └─North_Dakota │ │ │ ├─Antarctica │ │ │ ├─Arctic │ │ │ ├─Asia │ │ │ ├─Atlantic │ │ │ ├─Australia │ │ │ ├─Brazil │ │ │ ├─Canada │ │ │ ├─Chile │ │ │ ├─Etc │ │ │ ├─Europe │ │ │ ├─Indian │ │ │ ├─Mexico │ │ │ ├─Pacific │ │ │ ├─SystemV │ │ │ └─US │ │ ├─thread2.7.3 │ │ └─tk8.6 │ │ ├─demos │ │ │ └─images │ │ ├─images │ │ ├─msgs │ │ └─ttk │ ├─libexec │ │ └─git-core │ │ └─mergetools │ ├─share │ │ ├─antiword │ │ ├─doc │ │ │ ├─connect │ │ │ ├─git-doc │ │ │ │ ├─howto │ │ │ │ └─technical │ │ │ └─nghttp2 │ │ ├─gettext-0.19.7 │ │ │ └─its │ │ ├─git │ │ │ ├─bindimage.txt │ │ │ └─completion │ │ ├─git-core │ │ │ └─templates │ │ │ ├─hooks │ │ │ └─info │ │ ├─git-gui │ │ │ └─lib │ │ ├─gitweb │ │ │ └─static │ │ ├─licenses │ │ │ ├─bzip2 │ │ │ ├─expat │ │ │ ├─gcc-libs │ │ │ ├─gettext │ │ │ │ ├─gettext-runtime │ │ │ │ │ ├─intl │ │ │ │ │ └─libasprintf │ │ │ │ ├─gettext-tools │ │ │ │ │ └─gnulib-lib │ │ │ │ │ └─libxml │ │ │ │ └─gnulib-local │ │ │ │ └─lib │ │ │ │ └─libxml │ │ │ ├─libffi │ │ │ ├─libiconv │ │ │ │ └─libcharset │ │ │ ├─libssh2 │ │ │ ├─libsystre │ │ │ ├─libtasn1 │ │ │ ├─libtre │ │ │ ├─libwinpthread │ │ │ │ └─mingw-w64-libraries │ │ │ │ └─winpthreads │ │ │ ├─openssl │ │ │ ├─wineditline │ │ │ └─zlib │ │ ├─nghttp2 │ │ ├─p11-kit │ │ │ └─modules │ │ ├─perl5 │ │ │ └─site_perl │ │ │ └─Git │ │ │ └─SVN │ │ │ └─Memoize │ │ └─pki │ │ └─ca-trust-source │ └─ssl │ └─certs ├─tmp └─usr ├─bin │ ├─core_perl │ └─vendor_perl ├─lib │ ├─awk │ ├─coreutils │ ├─gawk │ ├─gnupg │ │ └─gnupg │ ├─openssl │ │ └─engines │ ├─p11-kit │ ├─perl5 │ │ ├─core_perl │ │ │ ├─auto │ │ │ │ ├─arybase │ │ │ │ ├─attributes │ │ │ │ ├─B │ │ │ │ ├─Compress │ │ │ │ │ └─Raw │ │ │ │ │ ├─Bzip2 │ │ │ │ │ └─Zlib │ │ │ │ ├─Cwd │ │ │ │ ├─Data │ │ │ │ │ └─Dumper │ │ │ │ ├─DB_File │ │ │ │ ├─Devel │ │ │ │ │ ├─Peek │ │ │ │ │ └─PPPort │ │ │ │ ├─Digest │ │ │ │ │ ├─MD5 │ │ │ │ │ └─SHA │ │ │ │ ├─Encode │ │ │ │ │ ├─Byte │ │ │ │ │ ├─CN │ │ │ │ │ ├─EBCDIC │ │ │ │ │ ├─JP │ │ │ │ │ ├─KR │ │ │ │ │ ├─Symbol │ │ │ │ │ ├─TW │ │ │ │ │ └─Unicode │ │ │ │ ├─Fcntl │ │ │ │ ├─File │ │ │ │ │ ├─DosGlob │ │ │ │ │ └─Glob │ │ │ │ ├─Filter │ │ │ │ │ └─Util │ │ │ │ │ └─Call │ │ │ │ ├─GDBM_File │ │ │ │ ├─Hash │ │ │ │ │ └─Util │ │ │ │ │ └─FieldHash │ │ │ │ ├─I18N │ │ │ │ │ └─Langinfo │ │ │ │ ├─IO │ │ │ │ ├─IPC │ │ │ │ │ └─SysV │ │ │ │ ├─List │ │ │ │ │ └─Util │ │ │ │ ├─Math │ │ │ │ │ └─BigInt │ │ │ │ │ └─FastCalc │ │ │ │ ├─MIME │ │ │ │ │ └─Base64 │ │ │ │ ├─mro │ │ │ │ ├─NDBM_File │ │ │ │ ├─ODBM_File │ │ │ │ ├─Opcode │ │ │ │ ├─PerlIO │ │ │ │ │ ├─encoding │ │ │ │ │ ├─mmap │ │ │ │ │ ├─scalar │ │ │ │ │ └─via │ │ │ │ ├─POSIX │ │ │ │ ├─re │ │ │ │ ├─SDBM_File │ │ │ │ ├─Socket │ │ │ │ ├─Storable │ │ │ │ ├─Sys │ │ │ │ │ ├─Hostname │ │ │ │ │ └─Syslog │ │ │ │ ├─threads │ │ │ │ │ └─shared │ │ │ │ ├─Tie │ │ │ │ │ └─Hash │ │ │ │ │ └─NamedCapture │ │ │ │ ├─Time │ │ │ │ │ ├─HiRes │ │ │ │ │ └─Piece │ │ │ │ ├─Unicode │ │ │ │ │ └─Collate │ │ │ │ ├─Win32 │ │ │ │ ├─Win32API │ │ │ │ │ └─File │ │ │ │ └─Win32CORE │ │ │ ├─B │ │ │ ├─Compress │ │ │ │ └─Raw │ │ │ ├─CORE │ │ │ ├─Data │ │ │ ├─Devel │ │ │ ├─Digest │ │ │ ├─Encode │ │ │ │ ├─CN │ │ │ │ ├─JP │ │ │ │ ├─KR │ │ │ │ ├─MIME │ │ │ │ │ └─Header │ │ │ │ └─Unicode │ │ │ ├─File │ │ │ │ └─Spec │ │ │ ├─Filter │ │ │ │ └─Util │ │ │ ├─Hash │ │ │ │ └─Util │ │ │ ├─I18N │ │ │ ├─IO │ │ │ │ └─Socket │ │ │ ├─IPC │ │ │ ├─List │ │ │ │ └─Util │ │ │ ├─Math │ │ │ │ └─BigInt │ │ │ ├─MIME │ │ │ ├─PerlIO │ │ │ ├─Scalar │ │ │ ├─Sub │ │ │ ├─Sys │ │ │ ├─threads │ │ │ ├─Tie │ │ │ │ └─Hash │ │ │ ├─Time │ │ │ ├─Unicode │ │ │ │ └─Collate │ │ │ └─Win32API │ │ └─vendor_perl │ │ ├─auto │ │ │ ├─HTML │ │ │ │ └─Parser │ │ │ ├─Net │ │ │ │ └─SSLeay │ │ │ ├─SVN │ │ │ │ ├─_Client │ │ │ │ ├─_Core │ │ │ │ ├─_Delta │ │ │ │ ├─_Fs │ │ │ │ ├─_Ra │ │ │ │ ├─_Repos │ │ │ │ └─_Wc │ │ │ └─Term │ │ │ └─ReadKey │ │ ├─HTML │ │ ├─Net │ │ │ └─SSLeay │ │ ├─SVN │ │ └─Term │ ├─pkcs11 │ ├─sasl2 │ ├─ssh │ ├─tar │ └─terminfo │ ├─63 │ ├─64 │ └─78 ├─libexec ├─share │ ├─bash-completion │ │ └─completions │ ├─cygwin │ ├─git │ ├─gnupg │ ├─licenses │ │ ├─curl │ │ ├─dos2unix │ │ ├─expat │ │ ├─file │ │ ├─gcc-libs │ │ ├─libffi │ │ ├─libsasl │ │ ├─libsqlite │ │ ├─libssh2 │ │ ├─mintty │ │ ├─ncurses │ │ ├─openssh │ │ ├─openssl │ │ ├─p11-kit │ │ ├─perl-Net-SSLeay │ │ ├─perl-TermReadKey │ │ ├─unzip │ │ ├─vim │ │ └─zlib │ ├─misc │ ├─p11-kit │ │ └─modules │ ├─perl5 │ │ ├─core_perl │ │ │ ├─App │ │ │ │ └─Prove │ │ │ │ └─State │ │ │ │ └─Result │ │ │ ├─Archive │ │ │ │ └─Tar │ │ │ ├─Attribute │ │ │ ├─autodie │ │ │ │ ├─exception │ │ │ │ └─Scope │ │ │ ├─B │ │ │ ├─Carp │ │ │ ├─Class │ │ │ ├─Compress │ │ │ ├─Config │ │ │ │ └─Perl │ │ │ ├─CPAN │ │ │ │ ├─Exception │ │ │ │ ├─FTP │ │ │ │ ├─HTTP │ │ │ │ ├─Kwalify │ │ │ │ ├─LWP │ │ │ │ ├─Meta │ │ │ │ └─Plugin │ │ │ ├─DBM_Filter │ │ │ ├─Devel │ │ │ ├─Digest │ │ │ ├─Encode │ │ │ ├─encoding │ │ │ ├─Exporter │ │ │ ├─ExtUtils │ │ │ │ ├─CBuilder │ │ │ │ │ └─Platform │ │ │ │ │ └─Windows │ │ │ │ ├─Command │ │ │ │ ├─Constant │ │ │ │ ├─Liblist │ │ │ │ ├─MakeMaker │ │ │ │ │ └─version │ │ │ │ ├─ParseXS │ │ │ │ └─Typemaps │ │ │ ├─File │ │ │ ├─Filter │ │ │ ├─Getopt │ │ │ ├─HTTP │ │ │ ├─I18N │ │ │ │ └─LangTags │ │ │ ├─IO │ │ │ │ ├─Compress │ │ │ │ │ ├─Adapter │ │ │ │ │ ├─Base │ │ │ │ │ ├─Gzip │ │ │ │ │ ├─Zip │ │ │ │ │ └─Zlib │ │ │ │ ├─Socket │ │ │ │ └─Uncompress │ │ │ │ └─Adapter │ │ │ ├─IPC │ │ │ ├─JSON │ │ │ │ └─PP │ │ │ ├─Locale │ │ │ │ ├─Codes │ │ │ │ └─Maketext │ │ │ ├─Math │ │ │ │ ├─BigFloat │ │ │ │ └─BigInt │ │ │ ├─Memoize │ │ │ ├─Module │ │ │ │ ├─CoreList │ │ │ │ └─Load │ │ │ ├─Net │ │ │ │ └─FTP │ │ │ ├─overload │ │ │ ├─Params │ │ │ ├─Parse │ │ │ │ └─CPAN │ │ │ ├─Perl │ │ │ ├─PerlIO │ │ │ │ └─via │ │ │ ├─Pod │ │ │ │ ├─Perldoc │ │ │ │ ├─Simple │ │ │ │ └─Text │ │ │ ├─Search │ │ │ ├─TAP │ │ │ │ ├─Formatter │ │ │ │ │ ├─Console │ │ │ │ │ └─File │ │ │ │ ├─Harness │ │ │ │ └─Parser │ │ │ │ ├─Iterator │ │ │ │ ├─Result │ │ │ │ ├─Scheduler │ │ │ │ ├─SourceHandler │ │ │ │ └─YAMLish │ │ │ ├─Term │ │ │ ├─Test │ │ │ │ ├─Builder │ │ │ │ │ ├─IO │ │ │ │ │ └─Tester │ │ │ │ ├─Tester │ │ │ │ └─use │ │ │ ├─Text │ │ │ ├─Thread │ │ │ ├─Tie │ │ │ ├─Time │ │ │ ├─Unicode │ │ │ │ └─Collate │ │ │ │ └─CJK │ │ │ ├─unicore │ │ │ │ ├─lib │ │ │ │ │ ├─Age │ │ │ │ │ ├─Alpha │ │ │ │ │ ├─Bc │ │ │ │ │ ├─BidiC │ │ │ │ │ ├─BidiM │ │ │ │ │ ├─Blk │ │ │ │ │ ├─Bpt │ │ │ │ │ ├─Cased │ │ │ │ │ ├─Ccc │ │ │ │ │ ├─CE │ │ │ │ │ ├─CI │ │ │ │ │ ├─CompEx │ │ │ │ │ ├─CWCF │ │ │ │ │ ├─CWCM │ │ │ │ │ ├─CWKCF │ │ │ │ │ ├─CWL │ │ │ │ │ ├─CWT │ │ │ │ │ ├─CWU │ │ │ │ │ ├─Dash │ │ │ │ │ ├─Dep │ │ │ │ │ ├─DI │ │ │ │ │ ├─Dia │ │ │ │ │ ├─Dt │ │ │ │ │ ├─Ea │ │ │ │ │ ├─Ext │ │ │ │ │ ├─Gc │ │ │ │ │ ├─GCB │ │ │ │ │ ├─GrBase │ │ │ │ │ ├─Hex │ │ │ │ │ ├─Hst │ │ │ │ │ ├─Hyphen │ │ │ │ │ ├─IDC │ │ │ │ │ ├─Ideo │ │ │ │ │ ├─IDS │ │ │ │ │ ├─In │ │ │ │ │ ├─Jg │ │ │ │ │ ├─Jt │ │ │ │ │ ├─Lb │ │ │ │ │ ├─LOE │ │ │ │ │ ├─Lower │ │ │ │ │ ├─Math │ │ │ │ │ ├─NChar │ │ │ │ │ ├─NFCQC │ │ │ │ │ ├─NFDQC │ │ │ │ │ ├─NFKCQC │ │ │ │ │ ├─NFKDQC │ │ │ │ │ ├─Nt │ │ │ │ │ ├─Nv │ │ │ │ │ ├─PatSyn │ │ │ │ │ ├─PatWS │ │ │ │ │ ├─Perl │ │ │ │ │ ├─QMark │ │ │ │ │ ├─SB │ │ │ │ │ ├─Sc │ │ │ │ │ ├─Scx │ │ │ │ │ ├─SD │ │ │ │ │ ├─STerm │ │ │ │ │ ├─Term │ │ │ │ │ ├─UIdeo │ │ │ │ │ ├─Upper │ │ │ │ │ ├─WB │ │ │ │ │ ├─XIDC │ │ │ │ │ └─XIDS │ │ │ │ └─To │ │ │ ├─User │ │ │ ├─version │ │ │ ├─warnings │ │ │ └─Win32API │ │ │ └─File │ │ └─vendor_perl │ │ ├─Authen │ │ │ └─SASL │ │ │ └─Perl │ │ ├─Convert │ │ ├─Date │ │ │ └─Language │ │ ├─Encode │ │ ├─Error │ │ ├─File │ │ ├─HTML │ │ ├─HTTP │ │ │ ├─Cookies │ │ │ ├─Headers │ │ │ └─Request │ │ ├─IO │ │ │ └─Socket │ │ │ └─SSL │ │ ├─LWP │ │ │ ├─Authen │ │ │ └─Protocol │ │ ├─Mail │ │ │ ├─Field │ │ │ └─Mailer │ │ ├─MIME │ │ │ ├─Decoder │ │ │ ├─Field │ │ │ └─Parser │ │ ├─Net │ │ │ ├─HTTP │ │ │ └─SMTP │ │ ├─Time │ │ ├─URI │ │ │ ├─file │ │ │ └─urn │ │ └─WWW │ │ └─RobotRules │ ├─pki │ │ └─ca-trust-source │ ├─tabset │ ├─terminfo │ │ ├─63 │ │ ├─64 │ │ └─78 │ └─vim │ └─vim74 │ ├─autoload │ │ └─xml │ ├─colors │ ├─compiler │ ├─doc │ ├─ftplugin │ ├─indent │ ├─keymap │ ├─macros │ │ ├─hanoi │ │ ├─life │ │ ├─maze │ │ └─urm │ ├─pack │ │ └─dist │ │ └─opt │ │ ├─dvorak │ │ │ ├─dvorak │ │ │ └─plugin │ │ ├─editexisting │ │ │ └─plugin │ │ ├─justify │ │ │ └─plugin │ │ ├─matchit │ │ │ ├─doc │ │ │ └─plugin │ │ ├─shellmenu │ │ │ └─plugin │ │ └─swapmouse │ │ └─plugin │ ├─plugin │ ├─print │ ├─spell │ ├─syntax │ ├─tools │ └─tutor └─ssl ├─certs └─misc 看到熟悉的 ./etc/bash.bashrc 文件,顿时亲切不少,设置一下别名再说. snowdreams1006@home MINGW64 /e/git $ echo \"# Set alias for tree command\" >> ./etc/bash.bashrc snowdreams1006@home MINGW64 /e/git $ echo \"alias tree='winpty tree.com'\" >> ./etc/bash.bashrc snowdreams1006@home MINGW64 /e/git $ source ./etc/bash.bashrc 现在测试一下能否正确打印出目录树: snowdreams1006@home MINGW64 /e/git $ cd /g/sublime/test snowdreams1006@home MINGW64 /g/sublime/test $ tree 卷 工作 的文件夹 PATH 列表 卷序列号为 000000A3 CC3C:50D0 G:. ├─cmd └─git 亲测有效,通过设置别名的方式可以简化命令,从而实现在 git bash 中优雅调用 tree 命令. 固执少年一意孤行 少年既然不听劝,那我只好和你一起一意孤行. git bash 不支持 tree 命令,意味着 mintty 终端不支持 tree 命令,但 mintty 既然作为一款优秀的终端模拟器不可能不支持 tree 命令,否则 cygwin ,msys2 和 mingw 等系统不可能将其作为默认终端. 回想起 linux 系统,最小化安装版也不支持 tree 命令,通过包管理工具自行扩展即可支持 tree 命令. 因此,思路有两种,像 linux 那样通过包管理工具安装 tree 命令,或者通过源码编译方式扩展 tree 命令. 第一种需要包管理工具,而git bash 使用的是 mintty 终端,并没有提供相应的包管理工具. 所以想要通过包管理工具进行安装 tree 命令也是无路可走. 包管理工具安装 如果能够提供包管理工具,那么我们就可以像 linux 系统那样安装第三方命令一样,安装 tree 命令了. 首先想到的是 mintty 官网有没有相关说明,遗憾的是,mintty 本身一般是通过包管理工具安装的,单独的终端并没有包管理的环境,因此无法调用相关命令. Administrator@snowdreams1006 MINGW64 /f/workspace/test $ mingw-get bash: mingw-get: command not found Administrator@snowdreams1006 MINGW64 /f/workspace/test $ pacman bash: pacman: command not found 然而,小小的挫折是不会轻易放弃的,既然 mintty 官网不能提供有效的帮助,那我们回到最初安装 git 的地方,看一下 git 能否提供相关的包管理工具. 频繁出现 Git For Windows 名词,根据软件的命令规则,Git for Windows 可能是独立的软件,更何况 git bash 集成的终端也不是自身研发的终端而是第三方的 mintty 终端. 所以,我们有理由相信 Git for Windows 是另外的团队在维护,而不是 Git 团队. git-for-windows 官网: https://gitforwindows.org/ 根据官方说明,安装后正在下载相关依赖,下载速度比较慢的话,请自行解决. 下载完成后,原来的 cmd 窗口会自动关闭并且打开新的 git bash 窗口. Administrator@snowdreams1006 MINGW64 / (master) $ sdk help The 'sdk' shell function helps you to get up and running with the Git for Windows SDK. The available subcommands are: create-desktop-icon: install a desktop icon that starts the Git for Windows SDK Bash. cd : initialize/update a worktree and cd into it. Known projects: git git-extra msys2-runtime installer build-extra MINGW-packages MSYS2-packages mingw-w64-busybox mingw-w64-curl mingw-w64-cv2pdb mingw-w64-git mingw-w64-git-credential-manager mingw-w64-git-lfs mingw-w64-git-sizer mingw-w64-wintoast bash curl gawk git-flow gnupg heimdal mintty nodejs openssh openssl perl perl-HTML-Parser perl-Locale-Gettext perl-Net-SSLeay perl-TermReadKey perl-XML-Parser perl-YAML-Syck subversion tig init : initialize and/or update a worktree. Known projects are the same as for the 'cd' command. build : builds one of the following: git-and-installer git git-extra msys2-runtime installer mingw-w64-busybox mingw-w64-curl mingw-w64-cv2pdb mingw-w64-git mingw-w64-git-credential-manager mingw-w64-git-lfs mingw-w64-git-sizer mingw-w64-wintoast bash curl gawk git-flow gnupg heimdal mintty nodejs openssh openssl perl perl-HTML-Parser perl-Locale-Gettext perl-Net-SSLeay perl-TermReadKey perl-XML-Parser perl-YAML-Syck subversion tig edit : edit a well-known file. Well-known files are: git-sdk.sh sdk.completion ReleaseNotes.md install.iss reload: reload the 'sdk' function. 现在安装完成后,我们再次打开 Git for Windows 的开发文档简介,从中不难发现该项目使用了 MSYS2 项目,那么问题迎刃而解. 根据科普知识,我们知道 MSYS2 和 MinGW 都是操作系统,而 Git For Windows 将两者结合在一起,默认使用 MSYS2 的包管理工具. Administrator@snowdreams1006 MINGW64 / (master) $ Pacman -h 用法: Pacman [...] 操作: Pacman {-h --help} Pacman {-V --version} Pacman {-D --database} Pacman {-F --files} [选项] [软件包] Pacman {-Q --query} [选项] [软件包] Pacman {-R --remove} [选项] Pacman {-S --sync} [选项] [软件包] Pacman {-T --deptest} [选项] [软件包] Pacman {-U --upgrade} [选项] 使用 'Pacman {-h --help}' 及某个操作以查看可用选项 激动人心的时刻就要来临,在正式使用 Pacman 安装 tree 命令外,我们再次检查当前系统环境以确保没有 git bash 无法调用 tree 命令. Administrator@snowdreams1006 MINGW64 / (master) $ tree bash: tree: 未找到命令 调用 Pacman -S tree 命令安装 tree 命令. Administrator@snowdreams1006 MINGW64 / (master) $ Pacman -S tree 正在解析依赖关系... 正在查找软件包冲突... 软件包 (1) tree-1.8.0-1 下载大小: 0.05 MiB 全部安装大小: 0.07 MiB :: 进行安装吗? [Y/n] y 警告:没有 /var/cache/pacman/pkg/ 缓存存在,正在创建... :: 正在获取软件包...... tree-1.8.0-1-x86_64 51.1 KiB 211K/s 00:00 [#####################] 100% (1/1) 正在检查密钥环里的密钥 [#####################] 100% (1/1) 正在检查软件包完整性 [#####################] 100% (1/1) 正在加载软件包文件 [#####################] 100% (1/1) 正在检查文件冲突 [#####################] 100% (1/1) 正在检查可用存储空间 [#####################] 100% :: 正在处理软件包的变化... (1/1) 正在安装 tree 验证安装成功,切换到测试目录调用 tree 命令真的打印出了目录树结构. Administrator@snowdreams1006 MINGW64 / (master) $ pwd / Administrator@snowdreams1006 MINGW64 / (master) $ cd /f/workspace/test Administrator@snowdreams1006 MINGW64 /f/workspace/test $ tree . ├── cmd └── gitbash 2 directories, 0 files tree.exe 文件确实已经存在,通过这样方式当然可以安装任意第三方命令了呢! Administrator@snowdreams1006 MINGW64 / (master) $ pwd / Administrator@snowdreams1006 MINGW64 / (master) $ ls usr/bin/tree.exe usr/bin/tree.exe* Administrator@snowdreams1006 MINGW64 / (master) $ ls usr/bin/tree.exe usr/bin/tree.exe* 然而,事情还没有结束,虽然打印当前路径显示的是在 / ,但是如果从普通的 git bash 命令行窗口进入 /,发现他们并不一致! 开发版左上角文字: SDK-64,普通版左上角: MinGW64. 事情应该不至于这么复杂,我猜测如果进入到 git sdk 的安装目录,应该是一样的! 源码编译安装 还有一种源码编译安装方式,可以猜想到的是将会比较麻烦,不仅要安装 c 编译环境,还可能会面临如何移植到 Windows 环境的问题. 恕再下先行一步,告辞! 但是老司机怎么能收走就走,不是说好一起闯天下得嘛? 然而,心有余而力不足,编译安装再到测试确实是不少挑战,最重要的是,电脑太卡了等不了. 所以,收集到了一些资料方便有条件的小伙伴去研究吧! 下载链接: ftp://mama.indstate.edu/linux/tree/tree-1.8.0.tgz 在 mingw ,msys2 或者 cygwin 系统上编译安装 tree 的 c 文件,最终生成 tree.exe 可执行文件. 独立安装 c 编译环境,生成的 tree.exe 可执行文件再想办法兼容到 git bash 所支持的 .exe 类型,或许也不用转换. 请参考 linux 系统的 tree 命令源码: http://mama.indstate.edu/users/ice/tree/ 我觉得我还可以再坚持一会,虽然不能从头开始编译生成 tree.exe 可执行文件,但是研究了这么多朋友圈关系,足够我找到解决方案了. tree 命令的源码文件编译成 tree.exe 可执行文件比较费劲,但是可以找到已经编译好的文件啊. 说道这里,不得不提一下神奇的 sourceforge 网站,提供源码和下载网站. 前面我们一直在说 mintty 终端模拟器也好,或者 mingw ,msys2 和 cygwin 操作系统也罢,他们或多或少和 GNU 有一些联系,而 GNU 是自由软件操作系统,源码会随程序一同发布. 所以我们应该可以从 sourceforge 网站上找到些蛛丝马迹,说不定还有打包好的可执行文件呢,如果那样的话就不用我们手动编译安装了! 功夫不负有心人,竟然真的找到了,搜索 GNU 发现了 gnuwin32 项目,完整提供了原生命令,其中就有我们需要的 tree 命令. 下载链接: https://sourceforge.net/projects/gnuwin32/files/tree/1.5.2.2/tree-1.5.2.2-bin.zip/download 于是下载二进制文件找到其中的 /bin/tree.exe 并将其复制到 /git/usr/bin 目录下,这样 git bash 本身就支持 tree 命令了. 首先清除掉上一步设置的别名,防止干扰以确保此二进制文件真实有效. snowdreams1006@home MINGW64 /g/sublime/test # 切换到 `git` 安装目录 $ cd /e/git snowdreams1006@home MINGW64 /e/git # 编辑 `bash.bashrc` 配置文件,移除别名 $ vim ./etc/bash.bashrc snowdreams1006@home MINGW64 /e/git # 查看配置文件内容,别名设置已移除 $ tail ./etc/bash.bashrc [[ \"$-\" != *i* ]] && return # Set a default prompt of: user@host, MSYSTEM variable, and current_directory #PS1='\\[\\e]0;\\w\\a\\]\\n\\[\\e[32m\\]\\u@\\h \\[\\e[35m\\]$MSYSTEM\\[\\e[0m\\] \\[\\e[33m\\]\\w\\[\\e[0m\\]\\n\\$ ' # Uncomment to use the terminal colours set in DIR_COLORS # eval \"$(dircolors -b /etc/DIR_COLORS)\" # Fixup git-bash in non login env shopt -q login_shell || . /etc/profile.d/git-prompt.sh snowdreams1006@home MINGW64 /e/git # 刷新配置文件,使其立即生效 $ source ./etc/bash.bashrc snowdreams1006@home MINGW64 /e/git # 移除 `tree` 别名,适用于命令行方式设置而不是文件设置 $ unalias tree snowdreams1006@home MINGW64 /e/git # 运行 `tree` 命令,确保已经无法通过别名方式调用系统的 `tree.com` 命令 $ tree bash: tree: command not found 真的成功添加了 tree.exe 命令,明显和 cmd 自带的 tree.com 命令不一致. snowdreams1006@home MINGW64 /e/git $ cd /g/sublime/test snowdreams1006@home MINGW64 /g/sublime/test # 扩展命令 `tree` 帮助信息 $ tree.exe --help usage: tree [-adfghilnpqrstuvxACDFNS] [-H baseHREF] [-T title ] [-L level [-R]] [-P pattern] [-I pattern] [-o filename] [--version] [--help] [--inodes] [--device] [--noreport] [--nolinks] [--dirsfirst] [--charset charset] [--filelimit #] [] -a All files are listed. -d List directories only. -l Follow symbolic links like directories. -f Print the full path prefix for each file. -i Don't print indentation lines. -q Print non-printable characters as '?'. -N Print non-printable characters as is. -p Print the protections for each file. -u Displays file owner or UID number. -g Displays file group owner or GID number. -s Print the size in bytes of each file. -h Print the size in a more human readable way. -D Print the date of last modification. -F Appends '/', '=', '*', or '|' as per ls -F. -v Sort files alphanumerically by version. -r Sort files in reverse alphanumeric order. -t Sort files by last modification time. -x Stay on current filesystem only. -L level Descend only level directories deep. -A Print ANSI lines graphic indentation lines. -S Print with ASCII graphics indentation lines. -n Turn colorization off always (-C overrides). -C Turn colorization on always. -P pattern List only those files that match the pattern given. -I pattern Do not list files that match the given pattern. -H baseHREF Prints out HTML format with baseHREF as top directory. -T string Replace the default HTML title and H1 header with string. -R Rerun tree when max dir level reached. -o file Output to file instead of stdout. --inodes Print inode number of each file. --device Print device ID number to which each file belongs. --noreport Turn off file/directory count at end of tree listing. --nolinks Turn off hyperlinks in HTML output. --dirsfirst List directories before files. --charset X Use charset X for HTML and indentation line output. --filelimit # Do not descend dirs with more than # files in them. snowdreams1006@home MINGW64 /g/sublime/test # 原生 `tree.com` 帮助信息 $ winpty tree.com /? 卷 软件 的文件夹 PATH 列表 卷序列号为 000000CA 223E:7300 E:\\GIT\\? 无效的路径 - \\GIT\\? 没有子文件夹 虽然原生 cmd 自带的 tree.com 命令也能打印出目录结构树,但是和扩展的第三方 tree.exe 命令相比,可配置的选项实在太少,难怪固执少年会执意扩展 tree 命令. 懒人直达 如果想要在 git bash 命令行中调用 tree 命令,总结了下列几种方法. 注意: 从上到下逐渐复杂,根据自己的情况自行选择. winpty tree.com : 直接调用 cmd 内置 tree.com 命令. winpty cmd //c tree : 通知 cmd 调用 tree 命令. 安装 Git For Windows 开发版,Pacman -S tree 安装 tree 命令,然后执行 tree 调用. 下载 已编译好的 tree.exe 文件到 git bash 安装目录下的 /usr/bin 目录,然后执行 tree 命令. 回顾总结 Git 本身并不支持 Windows 系统,Git For Windows 团队为了移植到 Windows 平台,付出了很多努力,最直观的感受就是提供了 Git For Windows 软件. 由于 Git for Windows 的出色工作得到 Git 的官方认可,现在默认下载的 Git 就是来源于 Git for Windows. 但 Git 底层是运行在类 linux 系统的,这种差异必然需要 Windows 到 Linux 的中间处理层进行转换. 因此,Git for Window 背后的技术中涉及到大量的 GNU 自由软件操作系统和 WSL (Windows Subsystem for Linux) 的相关代码. 正是由于背后千丝万缕的联系,为我们扩展 git bash 命令行提供了一些思路. 基于 Cygwin 系统进行扩展,安装完整的操作系统,模拟出 linux 运行环境,在 Windows 上也能找到 linux 的感觉. 当然,Cygwin 由于比较完整,体积也相当大,所以在此衍生出 MSYS2 和 MinGW 操作系统. 殊途同归,他们的底层架构有些不同,但目的是一致的,都是在 Windows 上找到 Linux 的感觉,并且他们的终端命令行几乎都是 mintty . 在终端中调用各自的包管理工具即可轻松扩展第三方命令,此外,由于他们大多数都有 c 编译环境,因此也可以选择重新编译安装. 所以 git bash 命令行看起来下像是 git 官方支持,其实却是由 Git for Windows 组织独立维护的开源项目. 默认 Git for Windows 没有提供包管理工具,开发版却默认集成了 MSYS2 环境.也就是说我们可以用 Pacman 来安装 tree 命令. 最后稍微总结本文知识要点: git bash 命令行并不能完全替代 cmd 命令,两者互补才能相得益彰. git bash 命令行中调用 cmd 程序需要通过 winpty 调用,比如 winpty tree.com git bash 默认安装程序无包管理工具等高级功能,如需扩展自定义命令应该下载Git for Windows SDK. git bash 环境兼容 MSYS2 ,MinGW ,Cygwin 等系列自由软件,其他类似系统的 tree.exe 可能并不支持. 好了,说了这么多,小伙伴们 get 到如何在 git bash 使用(扩展) tree 命令了吗? 如有疑问,欢迎留言告诉我! © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:07:59 "},"git/tools/git-bash-extend-up.html":{"url":"git/tools/git-bash-extend-up.html","title":"git bash 扩展命令(上)","keywords":"","body":"git bash 扩展命令(上) 已投稿给脚本之家公众号,如需查看请访问: 三招教你轻松扩展 git bash 命令(上) https://mp.weixin.qq.com/s/29laLQ9k1YAPS_Rx3IAeQQ © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:07:58 "},"git/tools/git-bash-extend-middle.html":{"url":"git/tools/git-bash-extend-middle.html","title":"git bash 扩展命令(中)","keywords":"","body":"git bash 扩展命令(中) 已投稿给脚本之家公众号,如需查看请访问: 三招教你轻松扩展 git bash 命令(中) https://mp.weixin.qq.com/s/qN4KbT8Lc1pQhnDdSeHcEQ © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:07:58 "},"git/tools/git-bash-extend-down.html":{"url":"git/tools/git-bash-extend-down.html","title":"git bash 扩展命令(下)","keywords":"","body":"git bash 扩展命令(下) 已投稿给脚本之家公众号,如需查看请访问: 三招教你轻松扩展 git bash 命令(下) https://mp.weixin.qq.com/s/yAtGh4FiGXbzoCeKnhZ2ag © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:07:58 "},"git/summary/about.html":{"url":"git/summary/about.html","title":"沙海拾贝","keywords":"","body":"沙海拾贝 © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:08:03 "},"git/summary/common.html":{"url":"git/summary/common.html","title":"知识速查","keywords":"","body":"知识速查 创建版本库 初始化项目 git init 从零开始创建项目 示例 git init 克隆项目 git clone 将已有项目拷贝到本地 示例 git clone [email protected]:snowdreams1006/snowdreams1006.github.io.git 添加文件 git add 将新文件或已修改文件添加到缓存区 示例 git add README.md 查看状态 git status 查看当前文件是否和上次提交内容是否有修改 示例 git status README.md 比较差异 git diff 查看当前文件和上次提交内容的具体差异 尚未缓存的修改: git diff 查看已缓存修改: git diff --cached 查看已缓存与未缓存的所有修改: git diff HEAD 显示摘要而非整个差异: git diff --stat 示例 git diff README.md 提交文件 git commit 将缓存区内容添加到版本库 示例 git commit -m \"remark\" 取消已缓存内容 git reset HEAD 将缓存区内容添加到版本库 示例 git reset HEAD 删除文件 git rm 从暂存区中移除且不保留在工作目录: git rm 强制从暂存区中移除且不保留在工作目录: git rm -f 从暂存区中移除但保留工作目录: git rm --cached 示例 git rm README.md 移动文件 git mv 移动或重命名文件,目录,软连接 示例 git mv README.md README_NEW.md commit push pull fetch merge 的区别与含义: git commit : 将本地修改过的文件提交到本地仓库中 git push : 将本地仓库的最新版本推送到远程库中 git pull : 从远程库获取最新版本到本地,并自动merge git fetch : 从远程库获取最新版本到本地,不会自动merge git merge : 将指定版本合并到当前分支 替换本地改动 丢弃当前文件修改内容,已添加到暂存区以及新文件都不会受到影响 示例 git checkout -- 丢弃本地所有改动 示例 git reset --hard 分支管理 创建分支 git branch 创建本地分支,但不自动切换新分支 示例 git branch dev 切换分支 git checkout 切换到指定分支 示例 git checkout dev 创建并切换分支 git checkout -b 创建本地分支并自动切换到新分支 示例 git checkout -b feature 合并分支 git merge 将指定分支合并到当前分支 示例 git merge dev 删除分支 git branch -d 删除指定分支 示例 git branch -d dev 列出分支 git branch 列出本地全部分支 示例 git branch 提交日志 git log 查看纳入版本库的提交日志 示例 git log 标签管理 创建标签 git tag -a 创建标签并提交备注 示例 git tag -a v1.0.0 追加标签 git tag -a 追加标签并更新备注 示例 git tag -a v0.9.0 6ad8956bc09a6a62c731711eabe796690aa6471c 删除标签 git tag -d 删除指定标签 示例 git tag -d v1.0.0 查看标签 git show 查看指定标签 示例 git show v1.0.0 列出标签 git tag 列出本地全部标签 示例 git tag © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:08:04 "},"git/summary/cheatsheet-translation.html":{"url":"git/summary/cheatsheet-translation.html","title":"备忘录[译]","keywords":"","body":"备忘录[译] 创建 | Create 克隆一个已存在的仓库 | Clone an existing repository git clone [email protected]:snowdreams1006/snowdreams1006.github.io.git 创建一个新的本地仓库 | Create a new local repository git init 本地更改 | Local Changes 工作目录中已更改文件 | Changed files in your working directory git status 已追踪文件的更改 | Changes to tracked files git diff 添加当前全部更改到下次提交版本 | Add all current changes to next commit git add . 添加文件中某些更改到下次提交版本 | Add some changes in to next commit git add -p 提交已追踪文件的全部本地更改 | Commit all local changes in tracked files git commit -a 提交上一次暂存区更改 | Commit previously staged changes git commit 更改上次提交 | Change the last commit 没有更改已发布的提交 | Don't amend publishd commits! git commit --amend 提交历史 | Commit history 显示全部提交,以最新的开头 | Show all commits,starting with newest git log 显示某个文件一段时间内的更改 | Show changes over time for a specific file git log -p 某文件是谁在什么时候更改了什么内容 | Who changed what and when in git blame 分支和标签 | Branches & Tags 列出全部已存在的分支 | List all existing branches git branch -av 切换到 HEAD 分支 | Switch HEAD branch git checkout 基于当前 HEAD 创建新分支 | Create a new branch based on your curent HEAD git branch 基于远程分支创建新的正在追踪分支 | Create a new tracking branch based on a remote branch git checkout --track 删除一个本地分支 | Delete a local branch git branch -d 为当前提交打上标签 | Make the current commit with a tag git tag 更新和发布 | Update & Publish 列出当前全部已配置的远程仓库 | List all currently configured remotes git remote -v 显示远程仓库信息 | Show information about a remote git remote show 添加的远程仓库 | Add new remote repository named git remote add 下载来自远程仓库的所有更改但是不合并到 HEAD | Download all changes from but don't integrate into HEAD git fetch 下载来自远程仓库指定分支的所有更改并且自动合并到 HEAD | Download changes and directly merge/integrate into HEAD git pull 在远程仓库上发布本地更改 | Publish local changes on a remote git push 在远程仓库上删除分支 | Delete a branch on the branch git branch -dr 发布你的标签 | Publish your tags git push --tags 合并和变基 | MERGE & REBASE 合并指定分支到你的 HEAD | Merge into your current HEAD git merge 变基到当前HEAD | Rebase your current HEAD onto 不要变基已发布的提交 | Don't rebase published commits! git rebase 取消变基 | Abort a rebase git rebase --abort 使用已配置的冲突工具去解决冲突 | Use your configured merge tool to solve conflicts git mergetool 使用编辑器手工解决冲突然后(解决之后)标记文件已解决冲突 | Use your editor to manually solve conflicts and (after resolving) mark file as resolved git add git rm 撤销 | UNDO 丢弃工作区全部更改 | Discard all local changes in your working directory git reset --hard HEAD 丢弃指定文件的本地更改 | Discard local changes in a specific file git checkout HEAD 抵消一个提交(通过产生一个新的相反的提交) | Revert a commit (by producing a new commit with contrary changes) git revert 重置当前 HEAD 指针到上一个提交...然后丢弃自那以后的全部更改 | Reset your HEAD pointer to a previous commit ... and discard all changes since then git reset --hard ...然后作为未缓存更改保存全部更改 | ... and preserve all changes as unstaged change git reset ...然后保存未提交的本地更改 | ... and preserve all changes as unstaged change git reset --keep 建议 | SUGGESTION 提交相关更改 | COMMIT RELATED CHANGES 提交应该是相关更改的包装,例如,修复两个不同的 bug 应该产生两个单独的提交. 小的提交让其他开发者更容易理解此次更改,并且万一出错方便回滚. 在暂存区这类工具以及暂存部分文件的能力下,git 很容易创建细粒度的提交. A commit should be a wrapper for related changes, For example,fixing two different bugs should produce two separete commits. Small commits make it easier for other developers to understand the changes and roll them back if something went wrong. With tools like the staging area and the ability to stage only parts of a file. Git makes it easy to create very granular commits. 经常提交 | COMMIT OFTEN 经常提交使得你的提交很小并且有助于仅提交相关更改. 此外,这样允许你更频繁地和其他人分享你的代码,对于每个人来说更容器定期合并更改,避免了遭遇合并冲突. ,很少的大提交,很少分享它们.相反很难解决冲突. Commiting often keeps your commits small and again helps you commit only related changes. Moreover,it allows you to share your code more frequently with others. That way it's easier for everyone to integrate changes regularly and avoid having merge conflicts.Having few large commits and sharing them rarely.in contrast,makes it hard to solve conflicts. 不要提交未完成工作 | DON'T COMMIT HALF-DONE WORK 你应该仅提交已完成代码,这并不意外着提交前你不得不完成一个完整的,很大的功能分支.恰恰相反,将功能分支划分成很多逻辑块并且记得早一点,频繁些提交. 如果仅仅是为了下班前仓库该有点什么就不要提交,如果你尝试提交仅仅是因为你需要一个干净的工作副本(检出分支,拉取更改),考虑使用 git 的 stash 特性. You should only commit code when it's completed. This doesn't mean you have to complete a whole ,large feature before commiting. Quite the contrary:split the feature's implementatiion into logical chunks and remember to commit early and often. But don't commit just to have something in the repository before leaving the ofice at the end of the day. If you're tempted to commit just because you need a clean working copy (to check out a branch,pull in changes ,etc.) consider using Git's feature instead. 提交前测试代码 | TEST CODE BEFORE YOU COMMIT 抵制自以为已完成的提交. 直接测试来确保它真的已完成并且没有副作用(显而易见的). 当提交半成品到本地仓库时要求你不得不自我谅解,让你的代码进过测试对发布或者分享你的代码也很重要. Resist the temptation to commit something that you think is completed. Test it thoroughly to make sure it really is completed and has no side effect (as far as one can tell). While committing half-baked thing in your local repository only requires you to forgive yourself,having your code tested is even more important when it comes to publishing/sharing your code with others. 编写代码提交信息 | WRITE CODE COMMIT MESSAGE 对你的更改以简短总结进行描述(达到50字符作为准则). 以包括空白行作为分割下述内容. 提交信息体应该提供下述问题的详细答案: 此次更改的动机是什么? 和上一个实现有什么不同? 使用必要的现在时语态(更改,不是已更改,或者变更)和使用形如 git merge 命令生成的信息保持一致. Begin your message with short summary of your changes(up to 50 characters as a guideline). Separate it from the following body by including a blank line. The body of your message should provide detailed answers to the following questions: What was the motivation for the change? How does it differ from the previous implementation? Use the imperative ,present tense(change,not changed or changes) to be consistent with generated messages from commands like git merge. 版本控制不是一个备份系统 | VERSION CONTROL IS NOT A BACKUP SYSTEM 在远程服务器存有文件的备份是版本控制系统的一个很好副作用.但是你不应该将VCS 视为一个备份系统. 当做版本控制时,你应该注意语义化提交,而不是死记硬背文件. Having your files backed up on a remote server is a nice side effect of having a version control system. But you should not use your VCS like it was a backup system. When doing version control,you should pay attention to committing semantically(see related changes) - you shouldn't just cram in files. 利用分支 | USE BRANCHES 分支是 git 最强大的特性之一,这不是偶然. 从第一天开始快速而简单的分支就是一个核心需求. 分支是帮助你避免弄混不同开发线的完美工具. 在你的开发流程中应该广泛使用分支,像新功能,修复 bug,新想法... Branching is one of Git's most powerful features-and this is not by accident:quick and easy branching was a central requirement from day one. Branches are the perfect tool to help you avoid mixing up different lines of development. You should use branches extensively in your development workflows:for new features,bug fixes,ideas... 认同工作流 | AGREE ON A WORKFLOW Git 允许你从大量不同的工作流中选择一个:长期运行的分支,主题分支,合并或变,基工作流... 具体选择哪一个取决于一系列因素:你的项目,你的总体开发和部署工作流和(可能是最重要的)你和你的团队的个人偏好. 不论你选择哪一个去工作,你需要确保准守一个大家都认同的工作流. Git lets you pick from a lot of different workflows:long-running branches,topic branches,merge or rebase,git-flow... Which one you choose depends on a couple of factors:your project,your overall development and deployment workflows and (maybe most importantly ) on your and your teammate's personal preferences. However you choose to work,just make sure to agree on a common workflow that everyone follows. 帮助和文档 | HELP & DOCUMENTATION 命令行下获取 git 帮助 git help Git help on the command line git help 免费在线资源 | FREE ONELINE RESOURCES http://www.git-tower.com/learn http://rogerdudler.github.io/git-guide/ http://www.git-scm.org/ © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:08:03 "},"github/":{"url":"github/","title":"github 入门教程","keywords":"","body":"github 入门教程 github 是一个基于 git 的代码托管平台,是平时工作学习的好帮手,学会如何用好 github 网站能够帮助我们更好分享代码或者与其他开发人员合作. 注册 github 账号 首先准备好邮箱和密码,然后在 github 官网注册新账号,和大多数网站类似的注册流程,唯一注意的是你要想好注册类型,针对个人用户来说,一般无外乎个人账号和项目账号两种,比如 snowdreams1006 就认为是个人账号,而这种 security-plus 认为是项目账号. 其实这两种账号对于 github 来说是一样的,不像是个人账号同企业账号的差异那么大,那为什么称个人账号和项目账号呢? 是因为,大多数个人开发者名下会有多款开源作品,这些作品既可以全部挂载在某一个开发者账号下面,也可以单独挂载某一个开发者账号下面,如果此时的账号名恰好是项目名岂不是清晰多了? 因为个人刚开始可能并没多大名气,如果一个产品直接挂载在个人名下,那么这个产品很大程度上就依赖于个人名气了,所以不妨反过来,用产品说话,事实胜于雄辩,这种做法也是一种常用的宣传手段,很多个人开源产品正是这么做的! 除此之外项目账号还有一个好处,利用 github 的静态网站托管服务可以免费快速搭建项目官网,只要创建一个snowdreams1006.github.io 的项目,那么这个项目就可以作为静态网站的源码项目了,访问 https://snowdreams1006.github.io 就能看到项目官网了! 注意: snowdreams1006仅仅是笔者用户名,实际需要替换成读者的用户名 配置 github 既然项目已经托管到 github 网站,那本地如何访问到远程仓库呢?常用的方式有两种,一种是 https 方式,每次都需要输入密码,另外一种是 ssh 方式,只需要一次配置ssh 密钥对. 这里我们重点介绍最常用也是最方便的第二种 ssh 方式访问 github ,大致思路是本地生成密钥对,然后将公钥上传给 github 表明身份,之后本地再次推送给远程仓库时,github 自然就能识别到我们身份了. 第一步: 生成密钥对 默认情况下,会在当前用户目录下生成一对密钥对. ssh-keygen -t rsa -C \"[email protected]\" 这里的邮箱 [email protected] 需要填写自己的 github 邮箱,之后会提示输入路径和密码,一路回车采用默认值即可,运行结束后会在当前用户目录下 生成一对密钥对,包括公钥和私钥.其中公钥可以发送给任何人,而私钥千万不可泄露. 第二步: 复制公钥 在当前用户根目录下打开 .ssh 目录,其中包括两个文件,一个是公钥 id_rsa.pub ,另一个是私钥 id_rsa,用记事本或者其他方式打开公钥文件,复制其中内容,准备粘贴到github 相关设置项. # 查看当前用户下的 ssh 目录 ls ~/.ssh # 查看生成的公钥内容 cat ~/.ssh/id_rsa.pub 第三步: 设置 github 回到 github,点击头像(Acount),选择设置(Settings),再选择左侧的 SSH and GPG keys,点击右侧的NEW SSH Key,然后填写标题(Title),最好是有意义的名称,比如[email protected] for github,密钥(Key)填写上一边生成的公钥,一般是以ssh-rsa 开头的一大串字符,最后保存(Add SSH Key). 第四步: 验证 ssh 利用 ssh 协议测试一下是否能够正常访问 github 网站,如果出现成功提示,那就证明我们的配置没问题. ssh -T [email protected] 创建远程仓库 登录 github 网站新建远程仓库(New Repository),例如git-demo,默认权限是公开的(public),也可以选择私有的(private),初始化 README.md 文件和 .gitignore 文件以及选择开源协议这些都是可选的,视具体情况而定. 刷新当前页面,应该能到看到已创建好的git-demo 项目,接下来准备将其克隆到本地电脑. 克隆到本地仓库 将远程项目克隆到本地工作空间,和之前本地仓库的开发流程一样,例如add commit status 等等,唯一不同的是,多了一步 push 命令,即本地仓库的最新版本需要推送给远程仓库中,只有这样其他小伙伴才能从远程仓库拉取最新版本,进而才能看到你的代码,因而打破各自为政局面,实现团队协同开发. # 克隆到本地仓库 git clone [email protected]:snowdreams1006/git-demo.git # 切换到当前项目 cd git-demo # 创建新文件 touch test.txt echo \"add test.txt\" > test.txt # 添加文件到暂存区 git add test.txt # 提交文件到本地仓库 git commit -m \"add test.txt\" # 推送到远程仓库 git push origin master 提交完成后,登录 github 网站,刷新当前项目 git-demo ,应该能看到我们刚刚提交的新文件test.txt. 添加仓库关联 添加本地仓库和远程仓库之间关联,默认本地仓库分支名和远程仓库分支名相同 git remote add origin2 [email protected]:snowdreams1006/git-demo.git 查看远程仓库 查看当前配置有哪些远程仓库 git remote 执行时加上-v 参数能够查看别名关联的具体地址,即 git remote -v 下载远程仓库 从远程仓库下载最新分支数据 git fetch 注意: 该命令并不会自动合并当前分支,如需要合并,需手动执行git merge 命令 拉取远程仓库 从远程仓库拉取最新分支数据,自动尝试合并到当前分支,如有冲突,需先解决冲突再合并到当前分支. git pull git pull 相当于 git fetch + git merge 推送远程分支 将本地最新版本推送到远程仓库 git push origin master 以上命令将本地 master 分支推送到 origin 远程仓库的 master 分支 删除远程仓库 git remote rm origin © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:09:25 "},"github/speedup.html":{"url":"github/speedup.html","title":"github 访问速度太慢","keywords":"","body":"github 访问速度太慢 github 是全世界最流行的开源项目托管平台,其代表的开源文化从根本上改变了软件开发的方式. 基本上所有的需求都能从 github 上或多或少找到现成的实现方案,再也不用重头开始造轮子而是自定义轮子! 然而,有时候国内访问 https://github.com/ 速度太慢,如何加速访问 github.com 网站就成了刚需. 由于 github.com 网站位于美国旧金山,所以初始访问 github.com 时网络寻址会比较耗费时间,这也是网站打开速度慢的其中一个原因. 国外在线检测网站: https://www.ipaddress.com/,无法访问的话,请另辟蹊径. 最初用户从浏览器中输入 github.com 网址时,浏览器并不知道这个域名对应的真实 ip 地址,先问问自己电脑认识不认识这个域名的门牌号,如果本机不认识会接着往上问,当地运行商也不认识这个域名的话,继续问上级,直到问出来 github.com 的门牌号是 192.30.253.113 为止! 如此繁琐的问路过程被称之为 DNS 寻址,如果问路的时间都占用很久,那么访问网站的速度自然会很慢. 所以,如果我们直接告诉浏览器目的地,那么浏览器也就不会一步一步去费劲问路了,这在一定程度上也就优化了访问网站的速度. $ ping github.com -c 3 PING github.com (192.30.253.113): 56 data bytes 64 bytes from 192.30.253.113: icmp_seq=0 ttl=41 time=405.924 ms 64 bytes from 192.30.253.113: icmp_seq=1 ttl=41 time=346.654 ms 64 bytes from 192.30.253.113: icmp_seq=2 ttl=41 time=345.485 ms --- github.com ping statistics --- 3 packets transmitted, 3 packets received, 0.0% packet loss round-trip min/avg/max/stddev = 345.485/366.021/405.924/28.220 ms ping github.com -c 3 查看 github.com 网站的门牌号 正常来说,网站的主域名下会存在多个子域名,由这些域名组合在一起提供完整的服务. 而 github.com 也不例外,其中 github.com是一级域名,也是主域名,其他的域名基本上都是二级域名或者说次域名. 所以我们不仅要告诉本机 github.com 的主域名,还要把相关的子域名也告诉本机,帮人帮到底,送福送到西! 那到哪里去查询域名和 ip 的对应关系呢? 想一想现实生活中,每个人都有自己的家,而这个家有具体的地址,也就是平时说的门牌号. 当然,有些人名下不只有一个家,有钱人的世界可以有很多家,毕竟狡兔还有三窟呢! 在这个家中既可以是单身窝,也可以是情侣房,或者是家庭房,具体容纳几个人是由房屋大小决定的. 对应到计算机世界中,如果域名是用户,那么 ip 地址就是用户的家. 同一个域名可以对应多个 ip 地址,同一个 ip 地址也可以有多个域名. 如果有人想要拜访您,肯定要有具体的地址才能到你家里做客,从你家到你家的地址这个过程可能是你告诉他的,也可能是他自己找别人打听到的消息. 域名到 ip 地址的过程同样也需要找人询问,这个信息一般会存在 dns 服务商那里,就像我们的地址登记到相关政府机构一样. 虽然相关机构的信息比较权威及时,但门槛有点高,所以不访问一下当地的\"消息通\". 互联网上的\"消息通\"更是数不胜数,这里推荐两个查询域名解析的网站. https://www.ipaddress.com/ http://tool.chinaz.com/dns/ 子域名有哪些 下面以 ipaddress.com 网站为例,查询下 github.com 网站的相关信息. 在输入框中输入 github.com 域名后开始解析该域名的相关信息,不仅找到了域名对应的 ip 地址还查询到相关网站的域名信息. 亲自去体验一下: 域名查询 根据查到的相关域名信息,再次查询出这些域名对应的 ip 地址,于是整理出以下内容. # github related website 192.30.253.113 github.com 151.101.185.194 github.global.ssl.fastly.net 192.30.253.118 gist.github.com 192.30.253.120 codeload.github.com 185.199.108.153 desktop.github.com 185.199.108.153 guides.github.com 185.199.108.153 blog.github.com 18.204.240.114 status.github.com 185.199.108.153 developer.github.com 185.199.108.153 services.github.com 192.30.253.175 enterprise.github.com 34.195.49.195 education.github.com 185.199.108.153 pages.github.com 34.196.237.103 classroom.github.com 就近 cdn 加速 大型网站服务器都不会是只有一台服务器,而是多台服务器组成的集群一起对外提供服务. 全世界都在使用 github ,如果每一次访问网站时走的都是美国服务器,即使浏览器知道目的地,但是距离太多遥远还是会很慢. 因此,如果能够就近访问 github 网站就能大幅提高访问速度了,幸运的是,网络上同样有现成的工具来帮助我们查看就近的网站地址. 亲自去体验一下: DNS查询 从上图中我们可以看出,同一个域名有很多不同的 ip 地址,从中选择 TTL 值最小的作为优化标准. 于是,将上述清单继续优化成以下内容: # github related website 192.30.253.113 github.com 151.101.185.194 github.global.ssl.fastly.net 203.98.7.65 gist.github.com 13.229.189.0 codeload.github.com 185.199.109.153 desktop.github.com 185.199.108.153 guides.github.com 185.199.108.153 blog.github.com 18.204.240.114 status.github.com 185.199.108.153 developer.github.com 185.199.108.153 services.github.com 192.30.253.175 enterprise.github.com 34.195.49.195 education.github.com 185.199.108.153 pages.github.com 34.196.237.103 classroom.github.com 最好亲自测试一下就近站点以求获得最佳体验,不过推测应该差异不是很大,所以直接复制也无妨. 告诉本机新地址 现在我们已经弄清楚域名和 ip 的映射关系,接下来要做的事情就是告诉本机,不同的操作系统具体文件存放的地址可能有些不同,下面我们以 Windows 和 Mac 举例说明. # github related website 192.30.253.113 github.com 151.101.185.194 github.global.ssl.fastly.net 203.98.7.65 gist.github.com 13.229.189.0 codeload.github.com 185.199.109.153 desktop.github.com 185.199.108.153 guides.github.com 185.199.108.153 blog.github.com 18.204.240.114 status.github.com 185.199.108.153 developer.github.com 185.199.108.153 services.github.com 192.30.253.175 enterprise.github.com 34.195.49.195 education.github.com 185.199.108.153 pages.github.com 34.196.237.103 classroom.github.com windows 映射文件存放于: C:\\Windows\\System32\\drivers\\etc\\hosts 打开 hosts 文件,将上述映射关系追加到文件末尾,保存并退出. 如果由于权限不足,无法保存,可以复制到桌面再编辑文件,最后移动并替换到 hosts 文件. 运行 ipconfig /flushdns 刷新 dns 缓存. mac 映射文件存放于: /etc/hosts 编辑 hosts 文件并追加上述映射关系. $ cat /etc/hosts # jetbrains 0.0.0.0 account.jetbrains.com 0.0.0.0 www.jetbrains.com # github related website 192.30.253.113 github.com 151.101.185.194 github.global.ssl.fastly.net 203.98.7.65 gist.github.com 13.229.189.0 codeload.github.com 185.199.109.153 desktop.github.com 185.199.108.153 guides.github.com 185.199.108.153 blog.github.com 18.204.240.114 status.github.com 185.199.108.153 developer.github.com 185.199.108.153 services.github.com 192.30.253.175 enterprise.github.com 34.195.49.195 education.github.com 185.199.108.153 pages.github.com 34.196.237.103 classroom.github.com 运行 sudo dscacheutil -flushcache 刷新 dns 缓存. $ ping github.com -c 3 PING github.com (192.30.253.113): 56 data bytes 64 bytes from 192.30.253.113: icmp_seq=0 ttl=41 time=395.808 ms 64 bytes from 192.30.253.113: icmp_seq=1 ttl=41 time=306.919 ms 64 bytes from 192.30.253.113: icmp_seq=2 ttl=41 time=298.188 ms --- github.com ping statistics --- 3 packets transmitted, 3 packets received, 0.0% packet loss round-trip min/avg/max/stddev = 298.188/333.638/395.808/44.105 ms $ 又到总结时间 本文讲述了如何解决 github.com 网站访问速度慢的问题,通过修改本机的 hosts 文件来绕过 dns 解析,这种方法仅仅适用于能够访问网站只不过是访问速度慢这一现象. 如果本身无法访问国外网站,那么这种方法就不适用,可能需要另辟蹊径! 最后再贴一下 hosts 文件内容: # github related website 192.30.253.113 github.com 151.101.185.194 github.global.ssl.fastly.net 203.98.7.65 gist.github.com 13.229.189.0 codeload.github.com 185.199.109.153 desktop.github.com 185.199.108.153 guides.github.com 185.199.108.153 blog.github.com 18.204.240.114 status.github.com 185.199.108.153 developer.github.com 185.199.108.153 services.github.com 192.30.253.175 enterprise.github.com 34.195.49.195 education.github.com 185.199.108.153 pages.github.com 34.196.237.103 classroom.github.com © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:09:30 "},"myGitbook/":{"url":"myGitbook/","title":"gitbook 入门教程","keywords":"","body":"gitbook 入门教程 gitBook 是一个基于node.js的命令行工具,使用 github/git 和 markdown/asciiDoc 构建精美的电子书. gitbook 支持输出静态网页和电子书等多种格式,其中默认输出静态网页格式. gitbook 不仅支持本地构建电子书,而且可以托管在 gitbook 官网上,并享受在线发布和托管图书的便利,完整的文档请参考 gitbook 新版文档(需FQ) 或 gitbook 旧版文档(不需FQ) 目前 gitbook 旧版文档已经不可访问,特提供压箱底存货邀君共享 适用场景 不仅适用于软件说明文档的发布更新,同样适用于文本文档的连载更新. 既适合具有一定编程经验的软件开发从业者,也适用于不满足传统书写方式的文学创作者. 简而言之,gitbook 可以条理清晰地整理出零碎知识,打造专属你自己的电子书,漂亮的主题,丰富的插件让你的知识变得从此与众不同! git + markdown = gitbook,其中 git 可以管理书籍内容的变更,并将其托管到云端实现团队协作,而 markdown 简洁的语法特点,使得我们不必关心布局排版问题,专注创作,重拾写作乐趣! 如果你还不了解 git 和 markdown 相关知识,赶紧去学习 markdown 快速入门 和 git 入门教程 吧! 先睹为快 gitbook 教程 gitbook 官网 gitbook 文档 参考文档 gitbook 官网(新) gitbook 官网(旧) gitbook 文档(新) gitbook 文档(旧) gitbook 文档(存货) git 官网 github 官网 gitbook 新版需要FQ,旧版不需要FQ,旧版暂不可用,可访问压箱底存货. © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:09:58 "},"myGitbook/preparation/prepare.html":{"url":"myGitbook/preparation/prepare.html","title":"准备阶段","keywords":"","body":"准备阶段 主要包括两部分: 前置知识和操作工具. 前置知识主要是涉及到 git 以及 markdown 的相关知识,其中 git 是分布式版本控制系统,方便管理我们的电子书,备份到云端,方便团队共享合作,而 markdown 则是一种简单标记语言,简单情况下可以替代 word 进行排版布局,能完全替换 txt 文本,最终实现媲美 html 的输出效果,简洁高效的书写体验,深受广大软件开发者的喜爱. 正是由于 git + markdown 的搭配组合,使得上手 gitbook 相当简单,带给一种全新的体验. 操作工具主要是环境准备,主要是指 gitbook 环境,因为涉及到 git ,所以也包括 git 环境,至于markdown 语法支持,不一定非要安装相应软件,按照规定的格式书写文档即可,不过新手可能更愿意使用可视化软件,毕竟所见即所得,写着放心,看着舒心! 知识准备 markdown 快速入门 git 入门教程 上述教程中有详细的安装以及使用方法,最好能够完全掌握,如果时间有限,不愿意细读的话,请看下一节,我会挑选出常用命令进行讲解! © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:10:16 "},"myGitbook/preparation/front-knowledge.html":{"url":"myGitbook/preparation/front-knowledge.html","title":"前置知识","keywords":"","body":"前置知识 markdown 基本知识 markdown 是一种简化的 html 语法,相比于 txt 无格式文本更强大. 你可以用专门的软件去编辑 markdown 文件,就像需要使用软件编辑 txt 文件一样,当然也可以什么软件也不用,甚至直接在记事本或命令行书写,只不过这样的缺点就是无法实时预览输出效果,安全依赖个人经验和想象力了. markdown 文件后缀名是.md,安装了相应插件的浏览器或专门软件能够看到输出效果. 标题 语法格式: # + 空格 + 文本 大多数markdown编辑器支持 h1~h6 级标题,而富文本编辑器一般仅支持到二级标题. 示例: # 标题1 ## 标题2 效果: 标题1 标题2 列表 列表包括有序列表,无序列表和任务列表,并支持列表嵌套. 大多数 markdown 编辑器和富文本编辑器均支持有序列表和无序列表,而任务列表和列表嵌套支持度就不是很好,存在平台兼容性问题. 有序列表 语法格式:数字 + . + 空格 + 文本 示例: 1. 有序列表1 2. 有序列表2 3. 有序列表3 效果: 有序列表1 有序列表2 有序列表3 无序列表 语法格式:'- 或 * 或 +' + 空格 + 文本 示例: - 无序列表1 * 无序列表2 + 无序列表3 效果: 无序列表1 无序列表2 无序列表3 链接和图片 markdown 编辑器和富文本编辑器均支持链接和图片,值得注意的是有些平台限制或禁止外链. 链接 语法格式:[显示文本] + (链接地址) 示例: [https://snowdreams1006.github.io](https://snowdreams1006.github.io/) 效果: https://snowdreams1006.github.io 图片 语法格式:! + [图片标题] + (图片地址) 示例:  效果: 代码 代码分为单行代码和多行代码,其中多行代码也叫做代码块. 大多数 markdown 编辑器均支持代码,富文本编辑器支持度不一样,有的支持单行代码有的支持代码块. 单行代码 语法格式:` + 单行代码 + ` 示例: `code` 效果: code 多行代码 语法格式:``` + 多行代码 + ``` 示例: ``` function fun(){ echo \"这是一句非常牛逼的代码\"; } fun(); ``` 效果: function fun(){ echo \"这是一句非常牛逼的代码\"; } fun(); 这里的富文本支持语法指的是 markdown 渲染后的内容能否正常显示,并不是指 markdown语法本身能够正常渲染,更多详情请参考 markdown 快速入门 git 基本知识 git 是全世界最先进的分布式版本控制系统,帮助项目更好地进行管理,支持版本历史管理和多人写作管理等功能. 简单地说,可以理解为一种优雅的文档备份方式,支持云端备份,多人协作等特点. 初始化项目 语法格式: git init 适合从零开始的本地项目,初始化后的项目才是能够被 git 管理的项目. 示例: git init 克隆项目 语法格式: git clone 适合已有远程项目需要下载到本地,作用是将远程项目克隆到本地,和 git init 实现类似的功能. 示例: git clone [email protected]:username/username.github.io.git 添加文件 语法格式: git add 将文件添加到暂存区,支持多次添加文件,相当于写入缓存区. 示例: git add . 提交文件 语法格式: git commit 将暂存区内容提交到版本库,完成一次历史版本. 示例: git commit -m \"写入提交备注,简短说明下提交意图和目标\" 推送文件 语法格式: git push 将本地版本库推送到远程版本库,相当于本地文件备份到云端服务器. 示例: git push origin master 拉取文件 语法格式: git pull 将远程版本库拉取到本地版本库,相当于云端服务器文件恢复到本地. 示例: git pull 查看状态 语法格式: git status 查看当前文件状态,包括文件被新增,被修改,被删除,未提交等等. 示例: git status 比较差异 语法格式: git diff 查看两个文件之间的具体差异 示例: git diff 历史日志 语法格式: git log 查看版本库的提交历史日志 示例: git log 上述仅介绍了 git 的简单命令,实际使用情况远不止这些,更多详情请参考 git 入门教程 © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:10:16 "},"myGitbook/preparation/environmental-requirements.html":{"url":"myGitbook/preparation/environmental-requirements.html","title":"环境要求","keywords":"","body":"环境要求 gitbook 是基于 node.js 的命令行工具,首先需要安装并配置好 node.js 环境,然后才能安装gitbook 相关工具. 由于安装工具全部都是国外网站,因此速度可能会很慢,也可能需要FQ,请耐心等待或者学会科学上网. 当然如果安装过程中遇到任何问题,也可以找我要一下安装包或者我帮你免费解决下. 环境预检查 检查 git 环境[可选] git 是免费开源的分布式版本控制系统,主要用于电子书的更新管理和团队协作,如果不需要将电子书托管到github 网站上,则可以不安装 git . 如果打印出 git 版本信息,则表示本机已安装 git 环境,跳过此步骤. $ git --version git 安装配置教程请参考初识 git 检查 node.js 环境[必须] node.js 是 js 在服务端运行的环境基础,从而使得 js 从浏览器端延伸到服务端领域,而 gitbook 则是运行在 node.js 基础之上的命令行工具,因此必须先安装好 node.js 开发环境. 如果打印出 node.js 版本信息,则表示本机已安装 node.js 环境,跳过此步骤. $ node --version nodejs 默认的包安装工具 npm 国内访问速度有点慢,安装完毕后建议 npm install cnpm -g --registry=https://registry.npm.taobao.org 使用淘宝镜像源代替默认的 npm ,详细教程请参考官方 https://nodejs.org/ 检查 gitbook 环境[必须] gitbook-cli 是 gitbook 的脚手架工具,帮助我们更方便构建 gitbook 应用,当然也可以直接安装 gitbook ,只不过那样的话,略显麻烦,不推荐. 如果打印出 gitbook 和 cli 版本信息,则表示本机已安装 gitbook 环境,跳过此步骤. $ gitbook --version 否则的话,本机可能并没有安装 gitbook 环境,则需要安装 gitbook 相关工具. 因为 gitbook 是基于 node.js 环境,而安装好 node.js 后默认提供了 npm 包管理工具,而我们则是通过 npm 来安装其他工具. 安装 gitbook-cli 工具[必须] 假设你已经搭建好 node.js 环境,现在我们开始安装 gitbook 相关工具了! $ sudo npm install -g gitbook-cli 如果使用 cnpm 安装的话,使用 cnpm install -g gitbook-cli 命令. 安装成功后会带有 gitbook 命令,现在再次运行下 gitbook --version 查看版本信息. # 打印出 `CLI` 和 `GitBook` 版本信息即可,安装版本可能已经大于 `2.3.2` $ gitbook --version CLI version: 2.3.2 GitBook version: 3.2.3 $ 安装 GitBook Editor 编辑器[可选] gitbook 官方客户端编辑器,支持 windows, mac 和 linux ,主要用于可视化编辑文档,组织文档结构. 下载相应平台的 GitBook Editor,正常安装即可. gitbook 的使用方法大致可以有三种,而 GitBook Editor 编辑器只是其中一种,所以这一步是可选的. 使用 gitbook-cli 脚手架提供的各种命令直接在命令行管理 gitbook,适合一定编程经验的软件从业人员. 使用 GitBook Editor 编辑器管理 gitbook ,适合无任何编程的文学创作者. 使用 gitbook.com 官网在线管理 gitbook ,适合不具备本地开发环境的萌新体验者. 小结 gitbook 基于 node.js 开发环境,因此首先要安装好 nodejs 环境,其次再使用 node.js 提供的 npm 包管理工具来安装 gitbook. 只需运行 sudo npm install -g gitbook-cli 即可安装,接着运行 gitbook -V 查看安装版本信息确认已经安装成功. 至此 gitbook 的必要开发环境已经准备妥当,接下来让我们赶紧体验一下 gitbook 的魅力吧! © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:10:16 "},"myGitbook/experience/preview.html":{"url":"myGitbook/experience/preview.html","title":"快速体验","keywords":"","body":"快速体验 本文主要介绍三种使用 gitbook 的方式,分别是 gitbook 命令行工具,Gitbook Editor 官方编辑器和 gitbook.com 官网. 总体来说,三种途径适合各自不同的人群,找到适合自己的方式就好,基本操作流程都是一样的. 命令行工具更适合具备编程经验开发者,具有简单高效易整合等特点. 编辑器更适合无任何编程经验的文学创作者,不熟悉 markdown 语法,不熟悉 git 工作流,这种情况下也推荐使用图形化操作的编辑器. 官网适合想要快速体验 gitbook 效果的萌新,只有觉得物超所值才能有动力搭建 gitbook 开发环境,不是吗? 当然,如果你想访问官网的话,你可能需要学会科学上网,网址见文章结尾. gitbook 命令行 首先需要创建存放书籍的目录,然后对该目录进行初始化,最后启动本地服务即可体验效果. 初始化项目 语法格式: gitbook init 如果是空目录会自动创建 README.md 和 SUMMARY.md 两个文件,当然也可以手动创建再初始化. 示例: # 创建 `gitbook` 演示项目 $ mkdir gitbook-demo # 初始化项目 $ gitbook init warn: no summary file in this book info: create README.md info: create SUMMARY.md info: initialization is finished # 当前目录结构 $ tree . ├── README.md └── SUMMARY.md 0 directories, 2 files $ gitbook init 命令可能会自动生成 README.md 和 SUMMARY.md 两个文件,如已存在则更新. 运行项目 语法格式: gitbook serve 将初始化后的项目启动成为一个本地服务,我们可以直接在浏览器访问项目,预览书籍效果. 示例: # 启动本地服务器 $ gitbook serve Live reload server started on port: 35729 Press CTRL+C to quit ... info: 7 plugins are installed info: loading plugin \"livereload\"... OK info: loading plugin \"highlight\"... OK info: loading plugin \"search\"... OK info: loading plugin \"lunr\"... OK info: loading plugin \"sharing\"... OK info: loading plugin \"fontsettings\"... OK info: loading plugin \"theme-default\"... OK info: found 1 pages info: found 0 asset files info: >> generation finished with success in 1.2s ! Starting server ... Serving book on http://localhost:4000 如果要停止服务器,只需同时按住 CTRL+C 即可,现在再看一下 gitbook-demo 目录结构. $ tree . ├── README.md ├── SUMMARY.md └── _book ├── gitbook │ ├── fonts │ │ └── fontawesome │ │ ├── FontAwesome.otf │ │ ├── fontawesome-webfont.eot │ │ ├── fontawesome-webfont.svg │ │ ├── fontawesome-webfont.ttf │ │ ├── fontawesome-webfont.woff │ │ └── fontawesome-webfont.woff2 │ ├── gitbook-plugin-fontsettings │ │ ├── fontsettings.js │ │ └── website.css │ ├── gitbook-plugin-highlight │ │ ├── ebook.css │ │ └── website.css │ ├── gitbook-plugin-livereload │ │ └── plugin.js │ ├── gitbook-plugin-lunr │ │ ├── lunr.min.js │ │ └── search-lunr.js │ ├── gitbook-plugin-search │ │ ├── lunr.min.js │ │ ├── search-engine.js │ │ ├── search.css │ │ └── search.js │ ├── gitbook-plugin-sharing │ │ └── buttons.js │ ├── gitbook.js │ ├── images │ │ ├── apple-touch-icon-precomposed-152.png │ │ └── favicon.ico │ ├── style.css │ └── theme.js ├── index.html └── search_index.json 11 directories, 27 files $ gitbook serve 命令可能会自动生成 _book 目录,如已存在则更新. gitbook editor 编辑器 下载 gitbook editor 并安装,如果下载遇到困难,可以找我来帮忙哟! 如果你没有梯子,可以暂不登录(Do that Later),只不过无法与 gitbook.com 保持同步. 更改图书路径 更改默认图书存放位置(Gitbook Editor => Change Library Path...),以后图书目录都在该目录下,比如设置的是 .../gitbook-editor/ 图书目录. 新建图书 新建图书项目,名字仍然是 gitbook-demo,这样方便比较和命令行创建的 gitbook-demo 区别. 图形化操作界面总体来说还是很容易上手的,自己好好研究一下即可,这里仅仅演示默认效果. 启动项目 现在先找到新建图书的具体目录,然后再启动本地服务器,同样地,我们在浏览器中体验电子书效果. 图书项目路径: /workspace/gitbook-editor/Import/gitbook-demo,其中 /workspace/gitbook-editor/ 是上一步更改的图书路径. # 启动本地服务器 $ gitbook serve 这里不再需要运行 gitbook init 命令了,因为已经创建过 README.md 和 SUMMARY.md 这两个文件. Gitbook Editor 编辑器新建的图书项目和 gitbook-cli 创建的图书项目本质上并没有什么不同,只不过编辑器集成了常用功能而已! gitbook.com 网站 由于受网络因素所限,暂时不分享这部分知识了,简单来说就是在线编辑并发布电子书,这一点和 github 的代码托管服务类似. gitbook 新版官网(需要FQ) : https://www.gitbook.com/ gitbook 旧版官网(无需FQ) : https://legacy.gitbook.com 小结 初始化项目 : gitbook init 启动项目 : gitbook serve 默认访问地址: http://localhost:4000 © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:10:09 "},"myGitbook/experience/gitbook-cli.html":{"url":"myGitbook/experience/gitbook-cli.html","title":"gitbook-cli 命令行操作","keywords":"","body":"gitbook-cli 命令行操作 gitbook 生成电子书主要有三种方式: gitbook-cli 命令行操作,简洁高效,适合从事软件开发的相关人员. gitbook-editor 编辑器操作,可视化编辑,适合无编程经验的文学创作者. gitbook.com 官网操作,在线编辑实时发布,适合无本地环境且科学上网的体验者. 本文主要讲解第一种 gitbook-cli 命令行操作流程,其他两种见另外两篇教程. gitbook 的一些常用命令 安装 gitbook-cli 脚手架工具 本机已安装 node.js 开发环境,安装完成后运行 gitbook -V 能够打印出版本信息,则表示安装成功. $ sudo npm install -g gitbook-cli 关于安装配置相关问题请参考 环境要求 初始化 gitbook 项目 初始化项目,按照 gitbook 规范会自动创建 README.md 和 SUMMARY.md 两个文件,具体用途见下文. 其实 SUMMARY.md 是电子书的章节目录,gitbook 会初始化相应的文件目录结构,所以主要是用于开发初始阶段. $ gitbook init 启动 gitbook 项目 启动本地服务,程序无报错则可以在浏览器预览电子书效果: http://localhost:4000 由于能够实时预览电子书效果,并且大多数开发环境搭建在本地而不是远程服务器中,所以主要用于开发调试阶段. $ gitbook serve 构建 gitbook 静态网页 构建静态网页而不启动本地服务器,默认生成文件存放在 _book/ 目录,当然输出目录是可配置的,暂不涉及,见高级部分. 输出静态网页后可打包上传到服务器,也可以上传到 github 等网站进行托管,因而主要用于发布准备阶段. $ gitbook build 章节小结 gitbook init 初始化 README.md 和 SUMMARY.md 两个文件. gitbook build 本地构建但不运行服务,默认输出到 _book/ 目录. gitbook serve 本地构建并运行服务,默认访问 http://localhost:4000 实时预览. # 创建 `gitbook` 演示项目 $ mkdir gitbook-demo # 初始化项目 $ gitbook init warn: no summary file in this book info: create README.md info: create SUMMARY.md info: initialization is finished # 启动本地服务器 $ gitbook serve Live reload server started on port: 35729 Press CTRL+C to quit ... info: 7 plugins are installed info: loading plugin \"livereload\"... OK info: loading plugin \"highlight\"... OK info: loading plugin \"search\"... OK info: loading plugin \"lunr\"... OK info: loading plugin \"sharing\"... OK info: loading plugin \"fontsettings\"... OK info: loading plugin \"theme-default\"... OK info: found 1 pages info: found 0 asset files info: >> generation finished with success in 1.2s ! Starting server ... Serving book on http://localhost:4000 # 查看当前目录结构 $ tree . ├── README.md ├── SUMMARY.md └── _book ├── gitbook │ ├── fonts │ │ └── fontawesome │ │ ├── FontAwesome.otf │ │ ├── fontawesome-webfont.eot │ │ ├── fontawesome-webfont.svg │ │ ├── fontawesome-webfont.ttf │ │ ├── fontawesome-webfont.woff │ │ └── fontawesome-webfont.woff2 │ ├── gitbook-plugin-fontsettings │ │ ├── fontsettings.js │ │ └── website.css │ ├── gitbook-plugin-highlight │ │ ├── ebook.css │ │ └── website.css │ ├── gitbook-plugin-livereload │ │ └── plugin.js │ ├── gitbook-plugin-lunr │ │ ├── lunr.min.js │ │ └── search-lunr.js │ ├── gitbook-plugin-search │ │ ├── lunr.min.js │ │ ├── search-engine.js │ │ ├── search.css │ │ └── search.js │ ├── gitbook-plugin-sharing │ │ └── buttons.js │ ├── gitbook.js │ ├── images │ │ ├── apple-touch-icon-precomposed-152.png │ │ └── favicon.ico │ ├── style.css │ └── theme.js ├── index.html └── search_index.json 11 directories, 27 files $ gitbook 的目录结构说明 既然要书写一本电子书,那么起码的章节介绍和章节详情自然是必不可少的. 当然还有标题,作者和联系方式等个性化信息需要指定,如果不指定的话,一旦采用默认配合,八成不符合我们的预期,说不定都会变成匿名电子书?所以配置文件一般也是需要手动设置的! 真正可选的文件要数词汇表了,毕竟不是每一本电子书都有专业词汇需要去解释说明.如果在章节详情顺便解释下涉及到的专业词汇,那么自然也就不需要词汇表文件了. 简单解释下各个文件的作用: README.md 是默认首页文件,相当于网站的首页 index.html ,一般是介绍文字或相关导航链接. SUMMARY.md 是默认概括文件,主要是根据该文件内容生成相应的目录结构,同 README.md 一样都是被gitbook init 初始化默认创建的重要文件. _book 是默认的输出目录,存放着原始 markdown 渲染完毕后的 html 文件,可以直接打包到服务器充当静态网站使用.一般是执行 gitbook build 或 gitbook serve 自动生成的. book.json 是配置文件,用于个性化调整 gitbook 的相关配置,如定义电子书的标题,封面,作者等信息.虽然是手动创建但一般是必选的. GLOSSARY.md 是默认的词汇表,主要说明专业词汇的详细解释,这样阅读到专业词汇时就会有相应提示信息,也是手动创建但是可选的. LANGS.md 是默认的语言文件,用于国际化版本翻译,和 GLOSSARY.md 一样是手动创建但是可选的. README.md 首页文件[必须] 编辑 README.md 文件,随便写点内容并启动本地服务(gitbook serve)实时预览效果. SUMMARY.md 概括文件[必须] 先停止本地服务,编辑章节目录结构,然后重新再初始化(gitbook init)自动创建相应目录. _book 输出目录[可选] 执行 gitbook build 或 gitbook serve 命令后会自动生成静态网页. # 构建电子书 $ gitbook build info: 7 plugins are installed info: 6 explicitly listed info: loading plugin \"highlight\"... OK info: loading plugin \"search\"... OK info: loading plugin \"lunr\"... OK info: loading plugin \"sharing\"... OK info: loading plugin \"fontsettings\"... OK info: loading plugin \"theme-default\"... OK info: found 5 pages info: found 0 asset files info: >> generation finished with success in 0.7s ! # 查看输出目录 $ tree _book/ _book/ ├── first │ ├── 01.html │ └── 02.html ├── first.html ├── gitbook │ ├── fonts │ │ └── fontawesome │ │ ├── FontAwesome.otf │ │ ├── fontawesome-webfont.eot │ │ ├── fontawesome-webfont.svg │ │ ├── fontawesome-webfont.ttf │ │ ├── fontawesome-webfont.woff │ │ └── fontawesome-webfont.woff2 │ ├── gitbook-plugin-fontsettings │ │ ├── fontsettings.js │ │ └── website.css │ ├── gitbook-plugin-highlight │ │ ├── ebook.css │ │ └── website.css │ ├── gitbook-plugin-lunr │ │ ├── lunr.min.js │ │ └── search-lunr.js │ ├── gitbook-plugin-search │ │ ├── lunr.min.js │ │ ├── search-engine.js │ │ ├── search.css │ │ └── search.js │ ├── gitbook-plugin-sharing │ │ └── buttons.js │ ├── gitbook.js │ ├── images │ │ ├── apple-touch-icon-precomposed-152.png │ │ └── favicon.ico │ ├── style.css │ └── theme.js ├── index.html ├── search_index.json └── second.html 10 directories, 28 files $ book.json 配置文件[可选] 在根目录下新建 book.json 配置文件,完整的支持项请参考官方文档,下面仅列举常用的一些配置项. title 标题 书籍的标题 示例: \"title\": \"雪之梦技术驿站\" author 作者 书籍的作者 示例: \"author\": \"snowdreams1006\" description 描述 书籍的简要描述 示例: \"description\": \"雪之梦技术驿站又名snowdreams1006的技术小屋.主要分享个人的学习经验,一家之言,仅供参考.\" isbn 国际标准书号 书籍的国际标准书号 示例: \"isbn\": \"978-0-13-601970-1\" 选填,请参考 ISBN Search language 语言 支持语言项: 默认英语(en),设置成简体中文(zh-hans) en, ar, bn, cs, de, en, es, fa, fi, fr, he, it, ja, ko, no, pl, pt, ro, ru, sv, uk, vi, zh-hans, zh-tw 示例: \"language\": \"zh-hans\" direction 阅读顺序 阅读顺序,支持从右到左(rtl)或从左到右(ltr),默认值取决于语言值. 示例: \"direction\" : \"ltr\" gitbook 版本 指定 gitbook 版本,支持SemVer规范,接受类似于 >=3.2.3 的条件. 示例: \"gitbook\": \"3.2.3\" root 根目录 指定存放 gitbook 文件(除了book.json文件本身)的根目录 示例: \"root\": \".\" links 侧边栏链接 左侧导航栏添加链接,支持外链 示例; \"links\": { \"sidebar\": { \"我的网站\": \"https://snowdreams1006.cn/\" } } styles 自定义样式 自定义全局样式 示例: \"styles\": { \"website\": \"styles/website.css\", \"ebook\": \"styles/ebook.css\", \"pdf\": \"styles/pdf.css\", \"mobi\": \"styles/mobi.css\", \"epub\": \"styles/epub.css\" } plugins 插件 配置额外的插件列表,添加新插件项后需要运行 gitbook install 安装到当前项目. gitbook 默认自带5个插件,分别是: highlight 语法高亮插件 search 搜索插件 sharing 分享插件 font-settings 字体设置插件 livereload 热加载插件 后续会介绍一些常用插件,如需获取更多插件请访问官网插件市场 示例: \"plugins\": [ \"github\", \"pageview-count\", \"mermaid-gb3\", \"-lunr\", \"-search\", \"search-plus\", \"splitter\", \"-sharing\", \"sharing-plus\", \"expandable-chapters-small\", \"anchor-navigation-ex\", \"edit-link\", \"copy-code-button\", \"chart\", \"favicon-plus\", \"donate\" ] pluginsConfig 插件配置 安装插件的相应配置项,具体有哪些配置项是由插件本身提供的,应访问插件官网进行查询. \"pluginsConfig\": { \"github\": { \"url\": \"https://github.com/snowdreams1006/snowdreams1006.github.io\" }, \"sharing\": { \"douban\": true, \"facebook\": false, \"google\": false, \"hatenaBookmark\": false, \"instapaper\": false, \"line\": false, \"linkedin\": false, \"messenger\": false, \"pocket\": false, \"qq\": true, \"qzone\": true, \"stumbleupon\": false, \"twitter\": false, \"viber\": false, \"vk\": false, \"weibo\": true, \"whatsapp\": false, \"all\": [ \"facebook\", \"google\", \"twitter\", \"weibo\", \"instapaper\", \"linkedin\", \"pocket\", \"stumbleupon\" ] }, \"edit-link\": { \"base\": \"https://github.com/snowdreams1006/snowdreams1006.github.io/blob/master\", \"label\": \"编辑本页\" }, \"chart\": { \"type\": \"c3\" }, \"favicon\": \"/images/favicon.ico\", \"appleTouchIconPrecomposed152\": \"/images/apple-touch-icon-precomposed-152.png\", \"output\": \"_book\", \"donate\": { \"wechat\": \"/images/wechat.jpg\", \"alipay\": \"/images/alipay.jpg\", \"title\": \"赏\", \"button\": \"捐赠\", \"alipayText\": \"支付宝\", \"wechatText\": \"微信\" } } structure 目录结构配置 指定README.md,SUMMARY.md,GLOSSARY.md 和 LANGS.md 文件名称. 配置项 描述 structure.readme readme 文件名(默认值是 README.md) structure.summary summary 文件名(默认值是 SUMMARY.md) structure.glossary glossary 文件名(默认值是 GLOSSARY.md) structure.languages languages 文件名(默认值是 LANGS.md) pdf 配置 定制 pdf 输出格式,可能需要安装 ebook-convert 等相关插件 配置项 描述 pdf.pageNumbers 添加页码(默认值是 true ) pdf.fontSize 字体大小(默认值是 12 ) pdf.fontFamily 字体集(默认值是 Arial ) pdf.paperSize 页面尺寸(默认值是 a4 ),支持a0,a1,a2,a3,a4,a5,a6,b0,b1,b2,b3,b4,b5,b6,legal,letter pdf.margin.top 上边界(默认值是 56 ) pdf.margin.bottom 下边界(默认值是 56 ) pdf.margin.left 左边界(默认值是 62 ) pdf.margin.right 右边界(默认值是 62 ) 电子书封面照片 cover.jpg 和 cover_small.jpg,后续会详细说明. GLOSSARY.md 词汇表文件[可选] 词汇表文件,用于全书的专业词汇解释说明,比如鼠标悬停在专业词汇上会有相应提示. 语法格式: ## + ` +专业词汇` 学习 gitbook 前最好先学习下markdown和git,你知道他们的用途吗? 示例: ## markdown 简洁优雅的排版语言,简化版的 `HTML`,加强版的 `TXT`,详情请参考 [https://snowdreams1006.github.io/markdown/](https://snowdreams1006.github.io/markdown/) ## git 分布式版本控制系统,详情请参考 [https://snowdreams1006.github.io/git/](https://snowdreams1006.github.io/git/) LANGS.md 语言文件[可选] 支持国际化编写图书,一种语言一个单独子目录,同样地,将语言文件放到根目录下. 示例: * [English](en/) * [French](fr/) * [Español](es/) 章节小结 开发初始阶段运行 gitbook init 命令按照 SUMMARY.md 文件内容自动创建对应目录结构,编写各自文件内容后运行 gitbook serve 启动本地服务实时预览效果. 开发到一定程度后打算发布服务,再运行 gitbook build 输出到 _book/ 目录,别忘了配置 book.json 文件,然后就可以将 _book/ 文件夹整个扔到 nginx 等静态服务器上,这样就能联网访问你的电子书了. 是不是很简单,后续还会有如何发布与导出等相关教程,今天先到这里,下次见! © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:10:08 "},"myGitbook/experience/gitbook-editor.html":{"url":"myGitbook/experience/gitbook-editor.html","title":"gitbook-editor 编辑器操作","keywords":"","body":"gitbook-editor 编辑器操作 亲测,目前已不再支持旧版 gitbook-editor 编辑器,而官网也没有相应的新版编辑器,如果哪位找到了新版编辑器,还望告知! 现在注册 gitbook 账号会默认重定向到 新版官网,而 旧版官网 的账号应该是可以正常使用的,前提是你必须之前注册过. 遗憾的是,最新注册的账号是无法使用 gitbook-editor 编辑器,不能登录到 gitbook ,也无法同步 github ,充其量只能算本地的 markdown 编辑器,所以这一节不再介绍了. 如果有兴趣了解 gitbook-editor 编辑器的基本使用,请参考 gitbook editor 编辑器. © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:10:09 "},"myGitbook/experience/gitbook-com.html":{"url":"myGitbook/experience/gitbook-com.html","title":"gitbook.com 官网操作","keywords":"","body":"gitbook.com 官网操作 gitbook 官网是官方提供的图书托管的在线平台,分为新版官网(需要FQ) https://www.gitbook.com/ 和旧版官网(无需FQ) https://legacy.gitbook.com 两个网站. 目前均正常提供服务,但令人遗憾的是,两个网站的信息相互独立,而且现在注册的账号默认只能在新版官网中使用,而新版官网的访问速度简直比 github 还要慢,所以国内用户在线访问你的电子书真的需要点技术手段了! 本文主要介绍 www.gitbook.com 官网的基本使用,而 legacy.gitbook.com 网站我就算是想介绍也没有账号测试啊. \"巧妇难为无米之炊\",明明你就在那里,可我却什么也做不了. 先大概说一下 gitbook.com 网站的一些个人总结吧. gitbook.com 提供收费和免费服务,有点像早期的 github ,免费账号只能创建一个私有的命名空间,其他命名空间只能是公开的,这里的命名空间可以理解为一本书. 这一点是不是有点像早期的 github.com?免费账号无法创建私有仓库,只能是公开仓库. (现在 github.com 已被微软收购,目前可以创建无限量的私有仓库了!) 再说 gitbook 的账号问题,像 github 一样提供用户名和邮箱登录方式,他们的用户名都可以作为二级域名,比如我的用户名是snowdreams1006,那么我的 gitbook 第一本电子书网址就是 https://snowdreams1006.gitbook.io/index/ ,再看一下我的 github 个人网址 https://snowdreams1006.github.io/ ,这两个是不是很类似?! 如果不仔细看的话,八成你会觉得一样,一个是gitbook.io,另一个是github.io. 所以我严重怀疑他俩是不是有着不为人知的私密关系,太多的相似性,鼓励分享,限制私有等等特点. 无图无真相,趁着这次教程顺便将 github 个人网站项目同步到 gitbook 电子书项目了,这样的好处是本地只需要推送到 github ,自动更新 github.io 网站(利用的是github 静态网站托管服务) ,然后再自动同步到 gitbook.io 网站. 是不是很神奇,一份源码,两个官网! gitbook : https://snowdreams1006.gitbook.io/ github : https://snowdreams1006.github.io/ 注册并登陆 gitbook.com 注册信息主要包括用户名和邮箱,还有一些其他信息,没什么特殊的注意事项. 访问 https://www.gitbook.com/ 需要 FQ 新建命名空间(电子书) 注册账后后会默认生成一个私有的命名空间,因为并不打算将私有电子书托管到 gitbook,所以接下来直接将其转变成公开电子书进行演示. 个性性配置 标题和图片 主题颜色和页面反馈 观众 观众指的是当前电子书面向的受众是谁,公开的和私有的的区别以及设置是否被谷歌搜索收录. 域名 默认域名是 https://snowdreams1006.gitbook.io/,如果需要自定义域名,请保证 dns 能够正确解析到该网站. url 设置的命名空间是 index,因此最终访问路径是 https://snowdreams1006.gitbook.io/index/ 整合 gitbook 默认提供4种整合方式,在下孤陋寡闻只了解 github ,其余三种没接触过,暂不涉及. 选择 github 进行整合 登录 github 并授权 选择列出公开的仓库,然后输入用户名和密码进行登录并授权. 选择目标仓库 授权成功后会列出当前 github 账号下全部的公开仓库,选择目标仓库并点击下一步. 这里以 snowdreams1006.github.io 公开仓库为例,因为该仓库是本人官网源码项目. 同步内容 选择同步分支 根据实际情况选择同步分支,因为我一般是直接推送到 master 分支,所以 master 分支是个人网站的维护分支,因此这一步我选择的是 master. 选择同步内容 选择同步内容的方式,是从 github 同步到 gitbook,还是从 gitbook 同步到 github,因为我的项目已托管到 github ,所以初次同步内容选择的是 github --> gitbook. 显示 github 按钮 生成的电子书网站是否显示 github 按钮,作用是点击该按钮会跳转到关联的github 仓库上. 此时心里在想,万一点进 github ,随手就是一个 star 呢?哈哈! 等待内容导入 根据目标仓库的大小不同,导入内容是的时长自然也不一样,耐心等待... 上线 导入完成,电子书终于正式上线了! 现在赶紧分享一下好消息吧,访问 https://.gitbook.io/ 在线阅读! 小结 本文以如何集成 github 为例,演示了 gitbook.com 发布电子书的基本流程,由于 gitbook 电子书内容来自于 github 项目,因此我们只要更新 github 仓库,我们的 gitbook 电子书网站自然也就相应更新了! gitbook 是 markdown 和 github 的完美结合体,借助 gitbook.com 官网我们很容易发布并托管电子书. 美中不足的是,国内无法正常访问 gitbook.com ,因此并不是很推荐将电子书发布到 gitbook.com 网站. 现在国内也有类似的产品,有一种产品叫做 看云,还不错! 后续还会介绍 gitbook 如何结合 github 发布个人网站,欢迎继续关注 gitbook 系列教程! 如何打造免费的个人官网,想了解 https://snowdreams1006.github.io/ 背后的故事吗? © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:10:08 "},"myGitbook/advance/advance.html":{"url":"myGitbook/advance/advance.html","title":"高级进阶","keywords":"","body":"高级进阶 不论是 gitbook-cli 命令行还是 gitbook editor 编辑器都离不开 gitbook 命令的操作使用,所以再次了解下常用命令. 注意 gitbook-cli 是 gitbook 的脚手架工具,是 gitbook 的扩展功能,同时着管理 gitbook. 查看 gitbook 帮助信息 语法格式: gitbook --help 示例: $ gitbook --help Usage: gitbook [options] [command] Options: -v, --gitbook [version] specify GitBook version to use -d, --debug enable verbose error -V, --version Display running versions of gitbook and gitbook-cli -h, --help output usage information Commands: ls List versions installed locally current Display currently activated version ls-remote List remote versions available for install fetch [version] Download and install a alias [folder] [version] Set an alias named pointing to uninstall [version] Uninstall a version update [tag] Update to the latest version of GitBook help List commands for GitBook * run a command with a specific gitbook version $ gitbook ls 列出本地安装版本 语法格式: gitbook ls 示例: # 列出本地已安装 `gitbook` 版本 $ gitbook ls gitbook current 列出当前使用版本 语法格式: gitbook current 示例: # 列出当前正在使用的 `gitbook` 版本 $ gitbook current gitbook ls-remote 列出远程可用版本 语法格式: gitbook ls-remote 示例: # 列出远程可用的 `gitbook` 版本 $ gitbook ls-remote gitbook fetch 安装指定版本 语法格式: gitbook fetch [version] 示例: # 下载并安装指定的 `gitbook` 版本 $ gitbook fetch 2.6.9 gitbook alias 指定文件夹别名 语法格式: gitbook alias [folder] [version] 示例: # 下载并安装指定的 `gitbook` 版本 $ gitbook alias /Users/sunpo/Desktop/book/gitbook/ 1.0.0 gitbook uninstall 卸载指定版本 语法格式: gitbook uninstall [version] 示例: # 卸载指定的 `gitbook` 版本 $ gitbook uninstall 2.6.9 gitbook update 更新指定版本 语法格式: gitbook update [tag] 示例: # 默认更新到最新的 `gitbook` 版本 $ gitbook update # 更新到指定的 `gitbook` 版本 $ gitbook update 2.6.9 列出 gitbook 可用命令 语法格式: gitbook help 示例: $ gitbook help build [book] [output] build a book --log Minimum log level to display (Default is info; Values are debug, info, warn, error, disabled) --format Format to build to (Default is website; Values are website, json, ebook) --[no-]timing Print timing debug information (Default is false) serve [book] [output] serve the book as a website for testing --port Port for server to listen on (Default is 4000) --lrport Port for livereload server to listen on (Default is 35729) --[no-]watch Enable file watcher and live reloading (Default is true) --[no-]live Enable live reloading (Default is true) --[no-]open Enable opening book in browser (Default is false) --browser Specify browser for opening book (Default is ) --log Minimum log level to display (Default is info; Values are debug, info, warn, error, disabled) --format Format to build to (Default is website; Values are website, json, ebook) install [book] install all plugins dependencies --log Minimum log level to display (Default is info; Values are debug, info, warn, error, disabled) parse [book] parse and print debug information about a book --log Minimum log level to display (Default is info; Values are debug, info, warn, error, disabled) init [book] setup and create files for chapters --log Minimum log level to display (Default is info; Values are debug, info, warn, error, disabled) pdf [book] [output] build a book into an ebook file --log Minimum log level to display (Default is info; Values are debug, info, warn, error, disabled) epub [book] [output] build a book into an ebook file --log Minimum log level to display (Default is info; Values are debug, info, warn, error, disabled) mobi [book] [output] build a book into an ebook file --log Minimum log level to display (Default is info; Values are debug, info, warn, error, disabled) $ gitbook build 构建电子书 语法格式: gitbook build [book] [output] 示例: # 默认输出到 `_book/` 目录 $ gitbook build # 指定输出目录 `/Users/sunpo/Desktop/book/` $ gitbook build ./ /Users/sunpo/Desktop/book/ # 指定输出格式 `json` $ gitbook build --format=json gitbook serve 启动本地服务器 语法格式: gitbook serve [book] [output] 示例: # 默认服务端口: `4000`,热部署端口: `35729` $ gitbook serve # 指定输出目录 `/Users/sunpo/Desktop/book/` $ gitbook serve ./ /Users/sunpo/Desktop/book/ # 指定服务端口: `5000` 和热部署端口: `45729` $ gitbook serve --port=5000 --lrport=45729 gitbook install 安装插件 语法格式: gitbook install [book] 示例: # 安装当前项目所需插件 $ gitbook install # 安装指定项目所需插件 `/Users/sunpo/Desktop/gitbook-demo/` $ gitbook install /Users/sunpo/Desktop/gitbook-demo/ # 安装当前项目所需插件且指定日志输出级别: `debug` $ gitbook install --log=debug gitbook parse 解析电子书 语法格式: gitbook parse [book] 示例: # 解析并输出当前项目的 `debug` 级别日志信息 $ gitbook parse # 解析并输出指定项目的 `/Users/sunpo/Desktop/gitbook-demo/` 的 `debug` 级别日志信息 $ gitbook parse /Users/sunpo/Desktop/gitbook-demo/ # 解析并输出当前项目的 `info` 级别日志信息 $ gitbook parse --log=info gitbook pdf 输出 PDF 电子书 语法格式: gitbook pdf [book] [output] 示例: # 默认输出到当前项目 $ gitbook pdf # 指定输出文件 `/Users/sunpo/Desktop/book.pdf` $ gitbook pdf ./ /Users/sunpo/Desktop/book.pdf # 指定输出日志级别: `debug` $ gitbook pdf --log=debug 可能需要安装 ebook-convert 相关插件,详情见相关系列教程. gitbook epub 输出 epub 电子书 语法格式: gitbook epub [book] [output] 示例: # 默认输出到当前项目 $ gitbook epub # 指定输出文件 `/Users/sunpo/Desktop/book.epub` $ gitbook epub ./ /Users/sunpo/Desktop/book.epub # 指定输出日志级别: `debug` $ gitbook epub --log=debug 可能需要安装 ebook-convert 相关插件,详情见相关系列教程. gitbook mobi 输出 mobi 电子书 语法格式: gitbook mobi [book] [output] 示例: # 默认输出到当前项目 $ gitbook mobi # 指定输出文件 `/Users/sunpo/Desktop/book.mobi` $ gitbook mobi ./ /Users/sunpo/Desktop/book.mobi # 指定输出日志级别: `debug` $ gitbook mobi --log=debug 可能需要安装 ebook-convert 相关插件,详情见相关系列教程. © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:09:58 "},"myGitbook/advance/plugin.html":{"url":"myGitbook/advance/plugin.html","title":"插件介绍","keywords":"","body":"插件介绍 插件是 gitbook 的扩展功能,很多炫酷有用的功能都是通过插件完成的,其中插件有官方插件和第三方插件之分. 推荐官方插件市场 https://plugins.gitbook.com/ 寻找或下载相应的插件. 当然也可以去 npm 市场搜索 gitbook 插件,根据 gitbook 插件规范, gitbook-plugin- 是功能插件,gitbook-theme- 是主体插件. 如果没有按照规范命名,还是直接百度搜索吧! npm 安装后再 gitbook 安装 语法格式: npm install gitbook-plugin- 安装到本地: npm install gitbook-plugin-advanced-emoji 激活安装插件: 配置 book.json 中 plugins 节点 安装到项目: gitbook install 启动并测试测试: gitbook serve 示例: # 安装 gitbook-plugin-advanced-emoji 插件 $ npm install gitbook-plugin-advanced-emoji # 安装 gitbook-plugin-advanced-emoji 插件 $ gitbook install npm 安装速度慢的话,可以使用 cnpm 加速安装(npm install cnpm),表情插件下载地址 Advanced Emoji gitbook 直接安装 语法格式: gitbook install 激活安装插件: 配置 book.json 中 plugins 节点 安装到项目: gitbook install 启动并测试测试: gitbook serve 示例: # 安装 gitbook-plugin-advanced-emoji 插件 $ gitbook install 表情插件下载地址 Advanced Emoji 插件示例 Advanced Emoji表情列表 Advanced Emoji下载地址 book.json 配置文件: \"plugins\": [ \"advanced-emoji\" ] 安装插件: $ gitbook install 使用示例: :bowtie: :laughing: :relaxed: © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:10:08 "},"myGitbook/advance/plugin-practical.html":{"url":"myGitbook/advance/plugin-practical.html","title":"实用插件","keywords":"","body":"实用插件 插件没有什么逻辑顺序,大家可以按照目录大纲直接定位到感兴趣的插件部分阅读即可. 更多插件正在陆续更新中,敬请期待... 最新更新插件 tbfed-pagefooter 版权页脚插件 gitalk 评论插件 search-plus 中文搜索插件 gitalk 评论插件并不是 gitbook 插件,因而集成方式和一般的插件安装方式不同! tbfed-pagefooter 版权页脚插件 如果希望将网页源码暴露出去并接受公众的监督校准的话,使用edit-link插件可以直接链接到源码文件. 链接地址: https://plugins.gitbook.com/plugin/tbfed-pagefooter 激活插件配置 在 book.json 中配置 tbfed-pagefooter 插件,详细说明请参考 tbfed-pagefooter 插件. 示例: { \"plugins\": [\"tbfed-pagefooter\"], \"pluginsConfig\": { \"tbfed-pagefooter\": { \"copyright\":\"© snowdreams1006\", \"modify_label\": \"文件修订时间:\", \"modify_format\": \"YYYY-MM-DD HH:mm:ss\" } } } 安装 tbfed-pagefooter 插件 示例: $ gitbook install 测试 tbfed-pagefooter 插件 启动本地服务后,每个页面的页脚处都会自动生成版权信息以及当前文件的最后更新时间. 功能慎用: 如果文档频繁更新适合生成最后更新时间,如果长时间不更新文档,岂不是最后更新时间还是几年前,给读者的感觉像是不再维护了一样! 示例: $ gitbook serve disqus 评论插件 discus 是一款集成评论的插件,可以为静态网站添加动态评论,让你的网站动起来! 遗憾的是,discus 插件只有 FQ 才能正常使用,暂时没找到其他较好的替代方案. 注册 disqus.com 账号 gitbook 集成 disqus 插件中最重要的配置项就是注册 disqus.com 网站唯一标识. 注册并绑定域名 如果没有注册账号请先注册,否则直接登录,当然也支持第三方账号登录(我使用的是谷歌账号). 人机验证时,选出符合条件的全部图形,直到没有新的图形为止,这一点和国内的静态图片验证是不同的! 选择安装 disqus 插件(I want to install Disqus on my site),接下来会绑定集成网站的域名. 接下来设置网站的相关信息,其中网站名称(snodreams1006)是唯一标示,接下来集成到 gitbook 用的就是这个简短名称,而分类和语言按照实际情况选择即可. 选择服务类型 disqus 网站提供的服务类型,有基础班(basic),加强版(plus),专业版(pro)和免费版(free). 每个版本计划有不同的收费标准以及相应的服务,可以根据实际情况选择适合自己的服务类型. 接下来以免费版为例进行有关演示 安装并配置 disqus 到网站 估计是这些网站提供了默认的集成方式,这里并没看到 gitbook 相关的网站,因此选择最后一个自定义网站. 填写网站的基本信息,其中网站缩写名称仍然是 snowdreams1006,网址填写 https://snowdreams1006.github.io/ ,至于其他信息根据实际情况填写即可. 至此 disqus.com 网站配置完成,接下来我们配置 gitbook 集成 disqus 插件. 安装并配置 disqus 插件 上一步我们已经获取到唯一的标识: snowdreams1006 ,接下来可以继续配置 disqus 插件了. 链接地址: https://plugins.gitbook.com/plugin/disqus 激活插件配置 在 book.json 中配置 disqus 插件,根据实际情况修改成自己的缩写名称(shortName). 示例: { \"plugins\": [\"disqus\"], \"pluginsConfig\": { \"disqus\": { \"shortName\": \"snowdreams1006\" } } } 安装 disqus 插件 示例: $ gitbook install 测试 disqus 插件 示例: $ gitbook serve 正常情况下(FQ),disqus 插件已经成功集成到 gitbook 网站了,因此推送到实际服务器上时看到的效果是这样的. 如果你不具备条件(FQ),那么你看到的仍然是这样的. gitalk 评论插件 本篇文章发表在开源中国后得到网友 @八一菜刀 的评论,让我推荐了gitalk 评论插件,初始使用了一下,确实不错,因此在这里更新下. 上述 disqus 评论插件虽然比较好用,但是注册是在 disqus.com 官网,需要特殊手段才能访问,即便成功配置了国内一般也是访问不到的,因此功能相当鸡肋. gitalk 评论插件解决了这一痛点,利用 github 的开发者接口授权,将讨论区的 issue 变成评论区,和 github 结合的如此紧密,适合用源码托管到 github 这类情况. 先混个脸熟,看一下 gitalk 官网 是如何介绍自己的呢. 看着效果确实不错,并且评论区的内容直接作为 github 仓库的 issue,这么好的想法我咋没想到呢! 好了,现在让我们开始集成到我们自己的项目中,遇到新鲜事物,当然先要参考官网介绍了. 申请 GitHub Application 授权 登录 github 账号,点击 在线申请 授权应用. 看到这一步,想必读者已经有个大概印象了,gitalk 插件是利用 github 的开发者服务,进行授权进而调用 issue 相关接口从而显示评论功能. 这种由官网提供的开发者服务还是比较好的,至少感觉比手动模拟提交要靠谱些,更何况走的是 OAuth 授权模式. 比如第三方应用提供微信登录,走的也是 OAuth 协议,这里的第三方应用当然就是现在说的 Gitalk 插件,微信就是我们的 github . 新建应用,首页 url 和授权回调 url 填写相同的首页链接即可,其他情况自定义填写. 应用登记成功后会生成 token 令牌,clientId 和 clientSecret 需要重点保存下来,待会需要用到. 安装并集成到网站 在需要添加评论的页面,添加下述内容引入 gitalk 插件,其中参数来自我们上一步获取的 clientId 和 clientSecret . 默认应该添加到 .html 页面,当然也可以添加到 .md 页面,毕竟 markdown 语法也支持 html 标签. var gitalk = new Gitalk({ \"clientID\": \"clientId\", \"clientSecret\": \"clientSecret\", \"repo\": \"GitHub repo\", \"owner\": \"GitHub repo owner\", \"admin\": [\"GitHub repo admin\"], \"id\": location.pathname, \"distractionFreeMode\": false }); gitalk.render(\"gitalk-container\"); 稍微解释下参数的含义: \"clientID\" : [必选] GitHub Application Client ID \"clientSecret\" : [必选] GitHub Application Client Secret \"repo\" : [必选] GitHub repository \"owner\" : [必选] GitHub repository 所有者,可以是个人或者组织 \"admin\" : [必选] GitHub repository 的所有者和合作者 (对这个 repository有写权限的用户) \"id\" : [可选] 页面的唯一标识,默认值: location.href, 长度必须小于50,否则会报错! \"distractionFreeMode\": [可选] 类似 Facebook 评论框的全屏遮罩效果,默认值: false 上述配置只是最简配置,如果想要了解更多高级配置,请参考 官方文档 测试集成效果 按照上述安装步骤,将代码复制到首页(README.md)文件中,然后推送到 github ,体验下集成效果. 注意: 这里必须推送到服务器,因为申请应用时填写的域名是线上地址,因而本地测试是不会成功的,会报错,这一点和微信支付的回调地址类似. 示例: var gitalk = new Gitalk({ \"clientID\": \"3f62415a283d19cbd696\", \"clientSecret\": \"aed0e1db0620bf5d0e3a3f0225f801997ad74e58\", \"repo\": \"snowdreams1006.github.io\", \"owner\": \"snowdreams1006\", \"admin\": [\"snowdreams1006\"], \"id\": location.pathname, \"distractionFreeMode\": false }); gitalk.render(\"gitalk-container\"); 上述参数仅供参考,实际使用中请替换成自己的配置,不然你也没有我仓库的权限,肯定会报错的啊! 心心相念的 gitalk 评论区呢?是不是哪里配置错了,为啥没有出来? 别急,要淡定,看一下提示说\"未找到的 Issue 进行评论,请联系 @snowdreams1006 初始化创建\",既然如此,那我们就操作一下吧! 点击下方的按钮 使用 Github登录 ,会跳转到相应的仓库,然后按照提示确定. 再次返回首页,刷新一下看看发生什么神奇的事情了? 终于集成了评论功能,而且还支持 markdown 格式的评论呢! 进一步思考 确实不错,心中自然是欣喜万分,但别高兴太早了,因为你会发现其他页面并没有评论区,也很好理解,我们目前仅仅在首页(README.md) 集成了 gitalk 插件,也就是说使用 gitbook build 输出的 index.html 首页才支持评论区,其他页面没有插入上述代码,自然是没有评论区功能的啊! 那如果想要实现全网站的所有页面都集成评论区功能,应该怎么办呢? 百度搜索了一下,并没有找到优雅的解决方案,如果有人能够提供更好的解决方案,还望不吝赐教,在此谢过. 既然网上找不到优雅的解决方案,那寻求专业人士的帮助也是一种好办法,我去哪找 gitalk 的使用者呢? 聪明的你或许已经想到了,解铃还须系铃人,当然是向推荐给我插件的大牛提问了! 他确实提供了一种思路,以下是网友@八一菜刀原话: 文档里面我用的是tbfed-pagefooter插件,不过我是在本地使用gitbook install后重写了该插件的js,无非就是在js里面加一段Gitalk的调用代码,这样使用gitbook build命令的时候,所有的页面都会有Gitalk的评论调用 人家既然已经提供了思路,不太好意思继续麻烦人家要源码,既然如此,那就自己动手吧! tbfed-pagefooter 插件很熟悉,一般是用于注明版权以及文章的修订时间的,而且作用于每个页面,这一点就满足了集成 gitalk 相关代码的基本要求. 大体方向确定后,目前就是解决如何在 tbfed-pagefooter 插件构建的相关生命周期内顺便执行我们的代码? 正常当前项目安装 tbfed-pagefooter 插件后应该存放于 /node_modules/gitbook-plugin-tbfed-pagefooter 目录,大致看一下插件的项目结构. gitbook-plugin-tbfed-pagefooter ├── LICENSE ├── README.md ├── assets │ └── footer.css ├── index.js └── package.json 1 directory, 5 files $ 为了基本看懂项目文件作用,特意去看了下 gitbook 插件开发文档,目标锁定在 index.js . 截取重要片段,原来是电子书构建前动态增加了 html 片段啊,这就好办了! hooks: { 'page:before': function(page) { var _label = '最后更新时间: ', _format = 'YYYY-MM-DD', _copy = 'powered by snowdreams1006' if(this.options.pluginsConfig['tbfed-pagefooter']) { _label = this.options.pluginsConfig['tbfed-pagefooter']['modify_label'] || _label; _format = this.options.pluginsConfig['tbfed-pagefooter']['modify_format'] || _format; var _c = this.options.pluginsConfig['tbfed-pagefooter']['copyright']; _copy = _c ? _c + ' all right reserved,' + _copy : _copy; } var _copy = ''+_copy+''; var str = ' \\n\\n' + _copy + '' + _label + '\\n' + _for10pmt + '\\n'; str += '\\n\\n'+ '\\n\\n'+ '\\n\\n'+ '\\n\\n'; page.content = page.content + str; return page; } } 看懂基本原理后顺便修改了版权说明以及修订时间格式,然后追加了集成 gitalk 的相关代码. 这里为了方便修改 gitalk 配置,特意将相关配置项单独托管到 github 专门的 gitalk-config.js 文件. 至于配置文件的内容,并没什么特殊之处,还是顺便贴一下吧! var gitalk = new Gitalk({ \"clientID\": \"3f62415a283d19cbd696\", \"clientSecret\": \"aed0e1db0620bf5d0e3a3f0225f801997ad74e58\", \"repo\": \"snowdreams1006.github.io\", \"owner\": \"snowdreams1006\", \"admin\": [\"snowdreams1006\"], \"id\": window.location.pathname, \"distractionFreeMode\": false }); gitalk.render(\"gitalk-container\"); 至此,之后再本地构建电子书时(gitbook build),gitbook-plugin-tbfed-pagefooter 自然会顺便帮我们运行集成 gitalk 的相关代码,这才是相对来说比较优雅的做法. 当然也不一定非要借助 gitbook-plugin-tbfed-pagefooter 插件帮忙,也可以借助别的插件进行集成,甚至自己写个更好的插件. 小结 gitalk 插件相对 disqus 插件来说,更符合基本国情,只不过默认的集成方式只能一个页面一个页面去集成,当数量比较多时,工作量不敢想象. 因此,通过 gitbook 插件开发的方式,在源码文件输出为目标文件时加入相关集成代码,相当于手写100条输出语句和循环写100条输出语句. 其实本质上并没有改变什么,仍然是集成到每个页面中,但是简化了人工操作的工作量就是效率的提升. 如果有更高效更优雅的集成方式,欢迎大家一起探讨. edit-link 编辑链接插件 如果希望将网页源码暴露出去并接受公众的监督校准的话,使用edit-link插件可以直接链接到源码文件. 链接地址: https://plugins.gitbook.com/plugin/edit-link 激活插件配置 在 book.json 中配置 edit-link 插件,详细说明请参考 edit-link 插件. 示例: { \"plugins\": [\"edit-link\"], \"pluginsConfig\": { \"edit-link\": { \"base\": \"https://github.com/snowdreams1006/snowdreams1006.github.io/blob/master\", \"label\": \"编辑本页\" } } } 安装 edit-link 插件 示例: $ gitbook install 测试 edit-link 插件 如果不能正常跳转到源码文件,多次试验后重新更改 edit-link.base 节点内容,重新 gitbook serve 即可正常跳转源码文件. 示例: $ gitbook serve github 插件 添加 github 图标链接,方便直接跳转到 github 指定仓库. 链接地址: https://plugins.gitbook.com/plugin/github 激活插件配置 在 book.json 中配置 github 插件,详细说明请参考 github 插件. 示例: { \"plugins\": [\"github\"], \"pluginsConfig\": { \"github\": { \"url\": \"https://github.com/snowdreams1006/snowdreams1006.github.io\" } } } 安装 github 插件 示例: $ gitbook install 测试 github 插件 示例: $ gitbook serve search-plus 中文搜索插件 默认的 search 搜索插件是不支持中文搜索的,而 search-plus 则功能更强大些,两者不能共存,需要禁用或移除 search 插件. 链接地址: https://plugins.gitbook.com/plugin/search-plus 激活插件配置 在 book.json 中配置 github 插件,详细说明请参考 github 插件. 示例: { \"plugins\": [ \"-lunr\", \"-search\", \"search-plus\" ] } 安装 search-plus 插件 示例: $ gitbook install 测试 search-plus 插件 测试是否能够进行中文搜索,如果不能,请确保已移除默认的 \"lunr\" 和 \"search\" 插件. 示例: $ gitbook serve © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:10:08 "},"myGitbook/advance/plugin-theme.html":{"url":"myGitbook/advance/plugin-theme.html","title":"主题插件","keywords":"","body":"主题插件 目前 gitbook 提供三类文档: Book 文档,API 文档和 FAQ 文档. 其中,默认的也是最常使用的就是 Book 文档,如果想要了解其他两种文档模式,需要引入相应的主题插件. 官方主题插件文档: https://toolchain.gitbook.com/themes/ Book 文档 theme-default 主题 插件地址: https://plugins.gitbook.com/plugin/theme-default theme-default 是 3.0.0 引入的默认主题,大多数插件针对的都是默认主题,如果切换到其他主题或者自定义主题,可能会造成某些情况下不兼容,甚至报错. 默认情况下,左侧菜单不显示层级属性,如果将 showLevel 属性设置为 true 可以显示层级数字. 示例: \"pluginsConfig\": { \"theme-default\": { \"showLevel\": true } } 效果: 默认情况下左侧菜单树不显示目录层级 开启层级显示设置后,左侧菜单树显示当前目录层级 theme-comscore 主题 插件地址: https://plugins.gitbook.com/plugin/theme-comscore default 默认主题是黑白的,而 comscore 主题是彩色的,即标题和正文颜色有所区分. 示例: \"plugins\": [ \"theme-comscore\" ] 效果: 默认情况下各级标题颜色均是黑色,不同级别的标题仅仅是大小区别. 设置 comscore 主题后,各级标题颜色不同,不仅仅是大小不同. API 文档 theme-api 插件 插件地址: https://plugins.gitbook.com/plugin/theme-api 如果文档本身是普普通文档模式,切换成 api 文档模式后并不会有太大变化,除非一开始就是接口文档,那样使用 theme-api 插件才能看出效果. 示例: { \"plugins\": [\"theme-api\"], \"pluginsConfig\": { \"theme-api\": { \"theme\": \"dark\" } } } 语法: 方法区 语法区 示例: 效果: 添加 api 相关方法后的文档效果,正常会两列显示并在右上角增加语言切换工具. FAQ 文档 theme-faq 插件 插件地址: https://plugins.gitbook.com/plugin/theme-faq theme-faq 可以帮助我们构建问答中心,预设好常见问题以及相应答案模式,同时为了方便搜索到问题或答案,一般需要搜索插件的配合. 示例: { \"plugins\": [ \"theme-faq\", \"-fontsettings\", \"-sharing\", \"-search\", \"search-plus\" ] } 帮助中心没有工具栏,因此涉及到工具类的插件一律失效或主动移除,同时默认搜索插件也会失效. 语法: 增加文章间的关联 --- related: - some/other/page.md - another_related_article.md --- Content of my article! 在当前页面底部显示延伸阅读,支持 yaml 语法关联到其他页面. 增加头部 logo 新建 _layouts/website/page.html 文件,用于扩展当前主题插件来增加自定义 logo. 增加导航栏链接 新建 _layouts/website/page.html 文件,用于扩展当前主题插件来增加自定义导航栏链接. 示例: 新建 _layouts/website/page.html 文件,增加自定义 logo 和导航栏链接. 效果: 小结 本节主要讲解了常用的三种文档模式,其中 default 主题插件,适合一般的博客类网站或静态网站,api 主题插件适合接口文档的编写,faq 主题插件则适合帮助中心. 三种主题插件分别对应不同的应用场景,默认情况下使用的是 default 主题插件,平时介绍的大多数功能插件也大多适合这种主题,另外两种主题可能就不能很好兼容第三方插件,需要亲身体验. © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:10:08 "},"myGitbook/advance/export.html":{"url":"myGitbook/advance/export.html","title":"导出电子书","keywords":"","body":"导出电子书 gitbook 既可以将源码文件单独输出,也可以仅输出单个文件,常见的导出电子书格式主要有三种(ePub, Mobi, PDF),而这三种格式都依赖于系统本身提供的 ebook-convert 工具. 安装依赖 如果直接运行 gitbook pdf 相关命令,可能会报错,提示需要安装 ebook-convert 插件,根据提示本地需要安装 calibre 软件,这样 gitbook 才能正常导出电子书. calibre 官网: https://calibre-ebook.com/ linux 系统 下载地址: https://calibre-ebook.com/download_linux 下载应用 $ sudo -v && wget -nv -O- https://download.calibre-ebook.com/linux-installer.sh | sudo sh /dev/stdin 配置软链接 $ sudo ln -s /usr/bin/nodejs /usr/bin/node mac 系统 下载地址: https://calibre-ebook.com/download_osx 下载应用 将 calibre.app 移动到应用程序文件,然后尝试是否能正常打开应用. 配置软链接 $ sudo ln -s ~/Applications/calibre.app/Contents/MacOS/ebook-convert /usr/bin 测试命令 $ ebook-convert --version 如果没有输出 ebook-convert 版本信息,可能需要配置环境变量. 配置封面 所有格式的电子书都可以配置自定义封面,在项目的根目录下提供 cover.jpg 和 cover_small.jpg 两种封面图片时,生成电子书会自动增加封面页. 当然你也可以使用 autocover 插件 自动生成封面,不过本人才疏学浅,几经尝试始终没有成功,如果有人成功了记得给我留言下哈! 封面的基本要求: cover.jpg 尺寸大小: 1800X2360 px,cover_small.jpg 尺寸大小: 200x262 px; 无边界 清晰可见的书名 任何重要的文字在小版本封面图片中也要清晰可见 更多封面相关规范请参考 https://toolchain.gitbook.com/ebook.html 基本命令 语法格式: gitbook pdf 或 gitbook epub 或 gitbook mobi 示例: # 生成 `pdf` 文件并输出 `debug` 级别日志 $ gitbook pdf ./ ./myBook.pdf --log=debug # 生成 `epub` 文件并输出 `debug` 级别日志 $ gitbook epub ./ ./myBook.epub --log=debug # 生成 `mobi` 文件并输出 `debug` 级别日志 $ gitbook mobi ./ ./myBook.mobi --log=debug 相信大家对 PDF 格式比较熟悉,其余两种格式只是不同电子书格式,因而需要相应软件支持. 生成 PDF 文件 示例: $ gitbook pdf 默认在当前项目的根目录下生成 book.pdf 文件名,如果配有封面,则首页显示封面,否则无封面. 生成 ePub 文件 示例: $ gitbook epub 默认在当前项目的根目录下生成 book.epub 文件名,如果配有封面,则首页显示封面,否则无封面. 生成 mobi 文件 示例: $ gitbook mobi 默认在当前项目的根目录下生成 book.mobi 文件名,如果配有封面,则首页显示封面,否则无封面. 小结 本节主要介绍了如何导出电子书,概括来说,首先系统需要安装 ebook-convert 工具,然后配置电子书封面,最后直接导出为目标格式(ePub, Mobi, PDF)进行输出. 随着电子书内容越来越多,生成电子书所花费的时间也越来越久,实属正常,耐心等待即可. 输出 PDF 文件并输出 debug 日志: gitbook pdf --log=debug © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:09:58 "},"myGitbook/advance/publish.html":{"url":"myGitbook/advance/publish.html","title":"发布电子书","keywords":"","body":"发布电子书 输出目标文件 语法格式: gitbook build [book] [output] 默认情况下,gitbook 输出方式是静态网站,其实 gitbook 的输出方式有三种: website, json,和 ebook. 只不过另外两种不是很常用,更多情况下我们是使用静态网页搭建个人官网,或托管到第三方平台,或部署到私有云服务器,但不管怎么样,还是离不开生成这一步. 示例: # 默认输出格式: `website` $ gitbook build --format=website # 更改输出格式: `json` $ gitbook build --format=json # 更改输出格式: `ebook` $ gitbook build --format=ebook 默认情况下输出目录: _book/,整个项目的入口文件是: index.html 集成 github 网站 本教程的电子书源码和输出文件均托管到 github 网站,所以这里介绍下如何利用 Github Pages 静态网页服务与 gitbook 进行集成. 什么是 GitHub Pages ? Github Pages 是 github 网站推出的一种免费的静态网页托管服务,适合搭建静态的项目主页或个人官网. 其中,网站项目的源码直接托管在 github 仓库中,当仓库文件更新后,该仓库所关联的网站自动更新,从而实现了源码与官网的联动更新. 如果想了解更多详情,请参考官网: https://pages.github.com/ 怎么做 GitHub Pages ? 每个账号有且只有一个主页站点,但允许无限制多的项目站点. 啥是主页站点,项目站点又是啥? 别急,让我先举个例子看一下最终效果. 假如用户名: zhangsan 名下有四个公开仓库,一个仓库名叫做: zhangsan.github.io,另外三种分别是: project01,project02,project03 . 如果想要对外暴露上述四个仓库作为我们的静态网站,那么最终效果就是下面这样的. 主页站点: https://zhangsan.github.io 项目01站点: https://zhangsan.github.io/project01 项目02站点: https://zhangsan.github.io/project02 项目03站点: https://zhangsan.github.io/project03 注意将 zhangsan 替换成自己的 github 用户名,否则八成是打不开网站,除非真的有 zhangsan 这个用户. 其实上述规则很好理解,github 网站作为一个托管中心,有成千上万的用户在使用 github 并且每个用户的用户名都是唯一并且不同的,因此 *.github.io 通配符域名刚好充当命名空间. 可以预料的是,不仅仅有 .github.io 这种二级域名,说不定还有 api.github.io,docs.github.io 等等,毕竟只需要购买 *.github.io 通配符域名证书就可以支持任意多的二级域名了,感谢 github 赠送我们免费的 https 网站. 说到这里,不得不吐槽下 gitbook 的命名空间策略了,gitbook 也有自己的电子书托管服务,但访问地址是 .gitbook.io/ . 很显然,gitbook 没有区分主页站点和项目站点,相当于全部都是项目站点,缺少主次之分. 闲言少叙,既然知道了输入内容和输出效果,那么接下来的任务就是了解中间过程了,让我们一起探讨下怎么发布网站吧! 主页站点 创建 .github.io 公开仓库 前往 https://github.com/ 网站创建名为 .github.io 的公开仓库. 比如我的用户名是: snowdreams1006 ,那么我的主页站点仓库就是: snowdreams1006.github.io 创建首页 index.html 文件 不管是在线直接创建 index.html 还是克隆到本地创建 index.html ,最终的 .github.io 仓库一定要有 index.html 首页文件. 示例: # 克隆到本地 $ git clone https://github.com/username/username.github.io # 切换到项目 $ cd username.github.io # 创建 `index.html` 文件 $ echo \"Hello World\" > index.html # 推送到远程仓库 $ git add --all $ git commit -m \"Initial commit\" $ git push -u origin master 访问主页站点 https://username.github.io 打开浏览器,输入网址: https://username.github.io 访问主页站点,显示的内容正是我们刚刚提交的 index.html 文件内容. 如果没有正常显示,清除浏览器缓存强制刷新试试看! 项目站点 相比主页站点来说,项目站点命名比较随意了,作为静态网站不可或缺的文件仍然是 index.html. 创建首页 index.html 文件 创建首页文件并添加测试内容,方便待会在线访问项目站点测试是否部署成功. 设置 GitHub Pages 选项 点击仓库首页右上方设置(Settings)选项卡,往下翻到 GitHub Pages 选项,选择源码目录,根据实际情况选择源码来源于 master 分支还是其他分支或者docs/ 目录. 方便起见,选择第一个 master 分支即可,注意下面的主题和这一步的来源只能两者选其一,否则主题优先级更高! 访问主页站点 https://username.github.io/ 打开浏览器,输入网址: https://username.github.io/repository 访问项目站点,显示的内容正是我们刚刚提交的 index.html 文件内容. 如果没有正常显示,清除浏览器缓存强制刷新试试看! 如何集成 gitbook ? 我们已经知道 Github Pages 是提供静态网站的免费托管,而 gitbook 默认生成的内容就是静态网站,两者如何结合自然不用我多说了吧? gitbook 默认输出目录 _book/ 包括了静态网站所需的全部资源,其中就包括 index.html 首页文件. 因此我们只需要每次生成后将 _book/ 整个目录复制到项目根目录,那么推送到远程仓库时自然就是输出后静态网站了啊! 示例: # 生成静态网站 $ gitbook build # 复制到项目根目录 $ cp -r _book/* . # 添加到本地版本库 $ git add . $ git commit -m \"publish\" # 推送到远程仓库 $ git push origin master 现在登录 github 网站看一下静态网站是否成功上传以及访问主页站点或项目站点看一下最新内容是否成功渲染吧! 小结 本节我们学习 gitbook 有三种输出方式,其中默认的网页输出最为常用. 除此之外,还讲解了如何与 github pages 进行结合,从而实现源码和网站的自动更新维护. 如果源码没有托管到 github 这种第三方服务商,你也可以搭建自己的服务器,比如将 _book/ 目录全部扔到 nginx 服务器做静态资源服务器等. 毕竟,源码和输出内容都在你手中,想怎么玩还不是自己说了算? © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:10:08 "},"myGitbook/issue/":{"url":"myGitbook/issue/","title":"常见问题","keywords":"","body":"常见问题 © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:10:15 "},"myGitbook/issue/rm-output-directory.html":{"url":"myGitbook/issue/rm-output-directory.html","title":"热加载失败治标之法","keywords":"","body":"热加载失败治标之法 破镜如何贴花黄 gitbook 在 Windows 系统无法热加载,总是报错! gitbook 是一款文档编写利器,可以方便地 markdown 输出成美观优雅的 html ,gitbook serve 启动服务器后,原来相貌平平的 markdown 丑小鸭摇身一变就成了倾国倾城的 html 绝色佳人. 如果源文件发生更改,Windows 却无法按照预期那样重启服务器,直接抛出一个异常,立即终止了 markdown 的化妆. Restart after change in file README.md Stopping server events.js:183 throw er; // Unhandled 'error' event ^ Error: EPERM: operation not permitted, lstat 'F:\\workspace\\private-cloud-backup\\gitbook-test\\_book' 对镜贴花黄 现在看一下 markdown 灰姑娘变身 html 小姐姐的神奇过程吧! $ gitbook serve --log=debug Live reload server started on port: 35729 Press CTRL+C to quit ... debug: readme found at README.md debug: summary file found at SUMMARY.md debug: cleanup folder \"G:\\sublime\\gitbook-test\\_book\" info: 7 plugins are installed info: loading plugin \"livereload\"... OK ... info: loading plugin \"theme-default\"... OK info: found 1 pages info: found 0 asset files debug: calling hook \"config\" debug: calling hook \"init\" debug: copy assets from theme C:\\Users\\snowdreams1006\\.gitbook\\versions\\3.2.3\\node_modules\\gitbook-plugin-theme-default\\_assets\\website ... debug: copy resources from plugin C:\\Users\\snowdreams1006\\.gitbook\\versions\\3.2.3\\node_modules\\gitbook-plugin-livereload\\book debug: generate page \"README.md\" debug: calling hook \"page:before\" debug: calling hook \"page\" debug: index page README.md debug: calling hook \"finish:before\" debug: calling hook \"finish\" debug: write search index info: >> generation finished with success in 1.5s ! Starting server ... Serving book on http://localhost:4000 根据上述输出日志,我们可以分析出 gitbook 的基本运行流程. 加载 readme 和 summary 文件,若存在 glossary 文件也会加载,并删除 _book 目录 debug: readme found at README.md debug: summary file found at SUMMARY.md debug: cleanup folder \"G:\\sublime\\gitbook-test\\_book\" 加载依赖插件,若没有找到相应插件会报错,提示运行 gitbook install 安装插件. info: 7 plugins are installed info: loading plugin \"livereload\"... OK info: loading plugin \"highlight\"... OK info: loading plugin \"search\"... OK info: loading plugin \"lunr\"... OK info: loading plugin \"sharing\"... OK info: loading plugin \"fontsettings\"... OK info: loading plugin \"theme-default\"... OK 扫描页面和静态资源文件 info: found 1 pages info: found 0 asset files 读取配置文件并初始化 debug: calling hook \"config\" debug: calling hook \"init\" 拷贝样式资源和插件资源 debug: copy assets from theme C:\\Users\\snowdreams1006\\.gitbook\\versions\\3.2.3\\node_modules\\gitbook-plugin-theme-default\\_assets\\website debug: copy resources from plugin C:\\Users\\snowdreams1006\\.gitbook\\versions\\3.2.3\\node_modules\\gitbook-plugin-fontsettings\\assets debug: copy resources from plugin C:\\Users\\snowdreams1006\\.gitbook\\versions\\3.2.3\\node_modules\\gitbook-plugin-sharing\\assets debug: copy resources from plugin C:\\Users\\snowdreams1006\\.gitbook\\versions\\3.2.3\\node_modules\\gitbook-plugin-lunr\\assets debug: copy resources from plugin C:\\Users\\snowdreams1006\\.gitbook\\versions\\3.2.3\\node_modules\\gitbook-plugin-search\\assets debug: copy resources from plugin C:\\Users\\snowdreams1006\\.gitbook\\versions\\3.2.3\\node_modules\\gitbook-plugin-highlight\\css debug: copy resources from plugin C:\\Users\\snowdreams1006\\.gitbook\\versions\\3.2.3\\node_modules\\gitbook-plugin-livereload\\book 开始生成单独页面,依次执行 page:before ,page 回调函数,全部页面执行完毕后执行 finish:before 和 finish 回调函数. debug: generate page \"README.md\" debug: calling hook \"page:before\" debug: calling hook \"page\" debug: index page README.md debug: calling hook \"finish:before\" debug: calling hook \"finish\" 生成搜索文件 debug: write search index 启动完毕,输出成功信息 Starting server ... Serving book on http://localhost:4000 默认情况下服务器启动后会占用两个端口,一个是对外暴露的 4000 端口,用于浏览器访问项目. 另外一个是 35729 端口,用于监听本地文件变化,重启服务器进而实现热加载功能. 本地服务器启动后我们就可以访问 http://localhost:4000 预览静态网站效果,markdown 源文件华丽演变成 html 富文本文件. 破镜怎化妆 不幸的是,Windows 热加载可能会有问题,也就是说如果启动服务器后,本地文件发生改变,此时会触发热加载功能而报错 Error: EPERM: operation not permitted ,这样一来浏览器又无法访问了. 刚刚变身的 markdown 瞬间又被打回原形,无法欣赏化妆后的容颜了,这样的体验相当不好! 边化妆边照镜子才是做到心中有谱,随时调整,如果不照镜子而直接化妆,那不是一般人能做到的. gitbook 启动本地服务器给我们提供了镜子,但热加载失败又把镜子摔碎了,还怎么愉快的化妆? Restart after change in file README.md Stopping server debug: readme found at README.md debug: summary file found at SUMMARY.md debug: cleanup folder \"G:\\sublime\\gitbook-test\\_book\" events.js:174 throw er; // Unhandled 'error' event ^ Error: EPERM: operation not permitted, lstat 'G:\\sublime\\gitbook-test\\_book' Emitted 'error' event at: at FSWatcher._handleError (C:\\Users\\snowdreams1006\\.gitbook\\versions\\3.2.3\\node_modules\\chokidar\\index.js:236:10) at ReaddirpReadable.emit (events.js:189:13) at Immediate. (C:\\Users\\snowdreams1006\\.gitbook\\versions\\3.2.3\\node_modules\\chokidar\\node_modules\\readdirp\\stream-api.js:82:32) at runCallback (timers.js:705:18) at tryOnImmediate (timers.js:676:5) at processImmediate (timers.js:658:5) 寻医问诊修破镜 现在问题已经复现,接下来就要开始寻医问诊,试图让破镜重圆,好让 markdown 灰姑娘变成人见人爱的 html 小姐姐. 根据报错信息描述,定位到删除 _book 目录再次创建该目录时,提示 EPERM: operation not permitted ,即无权操作. 柯南附体 既然说是操作权限的问题,那我们看一下 _book 目录现在是怎样状态吧! $ ls gitbook-errorforwindows-preview.png README.md SUMMARY.md 当前项目已经没有 _book 目录,证明发生报错时确实已经删除了 _book 目录,但是某种原因无权再次创建该文件夹而重启失败. 然而,这只是表现现象,老师告诉我们,要透过现象看本质,即使现在没有 _book 文件再次启动服务器还是会启动成功并创建 _book 文件的,所以真想只有一个! 那就是,gitbook 控制台在说谎! 虽然排除了 gitbook 无权创建 _book 目录的嫌疑,那又怎么解释重启服务器却没能创建 _book目录这件事呢? debug: cleanup folder \"G:\\sublime\\gitbook-test\\_book\" events.js:174 throw er; // Unhandled 'error' event ^ Error: EPERM: operation not permitted, lstat 'G:\\sublime\\gitbook-test\\_book' Emitted 'error' event at: at FSWatcher._handleError (C:\\Users\\snowdreams1006\\.gitbook\\versions\\3.2.3\\node_modules\\chokidar\\index.js:236:10) at ReaddirpReadable.emit (events.js:189:13) at Immediate. (C:\\Users\\snowdreams1006\\.gitbook\\versions\\3.2.3\\node_modules\\chokidar\\node_modules\\readdirp\\stream-api.js:82:32) at runCallback (timers.js:705:18) at tryOnImmediate (timers.js:676:5) at processImmediate (timers.js:658:5) 先看一下 FSWatcher._handleError 异常信息: sed -n \"223,239p\" ~/.gitbook/versions/3.2.3/node_modules/chokidar/index.js . 分析发现: FSWatcher._handleError 是私有方法,作用是处理异常信息,和这起事故关联不大. Administrator@snowdreams1006 MINGW64 /f/workspace/private-cloud-backup/gitbook-test (master) $ sed -n \"223,239p\" ~/.gitbook/versions/3.2.3/node_modules/chokidar/index.js // Private method: Common handler for errors // // * error - object, Error instance // // Returns the error if defined, otherwise the value of the // FSWatcher instance's `closed` flag FSWatcher.prototype._handleError = function(error) { var code = error && error.code; var ipe = this.options.ignorePermissionErrors; if (error && code !== 'ENOENT' && code !== 'ENOTDIR' && (!ipe || (code !== 'EPERM' && code !== 'EACCES')) ) this.emit('error', error); return error || this.closed; }; 我们接着往下找,再看一下 ReaddirpReadable.emit (events.js:189:13) ,这里没有给出文件的具体路径,所以暂时无法定位. 那我们再看下一个 Immediate. : sed -n \"78,96p\" ~/.gitbook/versions/3.2.3/node_modules/chokidar/node_modules/readdirp/stream-api.js Administrator@snowdreams1006 MINGW64 /f/workspace/private-cloud-backup/gitbook-test (master) $ sed -n \"78,96p\" ~/.gitbook/versions/3.2.3/node_modules/chokidar/node_modules/readdirp/stream-api.js proto._handleFatalError = function (err) { var self = this; setImmediate(function () { if (self._paused) return self._errors.push(err); if (!self._destroyed) self.emit('error', err); }); } function createStreamAPI () { var stream = new ReaddirpReadable(); return { stream : stream , processEntry : stream._processEntry.bind(stream) , done : stream._done.bind(stream) , handleError : stream._handleError.bind(stream) , handleFatalError : stream._handleFatalError.bind(stream) }; } 遗憾的是,仍然没有找到具体问题,那就继续看一下一条线索. timers.js:705:18 和 events.js:189:13 都没有显示具体的文件位置,如果也在 chokidar 模块的话就好了. Administrator@snowdreams1006 MINGW64 /f/workspace/private-cloud-backup/gitbook-test (master) $ tree -P \"events.js\" --prune ~/.gitbook/versions/3.2.3/ /c/Users/Administrator/.gitbook/versions/3.2.3/ └── node_modules ├── cheerio │ └── node_modules │ └── jsdom │ └── lib │ └── jsdom │ └── level2 │ └── events.js └── gitbook-plugin-theme-default └── src └── js └── core └── events.js 11 directories, 2 files Administrator@snowdreams1006 MINGW64 /f/workspace/private-cloud-backup/gitbook-test (master) $ tree -P \"timers.js\" --prune ~/.gitbook/versions/3.2.3/ /c/Users/Administrator/.gitbook/versions/3.2.3/ 0 directories, 0 files git-bash 命令行正常没有 tree 命令,如需扩展参考我另外一篇文章. 经过肉眼验证,发现 events.js 根本就没有 174 行文件,所以这两个文件大都不是目标文件. 既然命令行中无法找到目标文件,那就请专业的搜索工具全系统查找这两个文件吧,这里使用的是 Everything 搜索工具. 然并卵,依然没有找到目标文件. 毕竟不是柯南,没有发现真相 求助官方 gitbook 可是开源产品,出现问题的应该不止我一个,所以去 github 看看有没有遇到和我一样的问题. 虽然找到了志同道合的小伙伴,但是并没有提供解决方案,连官方都放弃了,那我还有什么可留恋的? 点击查看 gitbook serve livereload error 自己动手 最害怕的不是 bug,而是发现了 bug 却无法定位,虽然控制台有报错信息但是没有找到真正的文件! 首先确认下当前系统版本,然后采取版本切换方式测试其他版本是否存在该问题. $ gitbook --version CLI version: 2.3.2 GitBook version: 3.2.3 升级到最新版 gitbook ls 是列出当前已安装的版本,而 gitbook ls-remote 则是列出远程服务器版本. # 列出本地已安装版本 $ gitbook ls GitBook Versions Installed: * 3.2.3 Run \"gitbook update\" to update to the latest version. # 列出远程可用版本 $ gitbook ls-remote Available GitBook Versions: 4.0.0-alpha.6, 4.0.0-alpha.5, 4.0.0-alpha.4, 4.0.0-alpha.3, 4.0.0-alpha.2, 4.0.0-alpha.1, 3.2.3, 3.2.2, 3.2.1, 3.2.0, 3.2.0-pre.1, 3.2.0-pre.0, 3.1.1, 3.1.0, 3.0.3, 3.0.2, 3.0.1, 3.0.0, 3.0.0-pre.15, 3.0.0-pre.14, 3.0.0-pre.13, 3.0.0-pre.12, 3.0.0-pre.11, 3.0.0-pre.10, 3.0.0-pre.9, 3.0.0-pre.8, 3.0.0-pre.7, 3.0.0-pre.6, 3.0.0-pre.5, 3.0.0-pre.4, 3.0.0-pre.3, 3.0.0-pre.2, 3.0.0-pre.1, 2.6.9, 2.6.8, 2.6.7, 2.6.6, 2.6.5, 2.6.4, 2.6.3, 2.6.2, 2.6.1, 2.6.0, 2.5.2, 2.5.1, 2.5.0, 2.5.0-beta.7, 2.5.0-beta.6, 2.5.0-beta.5, 2.5.0-beta.4, 2.5.0-beta.3, 2.5.0-beta.2, 2.5.0-beta.1, 2.4.3, 2.4.2, 2.4.1, 2.4.0, 2.3.3, 2.3.2, 2.3.1, 2.3.0, 2.2.0, 2.1.0, 2.0.4, 2.0.3, 2.0.2, 2.0.1, 2.0.0, 2.0.0-beta.5, 2.0.0-beta.4, 2.0.0-beta.3, 2.0.0-beta.2, 2.0.0-beta.1, 2.0.0-alpha.9, 2.0.0-alpha.8, 2.0.0-alpha.7, 2.0.0-alpha.6, 2.0.0-alpha.5, 2.0.0-alpha.4, 2.0.0-alpha.3, 2.0.0-alpha.2, 2.0.0-alpha.1 Tags: latest : 2.6.9 pre : 4.0.0-alpha.6 目前最新发布版本是 3.2.3 ,而我们本地已安装的版本正是该版本,所以现在应该测试 4.0.0-alpha.6 版. 看到 4.0.0-alpha.6 心里有些忐忑,根据版本管理约定,版本号一般有三部分组成,第一部分代表不兼容的重大升级,第二部分代表主干兼容的功能升级,第三部分是小版本修复. 由 3.2.3 直接跨度到 4.0.0-alpha.6 意味着 gitbook 发生了重大重构! 算了,先下载试试看! gitbook fetch 下载 和 gitbook update升级,两种方式都可以体验最新版本,这里选择下载方式方便进行不同版本的切换. # 下载 `4.0.0-alpha.6` 版本 $ gitbook fetch 4.0.0-alpha.6 Installing GitBook 4.0.0-alpha.6 [email protected] C:\\Users\\SNOWDR~1\\AppData\\Local\\Temp\\tmp-8912hSrxNvTCrFEH\\node_modules\\gitbook ├── [email protected] ├── [email protected] ├── [email protected] ├── [email protected] └── [email protected] ([email protected], [email protected], [email protected], [email protected], [email protected], [email protected], [email protected], [email protected], [email protected], [email protected], [email protected], [email protected], [email protected], [email protected], [email protected], [email protected], [email protected], [email protected]) GitBook 4.0.0-alpha.6 has been installed 先看一下本地安装 gitbook 版本,确保待会运行时使用最新的 4.0.0-alpha.6 版本. # 列出本地已安装版本 $ gitbook ls GitBook Versions Installed: * 4.0.0-alpha.6 3.2.3 Run \"gitbook update\" to update to the latest version. # 列出当前正在使用版本 $ gitbook current GitBook version is 3.2.3 gitbook serve --gitbook=4.0.0-alpha.6 --log=debug 运行 4.0.0-alpha.6 版本并打印 debug 级别日志. 意外的是,竟然没有连启动都没启动成功,提示无法打开 ~\\.gitbook\\versions\\4.0.0-alpha.6\\node_modules\\gitbook-plugin-livereload\\_assets\\plugin.js 文件. 回想到版本号规范,可能 v3 到 v4 更改比较大,版本不兼容吧,重新初始化项目试试看! # 初始化项目并指定 `gitbook` 运行版本 $ gitbook init --gitbook=4.0.0-alpha.6 Warning: Accessing PropTypes via the main React package is deprecated, and will be removed in React v16.0. Use the latest available v15.* prop-types package from npm instead. For info on usage, compatibility, migration and more, see https://fb.me/prop-types-docs info: create SUMMARY.md info: initialization is finished 然而,仍然还是同样的报错,依旧无法启动. $ gitbook serve --gitbook=4.0.0-alpha.6 --log=debug Warning: Accessing PropTypes via the main React package is deprecated, and will be removed in React v16.0. Use the latest available v15.* prop-types package from npm instead. For info on usage, compatibility, migration and more, see https://fb.me/prop-types-docs Live reload server started on port: 35729 Press CTRL+C to quit ... ... Error: ENOENT: no such file or directory, open 'C:\\Users\\snowdreams1006\\.gitbook\\versions\\4.0.0-alpha.6\\node_modules\\gitbook-plugin-livereload\\_assets\\plugin.js' 此路不通,再换一条,既然向上无法处理,那向下回退会不会有结果呢? 回退版本 当前系统版本是 3.2.3,最新测试版本是 4.0.0-alpha.6 ,然而最近一次提交的版本却是 2.6.9 ? 为什么 gitbook-ci 管理的 gitbook 版本号会突然跳水,会不会有什么猫腻,难不成修复了什么 bug ? $ gitbook ls-remote Available GitBook Versions: 4.0.0-alpha.6, 4.0.0-alpha.5, 4.0.0-alpha.4, 4.0.0-alpha.3, 4.0.0-alpha.2, 4.0.0-alpha.1, 3.2.3, 3.2.2, 3.2.1, 3.2.0, 3.2.0-pre.1, 3.2.0-pre.0, 3.1.1, 3.1.0, 3.0.3, 3.0.2, 3.0.1, 3.0.0, 3.0.0-pre.15, 3.0.0-pre.14, 3.0.0-pre.13, 3.0.0-pre.12, 3.0.0-pre.11, 3.0.0-pre.10, 3.0.0-pre.9, 3.0.0-pre.8, 3.0.0-pre.7, 3.0.0-pre.6, 3.0.0-pre.5, 3.0.0-pre.4, 3.0.0-pre.3, 3.0.0-pre.2, 3.0.0-pre.1, 2.6.9, 2.6.8, 2.6.7, 2.6.6, 2.6.5, 2.6.4, 2.6.3, 2.6.2, 2.6.1, 2.6.0, 2.5.2, 2.5.1, 2.5.0, 2.5.0-beta.7, 2.5.0-beta.6, 2.5.0-beta.5, 2.5.0-beta.4, 2.5.0-beta.3, 2.5.0-beta.2, 2.5.0-beta.1, 2.4.3, 2.4.2, 2.4.1, 2.4.0, 2.3.3, 2.3.2, 2.3.1, 2.3.0, 2.2.0, 2.1.0, 2.0.4, 2.0.3, 2.0.2, 2.0.1, 2.0.0, 2.0.0-beta.5, 2.0.0-beta.4, 2.0.0-beta.3, 2.0.0-beta.2, 2.0.0-beta.1, 2.0.0-alpha.9, 2.0.0-alpha.8, 2.0.0-alpha.7, 2.0.0-alpha.6, 2.0.0-alpha.5, 2.0.0-alpha.4, 2.0.0-alpha.3, 2.0.0-alpha.2, 2.0.0-alpha.1 Tags: latest : 2.6.9 pre : 4.0.0-alpha.6 带着这些疑问,不妨下载 2.6.9 版本试试,看一下能否热加载? gitbook serve --log=debug --gitbook=2.6.9 指定 gitbook 版本,依旧失败! $ gitbook serve --log=debug --gitbook=2.6.9 Error loading version latest: Error: Cannot find module 'q' at Function.Module._resolveFilename (internal/modules/cjs/loader.js:582:15) at Function.Module._load (internal/modules/cjs/loader.js:508:25) at Module.require (internal/modules/cjs/loader.js:637:17) at require (internal/modules/cjs/helpers.js:22:18) at Object. (C:\\Users\\myHome\\.gitbook\\versions\\2.6.9\\lib\\index.js:3:9) at Module._compile (internal/modules/cjs/loader.js:701:30) at Object.Module._extensions..js (internal/modules/cjs/loader.js:712:10) at Module.load (internal/modules/cjs/loader.js:600:32) at tryModuleLoad (internal/modules/cjs/loader.js:539:12) at Function.Module._load (internal/modules/cjs/loader.js:531:3) TypeError: Cannot read property 'commands' of null 重回现场 现在把目光再次聚焦到最初的案发现场,这一次只能背水一战了,自己动手要么丰衣足食要么饿死冻死! Stopping server debug: readme found at README.md debug: summary file found at SUMMARY.md debug: cleanup folder \"G:\\sublime\\private-cloud-backup\\gitbook-test\\_book\" events.js:174 throw er; // Unhandled 'error' event ^ Error: EPERM: operation not permitted, lstat 'G:\\sublime\\private-cloud-backup\\gitbook-test\\_book' Emitted 'error' event at: at FSWatcher._handleError (C:\\Users\\myHome\\.gitbook\\versions\\3.2.3\\node_modules\\chokidar\\index.js:236:10) at ReaddirpReadable.emit (events.js:189:13) at Immediate. (C:\\Users\\myHome\\.gitbook\\versions\\3.2.3\\node_modules\\chokidar\\node_modules\\readdirp\\stream-api.js:82:32) at runCallback (timers.js:705:18) at tryOnImmediate (timers.js:676:5) at processImmediate (timers.js:658:5) 关于上述错误描述中,在真相只有一个章节中已经探讨过,当时得出的结论是 gitbook 是删除 _book 文件夹再新建 _book 文件夹时发生了意外. 如果这个行为不是由 gitbook 发生而是由我们手动干预的话,也就是说,当成功启动本地服务器后并在即将发生热加载之前,此时人为删除 _book 文件夹,会发生什么? 我的猜想是: 因为 gitbook 的热加载机制是监听本地文件目录系统发生改变,进而停止服务器再重新启动服务器. 当我们手动删除了 _book 文件夹,对于 gitbook 来说,再触发重启服务器的那一刻来说,突然发现没有 _book 文件夹,此时就不会删除也不会新建时发生异常,相当于直接新建 _book 文件夹,变相把热加载弄成了初始启动模式! 希望苍天不负我,如若不行,只能看源码逻辑找 bug 了! 你猜猜会怎么样? it works ! 在实验中,gitbook serve --log=debug 启动本地服务器后,如果本地文件发生修改会重启失败! 但是,如果在启动本地服务器后立即删除 _book 目录,当本地文件发生修改时重启服务就能成功了. 到此为止,总算找到一个解决方案,那就是启动服务后立即删除 _book 目录. 不算完美的总结 windows 系统上启动 gitbook 服务后,如果本地文件发生更改,热加会失败. 如果启动服务器后立即删除 _book 目录,那么之后再怎么修改本地文件都能顺利重启. 目前还没有找到问题的根源,下一次将深入源码继续探讨到底是哪里出问题导致 Windows 系统无法重启. 虽然及时删除 _book 目录并不算是很好的解决方案,但至少 markdown 灰姑娘又能化妆成 html 小姐姐了呢! © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:10:15 "},"myGitbook/reference/":{"url":"myGitbook/reference/","title":"更多学习笔记","keywords":"","body":"更多学习笔记 gitbook 简体中文官方文档 gitbook 繁体中文官方文档 敖小剑的 gitbook 学习笔记 gitbook 插件使用笔记 © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:10:16 "},"java8/":{"url":"java8/","title":"java8新特性","keywords":"","body":"java8新特性 java8作为 java的一个重要版本,目前为大多数企业和个人所接受,了解其本身为我们提供了哪些改变有助于我们去解决实际问题,窥探其今后可能的发展方向. 接下来本教程将从java语言本身入手,结合笔者实际经验,带你了解常用的新特性以及日常工作中如何应用. 最好拥有一定的java基础,只有经历过痛苦的折磨才能感受到新特性的愉快 © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:09:57 "},"java8/lambda.html":{"url":"java8/lambda.html","title":"lambda表达式","keywords":"","body":"lambda表达式 下面通过对比 java8 之前的普通实现和 java8 引入lambda 表达式的实现,帮助读者快速了解代码差异性,眼见为实,赶紧开始吧! 求给定数组最小值 public static void main(String[] args) { // 方法1: jdk8以前的实现 int[] nums = {33, 55, -55, 90, -666, 90}; int min = Integer.MAX_VALUE; for (int i : nums) { if (i 方法1体现的是命令式编程的思维,需要明确编程细节,如果细节拿捏不准,那么结果自然不对. 而方法2则是函数式编程,只关注目标和结果,忽略过程,思路清晰更容易理解. 小结 命令式编程 强调过程,注重如何实现的细节,一步错,满盘皆输! 函数式编程 关注结果,不在乎实现细节,分配任务让下属去处理! lambda 表达式 是函数式编程 的重要体现,简洁易于理解. 创建线程运行任务 public static void main(String[] args) { // 方法1: jdk8以前一般写法 new Thread(new Runnable() { @Override public void run() { System.out.println(\"ok\"); } }).start(); // 方法2: jdk8采用 lambda 表达式写法 new Thread(() -> System.out.println(\"ok\")).start(); } 从上例可以看出,这里的lambda 表达式 返回的是实现了指定接口的对象实例. 小结 lambda 表达式 实现指定接口方法并返回该接口实例对象 lambda 表达式 由输入和输出以及实现过程三部分组成,不在乎方法名. 初体验 // 定义只有一个方法的接口 interface TestInterface { int doubleNum(int i); } public static void main(String[] args) { // 方法1: 实现doubleNum方法,实现过程是输入i返回 i*2,最终得到实现了该接口的实例对象 TestInterface i1 = i -> i * 2; System.out.println(i1.doubleNum(1)); // 方法2: 多个入参时需要用()包围起来,只有一个入参时可省略,见方法1 TestInterface i2 = (i) -> i * 2; System.out.println(i2.doubleNum(1)); // 方法3: 入参可指定类型,没有明确指定类型时自动推断,如方法1和方法2均未指定入参类型 TestInterface i3 = (int i) -> i * 2; System.out.println(i3.doubleNum(1)); // 方法4: 实现体不止一句代码时,需要用{}包围起来,并在实现体内部处理出参,当然实际情况中也可能并没有返回值 TestInterface i4 = (int i) -> { System.out.println(\"regular lambda expression\"); return i * 2; }; System.out.println(i4.doubleNum(1)); } lambda 表达式 实现的接口有且仅有一个要实现的接口,体现了 java 的单一职责原则. 这一点也很好理解,如果不止一个接口需要实现,那实现的具体是哪一个方法呢?所以这样的情况在 ide 中一般都会有相应的提示. 此外,java8 引入新的注解 @FunctionInterface 来支持lambda 表达式,也可用于ide 自动提示. 而上例中,我们的接口并没有标注过该注解,不是也能正常工作吗?是的,确实是这样的,所以@FunctionInterface 只是声明式注解,并没有实际作用,不过实际工作中最好加上@FunctionInterface 该注解,万一以后有用呢? 小结 lambda 表达式 实现的接口需要有 @FunctionInterface 注解,并且要实现的方法有且仅有一个. 日常工作中也应该学习单一职责原则,接口设计要足够细,方便使用lambda 表达式,术业有专攻. 利用接口多继承特性,做到接口的集大成者. © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:09:57 "},"java8/functionInterface.html":{"url":"java8/functionInterface.html","title":"函数式接口","keywords":"","body":"函数式接口 接口允许有默认实现 @FunctionalInterface interface TestInterface { // 要实现的方法 int doubleNum(int i); // 默认实现方法 default int add(int x, int y) { return x + y; } } public static void main(String[] args) { // lambda 表达式实现doubleNum方法并返回该接口的实例对象 TestInterface interface1 = i -> i * 2; // 调用接口的doubleNum方法和add方法 System.out.println(interface1.doubleNum(1)); System.out.println(interface1.add(1,2)); } 小结 值得说明的是,默认接口实现应该是对所有的实现类来说有价值的默认实现,接口是对行为的规范,对接口的定义必须相当谨慎.那我们熟悉的 List 来说,查询源码,搜索@since发现,大多 api 是1.2,1.3,1.4...竟然没有,而1.8新增的也是 default 接口,为什么? 因为增加接口就需要实现,所以轻易不会新增接口,但是 jdk8允许有默认接口实现,这就解决了需要重新实现接口方法的问题,这也是为什么我们认为这是 jdk8的重大更新; 这里需要说明下 jdk8新增了默认接口实现方法,我们称之为默认接口实现方法,而 lambda 表达式实现的接口方法,我们称之为默认方法吧; 默认接口实现方法可以认为是内部实现类,也可以使用 this; @FunctionalInterface interface Interface1 { int doubleNum(int i); default int add(int x, int y) { System.out.println(this.doubleNum(1)); return x + y; } } public static void main(String[] args) { Interface1 interface1 = i -> i * 2; System.out.println(interface1.add(1, 2)); } 接口多继承时,需要指明具体覆盖哪一个默认实现方法 @FunctionalInterface interface Interface1 { int doubleNum(int i); default int add(int x, int y) { System.out.println(\"Interface1 add\"); return x + y; } } @FunctionalInterface interface Interface2 { int doubleNum(int i); default int add(int x, int y) { System.out.println(\"Interface2 add\"); return x + y; } } @FunctionalInterface interface Interface3 extends Interface2, Interface1 { @Override default int add(int x, int y) { System.out.println(\"Interface3 add\"); return Interface1.super.add(x, y); } } public static void main(String[] args) { Interface3 interface3 = i -> i * 2; System.out.println(interface3.add(1, 2)); } 接口真的需要吗,有没有更简洁的方法 public class MyMoneyDemo { public static void main(String[] args) { MyMoney myMoney = new MyMoney(9999999); myMoney.printMoney(i -> new DecimalFormat(\"#,###\").format(i)); } } interface IMoneyFormat { String format(int money); } class MyMoney { private final int money; public MyMoney(int money) { this.money = money; } public void printMoney(IMoneyFormat moneyFormat) { System.out.println(\"MyMoney is \" + moneyFormat.format(this.money)); } } 以上例子,定义一个接口,然后 lambda 表达式生成接口的实现类,从而实现接口功能; 其实不难发现,lambda 表达式实现的过程中,我们并不关心接口的参数以及方法名,我们仅仅关心入参和出参,那让我们进一步简化吧; public class MyMoneyDemo { public static void main(String[] args) { MyMoney myMoney = new MyMoney(9999999); myMoney.printMoney(i -> new DecimalFormat(\"#,###\").format(i)); } } class MyMoney { private final int money; public MyMoney(int money) { this.money = money; } public void printMoney(Function moneyFormat) { System.out.println(\"MyMoney is \" + moneyFormat.apply(this.money)); } } 这就是函数式编程,不必定义那么多接口,此外函数式接口还支持链式操作; public class MyMoneyDemo { public static void main(String[] args) { MyMoney myMoney = new MyMoney(9999999); Function moneyFormat = i -> new DecimalFormat(\"#,###\").format(i); myMoney.printMoney(moneyFormat.andThen(s -> \"RMB: \" + s)); } } class MyMoney { private final int money; public MyMoney(int money) { this.money = money; } public void printMoney(Function moneyFormat) { System.out.println(\"MyMoney is \" + moneyFormat.apply(this.money)); } } 小结 接口 输入参数 返回类型 说明 Predicate T boolean 断言 Consumer T / 消费一个数据 Function T R 输入T输出R的函数 Supplier / R 提供一个数据 UnaryOperator T T 一元函数(输出输入类型相同) BiFunction R 两个输入的函数 BinaryOperator T 两元函数(输出输入类型相同) © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:09:57 "},"php/":{"url":"php/","title":"php 学习笔记","keywords":"","body":"php 学习笔记 php 是世界上最好的语言,我正在学习 php,多多指教! © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:10:20 "},"php/php-setup-environment-mac.html":{"url":"php/php-setup-environment-mac.html","title":"搭建开发环境( mac 版)","keywords":"","body":"搭建开发环境( mac 版) Mac 系统默认集成了很多开发工具,其中就包括 php 所需要的一些软件工具. 下面我们将搭建最简单的 php 开发环境,每一步都会验证上一步的操作结构,请一步一步跟我一起搭建吧! web 服务器之 apache apache 是一款 web 服务器,用于运行 php 文件,除了 apache 外也可以是 nginx 服务器. 默认情况下 mac 已经预装了 apach 服务,自然不用 nginx 服务器了. 现在什么也没有配置的情况下,直接启动 apache 服务器看一下能否正常运行. $ sudo apachectl start 常用命令 查看 apache 版本 语法: apachectl -v 示例: $ apachectl -v Server version: Apache/2.4.34 (Unix) Server built: Feb 22 2019 19:30:04 启动 apache 服务 语法: sudo apachectl start 示例: $ sudo apachectl start Password: 停止 apache 服务 语法: sudo apachectl stop 示例: $ sudo apachectl stop 重启 apache 服务 语法: sudo apachectl restart 示例: $ sudo apachectl restart 安装路径 apache 默认安装于 /private/etc/apache2 目录,属于系统隐藏目录,可以在终端中直接进入也可以在访达中直接前往文件夹. 示例: $ tree /private/etc/apache2 /private/etc/apache2 ├── extra │ ├── httpd-autoindex.conf │ ├── httpd-autoindex.conf~previous │ ├── httpd-dav.conf │ ├── httpd-dav.conf~previous │ ├── httpd-default.conf │ ├── httpd-default.conf~previous │ ├── httpd-info.conf │ ├── httpd-info.conf~previous │ ├── httpd-languages.conf │ ├── httpd-languages.conf~previous │ ├── httpd-manual.conf │ ├── httpd-manual.conf~previous │ ├── httpd-mpm.conf │ ├── httpd-mpm.conf~previous │ ├── httpd-multilang-errordoc.conf │ ├── httpd-multilang-errordoc.conf~previous │ ├── httpd-ssl.conf │ ├── httpd-ssl.conf~previous │ ├── httpd-userdir.conf │ ├── httpd-userdir.conf~previous │ ├── httpd-vhosts.conf │ ├── httpd-vhosts.conf~previous │ └── proxy-html.conf ├── httpd.conf ├── httpd.conf.pre-update ├── httpd.conf~previous ├── magic ├── mime.types ├── original │ ├── extra │ │ ├── httpd-autoindex.conf │ │ ├── httpd-dav.conf │ │ ├── httpd-default.conf │ │ ├── httpd-info.conf │ │ ├── httpd-languages.conf │ │ ├── httpd-manual.conf │ │ ├── httpd-mpm.conf │ │ ├── httpd-multilang-errordoc.conf │ │ ├── httpd-ssl.conf │ │ ├── httpd-userdir.conf │ │ ├── httpd-vhosts.conf │ │ └── proxy-html.conf │ └── httpd.conf ├── other │ └── php7.conf └── users └── Guest.conf 5 directories, 43 files 如果想要修改项目部署路径以及服务器端口等自定义配置,可打开 /private/etc/apache2/httpd.conf 文件进行编辑,如果权限不足,要么提升权限要么复制到别处修改好再替换掉原来的配置文件. 配置文件一旦修改,请一定要重启服务器,不然并不会生效! vim 搜索文件内容时临时高亮设置: :set hlsearch ,取消高亮设置: :set nohlsearch . 修改项目部署路径 DocumentRoot : 默认部署路径于 /Library/WebServer/Documents 终端输入 vim 命令查找并编辑目标节点. $ vim /private/etc/apache2/httpd.conf 输入 vim /private/etc/apache2/httpd.conf 进入命令行模式,输入 :/DocumentRoot 从头搜索文件内容,紧接着输入 n 表示查找下一项匹配字符,N 表示查找上一项匹配内容. 如果不熟悉 vim 语法也可以选择熟悉的编辑器打开 httpd.conf 配置文件进行修改配置. 修改项目部署端口 Listen : 默认监听端口 80 如果端口冲突的话,可以修改成其他端口,80 端口的好处在于可以直接访问服务器地址而不用显示带上端口号. # 等价于 http://localhost:80 http://localhost # 等价于 http://127.0.0.1:80 http://127.0.0.1 部署路径 默认情况下,apache 的部署路径位于 /Library/WebServer/Documents ,除非你更改了 httpd.conf#DocumentRoot 的节点配置. $ tree /Library/WebServer/Documents /Library/WebServer/Documents ├── PoweredByMacOSX.gif ├── PoweredByMacOSXLarge.gif ├── index.html.en └── index.html.en~orig 0 directories, 4 files 当然你可以通过访达直接前往 /Library/WebServer/Documents 目录或者 open /Library/WebServer/Documents 直接调用内置程序打开目录. 如果非要一步一步找到部署路径,打开 访达 后选择左侧最下方的本地光盘(个人用户名称),然后依次选择 Machintosh HD > 资源库 (Library) > WebServer > Documents 世界上最好的语言之 php php 在行业内赢得\"世界上最好的语言\"称号,自嘲为\"拍簧片\".不管怎样,既然我们决定 pai(拍)huang(簧)pian(片) ,那总要配置一下 php 的基本环境吧! Mac 系统一如既往内置了 php 环境,不用我们费心去安装 php 了,现在看一下 php 的基本信息吧! $ php -version PHP 7.1.23 (cli) (built: Feb 22 2019 22:08:13) ( NTS ) Copyright (c) 1997-2018 The PHP Group Zend Engine v3.1.0, Copyright (c) 1998-2018 Zend Technologies php 是一种服务端脚本解释性语言,依赖于 web 服务器进行解析,所以 php 想要正常工作离不开上一步配置的 apache 服务器. 还记得 apache 配置文件的位置吗? apache 配置文件路径 : /private/etc/apache2/httpd.conf 打开 httpd.conf 配置文件并搜索 LoadModule php 字符串,将前面的 # 去掉即可引入 php 支持,配置文件修改后记得重启才能生效哟! 是时候展示真正的技术了,现在万事俱备只待测试 php 到底有没有配置成功?! 在项目部署根目录下新建 info.php 测试文件,启动服务器后访问 http://localhost/info.php 如果能正常打印出 php 相关信息,那就证明 php 和 apache 整合无误,否则可能是某一步配置有误! phpinfo(); 持久化存储之 mysql 数 Mac 系统并没有默认安装 mysql 服务,因此我们需要手动安装 mysql . 一般来说,我们谈到 mysql 数据库指的是 mysql 的服务端,作为生产环境服务端足够了并不需要客户端. 但是,日常开发中如果没有客户端我们很难直观管理数据,所以一般来说,我们还会安装 mysql 客户端,当然一般是各种功能强大的图形化工具. mysql 服务端 下载链接: macOS 10.14 (x86, 64-bit), DMG Archive 和正常的软件安装一样,将安装文件移动到应用里即可完成,比 Windows 的下一步下一步安装还要方便快捷! 安装完成后,可以在系统偏好设置中找到 MySQL 图标,查看管理 mysql 服务端. 点击 MySQL 图标,可以进行简单的设置以及重启服务等操作. 但是如果想要在终端中无痛连接上 mysql 服务端,那么还需要一步设置软链接,类似于 Windows 的快捷方式. $ sudo ln -fs /usr/local/mysql/bin/mysql /usr/local/bin/mysql 现在我们就可以在终端内愉快的连接上 mysql 服务端了呢! # 登录 `mysql` 服务端 $ mysql -u root -p Enter password: Welcome to the MySQL monitor. Commands end with ; or \\g. Your MySQL connection id is 29 Server version: 5.7.24 MySQL Community Server (GPL) Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved. Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners. Type 'help;' or '\\h' for help. Type '\\c' to clear the current input statement. # 查看当前数据库列表 mysql> show databases; +---------------------+ | Database | +---------------------+ | information_schema | | mysql | | performance_schema | | security-plus | | sys | | test | +---------------------+ 6 rows in set (0.00 sec) # 退出当前数据库会话 mysql> exit Bye $ 查看 mysql 服务端版本 语法: mysql --version 示例: $ mysql --version mysql Ver 14.14 Distrib 5.7.24, for macos10.14 (x86_64) using EditLine wrapper 查看 mysql 服务端状态 语法: sudo /usr/local/mysql/support-files/mysql.server status 示例: $ sudo /usr/local/mysql/support-files/mysql.server status SUCCESS! MySQL running (73088) 启动 mysql 服务端 语法: sudo /usr/local/mysql/support-files/mysql.server start 示例: $ sudo /usr/local/mysql/support-files/mysql.server start Starting MySQL . SUCCESS! 停止 mysql 服务端 语法: sudo /usr/local/mysql/support-files/mysql.server stop 示例: $ sudo /usr/local/mysql/support-files/mysql.server stop Shutting down MySQL .. SUCCESS! 重启 mysql 服务端 语法: sudo /usr/local/mysql/support-files/mysql.server restart 示例: $ sudo /usr/local/mysql/support-files/mysql.server restart Shutting down MySQL . SUCCESS! Starting MySQL . SUCCESS! mysql 客户端 如果说生产环境没有 mysql 的图形化工具也就罢了,但是如果日常开发时也不没有图形化工具的话,那就真的太不方便了. 这里推荐两个客户端工具,一个是人畜无害的 Sequel Pro,另一个则是 php 专属的 phpMyAdmin. BS 架构的 phpMyAdmin 下载地址 : phpMyAdmin phpMyAdmin 是一款 web 版数据款管理软件,可以在浏览器中在线访问,像访问你的网站一样访问数据库. 下载完成后解压并重命名为 phpMyAdmin,然后移动到 apache 的项目部署路径下,如果没有更改过默认的部署路径,那么应该是 /Library/WebServer/Documents 目录. 现在部署路径下不仅有个 info.php 文件还有 phpMyAdmin 文件夹. # 仅仅显示两级文件目录 $ tree -L 2 . ├── PoweredByMacOSX.gif ├── PoweredByMacOSXLarge.gif ├── index.html.en ├── index.html.en~orig ├── info.php └── phpMyAdmin ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── export.php ├── favicon.ico ├── gis_data_editor.php ├── import.php ├── import_status.php ├── index.php ├── view_operations.php └── yarn.lock 11 directories, 108 files 移动完成后先复制一份 config.sample.inc.php 文件并重命名为 config.inc.php 文件. 执行 vim /Library/WebServer/Documents/phpMyAdmin/config.inc.php 搜索并编辑 host 节点内容,将 localhost 更改成 127.0.0.1 . 示例: # 修改前 $cfg['Servers'][$i]['host'] = 'localhost'; # 修改后: 将 `localhost` 更改成 `127.0.0.1` $cfg['Servers'][$i]['host'] = '127.0.0.1'; 重启 apache 服务,访问 http://localhost/phpMyAdmin/ 开始登陆数据库吧! 输入 mysql 的用户名和密码登录成功后就能管理本地数据库了. CS 架构的 Sequel Pro 下载地址 : v1.1.2 OS X 10.6 or Higher Sequel Pro 是简单易用的数据库管理工具,与上述的 phpMyAdmin 不同之处在于并不依赖 php 环境,可以独立安装部署. 安装完成后输入数据库连接信息连接到本地数据库,参考信息如下. 点击连接(Connect) 连接到本地服务器,由于刚才并没有选择数据库,因此登陆后需要选定数据库,这里根据实际情况选择即可. php 集成 mysql 如果没有数据库提供持久化存储能力,那么 php 只能临时运行而没有记忆功能,所以想要记住网站大量信息自然离不开数据库. 准备数据 为了接下来演示 php 集成 mysql 数据库,现在先创建一个测试数据库并插入一些测试数据. 下面主要是通过终端方式进行操作,小伙伴们也可以使用上述安装的图形化工具进行可视化编辑. 连接到本地服务器 语法 : mysql -u -p 示例: # 连接到本地数据库,用户名 `root`,密码自定义 $ mysql -u root -p Enter password: Welcome to the MySQL monitor. Commands end with ; or \\g. Your MySQL connection id is 109 Server version: 5.7.24 MySQL Community Server (GPL) Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved. Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners. Type 'help;' or '\\h' for help. Type '\\c' to clear the current input statement. 列出当前数据库列表 语法 : show databases 示例: mysql> show databases; +--------------------+ | Database | +--------------------+ | information_schema | | mysql | | performance_schema | | security-plus | | sys | +--------------------+ 5 rows in set (0.00 sec) 创建测试数据库 语法 : create database 示例: # 创建 `test` 数据库并指定编码格式为 `utf8` mysql> create database IF NOT EXISTS test default charset utf8 COLLATE utf8_general_ci; Query OK, 1 row affected (0.00 sec) # 再次查询当前数据库列表,新增 `test` 数据库 mysql> show databases; +--------------------+ | Database | +--------------------+ | information_schema | | mysql | | performance_schema | | security-plus | | sys | | test | +--------------------+ 6 rows in set (0.00 sec) 列出当前数据表列表 语法 : show tables 示例: # 使用 `test` 测试数据库 mysql> use test; Database changed # 列出当前全部数据表 mysql> show tables; Empty set (0.00 sec) 创建测试数据表 语法 : create tabel ( )) # 创建 `user` 用户表 mysql> CREATE TABLE `test`.`user` ( `id` BIGINT(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '用户 id', `name` VARCHAR(45) NOT NULL DEFAULT '' COMMENT '姓名', PRIMARY KEY (`id`), UNIQUE INDEX `id_UNIQUE` (`id` ASC)) ENGINE = InnoDB DEFAULT CHARACTER SET = utf8 COMMENT = '用户表'; Query OK, 0 rows affected (0.01 sec) # 再次列出当前数据表列表 mysql> show tables; +----------------+ | Tables_in_test | +----------------+ | user | +----------------+ 1 row in set (0.00 sec) 查看数据表结构 语法 : desc 示例: mysql> desc user; +-------+---------------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +-------+---------------------+------+-----+---------+----------------+ | id | bigint(11) unsigned | NO | PRI | NULL | auto_increment | | name | varchar(45) | NO | | | | +-------+---------------------+------+-----+---------+----------------+ 2 rows in set (0.00 sec) 查看数据表创建语句 语法 : show create table 示例: mysql> show create table user \\G *************************** 1. row *************************** Table: user Create Table: CREATE TABLE `user` ( `id` bigint(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '用户 id', `name` varchar(45) NOT NULL DEFAULT '' COMMENT '姓名', PRIMARY KEY (`id`), UNIQUE KEY `id_UNIQUE` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户表' 1 row in set (0.00 sec) 查询数据 语法 : select [fields] from tableName [where condition] [limit N][ offset M] 示例: mysql> select id,name from user; Empty set (0.00 sec) 插入数据 语法 : insert into ([fields]) VALUES ([values]) 示例: mysql> INSERT INTO `test`.`user` (`name`) VALUES ('snowdreams1006'); Query OK, 1 row affected (0.00 sec) mysql> INSERT INTO `test`.`user` (`name`) VALUES ('雪之梦技术驿站'); Query OK, 1 row affected (0.00 sec) mysql> INSERT INTO `test`.`user` (`name`) VALUES ('测试用户姓名'); Query OK, 1 row affected (0.00 sec) mysql> select id,name from user; +----+-----------------------+ | id | name | +----+-----------------------+ | 1 | snowdreams1006 | | 2 | 雪之梦技术驿站 | | 3 | 测试用户姓名 | +----+-----------------------+ 3 rows in set (0.00 sec) 退出数据库 语法 : exit 示例: mysql> exit Bye $ 导出数据 语法 : mysqldump -u -p > exportName.sql 备份数据用到的是 mysqldump 工具,默认情况下该命令位于 /usr/local/mysql/bin 目录下,正常情况下需要指定该路径才能调用 mysqldump 命令. $ tree /usr/local/mysql/bin /usr/local/mysql/bin ├── innochecksum ├── lz4_decompress ├── my_print_defaults ├── myisam_ftdump ├── myisamchk ├── myisamlog ├── myisampack ├── mysql ├── mysql_client_test_embedded ├── mysql_config ├── mysql_config_editor ├── mysql_embedded ├── mysql_install_db ├── mysql_plugin ├── mysql_secure_installation ├── mysql_ssl_rsa_setup ├── mysql_tzinfo_to_sql ├── mysql_upgrade ├── mysqladmin ├── mysqlbinlog ├── mysqlcheck ├── mysqld ├── mysqld-debug ├── mysqld_multi ├── mysqld_safe ├── mysqldump ├── mysqldumpslow ├── mysqlimport ├── mysqlpump ├── mysqlshow ├── mysqlslap ├── mysqltest_embedded ├── mysqlxtest ├── perror ├── replace ├── resolve_stack_dump ├── resolveip └── zlib_decompress 0 directories, 38 files 所以,应该是如下命令才能调用 mysqldump 命令. $ /usr/local/mysql/bin/mysqldump --version mysqldump Ver 10.13 Distrib 5.7.24, for macos10.14 (x86_64) 不过这也太长了吧,肯定不是很不变,一劳永逸的方法是将 /usr/local/mysql/bin 加入到环境变量中就不用添加额外的路径信息了. 还记得 mysql 服务端刚安装完毕,我们想要通过终端连接到本地数据库服务器时设置了 mysql 的软链接,所以才能直接使用 mysql -u root -p 进行登录. mac 的软链接方式相当于 windows 系统的快捷方式,只针对具体命令,现在需要 mysqldump 命令,继续使用软链接还要添加类似的快捷方式. $ sudo ln -fs /usr/local/mysql/bin/mysql /usr/local/bin/mysql $ sudo ln -fs /usr/local/mysql/bin/mysqldump /usr/local/bin/mysqldump 实测可用,但是这并不是优雅的操作方式,/usr/local/mysql/bin/ 目录下那么多命令,下次需要用到其他命令岂不是要设置很多软链接? $ mysqldump --version mysqldump Ver 10.13 Distrib 5.7.24, for macos10.14 (x86_64) 所以,现在我们考虑将 /usr/local/mysql/bin 加入到系统环境变量中,这样一来就能一劳永逸不用频繁设置软链接了! # mysql export PATH=$PATH:/usr/local/mysql/bin 设置完毕后下次重启电脑就会生效,或者运行下述命令立即生效. $ source ~/.bash_profile 为了测试环境变量是否生效,我们先删除原来的软链接. $ rm -rf /usr/local/bin/mysql $ rm -rf /usr/local/bin/mysqldump 依然能够正常调用 mysql 相关命令. # `mysql` 版本信息 $ mysql --version mysql Ver 14.14 Distrib 5.7.24, for macos10.14 (x86_64) using EditLine wrapper # `mysqldump` 版本信息 $ mysqldump --version mysqldump Ver 10.13 Distrib 5.7.24, for macos10.14 (x86_64) 示例: $ mysqldump -u root -p test > database_test.sql; Enter password: $ 注意 : mysqldump 和 mysql 相互独立的命令行程序,并不是在 mysql 会话中执行的 sql. 查看当前备份文件内容: # 备份文件位于当前目录 $ cat $(pwd)/database_test.sql 备份 sql 文件内容,可以直接复制执行. -- MySQL dump 10.13 Distrib 5.7.24, for macos10.14 (x86_64) -- -- Host: localhost Database: test -- ------------------------------------------------------ -- Server version 5.7.24 /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; /*!40101 SET NAMES utf8 */; /*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; /*!40103 SET TIME_ZONE='+00:00' */; /*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; -- -- Table structure for table `user` -- DROP TABLE IF EXISTS `user`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `user` ( `id` bigint(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '用户 id', `name` varchar(45) NOT NULL DEFAULT '' COMMENT '姓名', PRIMARY KEY (`id`), UNIQUE KEY `id_UNIQUE` (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COMMENT='用户表'; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `user` -- LOCK TABLES `user` WRITE; /*!40000 ALTER TABLE `user` DISABLE KEYS */; INSERT INTO `user` VALUES (1,'snowdreams1006'),(2,'雪之梦技术驿站'),(3,'测试用户姓名'); /*!40000 ALTER TABLE `user` ENABLE KEYS */; UNLOCK TABLES; /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; -- Dump completed on 2019-05-19 12:49:35 导入数据 语法 : source 示例: # 创建 `test_import` 数据库 mysql> create database test_import; # 使用 `test_import` 数据库 mysql> use test_import; # 导入 `database_test.sql` 文件 mysql> source /Users/sunpo/Documents/workspace/snowdreams1006.github.io/database_test.sql 删除数据库 语法 : drop database 示例: mysql> drop database test_import; Query OK, 1 row affected (0.01 sec) 编程连接 如果没有更改过项目的部署路径,那么我们之前有个测试 php 环境的文件,即 /Library/WebServer/Documents/info.php ,现在我们继续编写该文件,通过编码的方式连接到 mysql 数据库. $username=\"root\"; $userpass=\"root\"; $dbhost=\"127.0.0.1\"; $dbdatabase=\"test\"; // 连接到本地服务器 $db=new mysqli($dbhost,$username,$userpass,$dbdatabase); $db->set_charset(\"utf8\"); if(mysqli_connect_error()){ echo \"连接失败: \" . mysqli_connect_error(); exit; } # 查询用户列表 $result = $db->query(\"SELECT id,name FROM user\"); if ($result->num_rows > 0) { // 输出数据 while($row = $result->fetch_assoc()) { echo var_dump($row). \"\"; } } # 关闭数据库连接 $db->close(); phpinfo(); 现在再次启动 apache 服务器,访问 http://localhost/info.php 测试成功! 环境搭建要点总结 apache 服务默认已安装,启动服务器后,在浏览器中访问 http://localhost/ 会显示It works!,表明 apache 能正常使用. 查看 apache 服务器版本 : apachectl -v 启动 apache 服务器 : sudo apachectl start 停止 apache 服务器 : sudo apachectl stop 重启 apache 服务器 : sudo apachectl restart apache 服务器安装路径 : /private/etc/apache2 apache 服务器部署路径 : /Library/WebServer/Documents php 服务默认已安装,集成到 apache 服务器只需要在 /private/etc/apache2/httpd.conf 配置文件中启用 LoadModule php7_module libexec/apache2/libphp7.so 模块即可,重启 apache 服务器即可支持 php 环境. 查看 php 版本信息 : php -version php 默认配置文件路径 : /private/etc/php.ini.default mysql 数据库默认没有安装,需要手动前往 https://www.mysql.com/downloads/ 官网进行下载安装. 如果需要在终端命令行内访问 mysql 服务端,最好将 mysql 的安装路径添加到系统环境中,或者添加软链接也可以. mysql 安装路径 : /usr/local/mysql 系统环境变量路径 : ~/.bash_profile mysql 二进制文件添加到系统环境变量 : export PATH=$PATH:/usr/local/mysql/bin 刷新系统环境变量配置 : source ~/.bash_profile mysql 命令添加软链接 : sudo ln -fs /usr/local/mysql/bin/mysql /usr/local/bin/mysql 查看 mysql 服务器状态 : sudo mysql.server status 启动 mysql 服务器 : sudo mysql.server start 停止 mysql 服务器 : sudo mysql.server stop 重启 mysql 服务器 : sudo mysql.server restart 查看 mysql 版本信息 : mysql --version 登录 mysql 服务器 : mysql -u root -p 退出 mysql 服务器 : exit 最后,php 不仅仅可以面向过程也可以面向对象,虽然是拍簧片,但真的很强大,魅力不小呢! © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:10:26 "},"php/set-timezone-method.html":{"url":"php/set-timezone-method.html","title":"关于时区的那点事","keywords":"","body":"关于时区的那点事 科普一下什么是时区 众所周知,地球绕着太阳转的同时也会自转,因此同一时刻不同地区所接收到太阳照射的情况不同,所以有的地区是日出,有的地区是日落,还有的地区可能是黑夜. 既然地球上的不同地区时间不同,那总要有统一的时间刻度才能方便文化科技交流吧,不然大家说的都是当地时间,这样岂不是乱套了? 有问题就要解决问题,不同地区时间不同就要统一时间标准,而统一时间标准的前提就是要弄清楚全球的时间差异到底在哪以及各地的当地时间如何互相转换. 原来的时间标准是格林尼治标准时间,随着精确计时的发展需要,已被新的时间标准所取代,目前的时间标准是世界协调时. 现在有了统一的时间标准,不同地区的时间就可以统一换算成世界协调时再转换成当地时间了,再也不会出现同一时刻不同时间了! 示例: 北京时间(UTC+8) : 2019-05-30 13:30:00 世界时间(UTC) : 2019-05-30 05:30:00 东部时间(UTC-5) : 2019-05-30 00:30:00 格林尼治标准时间 格林尼治标准时间(又称格林威治平均时间或格林威治标准时间,英文是GreenwichMeanTime,简称GMT ),格林尼治标准时间的正午是指当太阳横穿格林尼治子午线时(也就是在格林尼治时)的时间. 格林尼治是英国伦敦的一个小镇,是地理经度的起点,本初子午线所在的经度是零度经度,所在的时区是零时区. 我们知道一天共有 24 小时,对应着全球 24 个时区,而地球自西向东自转,零时区后依次是东一区到东十一区,然后是东十二区.紧接着是西十二区,西十一区到西一区,最后又回到零时区. 其中东十二区和西十二区是同一个时区,又被称为东西十二区. 总的来说,时区分为东十二区和西十二区以及零时区,其中东西十二区是同一个时区,因此共有 24 个时区. 示例: 由于北京位于东八区,比零时区多个 8 个时区,意味着北京时间比格林标准时间快 8 个小时. 所以,北京时间中午 12 点整的时候,格林尼治才清早 4 点钟,估计还在睡觉呢! ( GMT+8 就是北京时间) 格林尼治标准时间与地球自转有关,不能满足精确计时的需求,因此不再作为标准时间,取而代之的是协调世界时. 协调世界时 协调世界时(又称世界统一时间或世界标准时间或国际协调时间,英文是Coordinated Universal Time,简称UTC ),协调世界时是以原子时秒长为基础,在时刻上尽量接近于格林尼治标准时间的一种时间计量系统. 在不需要精确到秒的情况下, GMT 和 UTC 基本一致,但 UTC 是以更加精确的原子时为基础,因此常用于科学计算领域,也是目前时间计量的统一标准. 示例: 北京时间 12:00 ,换算成 GMT 或 UTC 时间都是 04:00 ( UTC+8 也是北京时间) 北京时间 北京时间(又称中国标准时间),是首都北京所在的时区作为中国的标准时间,比格林尼治标准时间快8小时. 我国幅员辽阔,从西到东横跨东五,东六,东七,东八和东九等五个时区.所以全国统一采用首都北京所在的东八时区的区时作为标准时间,也就是北京时间. 时区信息数据库 时区信息数据库,又称 Olson数据库,是一个主要应用于电脑程序以及操作系统的可协作编辑世界时区信息的数据库. 时区信息数据库采用按“区域/位置”命名规范,方便应用于计算机世界,其中英文地名中的空格用下划线“_”代替,连词符“-”只在英文地名本身包含时使用. 示例: Asia/Hong_Kong : 亚洲/香港 Asia/Macau : 亚洲/澳门 Asia/Shanghai : 亚洲/上海 Asia/Taipei : 亚洲/台北 Asia/Urumqi : 亚洲/乌鲁木齐 上述时区主要是 php 中所支持的中国时区,参考 亚洲所支持的时区列表 关于时区的编程实现 时区不仅仅是现实生活的问题,计算机编程世界也有时间,自然也离不开时区的概念. 在计算机世界中,一切都是数据,最好时区的概念也能体现在相关数据库中,幸运的是已经有前辈为我们提供了时区信息数据库,而各类语言基本都会提供时区的工具类. php 中的日期时间函数库是 php 内置函数库,我们可以方便地操作时间,设置时区等. 正常情况下, date 扩展默认是启用的,我们可以输入 phpinfo 打印出 php 的基本信息,然后搜索关键字 date 就可以找到关于 date 扩展的相关信息. 示例 \"; // 获取当前时区 echo \"当前时区: \".date_default_timezone_get().\"\"; // 当前时间 echo \"当前时间: \".date(\"Y-m-d H:i:s\").\"\"; // 打印 php 信息 phpinfo(); ?> 结果 默认时区: 当前时区: UTC 当前时间: 2019-05-30 05:30:00 选项 值 date/time support(日期时间支持情况) enabled(已启用) timelib version(时间库版本) 2016.05(2016.05) \"Olson\" Timezone Database Version(Olson 时区数据库版本) 2018.5(2018.5) Timezone Database(时区数据库) internal(内部的) Default timezone(默认时区) UTC(协调世界时) 由此可见,假设不设置时区的话,默认时区是协调世界时,该时区和北京时间相比慢 8 个小时! 设置时区的三种姿势 翻阅 php 开发文档中可以找到目前所有支持时区列表,下面整理出关于中国的主要时区. Asia/Hong_Kong : 亚洲/香港 Asia/Macau : 亚洲/澳门 Asia/Shanghai : 亚洲/上海 Asia/Taipei : 亚洲/台北 Asia/Urumqi : 亚洲/乌鲁木齐 常用时区是上海,并没有北京,当然也可以设置成 PRC (中华人民共和国)! 如果没有设置时区的话,默认时区应该是协调世界时(UTC),虽说是通用的时间标准,但转换成当地时间还是需要一定换算的,而且看起来也不太舒服. 所以最好还是修改一下时区的设置项,如果业务不考虑国际化需求,那么永久性把时区固定就可以了,如果有国际化的业务场景,那么最好能够动态设置时区,这样就能清楚知道当地时间了. 因此,下面主要提供两种方式来设置时区,分别是静态设置和动态设置,其中动态设置又提供了两种方法. 静态修改 php 配置 php 的配置文件默认位于 : /private/etc/php.ini ,打开文件后修改 date.timezone 选项. 由于这种方法是直接修改配置文件,因此时区设置后适用于所有脚本,只不过需要重启服务器方可生效. 示例 [Date] ; Defines the default timezone used by the date functions ; http://php.net/date.timezone ;date.timezone = ; http://php.net/date.default-latitude ;date.default_latitude = 31.7667 ; http://php.net/date.default-longitude ;date.default_longitude = 35.2333 ; http://php.net/date.sunrise-zenith ;date.sunrise_zenith = 90.583333 ; http://php.net/date.sunset-zenith ;date.sunset_zenith = 90.583333 结果 # 去掉;并设置时区,取值可以是PRC,也可以是Asia/Shanghai等时区 date.timezone = PRC 详情请参考: http://php.net/date.timezone 动态设置 php 配置 ini_set 支持设置当前脚本的默认时区选项. 静态设置时区仅仅适合时区固定的情况,如果需要动态切换时区,修改配置文件就不能满足这种情况了. 因此,php 中还提供了动态修改 php.ini 配置文件的方法,ini_set() 方法刚好支持动态设置时区. 不用重启服务器,但仅仅针对当前脚本生效,其中 ini_set 方法支持的 php.ini 配置选项列表 示例 // 设置当前时区 ini_set(\"date.timezone\", \"Asia/Tokyo\"); // 获取默认时区 echo \"当前时区: \".ini_get(\"date.timezone\").\"\"; // 当前时间 echo \"当前时间: \".date(\"Y-m-d H:i:s\").\"\"; 结果 当前时区: Asia/Tokyo 当前时间: 2019-05-30 14:30:00 动态设置 php 时区 date_default_timezone_set 方法用于设置当前脚本的默认时区. ini_set 虽然支持设置时区选项,但毕竟不是专业设置时区的方法,date_default_timezone_set 才是专门设置时区的方法. 这两个方法都属于动态设置时区,也都是针对当前脚本生效,也都不用重启服务器就能立马生效. 示例 // 获取当前时区 echo \"当前时区: \".date_default_timezone_get().\"\"; // 设置当前时区 date_default_timezone_set(\"UTC\"); // 获取当前时区 echo \"当前时区: \".date_default_timezone_get().\"\"; // 当前时间 echo \"当前时间: \".date(\"Y-m-d H:i:s\").\"\"; 结果 当前时区: Asia/Tokyo 当前时区: UTC 当前时间: 2019-05-30 05:30:00 关于时区的一些总结 时区和时间密切相关,统一时间说的其实是统一时间的标准,这样一个地区的当地时间就可以轻易转换成另一个地区的当地时间了. 目前世界上统一的时间标准是协调时间时(UTC),中国的时间标准是北京时间,北京时间比协调时间时快 8 个小时. 现实世界的时区也要反映到计算机世界,其中时区信息数据库就是用于表示现实世界的时区概念. 由此可见,时区是一个通用概念,不仅 php 有时区,java 和 js 等语言也有时区概念,可以说只要有时间的地方都离不开时区. 关于时区的小技能,你 get 到了吗? 美剧>当地时间是星期二晚上八点开播,请问北京时间何时开播? 英剧>北京时间凌晨四点半开播,请问当地时间是何时开播? 泰剧>当地时间是晚上九点二十开播,请问北京时间是何时? 参考资料 时区相关知识扫盲 时区时差时间换算 © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:10:26 "},"php/datetime-overview.html":{"url":"php/datetime-overview.html","title":"日期时间操作一箩筐","keywords":"","body":"日期时间操作一箩筐 格式化日期时间 date : 格式化日期时间 场景 将当前日期时间或者特定日期时间格式化输出为特定格式的字符串,常用于人性化展示信息. 说明 返回给定时间戳格式化后所产生的日期时间字符串,如果没有给出时间戳则默认使用本地当前时间. 备注 格式 说明 返回值示例 Y 4 位数字完整表示的年份 2019 y 2 位数字表示的年份 19 M 三个字母缩写表示的月份 Jan 到 Dec m 数字表示的月份,有前导零 01 到 12 D 星期中的第几天,文本表示,3个字母 Mon 到 Sun d 月份中的第几天,有前导零的 2 位数字 01 到 31 H 小时,24 小时格式,有前导零 00 到 23 h 小时,12 小时格式,有前导零 01 到 12 I 是否为夏令时 如果是夏令时为1 ,否则为 0 i 有前导零的分钟数 00 到 59 S 每月天数后面的英文后缀,2 个字符 st,nd,rd 或者 th ,可以和 j 一起用 s 秒数,有前导零 00 到 59 常用格式 // 形如 2019-05-31 12:00:00 echo date(\"Y-m-d H:i:s\"); // 形如 2019/05/31 12:00:00 echo date(\"Y/m/d H:i:s\"); // 形如 2019年05月31日 12时00分00秒 echo date(\"Y年m月d日 H时i分s秒\"); 示例 // 设置当前时区为上海时区 date_default_timezone_set(\"Asia/Shanghai\"); // 获取当前时区 : Asia/Shanghai echo \"当前时区 : \".date_default_timezone_get().\"\"; // `Y年m月d日 H时i分s秒` 格式化当前时间 : 2019年05月30日 22时32分46秒 echo \"当前时间 : \".date(\"Y年m月d日 H时i分s秒\").\"\"; // `Y-m-d H:i:s` 格式化当前时间 : 2019-05-30 22:32:46 echo \"当前时间 : \".date(\"Y-m-d H:i:s\").\"\"; // `w` 星期中的第几天,数字表示: 0(表示星期天)到 6(表示星期六) switch (date(\"w\")) { case '0': $dayStr = \"日\"; break; case '1': $dayStr = \"一\"; break; case '2': $dayStr = \"二\"; break; case '3': $dayStr = \"三\"; break; case '4': $dayStr = \"四\"; break; case '5': $dayStr = \"五\"; break; case '6': $dayStr = \"六\"; break; default: $dayStr = \"未知\"; break; } // 2019年05月30日 星期四 echo \"当前时间 : \".date(\"Y年m月d日\").\" 星期\".$dayStr.\"\"; echo \"\"; // `z` 年份中的第几天 : 今天是全年的第149天 echo \"今天是全年的第\".date(\"z\").\"天\"; // `W` ISO-8601 格式年份中的第几周,每周从星期一开始 : 本周是全年的第22周 echo \"本周是全年的第\".date(\"W\").\"周\"; // `t` 指定的月份有几天 : 本月共有31天 echo \"本月共有\".date(\"t\").\"天\"; 日期转化时间戳 time : 返回当前的 Unix 时间戳 场景 获取当前日期时间或特定日期时间的时间戳,常用于日期时间之间的相互转换. 说明 返回自从 Unix 纪元(格林威治时间 1970年1月1日 00:00:00)到当前时间的秒数. 示例 // 设置当前时区为上海时区 date_default_timezone_set(\"Asia/Shanghai\"); // 获取当前时区 echo \"当前时区 : \".date_default_timezone_get().\"\"; // 一周前的日期时间: 7 days; 24 hours; 60 mins; 60 secs $preWeek = time() - (7 * 24 * 60 * 60); echo \"现在是\".date(\"Y-m-d H:i:s\").\",上周是\".date(\"Y-m-d H:i:s\",$preWeek).\"\"; // 一周后的日期时间: 7 days; 24 hours; 60 mins; 60 secs $nextWeek = time() + (7 * 24 * 60 * 60); echo \"现在是\".date(\"Y-m-d H:i:s\").\",下周是\".date(\"Y-m-d H:i:s\",$nextWeek).\"\"; microtime : 返回当前 Unix 时间戳和微秒数 场景 获取当前日期时间或特定日期时间的时间戳,常用于程序运行过程打点分析,也可以用于日期时间之间的互相转换. 说明 当前 Unix 时间戳以及微秒数,本函数仅在支持 `gettimeofday()`` 系统调用的操作系统下可用. 示例 // 设置当前时区为上海时区 date_default_timezone_set(\"Asia/Shanghai\"); // 获取当前时区 echo \"当前时区 : \".date_default_timezone_get().\"\"; // 当前日期时间戳 echo \"当前日期时间戳: \".time().\" \".microtime().\" \".microtime(TRUE).\"\"; mktime : 取得一个日期的 Unix 时间戳 场景 获取给定日期的时间戳,按照\"时分秒 月日年\"格式依次解析,返回时间戳. 说明 根据给出的参数返回 Unix 时间戳. 备注 格式 说明 参数示例 H hour 小时数 00 到 23 i minute 分钟数 00 到 59 s second 秒数 00 到 59 n month 月份数 01 到 12 j day 天数 01 到 31 Y year 年份数,可以是两位或四位数字 0-69 对应于 2000-2069 ,70-100 对应于 1970-2000 格式: 时分秒 月日年,支持从右往左依次省略,被省略的值取当前时间的对应值. 示例 // 设置当前时区为上海时区 date_default_timezone_set(\"Asia/Shanghai\"); // 获取当前时区 echo \"当前时区 : \".date_default_timezone_get().\"\"; // 指定日期时间戳: 时分秒 月日年 : 1559275200 2019-05-31 12:00:00 echo \"2019年05月31日 12:00:00 的时间戳: \".mktime(12,0,0,5,31,2019).\" \".date(\"Y-m-d H:i:s\", mktime(12,0,0,5,31,2019)).\"\"; // 距离国庆节还有多少天,单位秒 : 今天是2019-05-31,距离国庆节还剩122天 $nationalDay = mktime(0,0,0,10,1,2019); $currentDay = time(); $remainingDay = floor(abs($nationalDay - $currentDay)/(24*3600)); echo \"今天是\".date(\"Y-m-d\").\",距离国庆节还剩\".$remainingDay.\"天\"; strtotime : 将任何字符串的日期时间描述解析为 Unix 时间戳 场景 将英文日期解析成时间戳,比直接解析日期方便,采用自然语义而不是编程语言进行转换日期. 说明 本函数预期接受一个包含美国英语日期格式的字符串并尝试将其解析为 Unix 时间戳(自 January 1 1970 00:00:00 GMT 起的秒数,其值相对于 now 参数给出的时间,如果没有提供此参数则用系统当前时间. 常用格式 // 2019-06-02 echo date(\"Y-m-d\", strtotime(\"2019-05-31 +2 days\")); // 2019-07-01 echo date(\"Y-m-d\", strtotime(\"2019-05-31 +1 month\")); // 2019-06-09 echo date(\"Y-m-d\", strtotime(\"2019-05-31 +1 week 2 days 4 hours 2 seconds\")); 示例 // 设置当前时区为上海时区 date_default_timezone_set(\"Asia/Shanghai\"); // 获取当前时区 echo \"当前时区 : \".date_default_timezone_get().\"\"; // 当前日期时间戳 echo \"当前日期时间戳: \".time().\" \".strtotime(\"now\").\" \".date(\"Y-m-d H:i:s\", strtotime(\"now\")).\"\"; // 一周后的日期时间: 7 days; 24 hours; 60 mins; 60 secs $nextWeek = time() + (7 * 24 * 60 * 60); echo \"现在是\".date(\"Y-m-d H:i:s\").\",下周是\".date(\"Y-m-d H:i:s\",$nextWeek).\" \".date(\"Y-m-d H:i:s\",strtotime(\"+1 week\")).\"\"; echo \"现在是\".date(\"Y-m-d H:i:s\").\",1周2天4小时2秒是\".date(\"Y-m-d H:i:s\",strtotime(\"+1 week 2 days 4 hours 2 seconds\")).\"\"; echo \"现在是\".date(\"Y-m-d H:i:s\").\",下周三是\".date(\"Y-m-d H:i:s\",strtotime(\"next Thursday\")).\"\"; 日期时间函总结 日期时间函数库是 php 内置的函数库,默认情况下已启用,值得注意的是,日期时间和时区有关,建议首先设置下时区. 纵观日期时间的操作方法,总的来说,可以大致分为两类,一类是给计算机用的,另一类是给人看的. 给人看的 date_default_timezone_set(\"Asia/Shanghai\") : 设置当前脚本使用的时区 date(\"Y-m-d H:i:s\") : 格式化日期时间 date(\"Y-m-d\", strtotime(\"2019-05-31 +2 days\")) : 格式化英文描述的日期时间 给计算机用的 time() : 当前时间的秒数 microtime() : 当前时间的秒数和微秒数 strtotime() : 将字符串形式的日期时间转换成时间戳 最后,文档那么齐全,不懂就去多看看,忘记有啥方法全靠 ide 智能提示就好,多用用就会慢慢熟练. © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:10:20 "},"go/":{"url":"go/","title":"go 学习笔记","keywords":"","body":"go 学习笔记 Go 是一种开源编程语言,可以轻松构建简单,可靠,高效的软件. Google 出品,必属精品,出身名门的 Go 语言天生支持并发,近年甚为流行. 诞生背景 为了解决 Google 在软件开发中遇到的困难,三位创始人开发出了 Go 语言,主要是以下问题: 多核硬件架构 超大规模的分布式计算集群 Web 模式导致的前所未有的开发规模和更新速度 这些也是广大的互联网公司遇到的问题,越来越多的国内公司也开始广泛使用 Go 语言开发,比如熟悉的Docker 和 K8s 也是 Go 语言开发的,也是 Go 语言被称为云端开发语言的原因之一. 语言特点 Go 语言是云计算时代的 C 语言也称为21 世纪的 C 语言,由此可见,Go 的地位非同一般. Go 语言的诞生是为了提高生产效率,专门对多处理器系统应用程序的编程进行了优化,使用Go编译的程序可以媲美C或C++代码的速度,而且更加安全,支持并行进程. 运行效率高,开发高效,部署简单. 运行效率高是因为编译性语言与解释性语言相比,开发高效是语法简单,部署简单是直接部署编译后的程序. 语言层面支持并发,易于利用多核实现并发. 不同于 php,只需要 go 配合 channel 即可完成进程或线程所做的工作. 内置 runtime 并支持垃圾回收 类似 Java 虚拟机支持垃圾回收,不必手动进行内存管理. 简单易学,丰富的标准库,强大的网络库. 学习成本低,语法简单但表达能力强,支持函数式编程,面向对象编程等多种编程范式. 内置强大的工具(gofmt),跨平台编译,内嵌C支持. 不同的人有不同的代码风格,可转化统一风格 知名应用 Docker : 是为开发人员构建和运行应用程序而构建的平台 https://www.docker.com/ Kubernetes : 自动化容器部署,扩展,管理的应用程序 https://kubernetes.io/ Etcd : 分布式键值对存储系统 https://etcd.io/ 学习文档 https://golang.org/ : The Go Programming Language https://golang.google.cn/ : The Go Programming Language https://tour.go-zh.org/welcome/1 : Go 语言之旅 https://studygolang.com/ : Go语言中文网- Golang中文社区 https://www.runoob.com/go/go-tutorial.html : Go 语言教程| 菜鸟教程 https://snowdreams1006.github.io/go/ : Go 学习笔记 © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:09:30 "},"go/base/about.html":{"url":"go/base/about.html","title":"初识 go","keywords":"","body":"go 学习笔记 Go 是一种开源编程语言,可以轻松构建简单,可靠,高效的软件. 摘录自 github: https://github.com/golang/go,其中官网(国外): https://golang.org 和官网(国内): https://golang.google.cn/ Go 是 Google 公司推出的静态强类型,编译型,并发型,并具有垃圾回收功能的开源编程语言,最初由 Robert Griesemer , Rob Pike ,Ken Thompson 三人主持开发,后来陆续加入其他开发者,最终于 2009 年 11 月正式开源. 创始人都是大神 Go 的三位主要创始人分别是: 罗伯特·格瑞史莫(Robert Griesemer),和肯·汤普逊(Ken Thompson) 罗伯特·格瑞史莫(Robert Griesemer) JS V8 引擎,Chubby ,Java HotSpot 虚拟机,Sawzall 语言和 Strongtalk 系统 github: https://github.com/griesemer 罗勃·派克(Rob Pike) Plan 9 操作系统和UTF-8 编码 github: https://github.com/robpike 肯·汤普逊(Ken Thompson) UNIX 操作系统 ,Plan 9 操作系统,B 语言,UTF-8 编码 github: https://github.com/ken 如此厉害的三位大牛合作创作的 Go 语言还能差到哪里去呢? 吉祥物也很可爱 原来的 logo 是一只可爱的囊地鼠,英文名叫 gopher. 新的 logo 是现代化图标,代表更快更强,体现速度和效率. 网上流传甚广的一组很萌很可爱的吉祥物,并附上 github 链接: https://github.com/tenntenn/gopher-stickers 体验 go 语言魅力 案例一 fmt.Println(\"Hello, 世界\") : 输出字符串并换行 package main import \"fmt\" func main() { fmt.Println(\"Hello, 世界\") } 案例二 a, b = b, a : 互换变量a 和 b 的值,而其他语言一般都需要引入临时变量. package main import \"fmt\" func main() { var a = 3 var b = 4 fmt.Println(a, b) a, b = b, a fmt.Println(a, b) } 案例三 go + chan 关键字轻松完成并行计算 package main import ( \"fmt\" \"math\" ) func main() { fmt.Println(pi(5000)) } func pi(n int) float64 { ch := make(chan float64) for k := 0; k 到底好用不好用 Go 语言是云计算时代的 C 语言也称为21 世纪的 C 语言,由此可见,Go 的地位非同一般. Go 语言的诞生是为了提高生产效率,专门对多处理器系统应用程序的编程进行了优化,使用Go编译的程序可以媲美C或C++代码的速度,而且更加安全,支持并行进程. 运行效率高,开发高效,部署简单. 运行效率高是因为编译性语言与解释性语言相比,开发高效是语法简单,部署简单是直接部署编译后的程序. 语言层面支持并发,易于利用多核实现并发. 不同于 php,只需要 go 配合 channel 即可完成进程或线程所做的工作. 内置 runtime 并支持垃圾回收 类似 Java 虚拟机支持垃圾回收,不必手动进行内存管理. 简单易学,丰富的标准库,强大的网络库. 学习成本低,语法简单但表达能力强,支持函数式编程,面向对象编程等多种编程范式. 内置强大的工具(gofmt),跨平台编译,内嵌C支持. 不同的人有不同的代码风格,可转化统一风格 志同道合有几人 Docker : 是为开发人员构建和运行应用程序而构建的平台 https://www.docker.com/ Kubernetes : 自动化容器部署,扩展,管理的应用程序 https://kubernetes.io/ Etcd : 分布式键值对存储系统 https://etcd.io/ baidu-netdisk-downloaderx : 一款图形界面的百度网盘不限速下载器,支持 Windows , Linux 和 Mac https://github.com/b3log/baidu-netdisk-downloaderx pan-light : 百度网盘不限速客户端, golang + qt5, 跨平台图形界面 https://github.com/peterq/pan-light 自学技术哪家强 https://golang.org/ : The Go Programming Language https://golang.google.cn/ : The Go Programming Language https://tour.go-zh.org/welcome/1 : Go 语言之旅 https://studygolang.com/ : Go语言中文网- Golang中文社区 https://www.runoob.com/go/go-tutorial.html : Go 语言教程| 菜鸟教程 自问自答解疑惑 go 和 golang 是什么关系? go 是 golang 的简称,golang 是 go language 的缩写,即 go 语言. go 的常用 IDE 有哪些? 独立集成工具主要有 LiteIDE 和 GoLand 两种,但是常见 IDE 基本上均提供各种 Go 插件,支持 Windows ,MacOS 和 Linux 常见操作系统. go 的常见 Web 开发框架有哪些? Beego ,Iris 和 Gin 等,国人用的比较多是 Beego,目前资料也比较齐全. 无总结不成文章 Go 是 Google 出品的开源编程语言,出身名门注定不凡,并且拥有十分活跃的社区环境. 国内使用 Go 语言比较早的公司主要有七牛云和 beego,其中七牛云主要是云存储方面的业务,这也正是 Go 支持高并发分布式的特色,而 Beego 是 Go 的 Web 开发框架,支持 MVC 编程模型,不愧是国人开发深受国人喜爱. 同时,今年著名的 bilibili 源代码泄露事件也让我们看到了 Go 的身影,侧面说明了 Go 越来越流行,不愧是 21 世纪的 C 语言啊! 好了,暂时没有别的废话了,本文到此为止,下一章将开始介绍 Go 语言的环境搭建与 IDE 的基本配置,敬请期待. © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:09:30 "},"go/base/setup.html":{"url":"go/base/setup.html","title":"环境搭建","keywords":"","body":"环境搭建 千里之行始于足下,开始 Go 语言学习之旅前,首先要搭建好本地开发环境,然后就可以放心大胆瞎折腾了. Go 的环境安装和其他语言安装没什么特别注意之处,下载安装包下一步下一步直到完成,可能唯一需要注意的就是 $GOPATH 环境变量的设置问题. 不过,简单起见,目前采用默认配置,等到比较熟悉 Go 语言时候再自定义设置也不迟,因此采用喜闻乐见的傻瓜式安装方式吧! Go 下载安装 由于众所周知的原因,谷歌被墙,因此 Go 的国外官网无法访问,好在已提供 Go 的中国官网可以正常访问. 国外官网: https://golang.org/ 国内官网: https://golang.google.cn/ Go 语言中文网: https://studygolang.com/ 除了官网下载,也可以从第三方网站下载,一般速度比较快,下面就以 Windows 系统为例演示下载安装过程. 双击下载文件 go1.12.7.windows-amd64.msi 按照提示一直下一步直到安装完毕. 默认情况下 Go 安装到 C:\\Go 目录,同时设置了 $GOPATH 环境变量. 如果安装时没有采用默认配置而是自定义安装位置,那么应该自行设置后续相关环境变量. 默认情况下,安装 Go 语言后新增了 GOPATH 和 PATH 用户系统变量,同时修改了系统环境变量PATH . 默认情况下,Go 安装成功后发生了如下改变: 增加了用户变量: GOPATH=%USERPROFILE%\\go 增加了用户变量: PATH=%USERPROFILE%\\go\\bin 修改了系统变量: PATH=%PATH%;C:\\Go\\bin 其中 %USERPROFILE% 代表的是当前计算机登录用户的家目录,比如我的登录名是 snowdreams1006-win7,那么 %USERPROFILE% 表示的是 C 盘下的 Users 目录下的 snowdreams1006-win7 目录,即 %USERPROFILE%=C:\\Users\\snowdreams1006-win7 Go 测试验证 安装前命令行中输入 go 提示无命令,安装后再次输入 go 则能正确显示命令信息. 安装后必须新打开命令行窗口才能生效,go version 可以打印出 Go 的版本信息. $ go version go version go1.12.7 windows/amd64 上述命令行操作验证了 Go 语言本身已安装正确,其中 go env 展示了 Go 相关的环境变量,目前重要的变量有两个: GOROOT : Go 的安装目录,默认 GOROOT=c:\\go GOPATH : Go 的工作空间,默认 GOPATH=C:\\Users\\snowdreams1006-win7\\go,其中 C:\\Users\\snowdreams1006-win7 表示用户家目录. 上述两个变量都是安装后自动设置的,除非自定义安装位置才需要手动调整一系列的环境变量,作为初次接触 Go 语言,不建议瞎折腾,为时过早,目前只要记住这些目录的位置在哪就可以了. 任意目录下打开命令行窗口都能调用 Go 相关命令,比如 go version 和 go env ,这是因为安装 Go 时已自动追加了系统环境变量 PATH=%PATH%;C:\\Go\\bin,如果上述验证失败,记得修改环境变量 PATH 试试看! Mac 安装配置 点此下载并双击安装 go1.12.7.darwin-amd64.pkg ,同样傻瓜式下一步安装操作. snowdreams1006-mac11deMac:~ snowdreams1006$ go version go version go1.12.7 darwin/amd64 安装完毕后打开终端验证 Go 的基本命令是否正常,运行 go version 和 go env 查看基本配置信息. 运行命令的过程中可能会询问是否安装命令行开发者工具,按照提示默认安装即可. Go 安装小结 Go 语言安装来说比较简单,建议采用默认配置进行安装,这样不用操心各种环境变量的配置,否则一上来就暴露在 GOROOT,GOPATH 和 PATH 等诸多陌生概念之中,影响初学者搭建环境的信心,而且这些概念在刚开始并不会用到,未免有些操之过急. 所以,采用默认安装配置后,只要记住 Go 默认位置以及接下来在哪写 Go 程序的位置足矣! 测试是否安装成功,只需要在命令行窗口中运行 go version 能够输出 Go 的版本信息表示安装成功,否则可能安装失败. 查询 Go 的安装位置以及工作空间位置只需运行 go env 就能找到相应的 GOROOT 和 GOPATH 信息. 如果是 Windows 系统: Go 的默认安装位置(GOROOT): C:\\go Go 的默认工作空间(GOPATH): C:\\Users\\yourusername\\go 如果是 Mac 系统: Go 的默认安装位置(GOROOT): /usr/local/go Go 的默认工作空间(GOPATH): /Users/yourusername/go 搭建基本的语言环境是第一步,命令行操作虽然简单,不需要额外依赖,但实际工作中总不能一直在命令行中编写代码吧,还需要一个称心如意的 IDE 来辅助我们开发,下一节见! © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:09:31 "},"go/base/workspace.html":{"url":"go/base/workspace.html","title":"工作空间","keywords":"","body":"工作空间 搭建好 Go 的基本环境后,现在可以正式开始 Go 语言的学习之旅,初学时建议在默认的 GOPATH 工作空间规范编写代码,基本目录结构大概是这个样子. . |-- bin | `-- hello.exe |-- pkg | `-- windows_amd64 | `-- github.com | `-- snowdreams1006 | `-- learn-go | `-- strings.a `-- src `-- github.com `-- snowdreams1006 `-- learn-go |-- README.md |-- hello | `-- hello.go `-- strings |-- reverse.go `-- reverse_test.go Go 相关的代码保存在工作空间中,而工作空间目录默认情况下是 GOPATH 环境变量所指向的目录(例如: GOPATH=C:\\Users\\snowdreams006-win7\\go). 工作空间下一般应包括三个一级子目录,分别是 src,pkg 和 bin 目录,其中最重要的就是 src 源码目录,其余两个目录都是派生目录. src 目录是源代码目录,是平时写代码的地方. pkg 目录是包对象目录,里面的文件大多以 .a 为后缀名,Go 工具自动处理,暂时不用关心. bin 目录是可执行命令目录,是最终产出的文件,例如 Windows 平台一般会生成 .exe 文件. 如果你刚刚安装 Go 语言或者不是默认形式安装的 Go,打开命令行窗口运行 go env 查看 GOPATH 那一项,GOPATH 指代的目录就是工作空间. 体验别人的 Go 命令 默认情况下,刚刚安装完毕的 Go 环境并不会自动创建工作空间目录,可以手动创建也可以运行别人的 Go 命令时顺便创建. snowdreams1006-win7@WIN-FANS2DDDB06 MINGW64 / # 初始时工作空间并未创建 $ ls $GOPATH ls: cannot access 'C:\\Users\\snowdreams1006-win7\\go': No such file or directory snowdreams1006-win7@WIN-FANS2DDDB06 MINGW64 / # 下载别人的 `Go` 命令顺便创建了工作空间 $ go get github.com/snowdreams1006/learn-go/hello snowdreams1006-win7@WIN-FANS2DDDB06 MINGW64 / # 已经按照标准目录结构创建完毕,目前有src和bin目录就足够了 $ ls $GOPATH bin/ src/ 运行完 go get github.com/snowdreams1006/learn-go/hello 命令后,工作空间目前已近乎标准目录. 如果 go get 命令半天没什么反应,不要着急,说不定正在下载,只是比较慢而已,如果想要看到下载过程,可以添加额外参数: go get -u -v 可以看出,go get 命令下载了 hello 命令所依赖的文件并生成 bin/hello.exe 可执行文件,现在终于可以说一声 Hello world! snowdreams1006-win7@WIN-FANS2DDDB06 MINGW64 ~/go $ hello !oG,olleH 这个是啥?怎么不是传说中的 Hello World ? 不管怎么说,创建工作空间的目的已经达到了,不是吗? 聪明的你,或许已经发现输出的语句的确不是 Hello World 而是 Hello Go 反过来写! 打造自己的 Go 命令 如果手头上没有 Go 项目或者说想要从零开发 Go 项目的话,那么只能手动创建工作空间了. 我们已经知道了工作空间的规范,但是现在涉及到自定义项目,同样需要确定项目的规范. 一般说来,项目需要唯一id用于区分其他可能出现的同名项目,也就是命名空间的概念. 作为个人开源项目,同广大的 Go 项目规范一样托管到 github.com 网站,因此命名空间 github.com/user 作为基本路径. 重要区别: Go 的命名空间(即基本路径)是域名正写: github.com/snowdreams1006,如果是其他语言,命名空间可能就是域名反写形式: com.github.snowdreams1006 . snowdreams1006-win7@WIN-FANS2DDDB06 MINGW64 ~ # 当前并未创建过工作空间 $ ls $GOPATH ls: cannot access 'C:\\Users\\snowdreams1006-win7\\go': No such file or directory snowdreams1006-win7@WIN-FANS2DDDB06 MINGW64 ~ # 在工作空间下创建 `learn-go` 项目 $ mkdir -p $GOPATH/src/github.com/snowdreams1006/learn-go/hello snowdreams1006-win7@WIN-FANS2DDDB06 MINGW64 ~ # 在 `learn-go` 项目下创建 `hello.go` 文件 $ vim $GOPATH/src/github.com/snowdreams1006/learn-go/hello/hello.go snowdreams1006-win7@WIN-FANS2DDDB06 MINGW64 ~ # `hello.go` 文件内容,输出 `Hello world` $ cat $GOPATH/src/github.com/snowdreams1006/learn-go/hello/hello.go package main import \"fmt\" func main() { fmt.Printf(\"Hello, world.\\n\") } 现在 Go 语言版的 Hello World 已经编写完毕,接下来让我们编辑输出并安装 hello 文件命令吧! 在命令行窗口运行 go install github.com/snowdreams1006/learn-go/hello 命令会在工作空间的 bin 目录下生成 hello.exe 可执行文件. 现在命令行窗口不负众望,输出了期待已久的 Hello World! 简单讲解下 Go 命令 无论是下载别人的远程代码还是自己从零编写代码,在命令行中都用到了相应命令,go get 和 go install 等. go get : 下载远程代码包,如果本地已安装,则执行逻辑退化为 go install. go install : 安装本地依赖包,如果本地没有相关依赖,则报错提示无法找到相关依赖包. 当然 Go 支持的常用命令远远不止这两个,直接输入 go 自然会提示有哪些命令可供使用,如果想要查询具体某一个命令的帮助文档,则输入 go help 即可,例如 go help get 这些命令比较简单,不用翻译也能猜出八九不离十,故此不浪费时间一一阐述. 下面直接上代码亲测运行一遍相关命令. go get 下载代码包 go get github.com/golang/example/hello 下载 Go 官方示例的 hello 代码包. snowdreams1006-win7@WIN-FANS2DDDB06 MINGW64 ~/go $ tree . |-- bin | `-- hello.exe `-- src `-- github.com `-- snowdreams1006 `-- learn-go `-- hello `-- hello.go 6 directories, 2 files snowdreams1006-win7@WIN-FANS2DDDB06 MINGW64 ~/go $ go get github.com/golang/example/hello snowdreams1006-win7@WIN-FANS2DDDB06 MINGW64 ~/go $ tree . |-- bin | `-- hello.exe `-- src `-- github.com |-- golang | `-- example | |-- LICENSE | |-- README.md | |-- appengine-hello | | |-- README.md | | |-- app.go | | |-- app.yaml | | `-- static | | |-- favicon.ico | | |-- index.html | | |-- script.js | | `-- style.css | |-- gotypes | | |-- Makefile | | |-- README.md | | |-- defsuses | | | `-- main.go | | |-- doc | | | `-- main.go | | |-- go-types.md | | |-- hello | | | `-- hello.go | | |-- hugeparam | | | `-- main.go | | |-- implements | | | `-- main.go | | |-- lookup | | | `-- lookup.go | | |-- nilfunc | | | `-- main.go | | |-- pkginfo | | | `-- main.go | | |-- skeleton | | | `-- main.go | | |-- typeandvalue | | | `-- main.go | | `-- weave.go | |-- hello | | `-- hello.go | |-- outyet | | |-- Dockerfile | | |-- containers.yaml | | |-- main.go | | `-- main_test.go | |-- stringutil | | |-- reverse.go | | `-- reverse_test.go | `-- template | |-- image.tmpl | |-- index.tmpl | `-- main.go `-- snowdreams1006 `-- learn-go `-- hello `-- hello.go 25 directories, 35 files snowdreams1006-win7@WIN-FANS2DDDB06 MINGW64 ~/go $ 原本 $GOPATH/src 源码目录只有我们自己的 github.com/snowdreams1006/learn-go 项目,获取远程 golang/hello 代码包后多了一大堆源码文件. 真的是神奇的操作,其余语言调用开源工具一般都是作为依赖放到非源码目录,Go 直接放到正在编写的源码目录,看起来第三方源码像是我们自己写的一样,只不过用命名空间区分开了而已! go test 测试代码包 Go 自带轻量级的测试框架,测试文件命名是 xxx_test.go ,文件内的方法签名是 TestXXX . go test github.com/golang/example/stringutil 测试 stringutil 代码包,是因为包内存在测试文件 reverse_test.go snowdreams1006-win7@WIN-FANS2DDDB06 MINGW64 ~/go $ go test github.com/golang/example/stringutil ok github.com/golang/example/stringutil 0.342s go install 安装代码包 go install github.com/golang/example/hello 安装代码包,运行可执行文件 hello 输出 Hello, Go examples! snowdreams1006-win7@WIN-FANS2DDDB06 MINGW64 ~/go $ go install github.com/golang/example/hello snowdreams1006-win7@WIN-FANS2DDDB06 MINGW64 ~/go $ hello Hello, Go examples! go build 编译代码包 上述命令我们都是在 golang/example 项目下进行演示的,不能厚此薄彼,下面这两个命令还是演示我们自己手写的 hello 命令文件吧! 首先切换到 hello 目录下,这样省的输入一长串的路径,在当前目录下运行 go 命令可以省略文件路径. snowdreams1006-win7@WIN-FANS2DDDB06 MINGW64 ~/go # 切换到 `learn-go` 项目的 `hello` 目录 $ cd $GOPATH/src/github.com/snowdreams1006/learn-go/hello snowdreams1006-win7@WIN-FANS2DDDB06 MINGW64 ~/go/src/github.com/snowdreams1006/earn-go/hello $ ls hello.go snowdreams1006-win7@WIN-FANS2DDDB06 MINGW64 ~/go/src/github.com/snowdreams1006/earn-go/hello # go build 省略文件路径表示在当前目录下进行编译,输出文件也是当前目录下 $ go build snowdreams1006-win7@WIN-FANS2DDDB06 MINGW64 ~/go/src/github.com/snowdreams1006/earn-go/hello $ ls hello.exe* hello.go snowdreams1006-win7@WIN-FANS2DDDB06 MINGW64 ~/go/src/github.com/snowdreams1006/earn-go/hello # 此时直接运行 `hello` 命令,运行的的是 `$GOPATH/bin` 目录下的命令而不是当前目录下的 `hello` $ hello Hello, Go examples! snowdreams1006-win7@WIN-FANS2DDDB06 MINGW64 ~/go/src/github.com/snowdreams1006/learn-go/hello $ ./hello Hello, world. 上述演示结果,展示了切换到当前目录下可以直接省略代码包路径,默认输出的可执行文件也由原先的 $GOPATH/bin 目录变成当前目录. 直接运行 hello 命令输出的结果是原先的 $GOPATH/bin/hello 命令而不是当前目录下的 hello,至于为什么如此,暂时不太理解. 当然想要运行当前目录下的 hello.exe 命令文件也很简单,指定路径即可: ./hello go run 运行代码包 go build 命令或者 go install 命令都会生成可执行二进制文件,然后运行该二进制文件才能输出命令结果. go run 就是一步到位的命令,不用生成文件直接输出命令的执行结果,有时候这种方式也很有用! snowdreams1006-win7@WIN-FANS2DDDB06 MINGW64 ~/go/src/github.com/snowdreams1006/learn-go/hello $ ls hello.go snowdreams1006-win7@WIN-FANS2DDDB06 MINGW64 ~/go/src/github.com/snowdreams1006/learn-go/hello $ go run hello.go Hello, world. snowdreams1006-win7@WIN-FANS2DDDB06 MINGW64 ~/go/src/github.com/snowdreams1006/learn-go/hello $ ls hello.go 值得注意的是,go run 后面紧跟着的是文件名,不能像 go build 那样省略包路径,否则会报错. Go 基本环境小结 默认安装的 Go 省心省力,自动帮我们设置好相关的环境变量,至于这些变量是干嘛用的以及怎么自定义修改,建议初学时不要深究,先搭建好基本的开发环境再说. go env 命令可以输出 Go 的相关配置信息,GOROOT 是 Go 的安装目录,GOPATH 是 Go 的工作空间目录,这是 Go 本身最基本的配置信息. 如果不太了解这部分内容,可以翻看上一篇文章,这里就不详细阐述了. Go 的工作空间下面有 src ,bin 和 pkg 三个平行目录,其中 src 下面才是我们真正编写代码的目录. Go 语言相关的项目既然都放在 src 目录下就有一定的命名规范,参考 github.com/snowdreams1006/learn-go 和 github.com/golang/example 这种形式. 测试 Go 语言的安装环境以及进行简单的命令验证,这些操作本身比较简单,用不着强大的 IDE ,但是学习 Go 语言如果没有 IDE 的辅助,那么很难想象会是什么样的局面. 因此,下一节将开始介绍 Go 开发的 IDE 环境配置,感觉离工程化开发又迈进一步呢! © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:09:31 "},"go/base/ide.html":{"url":"go/base/ide.html","title":"IDE编辑器","keywords":"","body":"IDE编辑器 工欲善其事必先利其器,命令行工具虽然能够在一定程度上满足基本操作的需求,但实际工作中总不能一直使用命令行工具进行编码操作吧? 学习 Go 语言同样如此,为此需要寻找一个强大的 IDE 集成环境帮助我们快速开发,据我所知,市面上比较流行的可能有三个选择: LiteIDE X : LiteIDE 是一款简单,开源,跨平台的 Go IDE. GoLand : GoLand is a cross-platform IDE built specially for Go developers. 第三方插件 : Idea, Sublime Text,VS Code ,等常见 IDE 一般均有 Go 的插件. 萝卜青菜各有所爱,选择哪个 IDE 都可以,甚至不用任何 IDE 也可以,不过还是推荐下 GoLand 吧! Goland 下载安装 官网地址: https://www.jetbrains.com/go/,如果无法访问,可能需要特殊手段绕过. Goland 并不像 Idea 那样分为专业版和社区版,目前只有收费版,提供 30 天免费试用,试用到期后可以选择购买正版也可以上淘宝购买激活码或网上寻求破解版等等. 直接点击页面中间的 Download 按钮后就会自动识别当前系统进行下载,也可以点击右上角的 Download 按钮自行选择目标平台进行下载. 安装过程比较简单,这里就不再赘述,简单的动图一闪而过看下大致过程吧! 其中安装位置,默认是 C 盘,可以自行选择合适的安装位置. 如果是 Mac 电脑,安装 Goland 更为简单,直接下载拖动到 Application 分类,连安装目录都不用选择,简单演示如下: Goland 克隆项目 初次打开 Goland 编辑器,界面出现三个选项: New Project : 新建项目,适合从零开发新项目 Open Project : 打开项目,适合本地已存在 Go 项目 Check out from Version Control : 从版本库中检出项目,适合团队合作时直接从线上项目下载到本地. 三种方式分别对应三种不同的场景,这里选择以第三种方式检出版本库为例,目录源码: https://github.com/snowdreams1006/learn-go 选择 git 版本库,并填写项目地址,然后点击右侧的 Test 按钮,如果提示失败,可能是 Git 基本环境没有配置过,请先配置下 Git,可以参考 git 入门教程 项目地址: [email protected]:snowdreams1006/learn-go.git 或者 https://github.com/snowdreams1006/learn-go.git 或者 https://github.com/snowdreams1006/learn-go 默认情况下,本地目录是 GolandProjects,一定要修改成自己的 GOPATH 目录,即 USERPROFILE/go 目录. 耐心等待,Goland 会自动下载项目相关依赖,右下角的进度条完毕后意味着项目初始化好了,可以正常工作了. Goland 打开项目 找到 hello/hello.go 文件,其中 main 方法左侧有个绿色的启动按钮,点击运行. 初始运行,提示配置进行命令配置,设置工作目录为 GOPATH 环境变量所在的目录. 此时配置页面左下角的红色报错消失了,保存后关闭该窗口,再次运行 main 方法,如我们所愿输出了 Hello Go! 的逆序. 现在打开 strings/reverse_test.go 文件,同样点击左侧的启动按钮运行测试文件,证明测试运行正常! Goland 配置总结 万事开头难,下载 Goland 并初始化项目,其实很简单,之所以特意写下这篇文章主要是为了克服陌生的恐惧,迈出第一步就会有第二步,接下来的 Go 语言学习之旅就可以顺利开始了,Go to Work ! © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:09:31 "},"go/base/var-and-const.html":{"url":"go/base/var-and-const.html","title":"你好,Go","keywords":"","body":"变量和常量 首先希望学习 Go 语言的爱好者至少拥有其他语言的编程经验,如果是完全零基础的小白用户,本教程可能并不适合阅读或尝试阅读看看,系列笔记的目标是站在其他语言的角度学习新的语言,理解 Go 语言,进而写出真正的 Go 程序. 编程语言中一般都有变量和常量的概念,对于学习新语言也是一样,变量指的是不同编程语言的特殊之处,而常量就是编程语言的共同点. 学习 Go 语言时尽可能站在宏观角度上分析变量,而常量可能一笑而过或者编程语言不够丰富,所谓的常量其实也是变量,不管怎么样现在让我们开始 Go 语言的学习之旅吧,本教程涉及到的源码已托管于 github,如需获取源码,请直接访问 https://github.com/snowdreams1006/learn-go 编写第一个 Hello World 程序 学习编程语言的第一件事就是编写出 Hello World,现在让我们用 Go 语言开发出第一个可运行的命令行程序吧! 环境前提准备可以参考 走进Goland编辑器 新建 main 目录,并新建 hello_world.go 文件,其中文件类型选择 Simple Application ,编辑器会帮助我们创建 Go 程序骨架. 首先输入 fmt 后触发语法提示选择 fmt.Println ,然后会自动导入 fmt 包. 完整内容如下,仅供参考: package main import \"fmt\" func main() { fmt.Println(\"Hello World\") } 点击左侧绿色启动按钮,可以直接运行程序或者利用程序自带的 Terminal 终端选项卡运行程序,当然也可以用外部命令行工具运行程序. go run 命令直接运行,而 go build 命令产生可执行文件,两种方式都能如愿以偿输出 Hello World . 知识点归纳 Go 应用程序入口的有以下要求: 必须是 main 包 :package main 必须是 main 方法 : func main() 文件名任意不一定是 main.go,目录名也任意不一定是 main 目录. 以上规则可以很容易在编辑器中得到验证,任意一条不符合规则,程序都会报错提示,这也是使用编辑器而不是命令行进行学习的原因,能够帮助我们及时发现错误,方便随时验证猜想. 总结来说,main 包不一定在 main 目录下,main 方法可以在任意文件中. 这也意味着程序入口所在的目录不一定叫做 main 目录却一定要声明为 main 包,虽然不理解为什么这么设计,这一点至少和 Java 完全不一样,至少意味着 Go文件可以直接迁移目录而不需要语言层面的重构,可能有点方面,同时也有点疑惑?! main 函数值得注意的不同之处: main 函数不支持返回值,但可以通过 os.Exit 返回退出状态 main 函数,不支持返回值,若此时强行运行 main 方法,则会报错: func main must have no arguments and no return values main 函数可以借助 os.Exit(-1) 返回程序退出时状态码,外界可以根据不同状态码识别相应状态. main 函数不支持传入参数,但可以通过 os.Args 获取参数 在 Terminal 终端选项卡中运行 go run hello_world.go snowdreams1006 命令 os.Args 输出命令路径和参数值. 在测试用例中边学边练基础语法 The master has failed more times than the beginner has tried 计算机编程不是理科而是工科,动手亲自实践一遍才能更好地掌握知识技能,幸运的是,Go 语言本身内置提供了测试框架,不用加载第三方类库扩展,非常有利于学习练习. 刚刚接触 Go 语言,暂时不需要深入讲解如何编写规范的测试程序,毕竟基础语法还没开始正式练习呢! 但是,简单的规则还是要说的,总体来说,只有两条规则: 测试文件名以 _test 结尾 : XXX_test.go 命令习惯和不同, Java 中的文件名一般是大驼峰命名法,相应的测试文件是 XXXTest 测试方法名以 Test 开头 : TestXXX 命名习惯和其他编程语言不同,Java 中的测试方法命名是一般是小驼峰命名法,相应的测试方法是 testXXX 测试方法有着固定的参数 : t *testing.T 其他编程语言中一般没有参数,Java 中的测试方法一定没有参数,否则抛出异常 java.lang.Exception: Method testXXX should have no parameters 新建 Go 文件,类型选择 Empty File ,文件名命名为 hello_world_test ,编辑器新建一个空白的测试文件. 此时编写测试方法签名,利用编辑器自动提示功能输入 t.Log 随便输出些内容,这样就完成了第一个测试文件. 和 main 程序一样,测试方法也是可执行的,编辑器窗口的左侧也会出现绿色启动按钮,运行测试用例在编辑器下方的控制台窗口输出 PASS 证明测试逻辑正确! 测试文件源码示例: package main import \"testing\" func TestHelloWorld(t *testing.T){ t.Log(\"Hello Test\") } 现在已经学习了两种基本方式,一种是把程序写在 main 方法中,另一种是把程序写在测试方法中. 两种方式都可以随时测试验证我们的学习成果,如果写在 main 方法中,知识点一般要包装成单独的方法,然后再在 main 方法中运行该方法. 如果写在测试方法中,可以单独运行测试方法,而不必在 main 方法中一次性全部运行. 当然,这两种方式都可以,只不过个人倾向于测试用例方式. 实现 Fibonacci 数列 形如 1,1,2,3,5,8,13,... 形式的数列就是斐波那契数列,特点是从三个元素开始,下一个元素的值就是前两两个元素值的总和,子子孙孙无穷尽也! 记得学习初中历史时,关于昭君出塞的故事广为人知,王昭君的美貌不是此次讨论的重点,而此次关注点是放到了昭君的悲惨人生. 汉朝和匈奴和亲以换取边境和平,汉朝皇帝不愿意自己的亲闺女远嫁塞北,于是从后宫中挑选了一名普通宫女充当和亲对象,谁成想这名宫女竟长得如此美貌,\"沉鱼落雁闭月羞花\",堪称古代中国四大美女之一! 昭君担负着和亲重任,从此开始了远离他乡的悲惨生活,一路上,黄沙飞扬,燥热忧伤,情之所至,昭君拿出随性的琵琶,演奏出感人泪下的>! \"千载琵琶作胡语,分明怨恨曲中论\",可能情感过于哀伤,竟然连天上的大雁都忘记了飞翔,因此收获落雁之美! 老单于这个肥波纳了个如花似玉的妾,做梦都能了醒吧,遗憾的是,命不久矣! 如此一来,昭君却满心欢喜,异族老公死了,使命完成了,应该能回到朝思梦想的大汉朝故土了吧? 命运弄人,匈奴文化,父死子继,肥波已逝,但还有小肥波啊,放到汉朝伦理纲常来看,都不能叫做近亲结婚了简直是乱伦好吗! 小肥波+昭君=小小肥波 ,只要昭君不死,而昭君的现任老公不幸先死,那么小小肥波又会继续纳妾生娃,理论上真的是子子孙孙无穷尽也! 肥波纳妾故事可能长成这个样子: 肥波,昭君,小肥波 昭君的第一任老公: 肥波+昭君=小肥波,此时昭君刚生一个娃 肥波,小肥波,昭君,小小肥波 昭君的第二任老公: 小肥波+昭君=小小肥波,昭君的娃娶了自己的妈?难怪昭君苦楚悲惨,有苦难言,幸运的是,这次昭君没有生娃,两个女孩! 肥波,小肥波,小小肥波,昭君 昭君的第三任老公,小小肥波+昭君=小小小肥波 ,兄终弟及,还是乱伦好吗,这辈分我是算不出来了. 肥波纳妾系列,理论上和愚公移山有的一拼,生命不息,子承父业也好,兄终弟及也罢,数量越来越多,肚子越来越大. 以上故事,纯属虚构,昭君出塞是一件伟大的事情,换来了百年和平,值得尊敬. 回归正题,下面让我们用 Go 语言实现斐波那契数列吧! func TestFib(t *testing.T) { var a = 1 var b = 1 fmt.Print(a) for i := 0; i 上述简单示例,展示了变量的基本使用,简单总结如下: 变量声明关键字用 var ,类型名在前,变量类型在后,其中变量类型可以省略. // 声明变量a和变量b var a = 1 var b = 1 上述变量语法咋一看像是 Js 赋值,严格来说其实并不是那样,上面变量赋值形式只是下面这种的简化 // 声明变量a并指定类型为 int,同理声明变量b并指定类型为int var a int = 1 var b int = 1 第一种写法省略了 int 类型,由赋值 1 自动推断为 int 在一定程度上简化了书写,当然这种形式还可以继续简化. // 省略相同的 var,增加一对小括号 (),将变量放到小括号里面 var ( a = 1 b = 1 ) 可能问,还能不能继续简化下,毕竟其余语言的简化形式可不是那样的,答案是可以的! // 连续声明变量并赋值 var a, b = 1, 1 当然,其余语言也有类似的写法,这并不值得骄傲,下面这种形式才是 Go 语言特有的精简版形式. // 省略了关键字var,赋值符号=改成了:=,表示声明变量并赋值 a, b := 1, 1 就问你服不服?一个小小的变量赋值都能玩出五种花样,厉害了,我的 Go ! 变量类型可以省略,由编译器自动进行类型推断 类似 Js 的书写习惯,但本质上仍然是强类型,不会进行不同类型的自动转换,还会说像 Js 的变量吗? 同一个变量语句可以对不同变量进行同时赋值 仍然以斐波那契数列为例,Go 官网的示例中使用到的就是变量同时赋值的特性,完整代码如下: package main import \"fmt\" // fib returns a function that returns // successive Fibonacci numbers. func fib() func() int { a, b := 0, 1 return func() int { a, b = b, a+b return a } } func main() { f := fib() // Function calls are evaluated left-to-right. fmt.Println(f(), f(), f(), f(), f()) } 如果对该特性认识不够清晰,可能觉得这并不是什么大不了的事情嘛! 实际上,俗话说,没有对比就没有伤害,举一个简单的例子: 交换变量 func TestExchange(t *testing.T) { a, b := 1, 2 t.Log(a,b) a, b = b, a t.Log(a,b) temp := a a = b b = temp t.Log(a,b) } 其他语言中如果需要交换两个变量,一般都是引入第三个临时变量的写法,而 Go 实现变量交换则非常简单清晰,也符合人的思考而不是计算机的思考. 虽然不清楚底层会不会仍然是采用临时变量交换,但不管怎么说,使用该特性交换变量确实很方便! 同时对多个变量进行赋值是 Go 特有的语法,其他语言可以同时声明多个变量但不能同时赋值. 常量同样很有意思,也有关键字声明 const. 有些编程语言对常量和变量没有强制规定,常量可以逻辑上被视为不会修改的变量,一般用全大写字母提示用户是常量,为了防止常量被修改,有的编程语言可能会有额外的关键字进行约束. 幸运的是,Go 语言的常量提供了关键字 const 约束,并且禁止重复赋值,这一点很好,简单方便. func TestConst(t *testing.T) { const pi = 3.14 t.Log(pi) // cannot assign to pi pi = 2.828 t.Log(pi) } 除了语言层面的 const 常量关键字,Go 语言还要一个特殊的关键字 iota ,常常和常量一起搭配使用! 当设置一些连续常量值或者有一定规律的常量值时,iota 可以帮助我们快速设置. func TestConstForIota(t *testing.T) { const ( Mon = 1 + iota Tue Wed Thu Fri Sat Sun ) // 1 2 3 4 5 6 7 t.Log(Mon, Tue, Wed, Thu, Fri, Sat, Sun) } 大多数编程语言中,星期一代表的数字几乎都是 0,星期二是 1,以此类推,导致和传统认识上偏差,为了校准偏差,更加符合国人习惯,因此将星期一代表的数字 0 加一,以此类推,设置初始 iota 后就可以剩余星期应用该规律,依次 1,2,3,4,5,6,7 如果不使用 iota 的话,可能需要手动进行连续赋值,比较繁琐,引入了 iota 除了帮助快速设置,还可以进行比特位级别的操作. func TestConstForIota(t *testing.T) { const ( Readable = 1 第一位比特位为 1 时,表示文件可读,第二位比特位为 1 时,表示可写,第三位比特位为 1 时,表示可执行,相应的 10 进制数值依次为 1,2,4 也就是左移一位,左移两位,左移三位,数学上也可以记成 2^0,2^1,2^2 . 文件的可读,可写,可执行三种状态代表了文件的权限状态码,从而实现了文件的基本权限操作,常见的权限码有 755 和 644. 按位与 & 运算与编程语言无关,\"两位同时为 1 时,按位与的结果才为 1 ,否则结果为 0 \",因此给定权限码我们可以很方便判断该权限是否拥有可读,可写,可执行等权限. // 0111 即 7,表示可读,可写,可执行 accessCode := 7 t.Log(accessCode&Readable == Readable, accessCode&Writing == Writing, accessCode&Executable == Executable) // 0110 即 6,表示不可读,可写,可执行 accessCode = 6 t.Log(accessCode&Readable == Readable, accessCode&Writing == Writing, accessCode&Executable == Executable) // 0100 即 4,表示不可读,不可写,可执行 accessCode = 4 t.Log(accessCode&Readable == Readable, accessCode&Writing == Writing, accessCode&Executable == Executable) // 0000 即 0,表示不可读,不可写,不可执行 accessCode = 0 t.Log(accessCode&Readable == Readable, accessCode&Writing == Writing, accessCode&Executable == Executable) accessCode&Readable 表示目标权限码和可读权限码进行按位与运算,而可读权限码的二进制表示值为 0001 ,因此只要目标权限码的二进制表示值第一位是 1 ,按位与的结果肯定是 0001 ,而 0001 又刚好是可读权限码,所以 accessCode&Readable == Readable 为 true 就意味着目标权限码拥有可读权限. 如果目标权限码的二进制位第一个不是 1 而是 0,则 0&1=0 ,(0|1)^0=0,所以按位与运算结果肯定全是 0 即 0000,此时 0000 == 0001 比较值 false ,也就是该权限码不可读. 同理可自主分析,accessCode&Writing == Writing 结果 true 则意味着可写,否则不可写,accessCode&Executable == Executable 结果 true 意味着可执行,false 意味着不可执行. 熟悉了 iota 的数学计算和比特位计算后,我们趁热打铁,用文件大小单位继续练习! func TestConstForIota(t *testing.T) { const ( B = 1 字节 Byte 与 千字节 Kilobyte 之间的进制单位是 1024 ,也就是 2^10 ,刚好可以用 iota 左移 10 位来表示,一次只移动一次,直接乘以 10 就好了! 怎么样,iota 是不是很神奇?同时是不是和我一样也有点小困惑,iota 这货到底是啥? 百度翻译给我们的解释是,这货表示\"微量\",类似英语单词的 little 一样,a little 也表示\"一点点\". 但是 iota 除了表示一点点之外,好像还拥有自增的能力,这可不是 little 这种量词能够传达的意思. 因此,有可能 iota 并不是原始英语含义,说不定是希腊字母的语言,查询了标准的 24 个希腊字母表以及对应的英语注释. 大写 小写 英文读音 国际音标 意义 Α α alpha /ˈælfə/ 角度,系数,角加速度 Β β beta /'beitə/ 磁通系数,角度,系数 Γ γ gamma /'gæmə/ 电导系数,角度,比热容比 Δ δ delta /'deltə/ 变化量,屈光度,一元二次方 Ε ε epsilon /ep'silon/ 对数之基数,介电常数 Ζ ζ zeta /'zi:tə/ 系数,方位角,阻抗,相对粘度 Η η eta /'i:tə/ 迟滞系数,效率 Θ θ theta /'θi:tə/ 温度,角度 Ι ι ℩ iota /ai'oute/ 微小,一点 Κ κ kappa /'kæpə/ 介质常数,绝热指数 ∧ λ lambda /'læmdə/ 波长,体积,导热系数 Μ μ mu /mju:/ 磁导系数,微动摩擦系(因)数,流体动力粘度 Ν ν nu /nju:/ 磁阻系数,流体运动粘度,光子频率 Ξ ξ xi /ksi/ 随机数,(小)区间内的一个未知特定值 Ο ο omicron /oumaik'rən/ 高阶无穷小函数 ∏ π pi /pai/ 圆周率,π(n)表示不大于n的质数个数 Ρ ρ rho /rou/ 电阻系数,柱坐标和极坐标中的极径,密度 ∑ σ ς sigma /'sigmə/ 总和,表面密度,跨导,正应力 Τ τ tau /tau/ 时间常数,切应力 Υ υ upsilon /ju:p'silən/ 位移 Φ φ phi /fai/ 磁通,角,透镜焦度,热流量 Χ χ chi /kai/ 统计学中有卡方(χ2)分布 Ψ ψ psi /psai/ 角速,介质电通量 Ω ω omega /'oumigə/ 欧姆,角速度,交流电的电角度 希腊字母常常用于物理,化学,生物,科学等学科,作为常量或者变量,不同于一般的英语变量或常量的是,希腊字母表示的变量或常量一般具有特定的语义! 因此,iota 应该是希腊字母 I 的英语表示,该变量或者说常量表示微小,一点的含义. 翻译成自然语言就是,这个符号表示一点点,如果表达改变的含义,那就是在原来基础上多那么一点点,如果表达不改变的含义,应该是只有一点点,仅此而已. 当然,以上均是个人猜测,更加专业的说法还是应该看下 Go 语言如何定义 iota ,按住 Ctrl 键,鼠标悬停在 iota 上可以点击进入源码部分,如下: // iota is a predeclared identifier representing the untyped integer ordinal // number of the current const specification in a (usually parenthesized) // const declaration. It is zero-indexed. const iota = 0 // Untyped int. 简短翻译: iota 是预定义标识符,代表当前常量中无符号整型的序号,是以 0 作为索引的. 上述注释看起来晦涩难懂,如果是常量那就就安安静静当做常量,不行吗?怎么从常量定义中还读出了循环变量索引的味道? 为了验证猜想,仍然以最简单的星期转换为例,模拟每一步时的 iota 的值. const ( // iota = 0,Mon = 1 + 0 = 1,符合输出结果 1,此时 iota = 1,即 iota 自增1 Mon = 1 + iota // iota = 1,Tue = 1 + iota = 1 + 1 = 2,符合输出结果 2,此时 iota = 2 Tue // iota = 2,Wed = 1 + iota = 1 + 2 = 3,符合输出结果 3,此时 iota = 3 Wed Thu Fri Sat Sun ) // 1 2 3 4 5 6 7 t.Log(Mon, Tue, Wed, Thu, Fri, Sat, Sun) 上述猜想中将 iota 当做常量声明循环中的变量 i,每声明一次,i++,因此仅需要定义循环初始条件和循环自增变量即可完成循环赋值. const ( Mon = 1 + iota Tue Wed Thu Fri Sat Sun ) // 1 2 3 4 5 6 7 t.Log(Mon, Tue, Wed, Thu, Fri, Sat, Sun) var days [7]int for i := 0; i 这样对应是不是觉得 iota 似乎就是循环变量的 i,其中 Mon = 1 + iota 就是循环初始体,Mon~Sun 有限常量就是循环的终止条件,每一个常量就是下一次循环. 如果一个例子不足以验证该猜想的话,那就再来一个! const ( Readable = 1 上述两个例子已经初步验证 iota 可能和循环变量 i 具有一定的关联性,还可以进一步接近猜想. const ( // iota=0 const=1+0=1 iota=0+1=1 first = 1 + iota // iota=1 const=1+1=2 iota=1+1=2 second // iota=2 const=2+2=4 iota=2+1=3 third = 2 + iota // iota=3 const=2+3=5 iota=3+1=4 forth // iota=4 const=2*4=8 iota=4+1=5 fifth = 2 * iota // iota=5 const=2*5=10 iota=5+1=6 sixth // iota=6 const=6 iota=6+1=7 seventh = iota ) // 1 2 4 5 8 10 6 t.Log(first, second, third, forth, fifth, sixth, seventh) const currentIota = iota // 0 t.Log(currentIota) var rank [7]int for i := 0; i iota 是一组常量初始化中的循环变量索引,当这一组变量全部初始化完毕后,iota 重新开始计算,因此新的变量 currentIota 的值为 0 而不是 7 因此,iota 常常用作一组有规律常量的初始化背后的原因可能就是循环变量进行赋值,按照这个思路理解前面关于 iota 的例子暂时是没有任何问题的,至于这种理解是否准确,有待继续学习 Go 作进一步验证,一家之言,仅供参考! 变量和常量的基本小结 变量用 var 关键字声明,常量用 const 关键字声明. 变量声明并赋值的形式比较多,使用时最好统一一种形式,避免风格不统一. 变量类型具备自动推断能力,但本质上仍然是强类型,不同类型之间并不会自动转换. 一组规律的常量可以用 iota 常量进行简化,可以暂时理解为采用循环方式对变量进行赋值,从而转化成常量的初始化. 变量和常量都具有相似的初始化形式,与其他编程语言不同之处在于一条语句中可以对多个变量进行赋值,一定程度上简化了代码的书写规则. 任何定义但未使用的变量或常量都是不允许存在的,既然用不着,为何要声明?!禁止冗余的设计,好坏暂无法评定,既然如何设计,那就遵守吧! 与众不同的变量和常量 斐波那契数列是一组无穷的递增数列,形如 1,1,2,3,5,8,13... 这种从第三个数开始,后面的数总是前两个数之和的数列就是斐波那契数列. 如果从第三个数开始考虑,那么前两个数就是斐波那契数列的起始值,以后的数字都符合既定规律,取前两个数字当做变量 a,b 采用循环的方式不断向后推进数列得到指定长度的数列. func TestFib(t *testing.T) { var a int = 1 var b int = 1 fmt.Print(a) for i := 0; i 虽然上述解法比较清晰明了,但还不够简洁,至少没有用到 Go 语言的特性.实际上,我们还可以做得更好,或者说用 Go 语言的特性来实现更加清晰简单的解法: func TestFibSimplify(t *testing.T) { a, b := 0, 1 for i := 0; i 和第一种解法不同的是,这一次将变量 a 向前移一位,人为制造出虚拟头节点 0,变量 a 的下一个节点 b 指向斐波那契数列的第一个节点 1,随着 a 和 b 相继向后推进,下一个循环中的节点 b 直接符合规定,相比第一种解法缩短了一个节点. a, b := 0, 1 是循环开始前的初始值,b 是斐波那契数列中的第一个节点,循环进行过程中 a, b = b, a+b 语义非常清楚,节点的 a 变成节点 b,节点 b 是 a+b 的值. 是不是很神奇,这里既没有用到临时变量存储变量 a 的值,也没有发生变量覆盖的情况,直接完成了变量的交换赋值操作. 由此可见, a, b = b, a+b 并不是 a=b 和 b=a+b 的执行结果的累加,而是同时完成的,这一点有些神奇,不知道 Go 是如何实现多个变量同时赋值的操作? 如果有小伙伴知道其中奥妙,还望不吝赐教,大家一起学习进步! 如果你觉得上述操作有点不好理解,那么接下来的操作,相信你一定会很熟悉,那就是两个变量的值进行交换. func TestExchange(t *testing.T) { a, b := 1, 2 t.Log(a, b) a, b = b, a t.Log(a, b) temp := a a = b b = temp t.Log(a, b) } 同样的是,a, b = b, a 多变量同时赋值直接完成了变量的交换,其他编程语言实现类似需求一般都是采用临时变量提前存储变量 a 的值以防止变量覆盖,然而 Go 语言的实现方式竟然和普通人的思考方式一样,不得不说,这一点确实不错! 通过简单的斐波那契数列,引入了变量和常量的基本使用,以及 Go 的源码文件相应规范,希望能够带你入门 Go 语言的基础,了解和其它编程语言有什么不同以及这些不同之处对我们实际编码有什么便捷之处,如果能用熟悉的编程语言实现 Go 语言的设计思想也未曾不是一件有意思的事情. 下面,简单总结下本文涉及到的主要知识点,虽然是变量和常量,但重点并不在如何介绍定义上,而是侧重于特殊之处以及相应的实际应用. 源码文件所在的目录和源码文件的所在包没有必然联系,即 package main 所在的源码文件并不一定在 main 目录下,甚至都不一定有 main 目录. hello.go 源码文件位于 hello 目录下,而 hello_word.go 位于 main 目录下,但是他们所在的包都是 package main 源码文件命名暂时不知道有没有什么规则,但测试文件命名一定是 xxx_test,测试方法命名是 TestXXX ,其中 Go 天生支持测试框架,不用额外加载第三方类库. 声明变量的关键字是 var,声明常量的关键字是 const,无论是变量还是常量,均存在好几种声明方式,更是存在自动类型推断更可以进行简化. 一般而言,实现其它编程语言中的全局变量声明用 var,局部变量声明 := 简化形式,其中多个变量可以进行同时赋值. 一组特定规律的常量值可以巧用 iota 来实现,可以理解为首次使用 iota 的常量是这组常量规律的第一个,其余的常量按照该规律依次初始化. Go 语言没有枚举类,可以用一组常量值实现枚举,毕竟枚举也是特殊的常量. 本文源码已上传到 https://github.com/snowdreams1006/learn-go 项目,感兴趣的小伙伴可以点击查看,如果文章中有描述不当之处,恳请指出,谢谢你的评论和转发. © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:09:31 "},"go/base/grammar.html":{"url":"go/base/grammar.html","title":"基础语法","keywords":"","body":"基础语法 在上篇文章中,我们动手亲自编写了第一个 Go 语言版本的 Hello World,并且认识了 Go 语言中有意思的变量和不安分的常量. 相信通过上篇文章的斐波那契数列,你已经初步掌握了 Go 语言的变量和常量与其他主要的编程语言的异同,为了接下来更好的学习和掌握 Go 的基础语法,下面先简单回顾一下变量和常量相关知识. 有意思的变量和不安分的常量 变量默认初始化有零值 func TestVariableZeroValue(t *testing.T) { var a int var s string // 0 t.Log(a, s) // 0 \"\" t.Logf(\"%d %q\", a, s) } int 类型的变量初始化默认零值是零 0,string 类型的变量默认初始化零值是空字符串 ,其他类型也有相应的零值. 多个变量可以同时赋值 func TestVariableInitialValue(t *testing.T) { var a, b int = 1, 2 var s string = \"hello Go\" // 1 2 hello Go t.Log(a, b, s) } 其他主要的编程语言大多支持多个变量初始化,但极少数有像 Go 语言这样,不仅支持同时初始化,还可以同时赋值. 多个变量可以用小括号 () 统一定义 func TestVariableShorter(t *testing.T) { var ( a int = 1 b int = 2 s string = \"hello go\" ) // 1 2 hello Go t.Log(a, b, s) } 用小括号 () 方式,省略了相同的 var 关键字,看起来更加统一 变量类型可以被自动推断 func TestVariableTypeDeduction(t *testing.T) { var a, b, s = 1, 2, \"hello Go\" // 1 2 hello Go t.Log(a, b, s) } Go 语言可以根据变量值推测出变量类型,所以可以省略变量类型,再一次简化了变量定义,但是变量类型仍然是强类型,并不像 Js 那样的弱类型. 变量可以用 := 形式更加简化 func TestVariableTypeDeductionShorter(t *testing.T) { a, b, s := 1, 2, \"hello Go\" // 1 2 hello Go t.Log(a, b, s) s = \"hello golang\" // 1 2 hello golang t.Log(a, b, s) } 省略了关键字 var,转而使用 := 符号声明并初始化变量值且利用自动类型推断能力进一步就简化变量定义,再次赋值时不能再使用 := 符号. 变量 var 声明作用域大于变量 := 声明 var globalTestId = 2 // globalTestName := \"type_test\" is not supported var globalTestName = \"type_test\" func TestVariableScope(t *testing.T) { // 2 type_test t.Log(globalTestId, globalTestName) globalTestName = \"TestVariableScope\" // 2 TestVariableScope t.Log(globalTestId, globalTestName) } var 声明的变量可以作用于函数外或函数内,而 := 声明的变量只能作用于函数内,Go 并没有全局变量的概念,变量的作用范围只是针对包而言. 常量的使用方式和变量一致 func TestConstant(t *testing.T) { const a, b = 3, 4 const s = \"hello Go\" // 3 4 hello Go t.Log(a, b, s) } 常量声明关键字 const,常量和变量的使用方式一致,具备类型推断能力,也存在多种简化常量定义的形式. 虽然没有枚举类型,但可以用 iota 配合常量来实现枚举 func TestConstant2Enum(t *testing.T) { const ( java = iota golang cpp python javascript ) // 0 1 2 3 4 t.Log(java, golang,cpp,python,javascript) } iota 在一组常量定义中首次出现时,其值为 0,应用到下一个常量时,其值为开始自增 1,再次遇到iota 恢复 0 .效果非常像 for 循环中的循环索引 i,明明是常量,偏偏玩出了变量的味道,也是我觉得 iota 不安分的原因. 常量 iota 有妙用,还可以进行位运算 func TestConstantIotaBitCalculate(t *testing.T){ const ( Readable = 1 定义二进制位最低位为 1 时表示可读的,左移一位表示可写的,左移两位表示可执行的,按照按位与运算逻辑,目标权限位若拥有可读权限,此时和可读常量进行按位与运算之后的结果一定是可读的,由此可见,iota 非常适合此类操作. 总体来说,Go 语言中的变量很有意思,常量 iota 不那么安分,从上述归纳总结中不难看出,Go 语言和其他主流的编程语言还是有很大不同的,学习时要侧重于这些特殊之处. 如果想要回顾本节知识点,可以关注公众号[雪之梦技术驿站]找到go 学习笔记之有意思的变量和不安分的常量 这篇文章进行查看. 简洁的类型中格外关照了复数 在学习 Go 语言中的变量和常量时,虽然没有特意强调变量或常量的类型,但是大多数编程语言的类型基本都是差不多的,毕竟大家所处的现实世界是一样的嘛! 光是猜测是不够的,现在我们要梳理一遍 Go 语言的类型有哪些,和其他主流的编程语言相比有什么不同? Go 语言的变量类型大致可以分为以下几种: bool 布尔类型 bool,表示真假 true|false (u)int ,(u)int8 , (u)int16, (u)int32,(u)int64,uintptr int 类型表示整数,虽然不带位数并不表示没有位数,32 位操作系统时长度为 32 位,64 位操作系统时长度为 64 位.最后一个 uintptr 是指针类型. byte(uint8) ,rune(int32),string byte 是字节类型,也是 uint8 的别名,而 rune 是 Go 中的字符类型,也是 int32 的别名. float32 ,float64 ,complex64 ,complex128 只有 float 类型表示小数,没有 double 类型,类型越少对于开发者而言越简单,不是吗? complex64=float32+float32 是复数类型,没错!就是高中数学书本上的复数,3+4i 那种奇怪的数字! Go 的类型还是比较简单的,整数,小数,复数,字节,字符和布尔类型,相同种类的类型没有继续细分不同的名称而是直接根据类型长度进行命名的,这样是非常直观的,见名知意,根据数据大小直接选用类型,不费脑! 作为一种通用的编程语言,Go 内建类型中居然格外关照了复数这种数学概念类型,是一件有意思的事情,是不是意味着 Go 在工程化项目上做得更好?就像 Go 天生支持并发一样? 既然为数不多的类型中格外关照了复数类型,那我们简单使用下复数类型吧,毕竟其他类型和其他主流的编程语言相差不大. func TestComplex(t *testing.T) { c := 3 + 4i // 5 t.Log(cmplx.Abs(c)) } 生命苦短,直接利用变量类型推断简化变量声明,求出复数类型 c 的模(绝对值) 既然学习了复数,怎么能少得了欧拉公式,毕竟是\"世界上最美的公式\",刚好用到了复数的相关知识,那我们就简单验证一下吧! func TestEuler(t *testing.T) { // (0+1.2246467991473515e-16i) t.Log(cmplx.Pow(math.E, 1i*math.Pi) + 1) // (0+1.2246467991473515e-16i) t.Log(cmplx.Exp(1i*math.Pi) + 1) // (0.000+0.000i) t.Logf(\"%.3f\", cmplx.Exp(1i*math.Pi)+1) } 由于复数 complex 是使用 float 类型表示的,而 float 类型无论是什么编程语言都是不准确的,所以欧拉公式的计算结果非常非常接近于零,当只保留小数点后三位时,计算结果便是 (0.000+0.000i) ,复数的模也就是 0,至此验证了欧拉公式. 看过复数还是要研究类型特点 复数很重要,但其他类型也很重要,简单了解过复数的相关知识后,我们仍然要把注意力放到研究这些内建类型的特殊之处上或者说这些类型总体来说相对于其他主流的编程语言有什么异同. 只有显示类型转换,不存在隐式类型转换 func TestExplicitTypeConvert(t *testing.T) { var a, b int = 3, 4 var c int c = int(math.Sqrt(float64(a*a + b*b))) // 3 4 5 t.Log(a, b, c) } 已知勾股定理的两条直角边计算斜边,根据勾股定理得,直角边长度的平方和再开根号即斜边长度,然而 math.Sqrt 方法接收的 float64 类型,返回的也是 float64 类型,可实际值全是 int 类型,这种情况下并不会自动进行类型转换,只能进行强制类型转换才能得到我们的期望值,这就是显示类型转换. 别名类型和原类型也不能进行隐式类型转换 func TestImplicitTypeConvert2(t *testing.T) { type MyInt64 int64 var a int64 = 1 var b MyInt64 // b = a : cannot use a (type int64) as type MyInt64 in assignment b = MyInt64(a) t.Log(a, b) } MyInt64 是 int64 的别名,别名类型的 b 和原类型的 a 也不能进行也不能进行隐式类型转换,会报错 cannot use a (type int64) as type MyInt64 in assignment,只能进行显示类型转换. 支持指针类型,但不支持任何形式的计算 func TestPointer(t *testing.T) { var a int = 1 var pa *int = &a // 0xc0000921d0 1 1 t.Log(pa, *pa, a) *pa = 2 // 0xc0000901d0 2 2 t.Log(pa, *pa, a) } 同样的,指针类型也是其他编程语言反过来书写的,个人觉得这种反而不错,指向 int 类型的指针 *int,&a 是变量 a 的内存地址,所以变量 pa 存的就是变量 a 的地址,*pa 刚好也就是变量 a 的值. 上例显示声明了变量类型却没有利用到 Go 的类型推断能力,摆在那的能力却不利用简直是浪费,所以提供一种更简短的方式重写上述示例,并顺便解释后半句: \"指针类型不支持任何形式的计算\" func TestPointerShorter(t *testing.T) { a := 1 pa := &a // 0xc0000e6010 1 1 t.Log(pa, *pa, a) *pa = 2 // 0xc0000e6010 2 2 t.Log(pa, *pa, a) // pa = pa + 1 : invalid operation: pa + 1 (mismatched types *int and int) //pa = pa + 1 // *int int int t.Logf(\"%T %T %T\", pa, *pa,a) } 变量 pa 是指针类型,存储的是变量的内存地址,只可远观而不可亵玩,*pa 就是指针所指向的变量的值,可以进行修改,当然没问题就像可以重新赋值变量 a 一样,但是指针 pa 是不可以进行任何形式的运算的,pa = pa + 1 就会报错 invalid operation. 你猜运算符操作有没有彩蛋呢 变量和类型还只是孤立的声明语句,没有计算不成逻辑,并不是所有的程序都是预定义的变量,Go 的运算符是简单还是复杂呢,让我们亲自体验一下! 算术运算符少了 ++i 和 --i func TestArithmeticOperator(t *testing.T) { a := 0 // 0 t.Log(a) a = a + 1 // 1 t.Log(a) a = a * 2 // 2 t.Log(a) a = a % 2 // 0 t.Log(a) a++ // 1 t.Log(a) } 支持大部分正常的运算符,不支持前置自增,前置自减,这也是好事,再也不会弄错 i++ 和 ++i 的运算结果啦,因为根本不支持 ++i ! 比较运算符是否相等有花样 func TestComparisonOperator(t *testing.T) { a, b := 0, 1 t.Log(a, b) // false true true t.Log(a > b, a 大于,小于,不等于这种关系很正常,Golang 也没玩出新花样,和其他主流的编程语言逻辑一样,不用特别关心.但是关于比较数组 ==,Go 表示有话要说! Go 中的数组是可以进行比较的,当待比较的两个数组的维度和数组元素的个数相同时,两个数组元素顺序一致且相同时,则两个数组相等,而其他主流的编程语言一般而言比较的都是数组的引用,所以这一点需要特别注意. func TestCompareArray(t *testing.T) { a := [...]int{1, 2, 3} //b := [...]int{2, 4} c := [...]int{1, 2, 3} d := [...]int{1, 2, 4} // a == b --> invalid operation: a == b (mismatched types [3]int and [2]int) //t.Log(a == b) // true false t.Log(a == c,a == d) } 数组 a 和 c 均是一维数组且元素个数都是 3,因此两个数组可以比较且相等,若数组a 和 b 进行比较,则报错 invalid operation,是因为两个数组的元素个数不相同,无法比较! 逻辑运算符老实本分无异常 func TestLogicalOperator(t *testing.T) { a, b := true, false t.Log(a, b) // false true false true t.Log(a&&b,a||b,!a,!b) } 位运算符新增按位清零 &^ 很巧妙 Go 语言中定义按位清零运算符是 &^,计算规律如下: 当右边操作位数为 1 时,左边操作为不论是 1 还是 0 ,结果均为 0; 当右边操作位数为 0 时,结果同左边操作位数. func TestClearZeroOperator(t *testing.T) { // 0 0 1 0 t.Log(1&^1, 0&^1, 1&^0, 0&^1) } 不知道还记不记得,在介绍常量 iota 时,曾经以文件权限为例,判断给定的权限码是否拥有特定权限,同样是给定的权限码,又该如何撤销特定权限呢? func TestClearZeroOperator(t *testing.T) { const ( Readable = 1 accessCode = accessCode &^ Readable 进行按位清零操作后就失去了可读权限,accessCode&Readable == Readable 再次判断时就没有可读权限了. 流程控制语句也有自己的傲娇 if 有话要说 有了变量类型和各种运算符的加入,现在实现简单的语句已经不是问题了,如果再辅助流程控制语句,那么实现较为复杂拥有一定逻辑的语句便可更上一层楼. Go 语言的 if 条件语句和其他主流的编程语言的语义是一样的,不一样的是书写规则和一些细节上有着自己特点. 条件表达式不需要小括号 () func TestIfCondition(t *testing.T) { for i := 0; i Go 语言的各种省略形式使得整体上非常简洁,但也让拥有其他主流编程语言的开发者初次接触时很不习惯,语句结束不用分号 ;,条件表达式不用小括号 () 等等细节,如果不用 IDE 的自动提示功能,这些细节肯定要耗费不少时间. 条件表达式中可以定义变量,只要最后的表达式结果是布尔类型即可 func TestIfConditionMultiReturnValue(t *testing.T) { const filename = \"test.txt\" if content, err := ioutil.ReadFile(filename); err != nil { t.Log(err) } else { t.Logf(\"%s\\n\", content) } } Go 语言的函数支持返回多个值,这一点稍后再细说,ioutil.ReadFile 函数返回文件内容和错误信息,当存在错误信息时 err != nil,输出错误信息,否则输出文件内容. 条件表达式中定义的变量作用域仅限于当前语句块 如果尝试在 if 语句块外访问变量 content,则报错 undefined: content switch 不甘示弱 同其他主流的编程语言相比,switch 语句最大的特点就是多个 case 不需要 break,Go 会自动进行 break,这一点很人性化. switch 会自动 break,除非使用 fallthrough func TestSwitchCondition(t *testing.T) { switch os := runtime.GOOS; os { case \"darwin\": t.Log(\"Mac\") case \"linux\": t.Log(\"Linux\") case \"windows\": t.Log(\"Windows\") default: t.Log(os) } } 条件表达式不限制为常量或整数 其他主流的编程语言中 switch 的条件表达式仅支持有限类型,使用方式存在一定局限性,Go 语言则不同,这一点变化也是很有意思的,使用 switch 做分支控制时不用担心变量类型了! case 语言支持多种条件,用逗号 , 分开,逻辑或 func TestSwitchMultiCase(t *testing.T) { for i := 0; i 省略 switch 的条件表达式时,switch 的逻辑和多个 if else 逻辑相同 func TestSwitchCaseCondition(t *testing.T) { for i := 0; i for 姗姗来迟 最后登场的是 for 循环,一个人完成了其他主流编程语言三个人的工作,Go 语言中既没有 while 循环也,也没有 do while 循环,有的只是 for 循环. 循环条件不需要小括号 () func TestForLoop(t *testing.T) { sum := 0 for i := 1; i 再一次看到条件表达式不需要小括号 () 应该不会惊讶了吧? if 的条件语句表达式也是类似的,目前为止,接触到明确需要小括号的 () 也只有变量或常量定义时省略形式了. 可以省略初始条件 func convert2Binary(n int) string { result := \"\" for ; n > 0; n /= 2 { lsb := n % 2 result = strconv.Itoa(lsb) + result } return result } func TestConvert2Binary(t *testing.T) { // 1 100 101 1101 t.Log( convert2Binary(1), convert2Binary(4), convert2Binary(5), convert2Binary(13), ) } 利用整数相除法,不断取余相除,得到给定整数的二进制字符串,这里就省略了初始条件,只有结束条件和递增表达式.这种写法同样在其他主流的编程语言是没有的,体现了 Go 设计的简洁性,这种特性在以后的编程中会越来越多的用到,既然可以省略初始条件,相信你也能猜到可不可以省略其他两个条件呢? 可以省略初始条件和递增表达式 func printFile(filename string) { if file, err := os.Open(filename); err != nil { panic(err) } else { scanner := bufio.NewScanner(file) for scanner.Scan() { fmt.Println(scanner.Text()) } } } func TestPrintFile(t *testing.T) { const filename = \"test.txt\" printFile(filename) } 打开文件并逐行读取内容,其中 scanner.Scan() 的返回值类型是 bool,这里省略了循环的初始条件和递增表达式,只有循环的终止条件,也顺便实现了 while 循环的效果. 初始条件,终止条件和递增表达式可以全部省略 func forever() { for { fmt.Println(\"hello go\") } } func TestForever(t *testing.T) { forever() } for 循环中没有任何表达式,意味着这是一个死循环,常用于 Web 请求中监控服务端口,是不是比 while(true) 要更加简单? 压轴的一等公民函数隆重登场 虽然没有特意强制函数,但是示例代码中全部都是以函数形式给出的,函数是封装的一种形式,更是 Go 语言的一等公民. 返回值在函数声明的最后,多个返回值时用小括号 () func eval(a, b int, op string) int { var result int switch op { case \"+\": result = a + b case \"-\": result = a - b case \"*\": result = a * b case \"/\": result = a / b default: panic(\"unsupported operator: \" + op) } return result } func TestEval(t *testing.T) { t.Log( eval(1, 2, \"+\"), eval(1, 2, \"-\"), eval(1, 2, \"*\"), eval(1, 2, \"/\"), //eval(1, 2, \"%\"), ) } 不论是变量的定义还是函数的定义,Go 总是和其他主流的编程语言相反,个人觉得挺符合思维顺序,毕竟都是先有输入才能输出,多个输出当然要统一隔离在一块了. 可以有零个或一个或多个返回值 func divide(a, b int) (int, int) { return a / b, a % b } func TestDivide(t *testing.T) { // 2 1 t.Log(divide(5, 2)) } 小学时就知道两个整数相除,除不尽的情况下还有余数.只不过编程中商和余数都是分别计算的,Go 语言支持返回多个结果,终于可以实现小学除法了! 返回多个结果时可以给返回值起名字 func divideReturnName(a, b int) (q, r int) { return a / b, a % b } func TestDivideReturnName(t *testing.T) { q, r := divideReturnName(5, 2) // 2 1 t.Log(q, r) } 还是整数除法的示例,只不过给返回值起了变量名称 (q, r int),但这并不影响调用者,某些 IDE 可能会基于次特性自动进行代码补全,调用者接收时的变量名不一定非要是 q,r . 其他函数可以作为当前函数的参数 func apply(op func(int, int) int, a, b int) int { p := reflect.ValueOf(op).Pointer() opName := runtime.FuncForPC(p).Name() fmt.Printf(\"Calling function %s with args (%d,%d)\\n\", opName, a, b) return op(a, b) } func pow(a, b int) int { return int(math.Pow(float64(a), float64(b))) } func TestApply(t *testing.T) { // 1 t.Log(apply(func(a int, b int) int { return a % b }, 5, 2)) // 25 t.Log(apply(pow, 5, 2)) } apply 函数的第一个参数是 op 函数,第二,第三个参数是 int 类型的 a,b.其中 op 函数也接收两个 int 参数,返回一个 int 结果,因此 apply 函数的功能就是将 a,b 参数传递给 op 函数去执行,这种方式比 switch 固定运算类型要灵活方便! 没有默认参数,可选参数等复杂概念,只有可变参数列表 func sum(numbers ...int) int { result := 0 for i := range numbers { result += numbers[i] } return result } func TestSum(t *testing.T) { // 15 t.Log(sum(1, 2, 3, 4, 5)) } range 遍历方式后续再说,这里可以简单理解为其他主流编程语言中的 foreach 循环,一般包括当前循环索引和循环项. 指针类型很方便同时也很简单 Go 的语言整体上比较简单,没有太多花里胡哨的语法,稍微有点特殊的当属变量的定义方式了,由于具备类型推断能力,定义变量的方式有点多,反而觉得选择困难症,不知道这种情况后续会不会有所改变? 在 Go 语言的为数不多的类型中就有指针类型,指针本来是 c 语言的概念,其他主流的编程语言也有类似的概念,可能不叫做指针而是引用,但 Go 语言的发展和 c++ 有一定关系,保留了指针的概念. 但是这并不意味着 Go 语言的指针像 C 语言那样复杂,相反,Go 语言的指针很方便也很简单,方便是由于提供我们操作内存地址的方式,简单是因为不能对指针做任何运算! 简单回忆一下指针的基本使用方法: func TestPointerShorter(t *testing.T) { a := 1 pa := &a // 0xc0000e6010 1 1 t.Log(pa, *pa, a) *pa = 2 // 0xc0000e6010 2 2 t.Log(pa, *pa, a) // pa = pa + 1 : invalid operation: pa + 1 (mismatched types *int and int) //pa = pa + 1 // *int int int t.Logf(\"%T %T %T\", pa, *pa,a) } & 可以获取变量的指针类型,* 指向变量,但不可以对指针进行运算,所以指针很简单! 当指针类型和其他类型和函数一起发生化学反应时,我们可能更加关心参数传递问题,其他主流的编程语言可能有值传递和引用传递两种方式,Go 语言进行参数传递时又是如何表现的呢? func swapByVal(a, b int) { a, b = b, a } func TestSwapByVal(t *testing.T) { a, b := 3, 4 swapByVal(a, b) // 3 4 t.Log(a, b) } swapByVal 函数内部实现了变量交换的逻辑,但外部函数 TestSwapByVal 调用后变量 a,b 并没有改变,可见 Go 语言这种参数传递是值传递而不是引用传递. 上面示例中参数传递的类型都是普通类型,如果参数是指针类型的话,结果会不会不一样呢? func swapByRef(a, b *int) { *a, *b = *b, *a } func TestSwapByRef(t *testing.T) { a, b := 3, 4 swapByRef(&a, &b) // 4 3 t.Log(a, b) } 指针类型进行参数传递时可以交换变量的值,拷贝的是内存地址,更改内存地址的指向实现了原始变量的交换,参数传递的仍然是值类型. 实际上,Go 语言进行参数传递的只有值类型一种,这一点不像其他主流的编程语言那样可能既存在值类型又存在引用类型. 既然是值类型进行参数传递,也就意味着参数传递时直接拷贝一份变量供函数调用,函数内部如何修改参数并不会影响到调用者的原始数据. 如果只是简单类型并且不希望参数值被修改,那最好不过,如果希望参数值被修改呢?那只能像上例那样传递指针类型. 简单类型不论是传递普通类型还是指针类型,变量的拷贝过程不会太耗费内存也不会影响状态. 如果传递的参数本身是比较复杂的类型,仍然进行变量拷贝过程估计就不能满足特定需求了,可能会设计成出传递复杂对象的某种内部指针,不然真的要进行值传递,那还怎么玩? Go 只有值传递一种方式,虽然简单,但实际中如何使用应该有特殊技巧,以后再具体分析,现在回到交换变量的例子,换一种思路. func swap(a, b int) (int, int) { return b, a } func TestSwap(t *testing.T) { a, b := 3, 4 a, b = swap(a, b) // 4 3 t.Log(a, b) } 利用 Go 函数可以返回多个值特性,返回交换后的变量值,调用者接收时相当于重新赋值,比传递指针类型要简单不少! 基础语法知识总结和下文预告 刚刚接触 Go 语言时觉得 Go 的语言很简单也很特别,和其他主流的编程语言相比,有着自己独特的想法. 语句结束不用分号 ; 而是直接回车换行,这一点有些不习惯,好在强大的 IDE 可以纠正这些细节. 变量声明时变量名在前,变量类型在后,可能更加符合大脑思维,但是习惯了先写变量类型再写变量名,这确实有一定程度的不方便,后来索性不写变量类型,自然就没有问题了. 函数声明同变量声明类似,返回值放到了最后部分,并且还可以有多个返回值,经过了变量的洗礼,再熟悉函数的这一特点也就不那么惊讶了,先输入后输出,想一想也有道理,难道其他编程语言的顺序都是错的? 接下来就是语法的细节,比如 if 的条件表达式可以进行变量赋值,switch 表达式可以不用 break,只有 for 循环一种形式等等. 这些细节总体来说比较简单方便,不用关心细节,放心大胆使用,从而专注于业务逻辑,等到语法不对时,IDE 自然会给出相应的报错提醒,放心大胆 Go ! 本文主要介绍了 Go 的基本语法以及和其他主流的编程语言的异同,你 Get 到了吗? 下文将开始介绍 Go 的内建容器类型,数组,切片,Map 来一遍! 欢迎大家一起学习交流,如有不当之处,恳请指正,如需完整源码,请在公众号[雪之梦技术驿站]留言回复,感谢你的评论与转发! © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:09:31 "},"go/container/about.html":{"url":"go/container/about.html","title":"内建容器","keywords":"","body":"内建容器 上篇文章中详细介绍了 Go 的基础语言,指出了 Go 和其他主流的编程语言的差异性,比较侧重于语法细节,相信只要稍加记忆就能轻松从已有的编程语言切换到 Go 语言的编程习惯中,尽管这种切换可能并不是特别顺畅,但多加练习尤其是多多试错,总是可以慢慢感受 Go 语言之美! 在学习 Go 的内建容器前,同样的,我们先简单回顾一下 Go 的基本语言,温度而知新可以为师矣! 上节知识回顾 如需了解详情,请于微信公众号[雪之梦技术驿站]内查看 go 学习笔记之值得特别关注的基础语法有哪些 文章,觉得有用的话,顺手转发一下呗! 内建类型种类 bool 布尔类型,可选 true|false,默认初始化零值 false . (u)int ,(u)int8 , (u)int16, (u)int32,(u)int64,uintptr 2^0=1,2^1=2 ,2^2=4 个字节长度的整型,包括有符号整型和无符号整型以及 uintptr 类型的指针类型,默认初始化零值 0 . byte(uint8) ,rune(int32),string byte 是最基础字节类型,是 uint8 类型的别名,而 rune 是 Go 中的字符类型,是 int32 的别名.最常用的字符串类型 string 应该不用介绍了吧? float32 ,float64 ,complex64 ,complex128 只有 float 类型的浮点型,没有 double 类型,同样是以字节长度来区分,complex64 是复数类型,实部和虚部由 float32 类型复合而成,因此写作 complex64 这种形式. 内建类型特点 类型转换只有显示转换,不存在任何形式的隐式类型转换 不同变量类型之间不会自动进行隐式类型转换,Go 语言的类型转换只有强制的,只能显示转换. 虽然提供指针类型,但指针本身不能进行任何形式的计算. 指针类型的变量不能进行计算,但是可以重新改变内存地址的指向. 变量声明后有默认初始化零值,变量零值视具体类型而定 int 类型的变量的初始化零值是 0,string 类型的初始化零值是空字符串,并不是 nil 基本运算符 算术运算符没有 ++i 和--i 只有 i++ 和 i-- 这种自增操作,再也不用担心两种方式的差异性了! 比较运算符 == 可以比较数组是否相等 当两个数组的维度和数组长度相等时,两个数组可以进行比较,顺序完全一致时,结果为 true,其他情况则是 false . 位运算符新增按位清零运算符 &^ 其他主流的编程语言虽然没有这种操作符,通过组合命令也可以实现类似功能,但既然提供了按位清零运算符,再也不用自己进行组合使用了! 流程控制语句 if 条件表达式不需要小括号并支持变量赋值操作 先定义临时变量并根据该变量进行逻辑判断,然后按照不同情况进行分类处理,Go 处理这种临时变量的情况,直接对条件表达式进行增强,这种情况以后会很常见! if 条件表达式内定义的变量作用域仅限于当前语句块 条件表达式内定义的变量是为了方便处理不同分支的逻辑,既然是临时变量,出了当前的 if 语句块就无法使用,也变得可以理解. switch 语句可以没有 break,除非使用了 fallthrough switch 语句的多个 case 结尾处可以没有 break,系统会自动进行 break 处理. switch 条件表达式不限制为常数或整数 和其他主流的编程语言相比,Go 语言的 switch 条件表达式更加强大,类型也较为宽松. switch 条件表达式可以省略,分支逻辑转向 case 语言实现. 省略 switch 条件表达式,多个 case 语言进行分支流程控制,功能效果和多重 if else 一样. 省略 switch 条件表达式后,每个 case 条件可以有多个条件,用逗号分隔. swicth 语句本质上是根据不同条件进行相应的流程控制,每个 case 的条件表达式支持多个,更是增强了流程控制的能力. for 循环的条件表达式也不需要小括号,且没有其他形式的循环. Go 语言只有 for 循环,没有 while 等其他形式的循环. for 循环的初始条件,终止条件和自增表达式都可以省略或者同时省略 条件表达式进行省略后可以实现 while 循环的效果,全部省略则是死循环. 函数和参数传递 函数声明按照函数名,入参,出参顺序定义,并支持多返回值 不论是变量定义还是函数定义,Go 总是和其他主流的编程语言反着来,如果按照输入输出的顺序思考就会发现,这种定义方式其实挺有道理的. 函数有多个返回值时可以给返回值命名,但对调用者而言没有差别 函数返回多个值时可以有变量名,见名知意方便调用者快速熟悉函数声明,但调用者并非一定要按照返回值名称接收调用结果. 函数的入参没有必填参数,可选参数等复杂概念,只支持可变参数列表 可变参数列表和其他主流的编程语言一样,必须是入参的最后一个. 函数参数传递只有值传递,没有引用传递,即全部需要重新拷贝变量 参数传递只有值传递,逻辑上更加简单,但是处理复杂情况时可以传递指针实现引用传递的效果. 内建容器有哪些 复习了 Go 语言的基础语法后,开始继续学习变量类型的承载者也就是容器的相关知识. 承载一类变量最基础的底层容器就是数组了,大多数高级的容器底层都可以依靠数组进行封装,所以先来了解一下 Go 的数组有何不同? 数组和切片 数组的声明和初始化 数组的明显特点就是一组特定长度的连续存储空间,声明数组时必须指定数组的长度,声明的同时可以进行初始化,当然不指定数组长度时也可以使用 ... 语法让编译器帮我们确定数组的长度. func TestArray(t *testing.T) { var arr1 [3]int arr2 := [5]int{1, 2, 3, 4, 5} arr3 := [...]int{2, 4, 6, 8, 10} // [0 0 0] [1 2 3 4 5] [2 4 6 8 10] t.Log(arr1, arr2, arr3) var grid [3][4]int // [[0 0 0 0] [0 0 0 0] [0 0 0 0]] t.Log(grid) } [3]int 指定数组长度为 3,元素类型为 int,当然也可以声明时直接赋值 [5]int{1, 2, 3, 4, 5} ,如果懒得指定数组长度,可以用 [...]int{2, 4, 6, 8, 10} 表示. 数组的遍历和元素访问 最常见的 for 循环进行遍历就是根据数组的索引进行访问,range arr 方式提供了简化遍历的便捷方法. func TestArrayTraverse(t *testing.T) { arr := [...]int{2, 4, 6, 8, 10} for i := 0; i range arr 可以返回索引值和索引项,如果仅仅关心索引项而不在乎索引值的话,可以使用 _ 占位符表示忽略索引值,如果只关心索引值,那么可以不写索引项.这种处理逻辑也就是函数的多返回值顺序接收,不可以出现未使用的变量. 数组是值类型可以进行比较 数组是值类型,这一点和其他主流的编程语言有所不同,因此相同纬度且相同元素个数的数组可以比较,关于这方面的内容前面也已经强调过,这里再次简单回顾一下. func printArray(arr [5]int) { arr[0] = 666 for i, v := range arr { fmt.Println(i, v) } } func TestPrintArray(t *testing.T) { var arr1 [3]int arr2 := [5]int{1, 2, 3, 4, 5} arr3 := [...]int{2, 4, 6, 8, 10} // [0 0 0] [1 2 3 4 5] [2 4 6 8 10] t.Log(arr1, arr2, arr3) // cannot use arr1 (type [3]int) as type [5]int in argument to printArray //printArray(arr1) fmt.Println(\"printArray(arr2)\") printArray(arr2) fmt.Println(\"printArray(arr3)\") printArray(arr3) // [1 2 3 4 5] [2 4 6 8 10] t.Log(arr2, arr3) } 因为参数传递是值传递,所以 printArray 函数无法更改调用者传递的外部函数值,如果想要在函数 printArray 内部更改传递过来的数组内容,可以通过指针来实现,但是有没有更简单的做法? 想要在 printArrayByPointer 函数内部修改参数数组,可以通过数组指针的方式,如果有不熟悉的地方,可以翻看上一篇文章回顾查看. func printArrayByPointer(arr *[5]int) { arr[0] = 666 for i, v := range arr { fmt.Println(i, v) } } func TestPrintArrayByPointer(t *testing.T) { var arr1 [3]int arr2 := [5]int{1, 2, 3, 4, 5} arr3 := [...]int{2, 4, 6, 8, 10} // [0 0 0] [1 2 3 4 5] [2 4 6 8 10] t.Log(arr1, arr2, arr3) fmt.Println(\"printArrayByPointer(arr2)\") printArrayByPointer(&arr2) fmt.Println(\"printArrayByPointer(arr3)\") printArrayByPointer(&arr3) // [666 2 3 4 5] [666 4 6 8 10] t.Log(arr2, arr3) } 修改数组的元素可以通过传递数组指针来实现,除此之外,Go 语言中数组还有一个近亲 slice,也就是切片,它可以实现类似的效果. 切片的声明和初始化 切片和数组非常类似,创建数组时如果没有指定数组的长度,那么最终创建的其实是切片并不是数组. func TestSliceInit(t *testing.T) { var s1 [5]int // [0 0 0 0 0] t.Log(s1) var s2 []int // [] t.Log(s2,len(s2)) } []int 没有指定长度,此时创建的是切片,默认初始化零值是 nil,并不是空数组! 同理,数组可以声明并初始化,切片也可以,并且语法也很类似,稍不注意还以为是数组呢! func TestSliceInitValue(t *testing.T) { var s1 = [5]int{1, 3, 5, 7, 9} // [1 3 5 7 9] t.Log(s1) var s2 = []int{1, 3, 5, 7, 9} // [1 3 5 7 9] t.Log(s2) } 仅仅是没有指定 [] 中的长度,最终创建的结果就变成了切片,真的让人眼花缭乱! 数组和切片如此相像,让人不得不怀疑两者之间有什么见不得人的勾当?其实可以从数组中得到切片,下面举例说明: func TestSliceFromArray(t *testing.T) { arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} // arr = [0 1 2 3 4 5 6 7 8 9] t.Log(\"arr = \", arr) // arr[2:6] = [2 3 4 5] t.Log(\"arr[2:6] = \", arr[2:6]) // arr[:6] = [0 1 2 3 4 5] t.Log(\"arr[:6] = \", arr[:6]) // arr[2:] = [2 3 4 5 6 7 8 9] t.Log(\"arr[2:] = \", arr[2:]) // arr[:] = [0 1 2 3 4 5 6 7 8 9] t.Log(\"arr[:] = \", arr[:]) } arr[start:end] 截取数组的一部分得到的结果就是切片,切片的概念也是很形象啊! 和其他主流的编程语言一样,[start:end] 是一个左闭右开区间,切片的含义也非常明确: 忽略起始索引 start 时,arr[:end] 表示原数组从头开始直到终止索引 end 的前一位; 忽略终止索引 end 时,arr[ start:] 表示原数组从起始索引 start 开始直到最后一位; 既忽略起始索引又忽略终止索引的情况,虽然不常见但是含义上将应该就是原数组,但是记得类型是切片不是数组哟! 目前为止,我们知道切片和数组很相似,切片相对于数组只是没有大小,那么切片和数组的操作上是否一样呢? func updateSlice(s []int) { s[0] = 666 } func TestUpdateSlice(t *testing.T) { arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} // arr = [0 1 2 3 4 5 6 7 8 9] t.Log(\"arr = \", arr) s1 := arr[2:6] // s1 = [2 3 4 5] t.Log(\"s1 = \", s1) s2 := arr[:6] // s2 = [0 1 2 3 4 5] t.Log(\"s2 = \", s2) updateSlice(s1) // s1 = [666 3 4 5] t.Log(\"s1 = \", s1) // arr = [0 1 666 3 4 5 6 7 8 9] t.Log(\"arr = \", arr) updateSlice(s2) // s2 = [666 1 666 3 4 5] t.Log(\"s2 = \", s2) // arr = [666 1 666 3 4 5 6 7 8 9] t.Log(\"arr = \", arr) } 切片竟然可以更改传递参数,这一点可是数组没有做到的事情啊!除非使用数组的指针类型,切片竟然可以轻易做到?除非切片内部是指针,因为参数传递只有值传递,根本没有引用传递方式! 切片和数组在参数传递的表现不同,具体表现为数组进行参数传递时无法修改数组,想要想改数组只有传递数组指针才行,而切片却实现了数组的改变! 由于参数传递只有值传递一种方式,因此推测切片内部肯定存在指针,参数传递时传递的是指针,所以函数内部的修改才能影响到到函数外部的变量. slice 的内部实现中有三个变量,指针 ptr,个数 len 和容量 cap ,其中 ptr 指向真正的数据存储地址. 正是由于切片这种内部实现,需要特性也好表现形式也罢才使得切换和数组有着千丝万缕的联系,其实这种数据结果就是对静态数组的扩展,本质上是一种动态数组而已,只不过 Go 语言叫做切片! 切片是动态数组,上述问题就很容易解释了,参数传递时传递的是内部指针,因而虽然是值传递拷贝了指针,但是指针指向的真正元素毕竟是一样的,所以切片可以修改外部参数的值. 数组可以在一定程度上进行比较,切片是动态数组,能不能进行比较呢?让接下来的测试方法来验证你的猜想吧! 不知道你有没有猜对呢?切片并不能进行比较,只能与 nil 进行判断. 切片的添加和删除 数组是静态结构,数组的大小不能扩容或缩容,这种数据结构并不能满足元素个数不确定场景,因而才出现动态数组这种切片,接下来重点看下切片怎么添加或删除元素. func printSlice(s []int) { fmt.Printf(\"s = %v, len(s) = %d, cap(s) = %d\\n\", s, len(s), cap(s)) } func TestSliceAutoLonger(t *testing.T) { var s []int // [] t.Log(s) for i := 0; i 添加元素 s = append(s, i) 需要扩容时,每次以 2 倍进行扩容,删除元素 s[1:] 时,递减缩容. s = append(s, i) 向切片中添加元素并返回新切片,由于切片是动态数组,当切片内部的数组长度不够时会自动扩容以容纳新数组,扩容前后的内部数组会进行元素拷贝过程,所以 append 会返回新的地址,扩容后的地址并不是原来地址,所以需要用变量接收添加后的切片. 当不断进行切片重新截取时 s[1:] ,切片存储的元素开始缩减,个数递减,容量也递减. 其实除了基于数组创建切片和直接创建切片的方式外,还存在第三种创建切片的方式,也是使用比较多的方式,那就是 make 函数. func TestMakeSlice(t *testing.T) { s1 := make([]int,10) // s1 = [0 0 0 0 0 0 0 0 0 0], len(s1) = 10, cap(s1) = 10 t.Logf(\"s1 = %v, len(s1) = %d, cap(s1) = %d\", s1, len(s1), cap(s1)) s2 := make([]int, 10, 32) // s2 = [0 0 0 0 0 0 0 0 0 0], len(s2) = 10, cap(s2) = 32 t.Logf(\"s2 = %v, len(s2) = %d, cap(s2) = %d\", s2, len(s2), cap(s2)) } 通过 make 方式可以设置初始化长度和容量,这是字面量创建切片所不具备的能力,并且这种方式创建的切片还支持批量拷贝功能! func TestCopySlice(t *testing.T) { var s1 = []int{1, 3, 5, 7, 9} var s2 = make([]int, 10, 32) copy(s2, s1) // s2 = [1 3 5 7 9 0 0 0 0 0], len(s2) = 10, cap(s2) = 32 t.Logf(\"s2 = %v, len(s2) = %d, cap(s2) = %d\", s2, len(s2), cap(s2)) var s3 []int copy(s3, s1) // s3 = [], len(s3) = 0, cap(s3) = 0 t.Logf(\"s3 = %v, len(s3) = %d, cap(s3) = %d\", s3, len(s3), cap(s3)) } func copy(dst, src []Type) int 是切片之间拷贝的函数,神奇的是,只有目标切片是 make 方式创建的切片才能进行拷贝,不明所以,有了解的小伙伴还请指点一二! 切片的底层结构是动态数组,如果切片是基于数组截取而成,那么此时的切片从效果上来看,切片就是原数组的一个视图,对切片的任何操作都会反映到原数组上,这也是很好理解的. 那如果对切片再次切片呢,或者说切片会不会越界,其实都比较简单了,还是稍微演示一下,重点就是动态数组的底层结构. func TestSliceOutOfBound(t *testing.T) { arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7} s1 := arr[2:6] // s1 = [2 3 4 5], len(s1) = 4, cap(s1) = 6 t.Logf(\"s1 = %v, len(s1) = %d, cap(s1) = %d\", s1, len(s1), cap(s1)) s2 := s1[3:5] // s2 = [5 6], len(s2) = 2, cap(s2) = 3 t.Logf(\"s2 = %v, len(s2) = %d, cap(s2) = %d\", s2, len(s2), cap(s2)) } [] 只能访问 len(arr) 范围内的元素,[:] 只能访问 cap(arr) 范围内的元素,一般而言 cap >= len 所以某些情况看起来越界,其实并不没有越界,只是二者的标准不同! 我们知道切片 slice 的内部数据结构是基于动态数组,存在三个重要的变量,分别是指针 ptr,个数 len 和容量 cap ,理解了这三个变量如何实现动态数组就不会掉进切片的坑了! 个数 len 是通过下标访问时的有效范围,超过 len 后会报越界错误,而容量 cap 是往后能看到的最大范围,动态数组的本质也是控制这两个变量实现有效数组的访问. 因为 s1 = [2 3 4 5], len(s1) = 4, cap(s1) = 6 ,所以 [] 访问切片 s1 元素的范围是[0,4) ,因此最大可访问到s1[3],而 s1[4] 已经越界了! 因为 s1 = [2 3 4 5], len(s1) = 4, cap(s1) = 6 ,所以 [:] 根据切片 s1 创建新切片的范围是 [0,6] ,因此最大可访问范围是 s1[0:6] ,而 s1[3:7] 已经越界! 集合 map 集合是一种键值对组成的数据结构,其他的主流编程语言也有类似概念,相比之下,Go 语言的 map 能装载的数据类型更加多样化. 字面量创建 map 换行需保留逗号 , func TestMap(t *testing.T) { m1 := map[string]string{ \"author\": \"snowdreams1006\", \"website\": \"snowdreams1006\", \"language\": \"golang\", } // map[name:snowdreams1006 site:https://snowdreams1006.github.io] t.Log(m1) } 一对键值对的结尾处加上逗号 , 可以理解,但是最后一个也要有逗号这就让我无法理解了,Why ? make 创建的 map 和字面量创建的 map 默认初始化零值不同 func TestMapByMake(t *testing.T) { // empty map m1 := make(map[string]int) // map[] false t.Log(m1, m1 == nil) // nil var m2 map[string]int // map[] true t.Log(m2, m2 == nil) } make 函数创建的 map 是空 map,而通过字面量形式创建的 map 是 nil,同样的规律也适合于切片 slice. range 遍历 map 是无序的 func TestMapTraverse(t *testing.T) { m := map[string]string{ \"name\": \"snowdreams1006\", \"site\": \"https://snowdreams1006.github.io\", } // map[name:snowdreams1006 site:https://snowdreams1006.github.io] t.Log(m) for k, v := range m { t.Log(k, v) } t.Log() for k := range m { t.Log(k) } t.Log() for _, v := range m { t.Log(v) } } 这里再一次遇到 range 形式的遍历,忽略键或值时用 _ 占位,也是和数组,切片的把遍历方式一样,唯一的差别就是 map 没有索引,遍历结果也是无序的! 获取元素时需判断元素是否存在 func TestMapGetItem(t *testing.T) { m := map[string]string{ \"name\": \"snowdreams1006\", \"site\": \"https://snowdreams1006.github.io\", } // snowdreams1006 t.Log(m[\"name\"]) // zero value is empty string t.Log(m[\"author\"]) // https://snowdreams1006.github.io if site, ok := m[\"site\"]; ok { t.Log(site) } else { t.Log(\"key does not exist \") } } Go 语言的 map 获取不存在的键时,返回的是值对应类型的零值,map[string]string 返回的默认零值就是空字符串,由于不会报错进行强提醒,这也就要求我们调用时多做一步检查.当键值对存在时,第二个返回值返回 true,不存在时返回 false. 删除键值对时用 delete 函数 func TestMapDeleteItem(t *testing.T) { m := map[string]string{ \"name\": \"snowdreams1006\", \"site\": \"https://snowdreams1006.github.io\", } // map[name:snowdreams1006 site:https://snowdreams1006.github.io] t.Log(m) delete(m, \"name\") // map[site:https://snowdreams1006.github.io] t.Log(m) delete(m, \"id\") // map[site:https://snowdreams1006.github.io] t.Log(m) } delete(map,key) 用于删除 map 的键值对,如果想要验证是否删除成功,别忘了使用 value,ok := m[k] 确定是否存在指定键值对 除 slice,map,func 外,其余类型均可键 因为 map 是基于哈希表实现,所以遍历是无序的,另一方面因为 slice,map,func 不可比较,因为也不能作为键.当然若自定义类型 struc 不包含上述类型,也可以作为键,并不要求实现 hashcode 和 equal 之类的. value 可以承载函数 func 类型 func TestMapWithFunValue(t *testing.T) { m := map[int]func(op int) int{} m[1] = func(op int) int { return op } m[2] = func(op int) int { return op * op } m[3] = func(op int) int { return op * op * op } // 1 4 27 t.Log(m[1](1), m[2](2), m[3](3)) } 再一次说明函数是一等公民,这部分会在以后的函数式编程中进行详细介绍. 没有 set Go 的默认类型竟然没有 set 这种数据结构,这在主流的编程语言中算是特别的存在了! 正如 Go 的循环仅支持 for 循环一样,没有 while 循环一样可以玩出 while 循环的效果,靠的就是增强的 for 能力. 所以,即使没有 set 类型,基于现有的数据结构一样能实现 set 效果,当然直接用 map 就可以封装成 set. func TestMapForSet(t *testing.T) { mySet := map[int]bool{} mySet[1] = true n := 3 if mySet[n] { t.Log(\"update\", mySet[n]) } else { t.Log(\"add\", mySet[n]) } delete(mySet, 1) } 使用 map[type]bool 封装实现 set 禁止重复性元素的特性,等到讲解到面向对象部分再好好封装,这里仅仅列出核心结构. 知识点总结梳理 Go 语言是十分简洁的,不论是基础语法还是这一节的内建容器都很好的体现了这一点. 数组作为各个编程语言的基础数据结构,Go 语言和其他主流的编程语言相比没有什么不同,都是一片连续的存储空间,不同之处是数组是值类型,所以也是可以进行比较的. 这并不是新鲜知识,毕竟上一节内容已经详细阐述过该内容,这一节的重点是数组的衍生版切片 slice . 因为数组本身是特定长度的连续空间,因为是不可变的,其他主流的编程语言中有相应的解决方案,其中就有不少数据结构的底层是基于数组实现的,Go 语言的 slice 也是如此,因此个人心底里更愿意称其为动态数组! 切片 slice 的设计思路非常简单,内部包括三个重要变量,包括数组指针 ptr,可访问元素长度 len 以及已分配容量 cap . 当新元素不断添加进切片时,总会达到已最大分配容量,此时切片就会自动扩容,反之则会缩容,从而实现了动态控制的能力! 指定元素个数的是数组,未指定个数的是切片 func TestArrayAndSlice(t *testing.T) { // array var arr1 [3]int // slice var arr2 []int // [0 0 0] [] t.Log(arr1,arr2) } 基于数组创建的切片是原始数组的视图 func TestArrayAndSliceByUpdate(t *testing.T) { arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} // arr = [0 1 2 3 4 5 6 7 8 9] t.Log(\"arr = \", arr) s := arr[2:6] // before update s = [2 3 4 5], arr = [0 1 2 3 4 5 6 7 8 9] t.Logf(\"before update s = %v, arr = %v\", s, arr) s[0] = 666 // after update s = [666 3 4 5], arr = [0 1 666 3 4 5 6 7 8 9] t.Logf(\"after update s = %v, arr = %v\", s, arr) } 添加或删除切片元素都返回新切片 func TestArrayAndSliceIncreasing(t *testing.T) { var s []int fmt.Println(\"add new item to slice\") for i := 0; i [index] 访问切片元素仅仅和切片的 len 有关,[start:end] 创建新切片仅仅和原切片的 cap 有关 func TestArrayAndSliceBound(t *testing.T) { arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} s1 := arr[5:8] // s1[0] = 5, s1[2] = 7 t.Logf(\"s1[0] = %d, s1[%d] = %d\", s1[0], len(s1)-1, s1[len(s1)-1]) // s1 = [5 6 7], len(s1) = 3, cap(s1) = 5 t.Logf(\"s1 = %v, len(s1) = %d, cap(s1) = %d\", s1, len(s1), cap(s1)) s2 := s1[3:5] // s2[0] = 8, s2[1] = 9 t.Logf(\"s2[0] = %d, s2[%d] = %d\", s2[0], len(s2)-1, s2[len(s2)-1]) // s2 = [8 9], len(s2) = 2, cap(s2) = 2 t.Logf(\"s2 = %v, len(s2) = %d, cap(s2) = %d\", s2, len(s2), cap(s2)) } 只有 map 没有 set func TestMapAndSet(t *testing.T) { m := map[string]string{ \"name\": \"snowdreams1006\", \"site\": \"https://snowdreams1006.github.io\", \"lang\": \"go\", } // https://snowdreams1006.github.io if site, ok := m[\"site\"]; ok { t.Log(site) } else { t.Log(\"site does not exist \") } s := map[string]bool{ \"name\": true, \"site\": true, \"lang\": true, } // Pay attention to snowdreams1006 if _, ok := m[\"isFollower\"]; ok { t.Log(\"Have an eye on snowdreams1006\") } else { s[\"isFollower\"] = true t.Log(\"Pay attention to snowdreams1006\") } } delete 函数删除集合 map 键值对 func TestMapAndSetByDelete(t *testing.T) { m := map[string]string{ \"name\": \"snowdreams1006\", \"site\": \"https://snowdreams1006.github.io\", \"lang\": \"go\", } delete(m, \"lang\") // delete lang successfully if _,ok := m[\"lang\"];!ok{ t.Log(\"delete lang successfully\") } } 关于 Go 语言中内建容器是不是都已经 Get 了呢?如果有表述不对的地方,还请指正哈,欢迎一起来公众号[雪之梦技术驿站]学习交流,每天进步一点点! © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-19 09:00:20 "},"go/container/array.html":{"url":"go/container/array.html","title":"数组切片","keywords":"","body":"数组切片 © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-19 09:00:20 "},"go/container/map.html":{"url":"go/container/map.html","title":"字典映射","keywords":"","body":"字典映射 © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-19 09:00:20 "},"go/cmd/about.html":{"url":"go/cmd/about.html","title":"标准命令详解","keywords":"","body":"标准命令详解 标准命令详解 Go语言的1.5版本在标准命令方面有了重大变更。这倒不是说它们的用法有多大的变化,而是说它们的底层支持已经大变样了。让我们先来对比一下$GOROOT/pkg/tool/中的内容。以下简称此目录为Go工具目录。 插播:平台相关目录即以_命名的目录,用于存放因特定平台的不同而不同的代码包归档文件或可执行文件。其中,代表特定平台的操作系统代号,而则代表特定平台的计算架构代号。使用go env命令便可查看它们在你的计算机中的实际值。 1.4版本的Go工具目录的内容如下: 5a 5l 6g 8c addr2line dist objdump tour 5c 6a 6l 8g cgo fix pack vet 5g 6c 8a 8l cover nm pprof yacc 下面是Go 1.5版本的: addr2line asm compile dist fix nm pack tour vet api cgo cover doc link objdump pprof trace yacc 可以看到,1.5版本的目录内容精简了不少。这是因为Go 1.5的编译器、链接器都已经完全用Go语言重写了。而在这之前,它们都是用C语言写的,因此不得不为每类平台编写不同的程序并生成不同的文件。例如,8g、6g和5g分别是gc编译器在x86(32bit)、x86-64(64bit)和ARM计算架构的计算机上的实现程序。相比之下,用Go语言实现的好处就是,编译器和链接器都将是跨平台的了。简要来说,Go 1.5版本的目录中的文件compile即是统一后的编译器,而文件link则是统一后的链接器。 本教程并不会讲解Go语言的编译器、链接器以及其它工具是怎样被编写出来的,并只会关注于怎样用好包含它们在内的Go语言自带的命令和工具。 为了让讲解更具关联性,也为了让读者能够更容易的理解这些命令和工具,本教程并不会按照这些命令的字典顺序描述它们,而会按照我们在实际开发过程中通常的使用顺序以及它们的重要程度来逐一进行说明。现在,我们就先从go build命令开始。 © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:09:32 "},"go/cmd/build.html":{"url":"go/cmd/build.html","title":"go build","keywords":"","body":"go build go build go build命令用于编译我们指定的源码文件或代码包以及它们的依赖包。 例如,如果我们在执行go build命令时不后跟任何代码包,那么命令将试图编译当前目录所对应的代码包。例如,我们想编译goc2p项目的代码包logging。其中一个方法是进入logging目录并直接执行该命令: hc@ubt:~/golang/goc2p/src/logging$ go build 因为在代码包logging中只有库源码文件和测试源码文件,所以在执行go build命令之后不会在当前目录和goc2p项目的pkg目录中产生任何文件。 插播:Go语言的源码文件有三大类,即:命令源码文件、库源码文件和测试源码文件。他们的功用各不相同,而写法也各有各的特点。命令源码文件总是作为可执行的程序的入口。库源码文件一般用于集中放置各种待被使用的程序实体(全局常量、全局变量、接口、结构体、函数等等)。而测试源码文件主要用于对前两种源码文件中的程序实体的功能和性能进行测试。另外,后者也可以用于展现前两者中程序的使用方法。 另外一种编译logging包的方法是: hc@ubt:~/golang/goc2p/src$ go build logging 在这里,我们把代码包logging的导入路径作为参数传递给go build命令。另一个例子:如果我们要编译代码包cnet/ctcp,只需要在任意目录下执行命令go build cnet/ctcp即可。 插播:之所以这样的编译方法可以正常执行,是因为我们已经在环境变量GOPATH中加入了goc2p项目的根目录(即~/golang/goc2p/)。这时,goc2p项目的根目录就成为了一个工作区目录。只有这样,Go语言才能正确识别我们提供的goc2p项目中某个代码包的导入路径。而代码包的导入路径是指,相对于Go语言自身的源码目录(即$GOROOT/src)或我们在环境变量GOPATH中指定的某个目录的src子目录下的子路径。例如,这里的代码包logging的绝对路径是~/golang/goc2p/src/logging。而不论goc2p项目的根文件夹被放在哪儿,logging包的导入路径都是logging。显而易见,我们在称呼一个代码包的时候总是以其导入路径作为其称谓。 言归正传,除了上面的简单用法,我们还可以同时编译多个Go源码文件: hc@ubt:~/golang/goc2p/src$ go build logging/base.go logging/console_logger.go logging/log_manager.go logging/tag.go 但是,使用这种方法会有一个限制。作为参数的多个Go源码文件必须在同一个目录中。也就是说,如果我们想用这种方法既编译logging包又编译basic包是不可能的。不过别担心,在需要的时候,那些被编译目标依赖的代码包会被go build命令自动的编译。例如,如果有一个导入路径为app的代码包,同时依赖了logging包和basic包。那么在执行go build app的时候,该命令就会自动的在编译app包之前去检查logging包和basic包的编译状态。如果发现它们的编译结果文件不是最新的,那么该命令就会先去的编译这两个代码包,然后再编译app包。 注意,go build命令在编译只包含库源码文件的代码包(或者同时编译多个代码包)时,只会做检查性的编译,而不会输出任何结果文件。 另外,go build命令既不能编译包含多个命令源码文件的代码包,也不能同时编译多个命令源码文件。因为,如果把多个命令源码文件作为一个整体看待,那么每个文件中的main函数就属于重名函数,在编译时会抛出重复定义错误。假如,在goc2p项目的代码包cmd(此代码包仅用于示例目的,并不会永久存在于该项目中)中包含有两个命令源码文件showds.go和initpkg_demo.go,那么我们在使用go build命令同时编译它们时就会失败。示例如下: hc@ubt:~/golang/goc2p/src/cmd$ go build showds.go initpkg_demo.go # command-line-arguments ./initpkg_demo.go:19: main redeclared in this block previous declaration at ./showds.go:56 请注意上面示例中的command-line-arguments。在这个位置上应该显示的是作为编译目标的源码文件所属的代码包的导入路径。但是,这里显示的并不是它们所属的代码包的导入路径cmd。这是因为,命令程序在分析参数的时候如果发现第一个参数是Go源码文件而不是代码包,则会在内部生成一个虚拟代码包。这个虚拟代码包的导入路径和名称都会是command-line-arguments。在其他基于编译流程的命令程序中也有与之一致的操作,比如go install命令和go run命令。 另一方面,如果我们编译的多个属于main包的源码文件中没有main函数的声明,那么就会使编译器立即报出“未定义main函数声明”的错误并中止编译。换句话说,在我们同时编译多个main包的源码文件时,要保证其中有且仅有一个main函数声明,否则编译是无法成功的。 现在我们使用go build命令编译单一命令源码文件。我们在执行命令时加入一个标记-v。这个标记的意义在于可以使命令把执行过程中构建的包名打印出来。我们会在稍后对这个标记进行详细说明。现在我们先来看一个示例: hc@ubt:~/golang/goc2p/src/basic/pkginit$ ls initpkg_demo.go hc@ubt:~/golang/goc2p/src/basic/pkginit$ go build -v initpkg_demo.go command-line-arguments hc@ubt:~/golang/goc2p/src/basic/pkginit$ ls initpkg_demo initpkg_demo.go 我们在执行命令go build -v initpkg_demo.go之后被打印出的command-line-arguments”`就是命令程序为命令源码文件initpkg_demo.go生成的虚拟代码包的包名。顺带说一句, 命令go build会把编译命令源码文件后生成的结果文件存放到执行该命令时所在的目录下。这个所说的结果文件就是与命令源码文件对应的可执行文件。它的名称会与命令源码文件的主文件名相同。 顺便说一下,如果我们有多个声明为属于main包的源码文件,且其中只有一个文件声明了main函数的话,那么是可以使用go build命令同时编译它们的。在这种情况下,不包含main函数声明的那几个源码文件会被视为库源码文件(理所当然)。如此编译之后的结果文件的名称将会与我们指定的编译目标中最左边的那个源码文件的主文件名相同。 其实,除了让Go语言编译器自行决定可执行文件的名称,我们还可以自定义它。示例如下: hc@ubt:~/golang/goc2p/src/basic/pkginit$ go build -o initpkg initpkg_demo.go hc@ubt:~/golang/goc2p/src/basic/pkginit$ ls initpkg initpkg_demo.go 使用-o标记可以指定输出文件(在这个示例中指的是可执行文件)的名称。它是最常用的一个go build命令标记。但需要注意的是,当使用标记-o的时候,不能同时对多个代码包进行编译。 标记-i会使go build命令安装那些编译目标依赖的且还未被安装的代码包。这里的安装意味着产生与代码包对应的归档文件,并将其放置到当前工作区目录的pkg子目录的相应子目录中。在默认情况下,这些代码包是不会被安装的。 除此之外,还有一些标记不但受到go build命令的支持,而且对于后面会提到的go install、go run、go test等命令同样是有效的。下表列出了其中比较常用的标记。 表0-1 go build命令的常用标记说明 标记名称 标记描述 -a 强行对所有涉及到的代码包(包含标准库中的代码包)进行重新构建,即使它们已经是最新的了。 -n 打印编译期间所用到的其它命令,但是并不真正执行它们。 -p n 指定编译过程中执行各任务的并行数量(确切地说应该是并发数量)。在默认情况下,该数量等于CPU的逻辑核数。但是在darwin/arm平台(即iPhone和iPad所用的平台)下,该数量默认是1。 -race 开启竞态条件的检测。不过此标记目前仅在linux/amd64、freebsd/amd64、darwin/amd64和windows/amd64平台下受到支持。 -v 打印出那些被编译的代码包的名字。 -work 打印出编译时生成的临时工作目录的路径,并在编译结束时保留它。在默认情况下,编译结束时会删除该目录。 -x 打印编译期间所用到的其它命令。注意它与-n标记的区别。 我们在这里忽略了一些并不常用的或作用于编译器或连接器的标记。在本小节的最后将会对这些标记进行简单的说明。如果读者有兴趣,也可以查看Go语言的官方文档以获取相关信息。 下面我们就用其中几个标记来查看一下在构建代码包logging时创建的临时工作目录的路径: hc@ubt:~/golang/goc2p/src$ go build -v -work logging WORK=/tmp/go-build888760008 logging 上面命令的结果输出的第一行是为了编译logging包,Go创建的一个临时工作目录,这个目录被创建到了Linux的临时目录下。输出的第二行是对标记-v的响应。这意味着此次命令执行时仅编译了logging包。关于临时工作目录的用途和内容,我们会在讲解go run命令和go test命令的时候详细说明。 现在我们再来看看如果强制重新编译会涉及到哪些代码包: hc@ubt:~/golang/goc2p/src$ go build -a -v -work logging WORK=/tmp/go-build929017331 runtime errors sync/atomic math unicode/utf8 unicode sync io syscall strings time strconv reflect os fmt log logging 怎么会多编译了这么多代码包呢?可以确定的是,代码包logging中的代码直接依赖了标准库中的runtime包、strings包、fmt包和log包。那么其他的代码包为什么也会被重新编译呢? 从代码包编译的角度来说,如果代码包A依赖代码包B,则称代码包B是代码包A的依赖代码包(以下简称依赖包),代码包A是代码包B的触发代码包(以下简称触发包)。 go build命令在执行时,编译程序会先查找目标代码包的所有依赖包,以及这些依赖包的依赖包,直至找到最深层的依赖包为止。在此过程中,如果发现有循环依赖的情况,编译程序就会输出错误信息并立即退出。此过程完成之后,所有的依赖关系也就形成了一棵含有重复元素的依赖树。对于依赖树中的一个节点(代码包)来说,它的直接分支节点(前者的依赖包),是按照代码包导入路径的字典序从左到右排列的。最左边的分支节点会最先被编译。编译程序会依此设定每个代码包的编译优先级。 执行go build命令的计算机如果拥有多个逻辑CPU核心,那么编译代码包的顺序可能会存在一些不确定性。但是,它一定会满足这样的约束条件:依赖代码包 -> 当前代码包 -> 触发代码包。 标记-p n可以限制编译过程中任务执行的并发数量,n默认为当前计算机的CPU逻辑核数。如果在执行go build命令时加入标记-p 1,那么就可以保证代码包编译顺序严格按照预先设定好的优先级进行。现在我们再来编译logging包: hc@ubt:~/golang/goc2p/src$ go build -a -v -work -p 1 logging WORK=/tmp/go-build114039681 runtime errors sync/atomic sync io math syscall time os unicode/utf8 strconv reflect fmt log unicode strings logging 我们可以认为,以上示例中所显示的代码包的顺序,就是logging包直接或间接依赖的代码包按照优先级从高到低排列后的排序。 另外,如果在命令中加入标记-n,那么编译程序只会输出所用到的命令而不会真正运行。在这种情况下,编译过程不会使用并发模式。 在本节的最后,我们对一些并不太常用的标记进行简要的说明: -asmflags 此标记可以后跟另外一些标记,如-D、-I、-S等。这些后跟的标记用于控制Go语言编译器编译汇编语言文件时的行为。 -buildmode 此标记用于指定编译模式,使用方式如-buildmode=default(这等同于默认情况下的设置)。此标记支持的编译模式目前有6种。借此,我们可以控制编译器在编译完成后生成静态链接库(即.a文件,也就是我们之前说的归档文件)、动态链接库(即.so文件)或/和可执行文件(在Windows下是.exe文件)。 -compiler 此标记用于指定当前使用的编译器的名称。其值可以为gc或gccgo。其中,gc编译器即为Go语言自带的编辑器,而gccgo编译器则为GCC提供的Go语言编译器。而GCC则是GNU项目出品的编译器套件。GNU是一个众所周知的自由软件项目。在开源软件界不应该有人不知道它。好吧,如果你确实不知道它,赶紧去google吧。 -gccgoflags 此标记用于指定需要传递给gccgo编译器或链接器的标记的列表。 -gcflags 此标记用于指定需要传递给go tool compile命令的标记的列表。 -installsuffix 为了使当前的输出目录与默认的编译输出目录分离,可以使用这个标记。此标记的值会作为结果文件的父目录名称的后缀。其实,如果使用了-race标记,这个标记会被自动追加且其值会为race。如果我们同时使用了-race标记和-installsuffix,那么在-installsuffix标记的值的后面会再被追加_race,并以此来作为实际使用的后缀。 -ldflags 此标记用于指定需要传递给go tool link命令的标记的列表。 -linkshared 此标记用于与-buildmode=shared一同使用。后者会使作为编译目标的非main代码包都被合并到一个动态链接库文件中,而前者则会在此之上进行链接操作。 -pkgdir 使用此标记可以指定一个目录。编译器会只从该目录中加载代码包的归档文件,并会把编译可能会生成的代码包归档文件放置在该目录下。 -tags 此标记用于指定在实际编译期间需要受理的编译标签(也可被称为编译约束)的列表。这些编译标签一般会作为源码文件开始处的注释的一部分,例如,在$GOROOT/src/os/file_posix.go开始处的注释为: // Copyright 2009 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // +build darwin dragonfly freebsd linux nacl netbsd openbsd solaris windows 最后一行注释即包含了与编译标签有关的内容。大家可以查看代码包go/build的文档已获得更多的关于编译标签的信息。 -toolexec 此标记可以让我们去自定义在编译期间使用一些Go语言自带工具(如vet、asm等)的方式。 © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:09:32 "},"go/cmd/install.html":{"url":"go/cmd/install.html","title":"go install","keywords":"","body":"go install go install 命令go install用于编译并安装指定的代码包及它们的依赖包。当指定的代码包的依赖包还没有被编译和安装时,该命令会先去处理依赖包。与go build命令一样,传给go install命令的代码包参数应该以导入路径的形式提供。并且,go build命令的绝大多数标记也都可以用于go install命令。实际上,go install命令只比go build命令多做了一件事,即:安装编译后的结果文件到指定目录。 在对go install命令进行详细说明之前,让我们先回顾一下goc2p的目录结构。为了节省篇幅,我在这里隐藏了代码包中的源码文件。如下: $HOME/golang/goc2p: bin/ pkg/ src/ cnet/ logging/ helper/ ds/ pkgtool/ 我们看到,goc2p项目中有三个子目录,分别是bin目录、pkg目录和src目录。现在只有src目录中包含了一些目录,而其他两个目录都是空的。 现在,我们来看看安装代码包的规则。 安装代码包 如果go install命令后跟的代码包中仅包含库源码文件,那么go install命令会把编译后的结果文件保存在源码文件所在工作区的pkg目录下。对于仅包含库源码文件的代码包来说,这个结果文件就是对应的代码包归档文件(也叫静态链接库文件,名称以.a结尾)。相比之下,我们在使用go build命令对仅包含库源码文件的代码包进行编译时,是不会在当前工作区的src目录以及pkg目录下产生任何结果文件的。结果文件会出于编译的目的被生成在临时目录中,但并不会使当前工作区目录产生任何变化。 如果我们在执行go install命令时不后跟任何代码包参数,那么命令将试图编译当前目录所对应的代码包。比如,我们现在要安装代码包pkgtool: hc@ubt:~/golang/goc2p/src/pkgtool$ go install -v -work WORK=D:\\cygwin\\tmp\\go-build758586887 pkgtool 我们在前面说过,执行go install命令后会对指定代码包先编译再安装。其中,编译代码包使用了与go build命令相同的程序。所以,执行go install命令后也会首先建立一个名称以go-build为前缀的临时目录。如果我们想强行重新安装指定代码包及其依赖包,那么就需要加入标记-a: hc@ubt:~/golang/goc2p/src/pkgtool$ go install -a -v -work WORK=/tmp/go-build014992994 runtime errors sync/atomic unicode unicode/utf8 sort sync io syscall strings bytes bufio time os path/filepath pkgtool 可以看到,代码包pkgtool仅仅依赖了Go语言标准库中的代码包。 现在我们再来查看一下goc2p项目目录: $HOME/golang/goc2p: bin/ pkg/ linux_386/ pkgtool.a src/ 现在pkg目录中多了一个子目录。读过0.0节的读者应该已经知道,linux386被叫做平台相关目录。它的名字可以由`${GOOS}${GOARCH}来得到。其中,${GOOS}和${GOARCH}分别是当前操作系统中的环境变量GOOS和GOARCH的值。如果它们不存在,那么Go语言就会使用其内部的预定值。上述示例在计算架构为386且操作系统为Linux的计算机上运行。所以,这里的平台相关目录即为linux_386。我们还看到,在goc2p项目中的平台相关目录下存在一个文件,名称是pkgtool.a。这就是代码包pkgtool`的归档文件,文件名称是由代码包名称与“.a”后缀组合而来的。 实际上,代码包的归档文件并不都会被直接保存在pkg目录的平台相关目录下,还可能被保存在这个平台相关目录的子目录下。 下面我们来安装cnet/ctcp包: hc@ubt:~/golang/goc2p/src/pkgtool$ go install -a -v -work ../cnet/ctcp WORK=/tmp/go-build083178213 runtime errors sync/atomic unicode unicode/utf8 math sync sort io syscall internal/singleflight bytes strings strconv bufio math/rand time reflect os fmt log runtime/cgo logging net cnet/ctcp 请注意,我们是在代码包pkgtool对应的目录下安装cnet/ctcp包的。我们使用了一个目录相对路径。 实际上,这种提供代码包位置的方式被叫做本地代码包路径方式,也是被所有Go命令接受的一种方式,这包括之前已经介绍过的go build命令。但是需要注意的是,本地代码包路径只能以目录相对路径的形式呈现,而不能使用目录绝对路径。请看下面的示例: hc@ubt:~/golang/goc2p/src/cnet/ctcp$ go install -v -work ~/golang/goc2p/src/cnet/ctcp can't load package: package /home/hc/golang/goc2p/src/cnet/ctcp: import \"/home/hc/golang/goc2p/src/cnet/ctcp\": cannot import absolute path 从上述示例中的命令提示信息我们可知,以目录绝对路径的形式提供代码包位置是不会被Go命令认可的。 这是由于Go认为本地代码包路径的表示只能以“./”或“../”开始,再或者直接为“.”或“..”,而代码包的代码导入路径又不允许以“/”开始。所以,这种用绝对路径表示代码包位置的方式也就不能被支持了。 上述规则适用于所有Go命令。读者可以自己尝试一下,比如在执行go build命令时分别以代码包导入路径、目录相对路径和目录绝对路径的形式提供代码包位置,并查看执行结果。 我们已经通过上面的示例强行的重新安装了cnet/ctcp包及其依赖包。现在我们再来看一下goc2p的项目目录: $HOME/golang/goc2p: bin/ pkg/ linux_386/ /cnet ctcp.a logging.a pkgtool.a src/ 我们发现在pkg目录的平台相关目录下多了一个名为cnet的目录,而在这个目录下的就是名为ctcp.a的代码包归档文件。由此我们可知,代码包归档文件的存放目录的相对路径(相对于当前工作区的pkg目录的平台相关目录)即为代码包导入路径除去最后一个元素后的路径。而代码包归档文件的名称即为代码包导入路径中的最后一个元素再加“.a”后缀。再举一个例子,如果代码包导入路径为x/y/z,则它的归档文件存放路径的相对路径即为x/y/,而这个归档文件的名称即为z.a。 回顾代码包pkgtool的归档文件的存放路径。因为它的导入路径中只有一个元素,所以其归档文件就被直接存放到了goc2p项目的pkg目录的平台相关目录下了。 此外,我们还发现pkg目录的平台相关目录下还有一个名为logging.a的文件。很显然,我们并没有显式的安装代码包logging。这是因为go install命令在安装指定的代码包之前,会先去安装指定代码包的依赖包。当依赖包被正确安装后,指定的代码包的安装才会开始。由于代码包cnet/ctcp依赖于goc2p项目(即当前工作区)中的代码包logging,所以当代码包logging被成功安装之后,代码包cnet/ctcp才会被安装。 还有一个问题:上述的安装过程涉及到了那么多代码包,那为什么goc2p项目的pkg目录中只包含该项目中代码包的归档文件呢?实际上,go install命令会把标准库中的代码包的归档文件存放到Go语言安装目录的pkg子目录中,而把指定代码包依赖的第三方项目的代码包的归档文件存放到当前工作区的pkg目录下。这样就实现了Go语言标准库代码包的归档文件与用户代码包的归档文件,以及处在不同工作区的用户代码包的归档文件之间的分离。 安装命令源码文件 除了安装代码包之外,go install命令还可以安装命令源码文件。为了看到安装命令源码文件是goc2p项目目录的变化,我们先把该目录还原到原始状态,即清除bin子目录和pkg子目录下的所有目录和文件。然后,我们来安装代码包helper/ds下的命令源码文件showds.go,如下: hc@ubt:~/golang/goc2p/src$ go install helper/ds/showds.go go install: no install location for .go files listed on command line (GOBIN not set) 这次我们没能成功安装。该Go命令认为目录/home/hc/golang/goc2p/src/helper/ds不在环境GOPATH中。我们可以通过Linux的echo命令来查看一下环境变量GOPATH的值: hc@ubt:~/golang/goc2p/src$ echo $GOPATH /home/hc/golang/lib:/home/hc/golang/goc2p 环境变量GOPATH的值中确实包含了goc2p项目的根目录。这到底是怎么回事呢? 我通过查看Go命令的源码文件找到了其根本原因。在上一小节我们提到过,在环境变量GOPATH中包含多个工作区目录路径时,我们需要在编译命令源码文件前先对环境变量GOBIN进行设置。实际上,这个环境变量所指的目录路径就是命令程序生成的结果文件的存放目录。go install命令会把相应的可执行文件放置到这个目录中。 由于命令go build在编译库源码文件后不会产生任何结果文件,所以自然也不用会在意结果文件的存放目录。在该命令编译单一的命令源码文件或者包含一个命令源码文件和多个库源码文件时,在结果文件存放目录无效的情况下会将结果文件(也就是可执行文件)存放到执行该命令时所在的目录下。因此,即使环境变量GOBIN的值无效,我们在执行go build命令时也不会见到这个错误提示信息。 然而,go install命令中一个很重要的步骤就是将结果文件(归档文件或者可执行文件)存放到相应的目录中。所以,go install命令在安装命令源码文件时,如果环境变量GOBIN的值无效,则它会在最后检查结果文件存放目录的时候发现这一问题,并打印与上述示例所示内容类似的错误提示信息,最后直接退出。 这个错误提示信息在我们安装多个库源码文件时也有可能遇到。示例如下: hc@ubt:~/golang/goc2p/src/pkgtool$ go install envir.go fpath.go ipath.go pnode.go util.go go install: no install location for .go files listed on command line (GOBIN not set) 而且,在我们为环境变量GOBIN设置了正确的值之后,这个错误提示信息仍然会出现。这是因为,只有在安装命令源码文件的时候,命令程序才会将环境变量GOBIN的值作为结果文件的存放目录。而在安装库源码文件时,在命令程序内部的代表结果文件存放目录路径的那个变量不会被赋值。最后,命令程序会发现它依然是个无效的空值。所以,命令程序会同样返回一个关于“无安装位置”的错误。这就引出一个结论,我们只能使用安装代码包的方式来安装库源码文件,而不能在go install命令罗列并安装它们。另外,go install命令目前无法接受标记-o以自定义结果文件的存放位置。这也从侧面说明了go install命令不支持针对库源码文件的安装操作。 至此,我们对怎样用go install命令来安装代码包以及命令源码文件进行了说明。如果你已经熟知了go build命令,那么理解这些内容应该不在话下。 © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:09:38 "},"go/cmd/get.html":{"url":"go/cmd/get.html","title":"go get","keywords":"","body":"go get go get hc@ubt:~$ go get github.com/hyper-carrot/go_lib/logging 命令go get可以根据要求和实际情况从互联网上下载或更新指定的代码包及其依赖包,并对它们进行编译和安装。在上面这个示例中,我们从著名的代码托管站点Github上下载了一个项目(或称代码包),并安装到了环境变量GOPATH中包含的第一个工作区中。与此同时,我们也知道了这个代码包的导入路径就是github.com/hyper-carrot/go_lib/logging。 一般情况下,为了分离自己与第三方的代码,我们会设置两个或更多的工作区。我们现在有一个目录路径为/home/hc/golang/lib的工作区,并且它是环境变量GOPATH值中的第一个目录路径。注意,环境变量GOPATH中包含的路径不能与环境变量GOROOT的值重复。好了,如果我们使用go get命令下载和安装代码包,那么这些代码包都会被安装在上面这个工作区中。我们暂且把这个工作区叫做Lib工作区。在我们运行go get github.com/hyper-carrot/go_lib/logging之后,这个代码包就应该会被保存在Lib工作的src目录下,并且已经被安装妥当,如下所示: /home/hc/golang/lib: bin/ pkg/ linux_386/ github.com/ hyper-carrot/ go_lib/ logging.a src/ github.com/ hyper-carrot/ go_lib/ logging/ ... 另一方面,如果我们想把一个项目上传到Github网站(或其他代码托管网站)上并被其他人使用的话,那么我们就应该把这个项目当做一个代码包来看待。其实我们在之前已经提到过原因,go get命令会将项目下的所有子目录和源码文件存放到第一个工作区的src目录下,而src目录下的所有子目录都会是某个代码包导入路径的一部分或者全部。也就是说,我们应该直接在项目目录下存放子代码包和源码文件,并且直接存放在项目目录下的源码文件所声明的包名应该与该项目名相同(除非它是命令源码文件)。这样做可以让其他人使用go get命令从Github站点上下载你的项目之后直接就能使用它。 实际上,像goc2p项目这样直接以项目根目录的路径作为工作区路径的做法是不被推荐的。之所以这样做主要是想让读者更容易的理解Go语言的工程结构和工作区概念,也可以让读者看到另一种项目结构。当然,如果你的项目使用了gb这样的工具那就是另外一回事了。这样的项目的根目录就应该被视为一个工作区(但是你不必把它加入到GOPATH环境变量中)。它应该由git clone下载到Go语言工作区之外的某处,而不是使用go get命令。 远程导入路径分析 实际上,go get命令所做的动作也被叫做代码包远程导入,而传递给该命令的作为代码包导入路径的那个参数又被叫做代码包远程导入路径。 go get命令不仅可以从像Github这样著名的代码托管站点上下载代码包,还可以从任何命令支持的代码版本控制系统(英文为Version Control System,简称为VCS)检出代码包。任何代码托管站点都是通过某个或某些代码版本控制系统来提供代码上传下载服务的。所以,更严格地讲,go get命令所做的是从代码版本控制系统的远程仓库中检出/更新代码包并对其进行编译和安装。 该命令所支持的VCS的信息如下表: 表0-2 go get命令支持的VCS 名称 主命令 说明 Mercurial hg Mercurial是一种轻量级分布式版本控制系统,采用Python语言实现,易于学习和使用,扩展性强。 Git git Git最开始是Linux Torvalds为了帮助管理 Linux 内核开发而开发的一个开源的分布式版本控制软件。但现在已被广泛使用。它是被用来进行有效、高速的各种规模项目的版本管理。 Subversion svn Subversion是一个版本控制系统,也是第一个将分支概念和功能纳入到版本控制模型的系统。但相对于Git和Mercurial而言,它只算是传统版本控制系统的一员。 Bazaar bzr Bazaar是一个开源的分布式版本控制系统。但相比而言,用它来作为VCS的项目并不多。 go get命令在检出代码包之前必须要知道代码包远程导入路径所对应的版本控制系统和远程仓库的URL。 如果该代码包在本地工作区中已经存在,则会直接通过分析其路径来确定这几项信息。go get命令支持的几个版本控制系统都有一个共同点,那就是会在检出的项目目录中存放一个元数据目录,名称为“.”前缀加其主命令名。例如,Git会在检出的项目目录中加入一个名为“.git”的子目录。所以,这样就很容易判定代码包所用的版本控制系统。另外,又由于代码包已经存在,我们只需通过代码版本控制系统的更新命令来更新代码包,因此也就不需要知道其远程仓库的URL了。对于已存在于本地工作区的代码包,除非要求强行更新代码包,否则go get命令不会进行重复下载。如果想要强行更新代码包,可以在执行go get命令时加入-u标记。这一标记会稍后介绍。 如果本地工作区中不存在该代码包,那么就只能通过对代码包远程导入路径进行分析来获取相关信息了。首先,go get命令会对代码包远程导入路径进行静态分析。为了使分析过程更加方便快捷,go get命令程序中已经预置了几个著名代码托管网站的信息。如下表: 表0-3 预置的代码托管站点的信息 名称 主域名 支持的VCS 代码包远程导入路径示例 Bitbucket bitbucket.org Git, Mercurial bitbucket.org/user/projectbitbucket.org/user/project/sub/directory GitHub github.com Git github.com/user/projectgithub.com/user/project/sub/directory Google Code Project Hosting code.google.com Git, Mercurial, Subversion code.google.com/p/projectcode.google.com/p/project/sub/directorycode.google.com/p/project.subrepositorycode.google.com/p/project.subrepository/sub/directory Launchpad launchpad.net Bazaar launchpad.net/projectlaunchpad.net/project/serieslaunchpad.net/project/series/sub/directorylaunchpad.net/~user/project/branchlaunchpad.net/~user/project/branch/sub/directory IBM DevOps Services hub.jazz.net Git hub.jazz.net/git/user/projecthub.jazz.net/git/user/project/sub/directory 一般情况下,代码包远程导入路径中的第一个元素就是代码托管网站的主域名。在静态分析的时候,go get命令会将代码包远程导入路径与预置的代码托管站点的主域名进行匹配。如果匹配成功,则在对代码包远程导入路径的初步检查后返回正常的返回值或错误信息。如果匹配不成功,则会再对代码包远程导入路径进行动态分析。至于动态分析的过程,我就不在这里详细展开了。 如果对代码包远程导入路径的静态分析或/和动态分析成功并获取到对应的版本控制系统和远程仓库URL,那么go get命令就会进行代码包检出或更新的操作。随后,go get命令会在必要时以同样的方式检出或更新这个代码包的所有依赖包。 自定义代码包远程导入路径 如果你想把你编写的(被托管在不同的代码托管网站上的)代码包的远程导入路径统一起来,或者不希望让你的代码包中夹杂某个代码托管网站的域名,那么你可以选择自定义你的代码包远程导入路径。这种自定义的实现手段叫做“导入注释”。导入注释的写法示例如下: package analyzer // import \"hypermind.cn/talon/analyzer\" 代码包analyzer实际上属于我的一个网络爬虫项目。这个项目的代码被托管在了Github网站上。它的网址是:https://github.com/hyper-carrot/talon。如果用标准的导入路径来下载analyzer代码包的话,命令应该这样写go get github.com/hyper-carrot/talon/analyzer。不过,如果我们像上面的示例那样在该代码包中的一个源码文件中加入导入注释的话,这样下载它就行不通了。我们来看一看这个导入注释。 导入注释的写法如同一条代码包导入语句。不同的是,它出现在了单行注释符//的右边,因此Go语言编译器会忽略掉它。另外,它必须出现在源码文件的第一行语句(也就是代码包声明语句)的右边。只有符合上述这两个位置条件的导入注释才是有效的。再来看其中的引号部分。被双引号包裹的应该是一个符合导入路径语法规则的字符串。其中,hypermind.cn是我自己的一个域名。实际上,这也是用来替换掉我想隐去的代码托管网站域名及部分路径(这里是github.com/hyper-carrot)的那部分。在hypermind.cn右边的依次是我的项目的名称以及要下载的那个代码包的相对路径。这些与其标准导入路径中的内容都是一致的。为了清晰起见,我们再来做下对比。 github.com/hyper-carrot/talon/analyzer // 标准的导入路径 hypermind.cn /talon/analyzer // 导入注释中的导入路径 你想用你自己的域名替换掉标准导入路径中的哪部分由你自己说了算。不过一般情况下,被替换的部分包括代码托管网站的域名以及你在那里的用户ID就可以了。这足以达到我们最开始说的那两个目的。 虽然我们在talon项目中的所有代码包中都加入了类似的导入注释,但是我们依然无法通过go get hypermind.cn/talon/analyzer命令来下载这个代码包。因为域名hypermind.cn所指向的网站并没有加入相应的处理逻辑。具体的实现步骤应该是这样的: 编写一个可处理HTTP请求的程序。这里无所谓用什么编程语言去实现。当然,我推荐你用Go语言去做。 将这个处理程序与hypermind.cn/talon这个路径关联在一起,并总是在作为响应的HTML文档的头中写入下面这行内容: hypermind.cn/talon/analyzer熟悉HTML的读者都应该知道,这行内容会被视为HTML文档的元数据。它实际上go get命令的文档中要求的写法。它的模式是这样的: 实际上,content属性中的import-prefix的位置上应该填入我们自定义的远程代码包导入路径的前缀。这个前缀应该与我们的处理程序关联的那个路径相一致。而vcs显然应该代表与版本控制系统有关的标识。还记得表0-2中的主命令列吗?这里的填入内容就应该该列中的某一项。在这里,由于talon项目使用的是Git,所以这里应该填入git。至于repo-root,它应该是与该处理程序关联的路径对应的Github网站的URL。在这里,这个路径是hypermind.cn/talon,那么这个URL就应该是https://github.com/hyper-carrot/talon。后者也是talon项目的实际网址。 好了,在我们做好上述处理程序之后,go get hypermind.cn/talon/analyzer命令的执行结果就会是正确的。analyzer代码包及其依赖包中的代码会被下载到GOPATH环境变量中的第一个工作区目录的src子目录中,然后被编译并安装。 注意,具体的代码包源码存放路径会是/home/hc/golang/lib/src/hypermind.cn/talon/analyzer。也就是说,存放路径(包括代码包源码文件以及相应的归档文件的存放路径)会遵循导入注释中的路径(这里是hypermind.cn/talon/analyzer),而不是原始的导入路径(这里是github.com/hyper-carrot/talon/analyzer)。另外,我们只需在talon项目的每个代码包中的某一个源码文件中加入导入注释,但这些导入注释中的路径都必须是一致的。在这之后,我们就只能使用hypermind.cn/talon/作为talon项目中的代码包的导入路径前缀了。一个反例如下: hc@ubt:~$ go get github.com/hyper-carrot/talon/analyzer package github.com/hyper-carrot/talon/analyzer: code in directory /home/hc/golang/lib/src/github.com/hyper-carrot/talon/analyzer expects import \"hypermind.cn/talon/analyzer\" 与自定义的代码包远程导入路径有关的内容我们就介绍到这里。从中我们也可以看出,Go语言为了让使用者的项目与代码托管网站隔离所作出的努力。只要你有自己的网站和一个不错的域名,这就很容易搞定并且非常值得。这会在你的代码包的使用者面前强化你的品牌,而不是某个代码托管网站的。当然,使你的代码包导入路径整齐划一是最直接的好处。 OK,言归正传,我下面继续关注go get这个命令本身。 命令特有标记 go get命令可以接受所有可用于go build命令和go install命令的标记。这是因为go get命令的内部步骤中完全包含了编译和安装这两个动作。另外,go get命令还有一些特有的标记,如下表所示: 表0-4 go get命令的特有标记说明 标记名称 标记描述 -d 让命令程序只执行下载动作,而不执行安装动作。 -f 仅在使用-u标记时才有效。该标记会让命令程序忽略掉对已下载代码包的导入路径的检查。如果下载并安装的代码包所属的项目是你从别人那里Fork过来的,那么这样做就尤为重要了。 -fix 让命令程序在下载代码包后先执行修正动作,而后再进行编译和安装。 -insecure 允许命令程序使用非安全的scheme(如HTTP)去下载指定的代码包。如果你用的代码仓库(如公司内部的Gitlab)没有HTTPS支持,可以添加此标记。请在确定安全的情况下使用它。 -t 让命令程序同时下载并安装指定的代码包中的测试源码文件中依赖的代码包。 -u 让命令利用网络来更新已有代码包及其依赖包。默认情况下,该命令只会从网络上下载本地不存在的代码包,而不会更新已有的代码包。 为了更好的理解这几个特有标记,我们先清除Lib工作区的src目录和pkg目录中的所有子目录和文件。现在我们使用带有-d标记的go get命令来下载同样的代码包: hc@ubt:~$ go get -d github.com/hyper-carrot/go_lib/logging 现在,让我们再来看一下Lib工作区的目录结构: /home/hc/golang/lib: bin/ pkg/ src/ github.com/ hyper-carrot/ go_lib/ logging/ ... 我们可以看到,go get命令只将代码包下载到了Lib工作区的src目录,而没有进行后续的编译和安装动作。这个加入-d标记的结果。 再来看-fix标记。我们知道,绝大多数计算机编程语言在进行升级和演进过程中,不可能保证100%的向后兼容(Backward Compatibility)。在计算机世界中,向后兼容是指在一个程序或者代码库在更新到较新的版本后,用旧的版本程序创建的软件和系统仍能被正常操作或使用,或在旧版本的代码库的基础上编写的程序仍能正常编译运行的能力。Go语言的开发者们已想到了这点,并提供了官方的代码升级工具——fix。fix工具可以修复因Go语言规范变更而造成的语法级别的错误。关于fix工具,我们将放在本节的稍后位置予以说明。 假设我们本机安装的Go语言版本是1.5,但我们的程序需要用到一个很早之前用Go语言的0.9版本开发的代码包。那么我们在使用go get命令的时候可以加入-fix标记。这个标记的作用是在检出代码包之后,先对该代码包中不符合Go语言1.5版本的语言规范的语法进行修正,然后再下载它的依赖包,最后再对它们进行编译和安装。 标记-u的意图和执行的动作都比较简单。我们在执行go get命令时加入-u标记就意味着,如果在本地工作区中已存在相关的代码包,那么就是用对应的代码版本控制系统的更新命令更新它,并进行编译和安装。这相当于强行更新指定的代码包及其依赖包。我们来看如下示例: hc@ubt:~$ go get -v github.com/hyper-carrot/go_lib/logging 因为我们在之前已经检出并安装了这个代码包,所以我们执行上面这条命令后什么也没发生。还记得加入标记-v标记意味着会打印出被构建的代码包的名字吗?现在我们使用标记-u来强行更新代码包: hc@ubt:~$ go get -v -u github.com/hyper-carrot/go_lib/logging github.com/hyper-carrot/go_lib (download) 其中,“(download)”后缀意味着命令从远程仓库检出或更新了该行显示的代码包。如果我们要查看附带-u的go get命令到底做了些什么,还可以加上一个-x标记,以打印出用到的命令。读者可以自己试用一下它。 智能的下载 命令go get还有一个很值得称道的功能。在使用它检出或更新代码包之后,它会寻找与本地已安装Go语言的版本号相对应的标签(tag)或分支(branch)。比如,本机安装Go语言的版本是1.x,那么go get命令会在该代码包的远程仓库中寻找名为“go1”的标签或者分支。如果找到指定的标签或者分支,则将本地代码包的版本切换到此标签或者分支。如果没有找到指定的标签或者分支,则将本地代码包的版本切换到主干的最新版本。 前面我们说在执行go get命令时也可以加入-x标记,这样可以看到go get命令执行过程中所使用的所有命令。不知道读者是否已经自己尝试了。下面我们还是以代码包github.com/hyper-carrot/go_lib为例,并且通过之前示例中的命令的执行此代码包已经被检出到本地。这时我们再次更新这个代码包: hc@ubt:~$ go get -v -u -x github.com/hyper-carrot/go_lib github.com/hyper-carrot/go_lib (download) cd /home/hc/golang/lib/src/github.com/hyper-carrot/go_lib git fetch cd /home/hc/golang/lib/src/github.com/hyper-carrot/go_lib git show-ref cd /home/hc/golang/lib/src/github.com/hyper-carrot/go_lib git checkout origin/master WORK=/tmp/go-build034263530 在上述示例中,go get命令通过git fetch命令将所有远程分支更新到本地,而后有用git show-ref命令列出本地和远程仓库中记录的代码包的所有分支和标签。最后,当确定没有名为“go1”的标签或者分支后,go get命令使用git checkout origin/master命令将代码包的版本切换到主干的最新版本。下面,我们在本地增加一个名为“go1”的标签,看看go get命令的执行过程又会发生什么改变: hc@ubt:~$ cd ~/golang/lib/src/github.com/hyper-carrot/go_lib hc@ubt:~/golang/lib/src/github.com/hyper-carrot/go_lib$ git tag go1 hc@ubt:~$ go get -v -u -x github.com/hyper-carrot/go_lib github.com/hyper-carrot/go_lib (download) cd /home/hc/golang/lib/src/github.com/hyper-carrot/go_lib git fetch cd /home/hc/golang/lib/src/github.com/hyper-carrot/go_lib git show-ref cd /home/hc/golang/lib/src/github.com/hyper-carrot/go_lib git show-ref tags/go1 origin/go1 cd /home/hc/golang/lib/src/github.com/hyper-carrot/go_lib git checkout tags/go1 WORK=/tmp/go-build636338114 将这两个示例进行对比,我们会很容易发现它们之间的区别。第二个示例的命令执行过程中使用git show-ref查看所有分支和标签,当发现有匹配的信息又通过git show-ref tags/go1 origin/go1命令进行精确查找,在确认无误后将本地代码包的版本切换到标签“go1”之上。 命令go get的这一功能是非常有用的。我们的代码在直接或间接依赖某些同时针对多个Go语言版本开发的代码包时,可以自动的检出其正确的版本。也可以说,go get命令内置了一定的代码包多版本依赖管理的功能。 到这里,我向大家介绍了go get命令的使用方式。go get命令与之前介绍的两个命令一样,是我们编写Go语言程序、构建Go语言项目时必不可少的辅助工具。 © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:09:35 "},"go/cmd/clean.html":{"url":"go/cmd/clean.html","title":"go clean","keywords":"","body":"go clean go clean 执行go clean命令会删除掉执行其它命令时产生的一些文件和目录,包括: 在使用go build命令时在当前代码包下生成的与包名同名或者与Go源码文件同名的可执行文件。在Windows下,则是与包名同名或者Go源码文件同名且带有“.exe”后缀的文件。 在执行go test命令并加入-c标记时在当前代码包下生成的以包名加“.test”后缀为名的文件。在Windows下,则是以包名加“.test.exe”后缀为名的文件。我们会在后面专门介绍go test命令。 如果执行go clean命令时带有标记-i,则会同时删除安装当前代码包时所产生的结果文件。如果当前代码包中只包含库源码文件,则结果文件指的就是在工作区的pkg目录的相应目录下的归档文件。如果当前代码包中只包含一个命令源码文件,则结果文件指的就是在工作区的bin目录下的可执行文件。 还有一些目录和文件是在编译Go或C源码文件时留在相应目录中的。包括:“_obj”和“_test”目录,名称为“_testmain.go”、“test.out”、“build.out”或“a.out”的文件,名称以“.5”、“.6”、“.8”、“.a”、“.o”或“.so”为后缀的文件。这些目录和文件是在执行go build命令时生成在临时目录中的。如果你忘记了这个临时目录是怎么回事儿,可以再回顾一下前面关于go build命令的介绍。临时目录的名称以go-build为前缀。 如果执行go clean命令时带有标记-r,则还包括当前代码包的所有依赖包的上述目录和文件。 我们再以goc2p项目的logging为例。为了能够反复体现每个标记的作用,我们会使用标记n。使用标记-n会让命令在执行过程中打印用到的系统命令,但不会真正执行它们。如果想既打印命令又执行命令则需使用标记-x。现在我们来试用一下go clean命令: hc@ubt:~/golang/goc2p/src$ go clean -x logging cd /home/hc/golang/goc2p/src/logging rm -f logging logging.exe logging.test logging.test.exe 现在,我们加上标记-i: hc@ubt:~/golang/goc2p/src$ go clean -x -i logging cd /home/hc/golang/goc2p/src/logging rm -f logging logging.exe logging.test logging.test.exe rm -f /home/hc/golang/goc2p/pkg/linux_386/logging.a 如果再加上标记-r又会打印出哪些命令呢?请读者自己试一试吧。 © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:09:32 "},"go/cmd/doc-godoc.html":{"url":"go/cmd/doc-godoc.html","title":"go doc 与 godoc","keywords":"","body":"go doc 与 godoc go doc与godoc go doc go doc命令可以打印附于Go语言程序实体上的文档。我们可以通过把程序实体的标识符作为该命令的参数来达到查看其文档的目的。 插播:所谓Go语言的程序实体,是指变量、常量、函数、结构体以及接口。而程序实体的标识符即是代表它们的名称。标识符又分非限定标识符和限定标识符。其中,限定标识符一般用于表示某个代码包中的程序实体或者某个结构体类型中的方法或字段。例如,标准库代码包io中的名为EOF的变量用限定标识符表示即io.EOF。又例如,如果我有一个sync.WaitGroup类型的变量wg并且想调用它的Add方法,那么可以这样写wg.Add()。其中,wg.Add就是一个限定标识符,而后面的()则代表了调用操作。 下面说明怎样使用go doc命令。先来看一下go doc命令可接受的标记。 表0-5 go doc命令的标记说明 标记名称 标记描述 -c 加入此标记后会使go doc命令区分参数中字母的大小写。默认情况下,命令是大小写不敏感的。 -cmd 加入此标记后会使go doc命令同时打印出main包中的可导出的程序实体(其名称的首字母大写)的文档。默认情况下,这部分文档是不会被打印出来的。 -u 加入此标记后会使go doc命令同时打印出不可导出的程序实体(其名称的首字母小写)的文档。默认情况下,这部分文档是不会被打印出来的。 这几个标记的意图都非常简单和明确,大家可以根据实际情况选用。 go doc命令可以后跟一个或两个参数。当然,我们也可以不附加任务参数。如果不附加参数,那么go doc命令会试图打印出当前目录所代表的代码包的文档及其中的包级程序实体的列表。 例如,我要在goc2p项目的loadgen代码包所在目录中运行go doc命令的话,那么就会是这样: hc@ubt:~/golang/goc2p/src/loadgen$ go doc package loadgen // import \"loadgen\" func NewGenerator( caller lib.Caller, timeoutNs time.Duration, lps uint32, durationNs time.Duration, resultCh chan *lib.CallResult) (lib.Generator, error) 如果你需要指定代码包或程序实体,那么就需要在go doc命令后附上参数了。例如,只要我本地的goc2p项目的所在目录存在于GOPATH环境变量中,我就可以在任意目录中敲入go doc loadgen。如此得到的输出一定是与上面那个示例一致的。 看过loadgen代码包中源码的读者会知道,其中只有一个可导出的程序实体,即NewGenerator函数。这也是上述示例中如此输出的原因。该代码包中的结构体类型myGenerator是不可导出,但是我们只需附加-u标记便可查看它的文档了: hc@ubt:~$ go doc -u loadgen.myGenerator type myGenerator struct { caller lib.Caller // 调用器。 timeoutNs time.Duration // 处理超时时间,单位:纳秒。 lps uint32 // 每秒载荷量。 durationNs time.Duration // 负载持续时间,单位:纳秒。 concurrency uint32 // 并发量。 tickets lib.GoTickets // Goroutine票池。 stopSign chan byte // 停止信号的传递通道。 cancelSign byte // 取消发送后续结果的信号。 endSign chan uint64 // 完结信号的传递通道,同时被用于传递调用执行计数。 callCount uint64 // 调用执行计数。 status lib.GenStatus // 状态。 resultCh chan *lib.CallResult // 调用结果通道。 } 载荷发生器的实现。 func (gen *myGenerator) Start() func (gen *myGenerator) Status() lib.GenStatus func (gen *myGenerator) Stop() (uint64, bool) func (gen *myGenerator) asyncCall() func (gen *myGenerator) genLoad(throttle 如此一来,loadgen.myGenerator类型的文档、字段和方法都尽收眼底。注意,这里我们使用到了限定标识符。下面再进一步,如果你只想查看loadgen.myGenerator类型的init方法的文档,那么只要续写这个限定标识符就可以了,像这样: hc@ubt:~$ go doc -u loadgen.myGenerator.init func (gen *myGenerator) init() error 初始化载荷发生器。 注意,结构体类型中的字段的文档是无法被单独打印的。另外,go doc命令根据参数查找代码包或程序实体的顺序是:先Go语言根目录(即GOROOT所环境变量指定的那个目录)后工作区目录(即GOPATH环境变量包含的那些目录)。并且,在前者或后者中,go doc命令的查找顺序遵循字典序。因此,如果某个工作区目录中的代码包与标准库中的包重名了,那么它是无法被打印出来的。go doc命令只会打印出第一个匹配的代码包或程序实体的文档。 我们在前面说过,go doc命令还可以接受两个参数。这是一种更加精细的指定代码包或程序实体的方式。一个显著的区别是,如果你想打印标准库代码包net/http中的结构体类型Request的文档,那么可以这样敲入go doc命令: go doc http.Request 注意,这里并没有写入net/http代码包的导入路径,而只是写入了其中的最后一个元素http。但是如果你把http.Request拆成两个参数(即http Request)的话,命令程序就会什么也查不到了。因为这与前一种用法的解析方式是不一样的。正确的做法是,当你指定两个参数时,作为第一个参数的代码包名称必须是完整的导入路径,即:在敲入命令go doc net/http Request后,你会得到想要的结果。 最后,在给定两个参数时,go doc会打印出所有匹配的文档,而不是像给定一个参数时那样只打印出第一个匹配的文档。这对于查找只有大小写不同的多个方法(如New和new)的文档来说非常有用。 godoc 命令godoc是一个很强大的工具,同样用于展示指定代码包的文档。在Go语言的1.5版本中,它是一个内置的标准命令。 该命令有两种模式可供选择。如果在执行命令时不加入-http标记,则该命令就以命令行模式运行。在打印纯文本格式的文档到标准输出后,命令执行就结束了。比如,我们用命令行模式查看代码包fmt的文档: hc@ubt:~$ godoc fmt 为了节省篇幅,我们在这里略去了文档查询结果。读者可以自己运行一下上述命令。在该命令被执行之后,我们就可以看到编排整齐有序的文档内容了。这包括代码包fmt及其中所有可导出的包级程序实体的声明、文档和例子。 有时候我们只是想查看某一个函数或者结构体类型的文档,那么我们可以将这个函数或者结构体的名称加入命令的后面,像这样: hc@ubt:~$ godoc fmt Printf 或者: hc@ubt:~$ godoc os File 如果我们想同时查看一个代码包中的几个函数的文档,则仅需将函数或者结构体名称追加到命令后面。比如我们要查看代码包fmt中函数Printf和函数Println的文档: hc@ubt:~$ godoc fmt Printf Println 如果我们不但想在文档中查看可导出的程序实体的声明,还想看到它们的源码,那么我们可以在执行godoc命令的时候加入标记-src,比如这样: hc@ubt:~$ godoc -src fmt Printf Go语言为程序使用示例代码设立了专有的规则。我们在这里暂不讨论这个规则的细节。只需要知道正因为有了这个专有规则,使得godoc命令可以根据这些规则提取相应的示例代码并把它们加入到对应的文档中。如果我们想在查看代码包net中的结构体类型Listener的文档的同时查看关于它的示例代码,那么我们只需要在执行命令时加入标记-ex。使用方法如下: hc@ubt:~$ godoc -ex net/http FileServer 注意,我们在使用godoc命令时,只能把代码包和程序实体的标识符拆成两个参数。也就是说,godoc命令不支持前文所述的go doc命令的单参数用法。 在实际的Go语言环境中,我们可能会遇到一个命令源码文件所产生的可执行文件与代码包重名的情况。比如,这里介绍的标准命令go和官方代码包go。现在我们要明确的告诉godoc命令要查看可执行文件go的文档,我们需要在名称前加入cmd/前缀: hc@ubt:~$ godoc cmd/go 另外,如果我们想查看HTML格式的文档,就需要加入标记-html。当然,这样在命令行模式下的查看效果是很差的。但是,如果仔细查看的话,可以在其中找到一些相应源码的链接地址。 一般情况下,godoc命令会去Go语言根目录和环境变量GOPATH包含的工作区目录中查找代码包。我们可以通过加入标记-goroot来制定一个Go语言根目录。这个被指定的Go语言根目录仅被用于当次命令的执行。示例如下: hc@ubt:~$ godoc -goroot=\"/usr/local/go\" fmt 现在让我们来看看另外一种模式。如果我们在执行命令时加上-http标记则会启用另一模式。这种模式被叫做Web服务器模式,它以Web页面的形式提供Go语言文档。 我们使用如下命令启动这个文档Web服务器: hc@ubt:~/golang/goc2p$ godoc -http=:6060 标记-http的值:6060表示启动的Web服务器使用本机的6060端口。之后,我们就可以通过在网络浏览器的地址栏中输入http://localhost:6060来查看以网页方式展现的Go文档了。 图0-1 本机的Go文档Web服务首页 这与Go语言官方站点的Web服务页面如出一辙。这使得我们在不方便访问Go语言官方站点的情况下也可以查看Go语言文档。并且,更便利的是,通过本机的Go文档Web服务,我们还可以查看所有本机工作区下的代码的文档。比如,goc2p项目中的代码包pkgtool的页面如下图: 图0-2 goc2p项目中的pkgtool包的Go文档页面 现在,我们在本机开启Go文档Web服务器,端口为9090。命令如下: hc@ubt:~$ godoc -http=:9090 -index 注意,要使用-index标记开启搜索索引。这个索引会在服务器启动时创建并维护。如果不加入此标记,那么无论在Web页面还是命令行终端中都是无法进行查询操作的。 索引中提供了标识符和全文本搜索信息(通过正则表达式为可搜索性提供支持)。全文本搜索结果显示条目的最大数量可以通过标记-maxresults提供。标记-maxresults默认值是10000。如果不想提供如此多的结果条目,可以设置小一些的值。甚至,如果不想提供全文本搜索结果,可以将标记-maxresults的值设置为0,这样服务器就只会创建标识符索引,而根本不会创建全文本搜索索引了。标识符索引即为对程序实体名称的索引。 正因为在使用了-index标记的情况下文档服务器会在启动时创建索引,所以在文档服务器启动之后还不能立即提供搜索服务,需要稍等片刻。在索引为被创建完毕之前,我们的搜索操作都会得到提示信息“Indexing in progress: result may be inaccurate”。 如果我们在本机用godoc命令启动了Go文档Web服务器,且IP地址为192.168.1.4、端口为9090,那么我们就可以在另一个命令行终端甚至另一台能够与本机联通的计算机中通过如下命令进行查询了。查询命令如下: hc@ubt:~$ godoc -q -server=\"192.168.1.4:9090\" Listener 命令的最后为要查询的内容,可以是任何你想搜索的字符串,而不仅限于代码包、函数或者结构体的名称。 标记-q开启了远程查询的功能。而标记-server=\"192.168.1.4:9090\"则指明了远程文档服务器的IP地址和端口号。实际上,如果不指明远程查询服务器的地址,那么该命令会自行将地址“:6060”和“golang.org”作为远程查询服务器的地址。这两个地址即是默认的本机文档Web站点地址和官方的文档Web站点地址。所以执行如下命令我们也可以查询到标准库的信息: hc@ubt:~$ godoc -q fmt 命令godoc还有很多可用的标记,但在通常情况下并不常用。读者如果有兴趣,可以在命令行环境下敲入godoc并查看其文档。 至于怎样才能写出优秀的代码包文档,我在《Go并发编程实战》的5.2节中做了详细说明。 © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:09:33 "},"go/cmd/run.html":{"url":"go/cmd/run.html","title":"go run","keywords":"","body":"go run go run 在《Go并发编程实战》的第二章中,我介绍了Go源码文件的分类。Go源码文件包括:命令源码文件、库源码文件和测试源码文件。其中,命令源码文件总应该属于main代码包,且在其中有无参数声明、无结果声明的main函数。单个命令源码文件可以被单独编译,也可以被单独安装(可能需要设置环境变量GOBIN)。当然,命令源码文件也可以被单独运行。我们想要运行命令源码文件就需要使用命令go run。 go run命令可以编译并运行命令源码文件。由于它其中包含了编译动作,因此它也可以接受所有可用于go build命令的标记。除了标记之外,go run命令只接受Go源码文件作为参数,而不接受代码包。与go build命令和go install命令一样,go run命令也不允许多个命令源码文件作为参数,即使它们在同一个代码包中也是如此。而原因也是一致的,多个命令源码文件会都有main函数声明。 如果命令源码文件可以接受参数,那么在使用go run命令运行它的时候就可以把它的参数放在它的文件名后面,像这样: hc@ubt:~/golang/goc2p/src/helper/ds$ go run showds.go -p ~/golang/goc2p 在上面的示例中,我们使用go run命令运行命令源码文件showds.go。这个命令源码文件可以接受一个名称为“p”的参数。我们用“-p”这种形式表示“p”是一个参数名而不是参数值。它与源码文件名之间需要用空格隔开。参数值会放在参数名的后面,两者成对出现。它们之间也要用空格隔开。如果有第二个参数,那么第二个参数的参数名与第一个参数的参数值之间也要有一个空格。以此类推。 go run命令只能接受一个命令源码文件以及若干个库源码文件(必须同属于main包)作为文件参数,且不能接受测试源码文件。它在执行时会检查源码文件的类型。如果参数中有多个或者没有命令源码文件,那么go run命令就只会打印错误提示信息并退出,而不会继续执行。 在通过参数检查后,go run命令会将编译参数中的命令源码文件,并把编译后的可执行文件存放到临时工作目录中。 编译和运行过程 为了更直观的体现出go run命令中的操作步骤,我们在执行命令时加入标记-n,用于打印相关命令而不实际执行。现在让我们来模拟运行goc2p项目中的代码包helper/ds的命令源码文件showds.go。示例如下: hc@ubt:~/golang/goc2p/src/helper/ds$ go run -n showds.go # # command-line-arguments # mkdir -p $WORK/command-line-arguments/_obj/ mkdir -p $WORK/command-line-arguments/_obj/exe/ cd /home/hc/golang/goc2p/src/helper/ds /usr/local/go1.5/pkg/tool/linux_amd64/compile -o $WORK/command-line-arguments.a -trimpath $WORK -p main -complete -buildid df49387da030ad0d3bebef3f046d4013f8cb08d3 -D _/home/hc/golang/goc2p/src/helper/ds -I $WORK -pack ./showds.go cd . /usr/local/go1.5/pkg/tool/linux_amd64/link -o $WORK/command-line-arguments/_obj/exe/showds -L $WORK -w -extld=clang -buildmode=exe -buildid=df49387da030ad0d3bebef3f046d4013f8cb08d3 $WORK/command-line-arguments.a $WORK/command-line-arguments/_obj/exe/showds 在上面的示例中并没有显示针对命令源码文件showds.go的依赖包进行编译和运行的相关打印信息。这是因为该源码文件的所有依赖包已经在之前被编译过了。 现在,我们来逐行解释这些被打印出来的信息。 以前缀“#”开始的是注释信息。我们看到信息中有三行注释信息,并在中间行出现了内容“command-line-arguments”。我们在讲go build命令的时候说过,编译命令在分析参数的时候如果发现第一个参数是Go源码文件而不是代码包时,会在内部生成一个名为“command-line-arguments”的虚拟代码包。所以这里的注释信息就是要告诉我们下面的几行信息是关于虚拟代码包“command-line-arguments”的。 打印信息中的“$WORK”表示临时工作目录的绝对路径。为了存放对虚拟代码包“command-line-arguments”的编译结果,命令在临时工作目录中创建了名为command-line-arguments的子目录,并在其下又创建了_obj子目录和_obj/exe子目录。 然后,命令程序使用Go语言工具目录compile命令对命令源码文件showds.go进行了编译,并把结果文件存放到了$WORK目录下,名为command-line-arguments.a。其中,compile是Go语言自带的编程工具。 在编译成功之后,命令程序使用链接命令link生成最终的可执行文件,并将其存于$WORK/command-line-arguments/_obj/exe/目录中。打印信息中的最后一行表示,命令运行了生成的可执行文件。 通过对这些打印出来的命令的解读,我们了解了临时工作目录的用途以和内容。 在上面的示例中,我们只是让go run命令打印出运行命令源码文件showds.go过程中需要执行的命令,而没有真正运行它。如果我们想真正运行命令源码文件showds.go并且想知道临时工作目录的位置,就需要去掉标记-n并且加上标记-work。当然,如果依然想看到过程中执行的命令,可以加上标记-x。如果读者已经看过之前我们对go build命令的介绍,就应该知道标记-x与标记-n一样会打印出过程执行的命令,但不同的这些命令会被真正的执行。调整这些标记之后的命令就像这样: hc@ubt:~/golang/goc2p/src/helper/ds$ go run -x -work showds.go 当命令真正执行后,临时工作目录中就会出现实实在在的内容了,像这样: /tmp/go-build604555989: command-line-arguments/ _obj/ exe/ showds command-line-arguments.a 由于上述命令中包含了-work标记,所以我们可以从其输出中找到实际的工作目录(这里是/tmp/go-build604555989)。有意思的是,我们恰恰可以通过运行命令源码文件showds.go来查看这个临时工作目录的目录树: hc@ubt:~/golang/goc2p/src/helper/ds$ go run showds.go -p /tmp/go-build604555989 读者可以自己试一试。 我们在前面介绍过,命令源码文件如果可以接受参数,则可以在执行go run命令运行这个命令源码文件时把参数名和参数值成对的追加在后面。实际上,如果在命令后追加参数,那么在最后执行生成的可执行文件的时候也会追加一致的参数。例如,如果这样执行命令: hc@ubt:~/golang/goc2p/src/helper/ds$ go run -n showds.go -p ~/golang/goc2p 那么带-x或-n标记的命令程序打印的最后一个命令就是: $WORK/command-line-arguments/_obj/exe/showds -p /home/hc/golang/goc2p 可见,go run命令会把追加到命令源码文件后面的参数原封不动的传给对应的可执行文件。 以上简要展示了一个命令源码文件从编译到运行的全过程。请记住,go run命令包含了两个动作:编译命令源码文件和运行对应的可执行文件。 © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:09:38 "},"go/cmd/test.html":{"url":"go/cmd/test.html","title":"go test","keywords":"","body":"go test go test go test命令用于对Go语言编写的程序进行测试。这种测试是以代码包为单位的。当然,这还需要测试源码文件的帮助。关于怎样编写并写好Go程序测试代码,我们会在本章的第二节加以详述。在这里,我们只讨论怎样使用命令启动测试。 go test命令会自动测试每一个指定的代码包。当然,前提是指定的代码包中存在测试源码文件。关于测试源码文件方面的知识,在我的图书《Go并发编程实战》中有详细介绍。测试源码文件是名称以“_test.go”为后缀的、内含若干测试函数的源码文件。测试函数一般是以“Test”为名称前缀并有一个类型为“testing.T”的参数声明的函数. 现在,我们来测试goc2p项目中的几个代码包。在使用go test命令时指定代码包的方式与其他命令无异——使用代码包导入路径。如果需要测试多个代码包,则需要在它们的导入路径之间加入空格以示分隔。示例如下: hc@ubt:~$ go test basic cnet/ctcp pkgtool ok basic 0.012s ok cnet/ctcp 2.014s ok pkgtool 0.014s go test命令在执行完所有的代码包中的测试文件之后,会以代码包为单位打印出测试概要信息。在上面的示例中,对应三个代码包的三行信息的第一列都是“ok”。这说明它们都通过了测试。每行的第三列显示运行相应测试所用的时间,以秒为单位。我们还可以在代码包目录下运行不加任何参数的运行go test命令。其作用和结果与上面的示例是一样的。 另外,我们还可以指定测试源码文件来进行测试。这样的话,go test命令只会执行指定文件中的测试,像这样: hc@ubt:~/golang/goc2p/src/pkgtool$ go test envir_test.go # command-line-arguments ./envir_test.go:25: undefined: GetGoroot ./envir_test.go:40: undefined: GetAllGopath ./envir_test.go:81: undefined: GetSrcDirs ./envir_test.go:83: undefined: GetAllGopath ./envir_test.go:90: undefined: GetGoroot FAIL command-line-arguments [build failed] 我们看到,与指定源码文件进行编译或运行一样,命令程序会为指定的源码文件生成一个虚拟代码包——“command-line-arguments”。但是,测试并没有通过。但其原因并不是测试失败,而是编译失败。对于运行这次测试的命令程序来说,测试源码文件envir_test.go是属于代码包“command-line-arguments”的。并且,这个测试源码文件中使用了库源码文件envir.go中的函数。但是,它却没有显示导入这个库源码文件所属的代码包。这显然会引起编译错误。如果想解决这个问题,我们还需要在执行命令时加入这个测试源码文件所测试的那个源码文件。示例如下: hc@ubt:~/golang/goc2p/src/pkgtool$ go test envir_test.go envir.go ok command-line-arguments 0.010s 现在,我们故意使代码包pkgtool中的某个测试失败。现在我们再来运行测试: hc@ubt:~$ go test basic cnet/ctcp pkgtool ok basic 0.010s ok cnet/ctcp 2.015s --- FAIL: TestGetSrcDirs (0.00 seconds) envir_test.go:85: Error: The src dir '/usr/local/go/src/pkg' is incorrect. FAIL FAIL pkgtool 0.009s 我们通过以上示例中的概要信息获知,测试源码文件中envir_test.go的测试函数TestGetSrcDirs中的测试失败了。在包含测试失败的测试源码文件名的那一行信息中,紧跟测试源码文件名的用冒号分隔的数字是错误信息所处的行号,在行号后面用冒号分隔的是错误信息。这个错误信息的内容是用户自行编写的。另外,概要信息的最后一行以“FAIL”为前缀。这表明针对代码包pkgtool的测试未通过。未通过的原因在前面的信息中已有描述。 一般情况下,我们会把测试源码文件与被测试的源码文件放在同一个代码包中。并且,这些源码文件中声明的包名也都是相同的。除此之外我们还有一种选择,那就是测试源码文件中声明的包名可以是所属包名再加“_test”后缀。我们把这种测试源码文件叫做包外测试源码文件。不过,包外测试源码文件存在一个弊端,那就是在它们的测试函数中无法测试被测源码文件中的包级私有的程序实体,比如包级私有的变量、函数和结构体类型。这是因为这两者的所属代码包是不相同的。所以,我们一般很少会编写包外测试源码文件。 关于标记 go test命令的标记处理部分是庞大且繁杂的,以至于使Go语言的开发者们不得不把这一部分的逻辑从go test命令程序主体中分离出来并建立单独的源码文件。因为go test命令中包含了编译动作,所以它可以接受可用于go build命令的所有标记。另外,它还有很多特有的标记。这些标记的用于控制命令本身的动作,有的用于控制和设置测试的过程和环境,还有的用于生成更详细的测试结果和统计信息。 可用于go test命令的几个比较常用的标记是-c、-i和-o。这两个就是用于控制go test命令本身的动作的标记。详见下表。 表0-6 go test命令的标记说明 标记名称 标记描述 -c 生成用于运行测试的可执行文件,但不执行它。这个可执行文件会被命名为“pkg.test”,其中的“pkg”即为被测试代码包的导入路径的最后一个元素的名称。 -i 安装/重新安装运行测试所需的依赖包,但不编译和运行测试代码。 -o 指定用于运行测试的可执行文件的名称。追加该标记不会影响测试代码的运行,除非同时追加了标记-c或-i。 上述这几个标记可以搭配使用。搭配使用的目的可以是让go test命令既安装依赖包又编译测试代码,但不运行测试。也就是说,让命令程序跑一遍运行测试之前的所有流程。这可以测试一下测试过程。注意,在加入-c标记后,命令程序会把用于运行测试的可执行文件存放到当前目录下。 除此之外,go test命令还有很多功效各异的标记。但是由于这些标记的复杂性,我们需要结合测试源码文件进行详细的讲解。所以我们在这里略过不讲。如果读者想了解相关详情,请参看《Go并发编程实战》的第5章。 © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:09:39 "},"go/cmd/list.html":{"url":"go/cmd/list.html","title":"go list","keywords":"","body":"go list go list go list命令的作用是列出指定的代码包的信息。与其他命令相同,我们需要以代码包导入路径的方式给定代码包。被给定的代码包可以有多个。这些代码包对应的目录中必须直接保存有Go语言源码文件,其子目录中的文件不算在内。否则,代码包将被看做是不完整的。现在我们来试用一下: hc@ubt:~$ go list cnet/ctcp pkgtool cnet/ctcp pkgtool 我们看到,在不加任何标记的情况下,命令的结果信息中只包含了我们指定的代码包的导入路径。我们刚刚提到,作为参数的代码包必须是完整的代码包。例如: hc@ubt:~$ go list cnet pkgtool can't load package: package cnet: no buildable Go source files in /home/hc/golang/goc2p/src/cnet/ pkgtool 这时,go list命令报告了一个错误——代码包cnet对应的目录下没有Go源码文件。但是命令还是把代码包pkgtool的导入路径打印出来了。然而,当我们在执行go list命令并加入标记-e时,即使参数中包含有不完整的代码包,命令也不会提示错误。示例如下: hc@ubt:~$ go list -e cnet pkgtool cnet pkgtool 标记-e的作用是以容错模式加载和分析指定的代码包。在这种情况下,命令程序如果在加载或分析的过程中遇到错误只会在内部记录一下,而不会直接把错误信息打印出来。我们为了看到错误信息可以使用-json标记。这个标记的作用是把代码包的结构体实例用JSON的样式打印出来。 这里解释一下,JSON的全称是Javascript Object Notation。它一种轻量级的承载数据的格式。JSON的优势在于语法简单、短小精悍,且非常易于处理。JSON还是一种纯文本格式,独立于编程语言。正因为如此,得到了绝大多数编程语言和浏览器的支持,应用非常广泛。Go语言当然也不例外,在它的标准库中有专门用于处理和转换JSON格式的数据的代码包encoding/json。关于JSON格式的具体内容,读者可以去它的官方网站查看说明。 在了解了这些基本概念之后,我们来试用一下-json标记。示例如下: hc@ubt:~$ go list -e -json cnet { \"Dir\": \"/home/hc/golang/goc2p/src/cnet\", \"ImportPath\": \"cnet\", \"Stale\": true, \"Root\": \"/home/hc/golang/goc2p\", \"Incomplete\": true, \"Error\": { \"ImportStack\": [ \"cnet\" ], \"Pos\": \"\", \"Err\": \"no Go source files in /home/hc/golang/goc2p/src/cnet\" } } 在上述JSON格式的代码包信息中,对于结构体中的字段的显示是不完整的。因为命令程序认为我们指定cnet就是不完整的。在名为Error的字段中,我们可以看到具体说明。Error字段的内容其实也是一个结构体。在JSON格式下,这种嵌套的结构体被完美的展现了出来。Error字段所指代的结构体实例的Err字段说明了cnet不完整的原因。这与我们在没有使用-e标记的情况下所打印出来的错误提示信息是一致的。我们再来看Incomplete字段。它的值为true。这同样说明cnet是一个不完整的代码包。 实际上,在从这个代码包结构体实例到JSON格式文本的转换过程中,所有的值为其类型的空值的字段都已经被忽略了。 现在我们使用带-json标记的go list命令列出代码包cnet/ctcp的信息: hc@ubt:~$ go list -json cnet/ctcp { \"Dir\": \"/home/hc/golang/github/goc2p/src/cnet/ctcp\", \"ImportPath\": \"cnet/ctcp\", \"Name\": \"ctcp\", \"Target\": \"/home/hc/golang/github/goc2p/pkg/darwin_amd64/cnet/ctcp.a\", \"Stale\": true, \"Root\": \"/home/hc/golang/github/goc2p\", \"GoFiles\": [ \"base.go\", \"tcp.go\" ], \"Imports\": [ \"bufio\", \"bytes\", \"errors\", \"logging\", \"net\", \"sync\", \"time\" ], \"Deps\": [ \"bufio\", \"bytes\", \"errors\", \"fmt\", \"internal/singleflight\", \"io\", \"log\", \"logging\", \"math\", \"math/rand\", \"net\", \"os\", \"reflect\", \"runtime\", \"runtime/cgo\", \"sort\", \"strconv\", \"strings\", \"sync\", \"sync/atomic\", \"syscall\", \"time\", \"unicode\", \"unicode/utf8\", \"unsafe\" ], \"TestGoFiles\": [ \"tcp_test.go\" ], \"TestImports\": [ \"bytes\", \"fmt\", \"net\", \"runtime\", \"strings\", \"sync\", \"testing\", \"time\" ] } 由于cnet/ctcp包是一个完整有效的代码包,所以我们不使用-e标记也是没有问题的。在上面打印的cnet/ctcp包的信息中没有Incomplete字段。这是因为完整的代码包中的Incomplete字段的其类型的空值false。它已经在转换过程中被忽略掉了。另外,在cnet/ctcp包的信息中我们看到了很多其它的字段。现在我就来看看在Go命令程序中的代码包结构体都有哪些公开的字段。如下表。 表0-7 代码包结构体中的基本字段 字段名称 字段类型 字段描述 Dir 字符串(string) 代码包对应的目录。 ImportPath 字符串(string) 代码包的导入路径。 ImportComment 字符串(string) 代码包声明语句右边的用于自定义导入路径的注释。 Name 字符串(string) 代码包的名称。 Doc 字符串(string) 代码包的文档字符串。 Target 字符串(string) 代码包的安装路径。 Shlib 字符串(string) 包含该代码包的共享库(shared library)的名称。 Goroot 布尔(bool) 该代码包是否在Go语言安装目录下。 Standard 布尔(bool) 该代码包是否属于标准库的一部分。 Stale 布尔(bool) 该代码包的最新代码是否未被安装。 Root 字符串(string) 该代码包所属的工作区或Go安装目录的路径。 表0-8 代码包结构体中与源码文件有关的字段 字段名称 字段类型 字段描述 GoFiles 字符串切片([]string) Go源码文件的列表。不包含导入了代码包“C”的源码文件和测试源码文件。 CgoFiles 字符串切片([]string) 导入了代码包“C”的源码文件的列表。 IgnoredGoFiles 字符串切片([]string) 忽略编译的源码文件的列表。 CFiles 字符串切片([]string) 名称中有“.c”后缀的源码文件的列表。 CXXFiles 字符串切片([]string) 名称中有“.cc”、“.cxx”或“.cpp”后缀的源码文件的列表。 MFiles 字符串切片([]string) 名称中“.m”后缀的源码文件的列表。 HFiles 字符串切片([]string) 名称中有“.h”后缀的源码文件的列表。 SFiles 字符串切片([]string) 名称中有“.s”后缀的源码文件的列表。 SwigFiles 字符串切片([]string) 名称中有“.swig”后缀的文件的列表。 SwigCXXFiles 字符串切片([]string) 名称中有“.swigcxx”后缀的文件的列表。 SysoFiles 字符串切片([]string) 名称中有“.syso”后缀的文件的列表。这些文件是需要被加入到归档文件中的。 表0-9 代码包结构体中与Cgo指令有关的字段 字段名称 字段类型 字段描述 CgoCFLAGS 字符串切片([]string) 需要传递给C编译器的标记的列表。针对Cgo。 CgoCPPFLAGS 字符串切片([]string) 需要传递给C预处理器的标记的列表。针对Cgo。 CgoCXXFLAGS 字符串切片([]string) 需要传递给C++编译器的标记的列表。针对Cgo。 CgoLDFLAGS 字符串切片([]string) 需要传递给链接器的标记的列表。针对Cgo。 CgoPkgConfig 字符串切片([]string) pkg-config的名称的列表。针对Cgo。 表0-10 代码包结构体中与依赖信息有关的字段 字段名称 字段类型 字段描述 Imports 字符串切片([]string) 该代码包中的源码文件显式导入的依赖包的导入路径的列表。 Deps 字符串切片([]string) 所有的依赖包(包括间接依赖)的导入路径的列表。 表0-11 代码包结构体中与错误信息有关的字段 字段名称 字段类型 字段描述 Incomplete 布尔(bool) 代码包是否是完整的,也即在载入或分析代码包及其依赖包时是否有错误发生。 Error *PackageError类型 载入或分析代码包时发生的错误。 DepsErrors []*PackageError类型 载入或分析依赖包时发生的错误。 表0-12 代码包结构体中与测试源码文件有关的字段 字段名称 字段类型 字段描述 TestGoFiles 字符串切片([]string) 代码包中的测试源码文件的名称列表。 TestImports 字符串切片([]string) 代码包中的测试源码文件显示导入的依赖包的导入路径的列表。 XTestGoFiles 字符串切片([]string) 代码包中的外部测试源码文件的名称列表。 XTestImports 字符串切片([]string) 代码包中的外部测试源码文件显示导入的依赖包的导入路径的列表。 代码包结构体中定义的字段很多,但有些时候我们只需要查看其中的一些字段。那要怎么做呢?标记-f可以满足这个需求。比如这样: hc@ubt:~$ go list -f {{.ImportPath}} cnet/ctcp cnet/ctcp 实际上,-f标记的默认值就是{{.ImportPath}}。这也正是我们在使用不加任何标记的go list命令时依然能看到指定代码包的导入路径的原因了。 标记-f的值需要满足标准库的代码包`text/template中定义的语法。比如,{{.S}}代表根结构体的S字段的值。在go list命令的场景下,这个根结构体就是指定的代码包所对应的结构体。如果S字段的值也是一个结构体的话,那么{{.S.F}}就代表根结构体的S字段的值中的F字段的值。如果我们要查看cnet/ctcp包中的命令源码文件和库源码文件的列表,可以这样使用-f标记: hc@ubt:~$ go list -f {{.GoFiles}} cnet/ctcp [base.go tcp.go] 如果我们想查看不完整的代码包cnet的错误提示信息,还可以这样: hc@ubt:~$ go list -e -f {{.Error.Err}} cnet no buildable Go source files in /home/hc/golang/goc2p/src/cnet 我们还可以利用代码包text/template中定义的强大语法让go list命令输出定制化更高的代码包信息。比如: hc@ubt:~$ go list -e -f 'The package {{.ImportPath}} is {{if .Incomplete}}incomplete!{{else}}complete.{{end}}' cnet The package cnet is incomplete! ```bash hc@ubt:~$ go list -f 'The imports of package {{.ImportPath}} is [{{join .Imports \", \"}}].' cnet/ctcp The imports of package cnet/ctcp is [bufio, bytes, errors, logging, net, sync, time]. 其中,join是命令程序在text/template包原有语法之上自定义的语法,在底层使用标准库代码包strings中的Join函数。关于更多的语法规则,请读者查看代码包text/template的相关文档。 另外,-tags标记也可以被go list接受。它与我们在讲go build命令时提到的-tags标记是一致的。读者可以查看代码包`go/build``的文档以了解细节。 go list命令很有用。它可以为我们提供指定代码包的更深层次的信息。这些信息往往是我们无法从源码文件中直观看到的。 © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:09:38 "},"go/cmd/fix-toolfix.html":{"url":"go/cmd/fix-toolfix.html","title":"go fix 与 go tool fix","keywords":"","body":"go fix 与 go tool fix go fix与go tool fix 命令go fix会把指定代码包的所有Go语言源码文件中的旧版本代码修正为新版本的代码。这里所说的版本即Go语言的版本。代码包的所有Go语言源码文件不包括其子代码包(如果有的话)中的文件。修正操作包括把对旧程序调用的代码更换为对新程序调用的代码、把旧的语法更换为新的语法,等等。 这个工具其实非常有用。在编程语言的升级和演进的过程中,难免会对过时的和不够优秀的语法及标准库进行改进。这样的改进对于编程语言的向后兼容性是个挑战。我们在前面提到过向后兼容这个词。简单来说,向后兼容性就是指新版本的编程语言程序能够正确识别和解析用该编程语言的旧版本编写的程序和软件,以及在新版本的编程语言的运行时环境中能够运行用该编程语言的旧版本编写的程序和软件。对于Go语言来说,语法的改变和标准库的变更都会使得用旧版本编写的程序无法在新版本环境中编译通过。这就等于破坏了Go语言的向后兼容性。对于一个编程语言、程序库或基础软件来说,向后兼容性是非常重要的。但有时候为了让软件更加优秀,软件的开发者或维护者不得不在向后兼容性上做出一些妥协。这是一个在多方利益之间进行权衡的结果。本小节所讲述的工具正是Go语言的创造者们为了不让这种妥协给语言使用者带来困扰和额外的工作量而编写的自动化修正工具。这也充分体现了Go语言的软件工程哲学。下面让我们来详细了解它们的使用方法和内部机理。 命令go fix其实是命令go tool fix的简单封装。这甚至比go fmt命令对gofmt命令的封装更简单。像其它的Go命令一样,go fix命令会先对作为参数的代码包导入路径进行验证,以确保它是正确有效的。像在本小节开始处描述的那样,go fix命令会把有效代码包中的所有Go语言源码文件作为多个参数传递给go tool fix命令。实际上,go fix命令本身不接受任何标记,它会把加入的所有标记都原样传递给go tool fix命令。go tool fix命令可接受的标记如下表。 表0-15 go tool fix命令的标记说明 标记名称 标记描述 -diff 不将修正后的内容写入文件,而只打印修正前后的内容的对比信息到标准输出。 -r 只对目标源码文件做有限的修正操作。该标记的值即为允许的修正操作的名称。多个名称之间用英文半角逗号分隔。 -force 使用此标记后,即使源码文件中的代码已经与Go语言的最新版本相匹配了,也会强行执行指定的修正操作。该标记的值就是需要强行执行的修正操作的名称,多个名称之间用英文半角逗号分隔。 在默认情况下,go tool fix命令程序会在目标源码文件上执行所有的修正操作。多个修正操作的执行会按照每个修正操作中标示的操作建立日期以从早到晚的顺序进行。我们可以通过执行go tool fix -?来查看go tool fix命令的使用说明以及当前支持的修正操作。 与本书对应的Go语言版本的go tool fix命令目前只支持两个修正操作。一个是与标准库代码包go/printer中的结构体类型Config的初始化代码相关的修正操作,另一个是与标准库代码包``net中的结构体类型IPAddr、UDPAddr和TCPAddr```的初始化代码相关的修正操作。从修正操作的数量来看,自第一个正式版发布以来,Go语言的向后兼容性还是很好的。从Go语言官网上的说明也可以获知,在Go语言的第二个大版本(Go 2.x)出现之前,它会一直良好的向后兼容性。 值得一提的是,上述的修正操作都是依靠Go语言的标准库代码包go及其子包中提供的功能来完成的。实际上,go tool fix命令程序在执行修正操作之前,需要先将目标源码文件中的内容解析为一个抽象语法树实例。这一功能其实就是由代码包go/parser提供的。而在这个抽象语法树实例中的各个元素的结构体类型的定义以及检测、访问和修改它们的方法则由代码包go/ast提供。有兴趣的读者可以阅读这些代码包中的代码。这对于深入理解Go语言对代码的静态处理过程是非常有好处的。 回到正题。与gofmt命令相同,go tool fix命令也有交互模式。我们同样可以通过执行不带任何参数的命令来进入到这个模式。但是与gofmt命令不同的是,我们在go tool fix命令的交互模式中输入的代码必须是完整的,即必须要符合Go语言源码文件的代码组织形式。当我们输入了不完整的代码片段时,命令程序将显示错误提示信息并退出。示例如下: hc@ubt:~$ go tool fix -r='netipv6zone' a := &net.TCPAddr{ip4, 8080} standard input:1:1: expected 'package', found 'IDENT' a 相对于上面的示例,我们必须要这样输入源码才能获得正常的结果: hc@ubt:~$ go tool fix -r='netipv6zone' package main import ( \"fmt\" \"net\" ) func main() { addr := net.TCPAddr{\"127.0.0.1\", 8080} fmt.Printf(\"TCP Addr: %s\\n\", addr) } standard input: fixed netipv6zone package main import ( \"fmt\" \"net\" ) func main() { addr := net.TCPAddr{IP: \"127.0.0.1\", Port: 8080} fmt.Printf(\"TCP Addr: %s\\n\", addr) } 上述示例的输出结果中有这样一行提示信息:“standard input: fixed netipv6zone”。其中,“standard input”表明源码是从标准输入而不是源码文件中获取的,而“fixed netipv6zone”则表示名为netipv6zone的修正操作发现输入的源码中有需要修正的地方,并且已经修正完毕。另外,我们还可以看到,输出结果中的代码已经经过了格式化。 © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:09:35 "},"go/cmd/vet-toolvet.html":{"url":"go/cmd/vet-toolvet.html","title":"go vet 与 go tool vet","keywords":"","body":"go vet 与 go tool vet go vet与go tool vet 命令go vet是一个用于检查Go语言源码中静态错误的简单工具。与大多数Go命令一样,go vet命令可以接受-n标记和-x标记。-n标记用于只打印流程中执行的命令而不真正执行它们。-n标记也用于打印流程中执行的命令,但不会取消这些命令的执行。示例如下: hc@ubt:~$ go vet -n pkgtool /usr/local/go/pkg/tool/linux_386/vet golang/goc2p/src/pkgtool/envir.go golang/goc2p/src/pkgtool/envir_test.go golang/goc2p/src/pkgtool/fpath.go golang/goc2p/src/pkgtool/ipath.go golang/goc2p/src/pkgtool/pnode.go golang/goc2p/src/pkgtool/util.go golang/goc2p/src/pkgtool/util_test.go go vet命令的参数既可以是代码包的导入路径,也可以是Go语言源码文件的绝对路径或相对路径。但是,这两种参数不能混用。也就是说,go vet命令的参数要么是一个或多个代码包导入路径,要么是一个或多个Go语言源码文件的路径。 go vet命令是go tool vet命令的简单封装。它会首先载入和分析指定的代码包,并把指定代码包中的所有Go语言源码文件和以“.s”结尾的文件的相对路径作为参数传递给go tool vet命令。其中,以“.s”结尾的文件是汇编语言的源码文件。如果go vet命令的参数是Go语言源码文件的路径,则会直接将这些参数传递给go tool vet命令。 如果我们直接使用go tool vet命令,则其参数可以传递任意目录的路径,或者任何Go语言源码文件和汇编语言源码文件的路径。路径可以是绝对的也可以是相对的。 实际上,vet属于Go语言自带的特殊工具,也是比较底层的命令之一。Go语言自带的特殊工具的存放路径是$GOROOT/pkg/tool/$GOOS$GOARCH/,我们暂且称之为Go工具目录。我们再来复习一下,环境变量GOROOT的值即Go语言的安装目录,环境变量GOOS的值代表程序构建环境的目标操作系统的标识,而环境变量$GOARCH的值则为程序构建环境的目标计算架构。另外,名为$GOOS$GOARCH的目录被叫做平台相关目录。Go语言允许我们通过执行go tool命令来运行这些特殊工具。在Linux 32bit的环境下,我们的Go语言安装目录是/usr/local/go/。因此,go tool vet命令指向的就是被存放在/usr/local/go/pkg/tool/linux_386目录下的名为vet的工具。 go tool vet命令的作用是检查Go语言源代码并且报告可疑的代码编写问题。比如,在调用Printf函数时没有传入格式化字符串,以及某些不标准的方法签名,等等。该命令使用试探性的手法检查错误,因此并不能保证报告的问题确实需要解决。但是,它确实能够找到一些编译器没有捕捉到的错误。 go tool vet命令程序在被执行后会首先解析标记并检查标记值。go tool vet命令支持的所有标记如下表。 表0-16 go tool vet命令的标记说明 标记名称 标记描述 -all 进行全部检查。如果有其他检查标记被设置,则命令程序会将此值变为false。默认值为true。 -asmdecl 对汇编语言的源码文件进行检查。默认值为false。 -assign 检查赋值语句。默认值为false。 -atomic 检查代码中对代码包sync/atomic的使用是否正确。默认值为false。 -buildtags 检查编译标签的有效性。默认值为false。 -composites 检查复合结构实例的初始化代码。默认值为false。 -compositeWhiteList 是否使用复合结构检查的白名单。仅供测试使用。默认值为true。 -methods 检查那些拥有标准命名的方法的签名。默认值为false。 -printf 检查代码中对打印函数的使用是否正确。默认值为false。 -printfuncs 需要检查的代码中使用的打印函数的名称的列表,多个函数名称之间用英文半角逗号分隔。默认值为空字符串。 -rangeloops 检查代码中对在```range```语句块中迭代赋值的变量的使用是否正确。默认值为false。 -structtags 检查结构体类型的字段的标签的格式是否标准。默认值为false。 -unreachable 查找并报告不可到达的代码。默认值为false。 在阅读上面表格中的内容之后,读者可能对这些标签的具体作用及其对命令程序检查步骤的具体影响还很模糊。不过没关系,我们下面就会对它们进行逐一的说明。 -all标记 如果标记-all有效(标记值不为false),那么命令程序会对目标文件进行所有已知的检查。实际上,标记-all的默认值就是true。也就是说,在执行go tool vet命令且不加任何标记的情况下,命令程序会对目标文件进行全面的检查。但是,只要有一个另外的标记(-compositeWhiteList和-printfuncs这两个标记除外)有效,命令程序就会把标记-all设置为false,并只会进行与有效的标记对应的检查。 -assign标记 如果标记-assign有效(标记值不为false),则命令程序会对目标文件中的赋值语句进行自赋值操作检查。什么叫做自赋值呢?简单来说,就是将一个值或者实例赋值给它本身。像这样: var s1 string = \"S1\" s1 = s1 // 自赋值 或者 s1, s2 := \"S1\", \"S2\" s2, s1 = s2, s1 // 自赋值 检查程序会同时遍历等号两边的变量或者值。在抽象语法树的语境中,它们都被叫做表达式节点。检查程序会检查等号两边对应的表达式是否相同。判断的依据是这两个表达式节点的字符串形式是否相同。在当前的场景下,这种相同意味着它们的变量名是相同的。如前面的示例。 有两种情况是可以忽略自赋值检查的。一种情况是短变量声明语句。根据Go语言的语法规则,当我们在函数中要在声明局部变量的同时对其赋值,就可以使用:=形式的变量赋值语句。这也就意味着:=左边的变量名称在当前的上下文环境中应该还未曾出现过(否则不能通过编译)。因此,在这种赋值语句中不可能出现自赋值的情况,忽略对它的检查也是合理的。另一种情况是等号左右两边的表达式个数不相等的变量赋值语句。如果在等号的右边是对某个函数或方法的调用,就会造成这种情况。比如: file, err := os.Open(wp) 很显然,这个赋值语句肯定不是自赋值语句。因此,不需要对此种情况进行检查。如果等号右边并不是对函数或方法调用的表达式,并且等号两边的表达式数量也不相等,那么势必会在编译时引发错误,也不必检查。 -atomic标记 如果标记-atomic有效(标记值不为false),则命令程序会对目标文件中的使用代码包sync/atomic进行原子赋值的语句进行检查。原子赋值语句像这样: var i32 int32 i32 = 0 newi32 := atomic.AddInt32(&i32, 3) fmt.Printf(\"i32: %d, newi32: %d.\\n\", i32, newi32) 函数AddInt32会原子性的将变量i32的值加3,并返回这个新值。因此上面示例的打印结果是: i32: 3, newi32: 3 在代码包sync/atomic中,与AddInt32类似的函数还有AddInt64、AddUint32、AddUint64和AddUintptr。检查程序会对上述这些函数的使用方式进行检查。检查的关注点在破坏原子性的使用方式上。比如: i32 = 1 i32 = atomic.AddInt32(&i32, 3) _, i32 = 5, atomic.AddInt32(&i32, 3) i32, _ = atomic.AddInt32(&i32, 1), 5 上面示例中的后三行赋值语句都属于原子赋值语句,但它们都破坏了原子赋值的原子性。以第二行的赋值语句为例,等号左边的atomic.AddInt32(&i32, 3)的作用是原子性的将变量i32的值增加3。但该语句又将函数的结果值赋值给变量i32,这个二次赋值属于对变量i32的重复赋值,也使原本拥有原子性的赋值操作被拆分为了两个步骤的非原子操作。如果在对变量i32的第一次原子赋值和第二次非原子的重复赋值之间又有另一个程序对变量i32进行了原子赋值,那么当前程序中的这个第二次赋值就破坏了那两次原子赋值本应有的顺序性。因为,在另一个程序对变量i32进行原子赋值后,当前程序中的第二次赋值又将变量i32的值设置回了之前的值。这显然是不对的。所以,上面示例中的第二行代码应该改为: atomic.AddInt32(&i32, 3) 并且,对第三行和第四行的代码也应该有类似的修改。检查程序如果在目标文件中查找到像上面示例的第二、三、四行那样的语句,就会打印出相应的错误信息。 另外,上面所说的导致原子性被破坏的重复赋值语句还有一些类似的形式。比如: i32p := &i32 *i32p = atomic.AddUint64(i32p, 1) 这与之前的示例中的代码的含义几乎是一样。另外还有: var counter struct{ N uint32 } counter.N = atomic.AddUint64(&counter.N, 1) 和 ns := []uint32{10, 20} ns[0] = atomic.AddUint32(&ns[0], 1) nps := []*uint32{&ns[0], &ns[1]} *nps[0] = atomic.AddUint32(nps[0], 1) 在最近的这两个示例中,虽然破坏原子性的重复赋值操作因结构体类型或者数组类型的介入显得并不那么直观了,但依然会被检查程序发现并及时打印错误信息。 顺便提一句,对于原子赋值语句和普通赋值语句,检查程序都会忽略掉对等号两边的表达式的个数不相等的赋值语句的检查。 -buildtags标记 前文已提到,如果标记-buildtags有效(标记值不为false),那么命令程序会对目标文件中的编译标签(如果有的话)的格式进行检查。什么叫做条件编译?在实际场景中,有些源码文件中包含了平台相关的代码。我们希望只在某些特定平台下才编译它们。这种有选择的编译方法就被叫做条件编译。在Go语言中,条件编译的配置就是通过编译标签来完成的。编译器需要依据源码文件中编译标签的内容来决定是否编译当前文件。编译标签可必须出现在任何源码文件(比如扩展名为“.go”,“.h”,“.c”,“.s”等的源码文件) 的头部的单行注释中,并且在其后面需要有空行。 至于编译标签的具体写法,我们就不在此赘述了。读者可以参看Go语言官方的相关文档。我们在这里只简单罗列一下-buildtags有效时命令程序对编译标签的检查内容: 若编译标签前导符“+build”后没有紧随空格,则打印格式错误信息。 若编译标签所在行与第一个多行注释或代码行之间没有空行,则打印错误信息。 若在某个单一参数的前面有两个英文叹号“!!”,则打印错误信息。 若单个参数包含字母、数字、“_”和“.”以外的字符,则打印错误信息。 若出现在文件头部单行注释中的编译标签前导符“+build”未紧随在单行注释前导符“//”之后,则打印错误信息。 如果一个在文件头部的单行注释中的编译标签通过了上述的这些检查,则说明它的格式是正确无误的。由于只有在文件头部的单行注释中编译标签才会被编译器认可,所以检查程序只会查找和检查源码文件中的第一个多行注释或代码行之前的内容。 -composites标记和-compositeWhiteList标记 如果标记-composites有效(标记值不为false),则命令程序会对目标文件中的复合字面量进行检查。请看如下示例: type counter struct { name string number int } ... c := counter{name: \"c1\", number: 0} 在上面的示例中,代码counter{name: \"c1\", number: 0}是对结构体类型counter的初始化。如果复合字面量中涉及到的类型不在当前代码包内部且未在所属文件中被导入,那么检查程序不但会打印错误信息还会将退出代码设置为1,并且取消后续的检查。退出代码为1意味着检查程序已经报告了一个或多个问题。这个问题比仅仅引起错误信息报告的问题更加严重。 在通过上述检查的前提下,如果复合字面量中包含了对结构体类型的字段的赋值但却没有指明字段名,像这样: var v = flag.Flag{ \"Name\", \"Usage\", nil, // Value \"DefValue\", } 那么检查程序也会打印错误信息,以提示在复合字面量中包含有未指明的字段赋值。 这有一个例外,那就是当标记-compositeWhiteList有效(标记值不为false)的时候。只要类型在白名单中,即使其初始化语句中含有未指明的字段赋值也不会被提示。这是出于什么考虑呢?先来看下面的示例: type sliceType []string ... st1 := sliceType{\"1\", \"2\", \"3\"} 上面示例中的sliceType{\"1\", \"2\", \"3\"}也属于复合字面量。但是它初始化的类型实际上是一个切片值,只不过这个切片值被别名化并被包装为了另一个类型而已。在这种情况下,复合字面量中的赋值不需要指明字段,事实上这样的类型也不包含任何字段。白名单中所包含的类型都是这种情况。它们是在标准库中的包装了切片值的类型。它们不需要被检查,因为这种情况是合理的。 在默认情况下,标记-compositeWhiteList是有效的。也就是说,检查程序不会对它们的初始化代码进行检查,除非我们在执行go tool vet命令时显示的将-compositeWhiteList标记的值设置为false。 -methods标记 如果标记-methods有效(标记值不为false),则命令程序会对目标文件中的方法定义进行规范性的进行检查。这里所说的规范性是狭义的。 在检查程序内部存有一个规范化方法字典。这个字典的键用来表示方法的名称,而字典的元素则用来描述方法应有的参数和结果的类型。在该字典中列出的都是Go语言标准库中使用最广泛的接口类型的方法。这些方法的名字都非常通用。它们中的大多数都是它们所属接口类型的唯一方法。我们在第4章中提到过,Go语言中的接口类型实现方式是非侵入式的。只要结构体类型实现了某一个接口类型中的所有方法,就可以说这个结构体类型是该接口类型的一个实现。这种判断方式被称为动态接口检查。它只在运行时进行。如果我们想让一个结构体类型成为某一个接口类型的实现,但又写错了要实现的接口类型中的方法的签名,那么也不会引发编译器报错。这里所说的方法签名包括方法的参数声明列表和结果声明列表。虽然动态接口检查失败时并不会报错,但是它却会间接的引发其它错误。而这些被间接引发的错误只会在运行时发生。示例如下: type MySeeker struct { // 忽略字段定义 } func (self *MySeeker) Seek(whence int, offset int64) (ret int64, err error) { // 想实现接口类型io.Seeker中的唯一方法,但是却把参数的顺序写颠倒了。 // 忽略实现代码 } func NewMySeeker io.Seeker { return &MySeeker{/* 忽略字段初始化 */} // 这里会引发一个运行时错误。 //由于MySeeker的Seek方法的签名写错了,所以MySeeker不是io.Seeker的实现。 } 这种运行时错误看起来会比较诡异,并且错误排查也会相对困难,所以应该尽量避免。-methods标记所对应的检查就是为了达到这个目的。检查程序在发现目标文件中某个方法的名字被包含在规范化方法字典中但其签名与对应的描述不对应的时候,就会打印错误信息并设置退出代码为1。 我在这里附上在规范化方法字典中列出的方法的信息: 表0-17 规范化方法字典中列出的方法 方法名称 参数类型 结果类型 所属接口 唯一方法 Format \"fmt.State\", \"rune\" fmt.Formatter 是 GobDecode \"[]byte\" \"error\" gob.GobDecoder 是 GobEncode \"[]byte\", \"error\" gob.GobEncoder 是 MarshalJSON \"[]byte\", \"error\" json.Marshaler 是 Peek \"int\" \"[]byte\", \"error\" image.reader 否 ReadByte \"int\" \"[]byte\", \"error\" io.ByteReader 是 ReadFrom \"io.Reader\" \"int64\", \"error\" io.ReaderFrom 是 ReadRune \"rune\", \"int\", \"error\" io.RuneReader 是 Scan \"fmt.ScanState\", \"rune\" \"error\" fmt.Scanner 是 Seek \"int64\", \"int\" \"int64\", \"error\" io.Seeker 是 UnmarshalJSON \"[]byte\" \"error\" json.Unmarshaler 是 UnreadByte \"error\" io.ByteScanner 否 UnreadRune \"error\" io.RuneScanner 否 WriteByte \"byte\" \"error\" io.ByteWriter 是 WriteTo \"io.Writer\" \"int64\", \"error\" io.WriterTo 是 -printf标记和-printfuncs标记 标记-printf旨在目标文件中检查各种打印函数使用的正确性。而标记-printfuncs及其值则用于明确指出需要检查的打印函数。-printfuncs标记的默认值为空字符串。也就是说,若不明确指出检查目标则检查所有打印函数。可被检查的打印函数如下表: 表0-18 格式化字符串中动词的格式要求 函数全小写名称 支持格式化 可自定义输出 自带换行 error 否 否 是 fatal 否 否 是 fprint 否 是 否 fprintln 否 是 是 panic 否 否 否 panicln 否 否 是 print 否 否 否 println 否 否 是 sprint 否 否 否 sprintln 否 否 是 errorf 是 否 否 fatalf 是 否 否 fprintf 是 是 否 panicf 是 否 否 printf 是 否 否 sprintf 是 是 否 以字符串格式化功能来区分,打印函数可以分为可打印格式化字符串的打印函数(以下简称格式化打印函数)和非格式化打印函数。对于格式化打印函数来说,其第一个参数必是格式化表达式,也可被称为模板字符串。而其余参数应该为需要被填入模板字符串的变量。像这样: fmt.Printf(\"Hello, %s!\\n\", \"Harry\") // 会输出:Hello, Harry! 而非格式化打印函数的参数则是一个或多个要打印的内容。比如: fmt.Println(\"Hello,\", \"Harry!\") // 会输出:Hello, Harry! 以指定输出目的地功能区分,打印函数可以被分为可自定义输出目的地的的打印函数(以下简称自定义输出打印函数)和标准输出打印函数。对于自定义输出打印函数来说,其第一个函数必是其打印的输出目的地。比如: fmt.Fprintf(os.Stdout, \"Hello, %s!\\n\", \"Harry\") // 会在标准输出设备上输出:Hello, Harry! 上面示例中的函数fmt.Fprintf既能够让我们自定义打印的输出目的地,又能够格式化字符串。此类打印函数的第一个参数的类型应为io.Writer接口类型。只要某个类型实现了该接口类型中的所有方法,就可以作为函数Fprintf的第一个参数。例如,我们还可以使用代码包bytes中的结构体Buffer来接收打印函数打印的内容。像这样: var buff bytes.Buffer fmt.Fprintf(&buff, \"Hello, %s!\\n\", \"Harry\") fmt.Print(\"Buffer content:\", buff.String()) // 会在标准输出设备上输出:Buffer content: Hello, Harry! 而标准输出打印函数则只能将打印内容到标准输出设备上。就像函数fmt.Printf和fmt.Println所做的那样。 检查程序会首先关注打印函数的参数数量。如果参数数量不足,则可以认为在当前调用打印函数的语句中并不会出现用法错误。所以,检查程序会忽略对它的检查。检查程序中对打印函数的最小参数是这样定义的:对于可以自定义输出的打印函数来说,最小参数数量为2,其它打印函数的最小参数数量为1。如果打印函数的实际参数数量小于对应的最小参数数量,就会被判定为参数数量不足。 对于格式化打印函数,检查程序会进行如下检查: 如果格式化字符串无法被转换为基本字面量(标识符以及用于表示int类型值、float类型值、char类型值、string类型值的字面量等),则检查程序会忽略剩余的检查。如果-v标记有效,则会在忽略检查前打印错误信息。另外,格式化打印函数的格式化字符串必须是字符串类型的。因此,如果对应位置上的参数的类型不是字符串类型,那么检查程序会立即打印错误信息,并设置退出代码为1。实际上,这个问题已经可以引起一个编译错误了。 如果格式化字符串中不包含动词(verbs),而格式化字符串后又有多余的参数,则检查程序会立即打印错误信息,并设置退出代码为1,且忽略后续检查。我现在举个例子。我们拿之前的一个示例作为基础,即: fmt.Printf(\"Hello, %s!\\n\", \"Harry\") 在这个示例中,格式化字符串中的“%s”就是我们所说的动词,“%”就是动词的前导符。它相当于一个需要被填的空。一般情况下,在格式化字符串中被填的空的数量应该与后续参数的数量相同。但是可以出现在格式化字符串中没有动词并且在格式化字符串之后没有额外参数的情况。在这种情况下,该格式化打印函数就相当于一个非格式化打印函数。例如,下面这个语句会导致此步检查不通过: fmt.Printf(\"Hello!\\n\", \"Harry\") 检查程序还会检查动词的格式。这部分检查会非常严格。检查程序对于格式化字符串中动词的格式要求如表0-19。表中对每个动词只进行了简要的说明。读者可以查看标准库代码包fmt的文档以了解关于它们的详细信息。命令程序会按照表5-19中的要求对格式化及其后续参数进行检查。如上表所示,这部分检查分为两步骤。第一个步骤是检查格式化字符串中的动词上是否附加了不合法的标记,第二个步骤是检查格式化字符串中的动词与后续对应的参数的类型是否匹配。只要检查出问题,检查程序就会打印出错误信息并且设置退出代码为1。 如果格式化字符串中的动词不被支持,则检查程序同样会打印错误信息后,并设置退出代码为1。 表0-19 格式化字符串中动词的格式要求 动词 合法的附加标记 允许的参数类型 简要说明 b “ ”,“-”,“+”,“.”和“0” int或float 用于二进制表示法。 c “-” rune或int 用于单个字符的Unicode表示法。 d “ ”,“-”,“+”,“.”和“0” int 用于十进制表示法。 e “ ”,“-”,“+”,“.”和“0” float 用于科学记数法。 E “ ”,“-”,“+”,“.”和“0” float 用于科学记数法。 f “ ”,“-”,“+”,“.”和“0” float 用于控制浮点数精度。 F “ ”,“-”,“+”,“.”和“0” float 用于控制浮点数精度。 g “ ”,“-”,“+”,“.”和“0” float 用于压缩浮点数输出。 G “ ”,“-”,“+”,“.”和“0” float 用于动态选择浮点数输出格式。 o “ ”,“-”,“+”,“.”,“0”和“#” int 用于八进制表示法。 p “-”和“#” pointer 用于表示指针地址。 q “ ”,“-”,“+”,“.”,“0”和“#” rune,int或string 用于生成带双引号的字符串形式的内容。 s “ ”,“-”,“+”,“.”和“0” rune,int或string 用于生成字符串形式的内容。 t “-” bool 用于生成与布尔类型对应的字符串值。(“true”或“false”) T “-” 任何类型 用于用Go语法表示任何值的类型。 U “-”和“#” rune或int 用于针对Unicode的表示法。 v “”,“-”,“+”,“.”,“0”和“#” 任何类型 以默认格式格式化任何值。 x “”,“-”,“+”,“.”,“0”和“#” rune,int或string 以十六进制、全小写的形式格式化每个字节。 X “”,“-”,“+”,“.”,“0”和“#” rune,int或string 以十六进制、全大写的形式格式化每个字节。 对于非格式化打印函数,检查程序会进行如下检查: 如果打印函数不是可以自定义输出的打印函数,那么其第一个参数就不能是标准输出os.Stdout或者标准错误输出os.Stderr。否则,检查程序将打印错误信息并设置退出代码为1。这主要是为了防止程序编写人员的笔误。比如,他们可能会把函数fmt.Println当作函数fmt.Printf来用。 如果打印函数是不自带换行的,比如fmt.Printf和fmt.Print,则它必须只少有一个参数。否则,检查程序将打印错误信息并设置退出代码为1。像这样的调用打印函数的语句是没有任何意义的。并且,如果这个打印函数还是一个格式化打印函数,那么这还会引起一个编译错误。需要注意的是,函数名称为Error的方法不会在被检查之列。比如,标准库代码包testing中的结构体类型T和B的方法Error。这是因为它们可能实现了接口类型Error。这个接口类型中唯一的方法Error无需任何参数。 如果第一个参数的值为字符串类型的字面量且带有格式化字符串中才应该有的动词的前导符“%”,则检查程序会打印错误信息并设置退出代码为1。因为非格式化打印函数中不应该出现格式化字符串。 如果打印函数是自带换行的,那么在打印内容的末尾就不应该有换行符“\\n”。否则,检查程序会打印错误信息并设置退出代码为1。换句话说,检查程序认为程序中如果出现这样的代码: fmt.Println(\"Hello!\\n\") 常常是由于程序编写人员的笔误。实际上,事实确实如此。如果我们确实想连续输入多个换行,应该这样写: fmt.Println(\"Hello!\") fmt.Println() 至此,我们详细介绍了go tool vet命令中的检查程序对打印函数的所有步骤和内容。打印函数的功能非常简单,但是go tool vet命令对它的检查却很细致。从中我们可以领会到一些关于打印函数的最佳实践。 -rangeloops标记 如果标记-rangeloop有效(标记值不为false),那么命令程序会对使用range进行迭代的for代码块进行检查。我们之前提到过,使用for语句需要注意两点: 不要在go代码块中处理在迭代过程中被赋予值的迭代变量。比如: mySlice := []string{\"A\", \"B\", \"C\"} for index, value := range mySlice { go func() { fmt.Printf(\"Index: %d, Value: %s\\n\", index, value) }() } 在Go语言的并发编程模型中,并没有线程的概念,但却有一个特有的概念——Goroutine。Goroutine也可被称为Go例程或简称为Go程。关于Goroutine的详细介绍在第6章和第7章。我们现在只需要知道它是一个可以被并发执行的代码块。 不要在defer语句的延迟函数中处理在迭代过程中被赋予值的迭代变量。比如: myDict := make(map[string]int) myDict[\"A\"] = 1 myDict[\"B\"] = 2 myDict[\"C\"] = 3 for key, value := range myDict { defer func() { fmt.Printf(\"Key: %s, Value: %d\\n\", key, value) }() } 其实,上述两点所关注的问题是相同的,那就是不要在可能被延迟处理的代码块中直接使用迭代变量。go代码块和defer代码块都有这样的特质。这是因为等到go函数(跟在go关键字之后的那个函数)或延迟函数真正被执行的时候,这些迭代变量的值可能已经不是我们想要的值了。 另一方面,当检查程序发现在带有range子句的for代码块中迭代出的数据并没有赋值给标识符所代表的变量时,则会忽略对这一代码块的检查。比如像这样的代码: func nonIdentRange(slc []string) { l := len(slc) temp := make([]string, l) l-- for _, temp[l] = range slc { // 忽略了使用切片值temp的代码。 if l > 0 { l-- } } } 就不会受到检查程序的关注。另外,当被迭代的对象的大小为0时,for代码块也不会被检查。 据此,我们知道如果在可能被延迟处理的代码块中直接使用迭代中的临时变量,那么就可能会造成与编程人员意图不相符的结果。如果由此问题使程序的最终结果出现偏差甚至使程序报错的话,那么看起来就会非常诡异。这种隐晦的错误在排查时也是非常困难的。这种不正确的代码编写方式应该彻底被避免。这也是检查程序对迭代代码块进行检查的最终目的。如果检查程序发现了上述的不正确的代码编写方式,就会打印出错误信息以提醒编程人员。 -structtags标记 如果标记``-structtags有效(标记值不为false```),那么命令程序会对结构体类型的字段的标签进行检查。我们先来看下面的代码: type Person struct { XMLName xml.Name `xml:\"person\"` Id int `xml:\"id,attr\"` FirstName string `xml:\"name>first\"` LastName string `xml:\"name>last\"` Age int `xml:\"age\"` Height float32 `xml:\"height,omitempty\"` Married bool Address Comment string `xml:\",comment\"` } 在上面的例子中,在结构体类型的字段声明后面的那些字符串形式的内容就是结构体类型的字段的标签。对于Go语言本身来说,结构体类型的字段标签就是注释,它们是可选的,且会被Go语言的运行时系统忽略。但是,这些标签可以通过标准库代码包reflect中的程序访问到。因此,不同的代码包中的程序可能会赋予这些结构体类型的字段标签以不同的含义。比如上面例子中的结构体类型的字段标签就对代码包encoding/xml中的程序非常有用处。 严格来讲,结构体类型的字段的标签应该满足如下要求: 标签应该包含键和值,且它们之间要用英文冒号分隔。 标签的键应该不包含空格、引号或冒号。 标签的值应该被英文双引号包含。 如果标签内容符合了第3条,那么标签的全部内容应该被反引号“`”包含。否则它需要被双引号包含。 标签可以包含多个键值对,其它们之间要用空格“ ”分隔。例如:key:\"value\" _gofix:\"_magic\" 检查程序首先会对结构体类型的字段标签的内容做去引号处理,也就是把最外面的双引号或者反引号去除。如果去除失败,则检查程序会打印错误信息并设置退出代码为1,同时忽略后续检查。如果去引号处理成功,检查程序则会根据前面的规则对标签的内容进行检查。如果检查出问题,检查程序同样会打印出错误信息并设置退出代码为1。 -unreachable标记 如果标记``-unreachable有效(标记值不为false```),那么命令程序会在函数或方法定义中查找死代码。死代码就是永远不会被访问到的代码。例如: func deadCode1() int { print(1) return 2 println() // 这里存在死代码 } 在上面示例中,函数deadCode1中的最后一行调用打印函数的语句就是死代码。检查程序如果在函数或方法中找到死代码,则会打印错误信息以提醒编码人员。我们把这段代码放到命令源码文件deadcode_demo.go中,并在main函数中调用它。现在,如果我们编译这个命令源码文件会马上看到一个编译错误:“missing return at end of function”。显然,这个错误侧面的提醒了我们,在这个函数中存在死代码。实际上,我们在修正这个问题之前它根本就不可能被运行,所以也就不存在任何隐患。但是,如果在这个函数不需要结果的情况下又会如何呢?我们稍微改造一下上面这个函数: func deadCode1() { print(1) return println() // 这里存在死代码 } 好了,我们现在把函数deadcode1的声明中的结果声明和函数中return语句后的数字都去掉了。不幸的是,当我们再次编译文件时没有看到任何报错。但是,这里确实存在死代码。在这种情况下,编译器并不能帮助我们找到问题,而go tool vet命令却可以。 hc@ubt:~$ go tool vet deadcode_demo.go deadcode_demo.go:10: unreachable code go tool vet命令中的检查程序对于死代码的判定有几个依据,如下: 在这里,我们把return语句、goto语句、break语句、continue语句和panic函数调用语句都叫做流程中断语句。如果在当前函数、方法或流程控制代码块的分支中的流程中断语句的后面还存在其他语句或代码块,比如: func deadCode2() { print(1) panic(2) println() // 这里存在死代码 } 或 func deadCode3() { L: { print(1) goto L } println() // 这里存在死代码 } 或 func deadCode4() { print(1) return { // 这里存在死代码 } } 则后面的语句或代码块就会被判定为死代码。但检查程序仅会在错误提示信息中包含第一行死代码的位置。 如果带有else的if代码块中的每一个分支的最后一条语句均为流程中断语句,则在此流程控制代码块后的代码都被判定为死代码。比如: func deadCode5(x int) { print(1) if x == 1 { panic(2) } else { return } println() // 这里存在死代码 } 注意,只要其中一个分支不包含流程中断语句,就不能判定后面的代码为死代码。像这样: func deadCode5(x int) { print(1) if x == 1 { panic(2) } else if x == 2 { return } println() // 这里并不是死代码 } 如果在一个没有显式中断条件或中断语句的for代码块后面还存在其它语句,则这些语句将会被判定为死代码。比如: func deadCode6() { for { for { break } } println() // 这里存在死代码 } 或 func deadCode7() { for { for { } break // 这里存在死代码 } println() } 而我们对这两个函数稍加改造后,就会消除go tool vet命令发出的死代码告警。如下: func deadCode6() { x := 1 for x == 1 { for { break } } println() // 这里存在死代码 } 以及 func deadCode7() { x := 1 for { for x == 1 { } break // 这里存在死代码 } println() } 我们只是加了一个显式的中断条件就能够使之通过死代码检查。但是,请注意!这两个函数中在被改造后仍然都包含死循环代码!这说明检查程序并不对中断条件的逻辑进行检查。 如果select代码块的所有case中的最后一条语句均为流程中断语句(break语句除外),那么在select代码块后面的语句都会被判定为死代码。比如: func deadCode8(c chan int) { print(1) select { case } 或 func deadCode9(c chan int) { L: print(1) select { case 另外,在空的select语句块之后的代码也会被认为是死代码。比如: func deadCode10() { print(1) select {} println() // 这里存在死代码 } 或 func deadCode11(c chan int) { print(1) select { case 上面这两个示例中的语句select {}都会引发一个运行时错误:“fatal error: all goroutines are asleep - deadlock!”。这就是死锁!关于这个错误的详细说明在第7章。 如果switch代码块的所有case和default case中的最后一条语句均为流程中断语句(除了break语句),那么在switch代码块后面的语句都会被判定为死代码。比如: func deadCode14(x int) { print(1) switch x { case 1: print(2) panic(3) default: return } println(4) // 这里存在死代码 } 我们知道,关键字fallthrough可以使流程从switch代码块中的一个case转移到下一个case或default case。死代码也可能由此产生。例如: func deadCode15(x int) { print(1) switch x { case 1: print(2) fallthrough default: return } println(3) // 这里存在死代码 } 在上面的示例中,第一个case总会把流程转移到第二个case,而第二个case中的最后一条语句为return语句,所以流程永远不会转移到语句println(3)上。因此,println(3)语句会被判定为死代码。如果我们把fallthrough语句去掉,那么就可以消除这个死代码判定。实际上,只要某一个case或者default case中的最后一条语句是break语句,就不会有死代码的存在。当然,这个break语句本身不能是死代码。另外,与select代码块不同的是,空的switch代码块并不会使它后面的代码成为死代码。 综上所述,死代码的判定虽然看似比较复杂,但其实还是有原则可循的。我们应该在编码过程中就避免编写可能会造成死代码的代码。如果我们实在不确定死代码是否存在,也可以使用go tool vet命令来检查。不过,需要提醒读者的是,不存在死代码并不意味着不存在造成死循环的代码。当然,造成死循环的代码也并不一定就是错误的代码。但我们仍然需要对此保持警觉。 -asmdecl标记 如果标记``-asmdecl有效(标记值不为false```),那么命令程序会对汇编语言的源码文件进行检查。对汇编语言源码文件及相应编写规则的解读已经超出了本书的范围,所以我们并不在这里对此项检查进行描述。如果读者有兴趣的话,可以查看此项检查的程序的源码文件asmdecl.go。它在Go语言安装目录的子目录src/cmd/vet下。 至此,我们对go vet命令和go tool vet命令进行了全面详细的介绍。之所以花费如此大的篇幅来介绍这两个命令,不仅仅是为了介绍此命令的使用方法,更是因为此命令程序的检查工作涉及到了很多我们在编写Go语言代码时需要避免的“坑”。由此我们也可以知晓应该怎样正确的编写Go语言代码。同时,我们也应该在开发Go语言程序的过程中经常使用go tool vet命来检查代码。 © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:09:40 "},"go/cmd/toolpprof.html":{"url":"go/cmd/toolpprof.html","title":"go tool pprof","keywords":"","body":"go tool pprof go tool pprof 我们可以使用go tool pprof命令来交互式的访问概要文件的内容。命令将会分析指定的概要文件,并会根据我们的要求为我们提供高可读性的输出信息。 在Go语言中,我们可以通过标准库的代码包runtime和runtime/pprof中的程序来生成三种包含实时性数据的概要文件,分别是CPU概要文件、内存概要文件和程序阻塞概要文件。下面我们先来分别介绍用于生成这三种概要文件的API的用法。 CPU概要文件 在介绍CPU概要文件的生成方法之前,我们先来简单了解一下CPU主频。CPU的主频,即CPU内核工作的时钟频率(CPU Clock Speed)。CPU的主频的基本单位是赫兹(Hz),但更多的是以兆赫兹(MHz)或吉赫兹(GHz)为单位。时钟频率的倒数即为时钟周期。时钟周期的基本单位为秒(s),但更多的是以毫秒(ms)、微妙(us)或纳秒(ns)为单位。在一个时钟周期内,CPU执行一条运算指令。也就是说,在1000 Hz的CPU主频下,每1毫秒可以执行一条CPU运算指令。在1 MHz的CPU主频下,每1微妙可以执行一条CPU运算指令。而在1 GHz的CPU主频下,每1纳秒可以执行一条CPU运算指令。 在默认情况下,Go语言的运行时系统会以100 Hz的的频率对CPU使用情况进行取样。也就是说每秒取样100次,即每10毫秒会取样一次。为什么使用这个频率呢?因为100 Hz既足够产生有用的数据,又不至于让系统产生停顿。并且100这个数上也很容易做换算,比如把总取样计数换算为每秒的取样数。实际上,这里所说的对CPU使用情况的取样就是对当前的Goroutine的堆栈上的程序计数器的取样。由此,我们就可以从样本记录中分析出哪些代码是计算时间最长或者说最耗CPU资源的部分了。我们可以通过以下代码启动对CPU使用情况的记录。 func startCPUProfile() { if *cpuProfile != \"\" { f, err := os.Create(*cpuProfile) if err != nil { fmt.Fprintf(os.Stderr, \"Can not create cpu profile output file: %s\", err) return } if err := pprof.StartCPUProfile(f); err != nil { fmt.Fprintf(os.Stderr, \"Can not start cpu profile: %s\", err) f.Close() return } } } 在函数startCPUProfile中,我们首先创建了一个用于存放CPU使用情况记录的文件。这个文件就是CPU概要文件,其绝对路径由*cpuProfile的值表示。然后,我们把这个文件的实例作为参数传入到函数`pprof.StartCPUProfile中。如果此函数没有返回错误,就说明记录操作已经开始。需要注意的是,只有CPU概要文件的绝对路径有效时此函数才会开启记录操作。 如果我们想要在某一时刻停止CPU使用情况记录操作,就需要调用下面这个函数: func stopCPUProfile() { if *cpuProfile != \"\" { pprof.StopCPUProfile() // 把记录的概要信息写到已指定的文件 } } 在这个函数中,并没有代码用于CPU概要文件写入操作。实际上,在启动CPU使用情况记录操作之后,运行时系统就会以每秒100次的频率将取样数据写入到CPU概要文件中。pprof.StopCPUProfile函数通过把CPU使用情况取样的频率设置为0来停止取样操作。并且,只有当所有CPU使用情况记录都被写入到CPU概要文件之后,pprof.StopCPUProfile函数才会退出。从而保证了CPU概要文件的完整性。 内存概要文件 内存概要文件用于保存在用户程序执行期间的内存使用情况。这里所说的内存使用情况,其实就是程序运行过程中堆内存的分配情况。Go语言运行时系统会对用户程序运行期间的所有的堆内存分配进行记录。不论在取样的那一时刻、堆内存已用字节数是否有增长,只要有字节被分配且数量足够,分析器就会对其进行取样。开启内存使用情况记录的方式如下: func startMemProfile() { if *memProfile != \"\" && *memProfileRate > 0 { runtime.MemProfileRate = *memProfileRate } } 我们可以看到,开启内存使用情况记录的方式非常简单。在函数startMemProfile中,只有在*memProfile和*memProfileRate的值有效时才会进行后续操作。*memProfile的含义是内存概要文件的绝对路径。*memProfileRate的含义是分析器的取样间隔,单位是字节。当我们将这个值赋给int类型的变量runtime.MemProfileRate时,就意味着分析器将会在每分配指定的字节数量后对内存使用情况进行取样。实际上,即使我们不给runtime.MemProfileRate变量赋值,内存使用情况的取样操作也会照样进行。此取样操作会从用户程序开始时启动,且一直持续进行到用户程序结束。runtime.MemProfileRate变量的默认值是512 * 1024,即512K个字节。只有当我们显式的将0赋给runtime.MemProfileRate变量之后,才会取消取样操作。 在默认情况下,内存使用情况的取样数据只会被保存在运行时内存中,而保存到文件的操作只能由我们自己来完成。请看如下代码: func stopMemProfile() { if *memProfile != \"\" { f, err := os.Create(*memProfile) if err != nil { fmt.Fprintf(os.Stderr, \"Can not create mem profile output file: %s\", err) return } if err = pprof.WriteHeapProfile(f); err != nil { fmt.Fprintf(os.Stderr, \"Can not write %s: %s\", *memProfile, err) } f.Close() } } 从函数名称上看,stopMemProfile函数的功能是停止对内存使用情况的取样操作。但是,它只做了将取样数据保存到内存概要文件的操作。在stopMemProfile函数中,我们调用了函数pprof.WriteHeapProfile,并把代表内存概要文件的文件实例作为了参数。如果pprof.WriteHeapProfile函数没有返回错误,就说明数据已被写入到了内存概要文件中。 需要注意的是,对内存使用情况进行取样的程序会假定取样间隔在用户程序的运行期间内都是一成不变的,并且等于runtime.MemProfileRate变量的当前值。因此,我们应该在我们的程序中只改变内存取样间隔一次,且应尽早改变。比如,在命令源码文件的main函数的开始处就改变它。 程序阻塞概要文件 程序阻塞概要文件用于保存用户程序中的Goroutine阻塞事件的记录。我们来看开启这项操作的方法: func startBlockProfile() { if *blockProfile != \"\" && *blockProfileRate > 0 { runtime.SetBlockProfileRate(*blockProfileRate) } } 与开启内存使用情况记录的方式类似,在函数startBlockProfile中,当*blockProfile和*blockProfileRate的值有效时,我们会设置对Goroutine阻塞事件的取样间隔。*blockProfile的含义为程序阻塞概要文件的绝对路径。*blockProfileRate的含义是分析器的取样间隔,单位是次。函数runtime.SetBlockProfileRate的唯一参数是int类型的。它的含义是分析器会在每发生几次Goroutine阻塞事件时对这些事件进行取样。如果我们不显式的使用runtime.SetBlockProfileRate函数设置取样间隔,那么取样间隔就为1。也就是说,在默认情况下,每发生一次Goroutine阻塞事件,分析器就会取样一次。与内存使用情况记录一样,运行时系统对Goroutine阻塞事件的取样操作也会贯穿于用户程序的整个运行期。但是,如果我们通过runtime.SetBlockProfileRate函数将这个取样间隔设置为0或者负数,那么这个取样操作就会被取消。 我们在程序结束之前可以将被保存在运行时内存中的Goroutine阻塞事件记录存放到指定的文件中。代码如下: func stopBlockProfile() { if *blockProfile != \"\" && *blockProfileRate >= 0 { f, err := os.Create(*blockProfile) if err != nil { fmt.Fprintf(os.Stderr, \"Can not create block profile output file: %s\", err) return } if err = pprof.Lookup(\"block\").WriteTo(f, 0); err != nil { fmt.Fprintf(os.Stderr, \"Can not write %s: %s\", *blockProfile, err) } f.Close() } } 在创建程序阻塞概要文件之后,stopBlockProfile函数会先通过函数pprof.Lookup将保存在运行时内存中的内存使用情况记录取出,并在记录的实例上调用WriteTo方法将记录写入到文件中。 更多的概要文件 我们可以通过pprof.Lookup函数取出更多种类的取样记录。如下表: 表0-20 可从pprof.Lookup函数中取出的记录 名称 说明 取样频率 goroutine 活跃Goroutine的信息的记录。 仅在获取时取样一次。 threadcreate 系统线程创建情况的记录。 仅在获取时取样一次。 heap 堆内存分配情况的记录。 默认每分配512K字节时取样一次。 block Goroutine阻塞事件的记录。 默认每发生一次阻塞事件时取样一次。 在上表中,前两种记录均为一次取样的记录,具有即时性。而后两种记录均为多次取样的记录,具有实时性。实际上,后两种记录“heap”和“block”正是我们前面讲到的内存使用情况记录和程序阻塞情况记录。 我们知道,在用户程序运行期间各种状态是在不断变化的。尤其对于后两种记录来说,随着取样次数的增多,记录项的数量也会不断增长。而对于前两种记录“goroutine”和“threadcreate”来说,如果有新的活跃Goroutine产生或新的系统线程被创建,其记录项数量也会增大。所以,Go语言的运行时系统在从内存中获取记录时都会先预估一个记录项数量。如果在从预估记录项数量到获取记录之间的时间里又有新记录项产生,那么运行时系统会试图重新获取全部记录项。另外,运行时系统使用切片来装载所有记录项的。如果当前使用的切片装不下所有记录项,运行时系统会根据当前记录项总数创建一个更大的切片,并再次试图装载所有记录项。直到这个切片足以装载所有的记录项为止。但是,如果记录项增长过快的话,运行时系统将不得不不断的进行尝试。这可能导致过多的时间消耗。对于前两种记录“goroutine”和“threadcreate”来说,运行时系统创建的切片的大小为当前记录项总数再加10。对于前两种记录“heap”和“block”来说,运行时系统创建的切片的大小为当前记录项总数再加50。虽然上述情况发生的概率可能并不会太高,但是如果我们在对某些高并发的用户程序获取上述记录的时候耗费的时间过长,可以先排查一下这类原因。实际上,我们在前面介绍的这几 种记录操作更适合用于对高并发的用户程序进行状态检测。 我们可以通过下面这个函数分别将四种记录输出到文件。 func SaveProfile(workDir string, profileName string, ptype ProfileType, debug int) { absWorkDir := getAbsFilePath(workDir) if profileName == \"\" { profileName = string(ptype) } profilePath := filepath.Join(absWorkDir, profileName) f, err := os.Create(profilePath) if err != nil { fmt.Fprintf(os.Stderr, \"Can not create profile output file: %s\", err) return } if err = pprof.Lookup(string(ptype)).WriteTo(f, debug); err != nil { fmt.Fprintf(os.Stderr, \"Can not write %s: %s\", profilePath, err) } f.Close() } 函数SaveProfile有四个参数。第一个参数是概要文件的存放目录。第二个参数是概要文件的名称。第三个参数是概要文件的类型。其中,类型ProfileType只是为string类型起的一个别名而已。这样是为了对它的值进行限制。它的值必须为“goroutine”、“threadcreate”、“heap”或“block”中的一个。我们现在来重点说一下第四个参数。参数debug控制着概要文件中信息的详细程度。这个参数也就是传给结构体pprof.Profile的指针方法WriteTo的第二个参数。而pprof.Profile结构体的实例的指针由函数pprof.Lookup产生。下面我们看看参数debug的值与写入概要文件的记录项内容的关系。 表0-21 参数debug对概要文件内容的影响 记录\\debug 小于等于0 等于1 大于等于2 goroutine 为每个记录项提供调用栈中各项的以十六进制表示的内存地址。 在左边提供的信息的基础上,为每个记录项的调用栈中各项提供与内存地址对应的带代码包导入路径的函数名和源码文件路径及源码所在行号。 以高可读的方式提供各活跃Goroutine的状态信息和调用栈信息。 threadcreate 同上。 同上。 同左。 heap 同上。 在左边提供的信息的基础上,为每个记录项的调用栈中各项提供与内存地址对应的带代码包导入路径的函数名和源码文件路径及源码所在行,并提供内存状态信息。 同左。 block 同上。 在左边提供的信息的基础上,为每个记录项的调用栈中各项提供与内存地址对应的带代码包导入路径的函数名和源码文件路径及源码所在行号。 同左。 从上表可知,当debug的值小于等于0时,运行时系统仅会将每个记录项中的基本信息写入到概要文件中。记录项的基本信息只包括其调用栈中各项的以十六进制表示的内存地址。debug的值越大,我们能从概要文件中获取的信息越多。但是,go tool pprof命令会无视那些除基本信息以外的附加信息。实际上,运行时系统在向概要文件中写入附加信息时会在最左边加入“#”,以表示当前行为注释行。也正因为有了这个前缀,go tool pprof命令才会略过对这些附加信息的解析。这其中有一个例外,那就是当debug大于等于2时,Goroutine记录并不是在基本信息的基础上附加信息,而是完全以高可读的方式写入各活跃Goroutine的状态信息和调用栈信息。并且,在所有行的最左边都没有前缀“#”。显然,这个概要文件是无法被go tool pprof命令解析的。但是它对于我们来说会更加直观和有用。 至此,我们已经介绍了使用标准库代码包runtime和runtime/pprof中的程序生成概要文件的全部方法。在上面示例中的所有代码都被保存在goc2p项目的代码包basic/prof中。代码包basic/prof中的这些程序非常易于使用。不过由于Go语言目前没有类似停机钩子(Shutdown Hook)的API(应用程序接口),所以代码包basic/prof中的程序目前只能以侵入式的方式被使用。 pprof工具 我们在上一小节中提到过,任何以go tool开头的Go命令内部指向的特殊工具都被保存在目录$GOROOT/pkg/tool/$GOOS_$GOARCH/中。我们把这个目录叫做Go工具目录。与其他特殊工具不同的是,pprof工具并不是用Go语言编写的,而是由Perl语言编写的。(Perl是一种通用的、动态的解释型编程语言)与Go语言不同,Perl语言可以直接读取源码并运行。正因为如此,pprof工具的源码文件被直接保存在了Go工具目录下。而对于其它Go工具,存在此目录的都是经过编译而生成的可执行文件。我们可以直接用任意一种文本查看工具打开在Go工具目录下的pprof工具的源码文件pprof。实际上,这个源码文件拷贝自Google公司发起的开源项目gperftools。此项目中包含了很多有用的工具。这些工具可以帮助开发者创建更健壮的应用程序。pprof就是其中的一个非常有用的工具。 因为pprof工具是用Perl语言编写的,所以执行go tool pprof命令的前提条件是需要在当前环境下安装Perl语言,推荐的版本号是5.x。关于Perl语言的安装方法就不在这里叙述了,读者可以自己找到方法并自行安装它。在安装完Perl语言之后,我们可以在命令行终端中进入到Go工具目录,并执行命令perl pprof。它与我们在任意目录下执行go tool pprof命令的效果是一样的。当然,如果想要让go tool pprof命令在任意目录下都可以被执行,我们需要先设置好与Go语言相关的环境变量。 我们在本小节已经讨论过,go tool pprof命令会分析指定的概要文件并使得我们能够以交互式的方式访问其中的信息。但是光有概要文件还不够,我们还需要概要文件中信息的来源——命令源码文件的可执行文件。毕竟,概要文件中的信息是对在运行期间的用户程序取样的结果。而可以运行的Go语言程序只能是编译命令源码文件后生成的可执行文件。因此,为了演示go tool pprof命令的用法,我们还创建或改造一个命令源码文件。在我们的goc2p项目的代码包中有一个名称为showpds.go的命令源码文件。这个命令源码文件用来解析指定的代码包的依赖关系,并将这些依赖关系打印到标准输出设备上。选用这个命令源码文件的原因是,我们可以通过改变指定的代码包来控制这个命令源码文件的运行时间的长短。不同的代码包可能会有不同数量的直接依赖包和间接依赖包。依赖包越多的代码包会使这个命令源码文件耗费更多的时间来解析它的依赖关系。命令源码文件运行的时间越长,我们得到的概要文件中的信息就越有意义。为了生成概要文件,我们需要稍微的改造一下这个命令源码文件。首先我们需要在这个文件中导入代码包basic/prof。然后,我们需要在它的main函数的开头加入一行代码prof.Start()。这行代码的含义是检查相关标记,并在标记有效时开启或设置对应的使用情况记录操作。最后,我们还需要在main函数的defer代码块中加入一行代码prof.Stop()。这行代码的含义是,获取已开启的记录的取样数据并将它们写入到指定的概要文件中。通过这三个步骤,我们就已经把生成运行时概要文件的功能附加到这个命令源码文件中了。为了开启这些功能,我还需要在通过执行go run命令来运行这个命令源码文件的时候,加入相应的标记。对代码包basic/prof中的程序有效的标记如下表。 表0-22 对代码包basic/prof的API有效的标记 标记名称 标记描述 -cpuprofile 指定CPU概要文件的保存路径。该路径可以是相对路径也可以是绝对路径,但其父路径必须已存在。 -blockprofile 指定程序阻塞概要文件的保存路径。该路径可以是相对路径也可以是绝对路径,但其父路径必须已存在。 -blockprofilerate 定义其值为n。此标记指定每发生n次Goroutine阻塞事件时,进行一次取样操作。 -memprofile 指定内存概要文件的保存路径。该路径可以是相对路径也可以是绝对路径,但其父路径必须已存在。 -memprofilerate 定义其值为n。此标记指定每分配n个字节的堆内存时,进行一次取样操作。 下面我们使用go run命令运行改造后的命令源码文件showpds.go。示例如下: hc@ubt:~/golang/goc2p$ mkdir pprof hc@ubt:~/golang/goc2p$ cd helper/pds hc@ubt:~/golang/goc2p/helper/pds$ go run showpds.go -p=\"runtime\" cpuprofile=\"../../../pprof/cpu.out\" -blockprofile=\"../../../pprof/block.out\" -blockprofilerate=1 -memprofile=\"../../../pprof/mem.out\" -memprofilerate=10 The package node of 'runtime': {/usr/local/go/src/pkg/ runtime [] [] false} The dependency structure of package 'runtime': runtime->unsafe 在上面的示例中,我们使用了所有的对代码包basic/prof的API有效的标记。另外,标记-p是对命令源码文件showpds.go有效的。其含义是指定要解析依赖关系的代码包的导入路径。 现在我们来查看一下goc2p项目目录下的pprof子目录: hc@ubt:~/golang/goc2p/helper/pds$ ls ../../../pprof block.out cpu.out mem.out 这个目录中的三个文件分别对应了三种包含实时性数据的概要文件。这也证明了我们对命令源码文件showpds.go的改造是有效的。 好了,一切准备工作就绪。现在,我们就来看看go tool pprof命令都能做什么。首先,我们来编译命令源码文件showpds.go。 hc@ubt:~/golang/goc2p/helper/pds$ go build showpds.go hc@ubt:~/golang/goc2p/helper/pds$ ls showpds showpds.go 然后,我们需要准备概要文件。标准库代码包runtime的依赖包极少,这使得可执行文件showpds在极短的时间内就会运行完毕。之前我们说过,程序运行的时间越长越好。所以我们需要找到一个直接和间接依赖包都很多的代码包。做过Web应用系统开发的同行们都知道,一个Web应用系统的后端程序可能会有很多的依赖,不论是代码库还是其他资源。根据我们的直觉,在Go语言的世界里也应该是在这样。在Go语言的标准库中,代码包net/http专门用来为Web应用系统开发提供各种API支持。我们就用这个代码包来生成所需的概要文件。 hc@ubt:~/golang/goc2p/helper/pds$ ./showpds -p=\"net/http\" -cpuprofile=\"../../../pprof/cpu.out\" -blockprofile=\"../../../pprof/block.out\" -blockprofilerate=1 -memprofile=\"../../../pprof/mem.out\" -memprofilerate=10 标准库代码包net/http的依赖包很多。也正因为如此,我忽略了所有输出的内容。读者可以自己试试上面的这个命令。我们一口气生成了所有能够生成的概要文件作为备用。这写概要文件被保存在了goc2p项目的pprof目录中。如果在上面的命令被执行前还没有pprof目录,命令会报错。所以读者需要先创建这个目录。 现在我们就以可执行文件showpds和pprof目录下的CPU概要文件cpu.out作为参数来执行go tool pprof命令。实际上,我们通过go tool pprof命令进入的就是pprof工具的交互模式的界面。 hc@ubt:~/golang/goc2p/helper/pds$ go tool pprof showpds ../../../pprof/cpu.out Welcome to pprof! For help, type 'help'. (pprof) 我们可以在提示符“(pprof)”后面输入一些命令来查看概要文件。pprof工具在交互模式下支持的命令如下表。 表0-23 pprof工具在交互模式下支持的命令 名称 参数 标签 说明 gv [focus] 将当前概要文件以图形化和层次化的形式显示出来。当没有任何参数时,在概要文件中的所有抽样都会被显示。如果指定了focus参数,则只显示调用栈中有名称与此参数相匹配的函数或方法的抽样。focus参数应该是一个正则表达式。 web [focus] 与gv命令类似,web命令也会用图形化的方式来显示概要文件。但不同的是,web命令是在一个Web浏览器中显示它。如果你的Web浏览器已经启动,那么它的显示速度会非常快。如果想改变所使用的Web浏览器,可以在Linux下设置符号链接/etc/alternatives/gnome-www-browser或/etc/alternatives/x-www-browser,或在OS X下改变SVG文件的关联Finder。 list [routine_regexp] 列出名称与参数“routine_regexp”代表的正则表达式相匹配的函数或方法的相关源代码。 weblist [routine_regexp] 在Web浏览器中显示与list命令的输出相同的内容。它与list命令相比的优势是,在我们点击某行源码时还可以显示相应的汇编代码。 top[N] [--cum] top命令可以以本地取样计数为顺序列出函数或方法及相关信息。如果存在标记“--cum”则以累积取样计数为顺序。默认情况下top命令会列出前10项内容。但是如果在top命令后面紧跟一个数字,那么其列出的项数就会与这个数字相同。 disasm [routine_regexp] 显示名称与参数“routine_regexp”相匹配的函数或方法的反汇编代码。并且,在显示的内容中还会标注有相应的取样计数。 callgrind [filename] 利用callgrind工具生成统计文件。在这个文件中,说明了程序中函数的调用情况。如果未指定“filename”参数,则直接调用kcachegrind工具。kcachegrind可以以可视化的方式查看callgrind工具生成的统计文件。 help 显示帮助信息。 quit 退出go tool pprof命令。Ctrl-d也可以达到同样效果。 在上面表格中的绝大多数命令(除了help和quit)都可以在其所有参数和标签后追加一个或多个参数,以表示想要忽略显示的函数或方法的名称。我们需要在此类参数上加入减号“-”作为前缀,并且多个参数之间需要以空格分隔。当然,我们也可以用正则表达式替代函数或方法的名称。追加这些约束之后,任何调用栈中包含名称与这类参数相匹配的函数或方法的抽样都不会出现在命令的输出内容中。下面我们对这几个命令进行逐一说明。 gv命令 对于命令gv的用法,请看如下示例: hc@ubt:~/golang/goc2p/helper/pds$ go tool pprof showpds ../../../pprof/cpu.out Welcome to pprof! For help, type 'help'. (pprof) gv Total: 101 samples sh: 1: dot: not found go tool pprof: signal: broken pipe 其中,“(pprof)”是pprof工具在交互模式下的提示符。 从输出信息中我们可以看到,gv命令并没有正确的被执行。原因是没有找到命令dot。经查,这个命令属于一个开源软件Graphviz。Graphviz的核心功能是图表的可视化。我们可以通过命令sudo apt-get install graphviz来安装这个软件。注意,上面这条命令仅可用于Debian的Linux发行版及其衍生版。如果是在Redhat的Linux发行版及其衍生版下,可以使用命令“yum install graphviz”来安装Graphviz。安装好Graphviz后,我们再来执行gv命令。 (pprof) gv Total: 101 samples gv -scale 0 (pprof) sh: 1: gv: not found 现在,输出信息有提示我们没有找到命令gv。gv是自由软件工程项目GNU(GNU's Not Unix)中的一款开源软件,用来以图形化的方式查看PDF文档。我们以同样的方式安装它。在Debian的Linux发行版及其衍生版下,执行命令sudo apt-get install gv,在Redhat的Linux发行版及其衍生版下,执行命令yum install gv。软件gv被安装好后,我们再次执行gv命令。在运行着图形界面软件的Linux操作系统下,会弹出这样一个窗口。如图5-3。 图0-3 pprof工具的gv命令的执行结果 我们看到,在概要图的最上面显示了一些基本的信息。其中,“showpds”是我们生成概要文件时用到的那个可执行文件。它也是概要文件中内容的来源。“Total samples:”后面的数字23的含义是在本次程序执行期间分析器一共进行了23次取样。我们已经知道,CPU使用情况的取样操作会以每10毫秒一次的频率进行。因此,取样23次就意味着程序运行所花费的CPU时间大约为10毫秒 * 23 = 0.23秒。由于我们并没有在gv命令后加入用于约束显示内容的参数focus,所以在“Focusing on:”后面的数字也是23。也正是由于这个原因,后边两行信息中的数字均为0。读者可以自行试验一下在gv命令后加入focus参数的情形,例如:gv ShowDepStruct。在下面的描述中,我们把函数和方法统称为函数。 现在,我们把视线放在主要的图形上。此图形由矩形和有向线段组成。在此图形的大多数矩形中都包含三行信息。第一行是函数的名字。第二行包含了该函数的本地取样计数(在括号左边的数字)及其在取样总数中所占的比例(在括号内的百分比)。第三行则包含了该函数的累积取样计数(括号左边的数字)及其在取样总数中所占的比例(在括号内的百分比)。 首先,读者需要搞清楚两个相似但不相同的概念,即:本地取样计数和累积取样计数。本地取样计数的含义是当前函数在取样中直接出现的次数。累积取样计数的含义是当前函数以及当前函数直接或间接调用的函数在取样中直接出现的次数。所以,存在这样一种场景:对于一个函数来说,它的本地取样计数是0。因为它没有在取样中直接出现过。但是,由于它直接或间接调用的函数频繁的直接出现在取样中,所以这个函数的累积取样计数却会很高。我们以上图中的函数mian.main为例。由于main.main函数在所有取样中都没有直接出现过,所以它的本地取样计数为0。但又由于它是命令源码文件中的入口函数,程序中其他的函数都直接或间接的被它调用。所以,它的累积取样计数是所有函数中最高的,达到了22。注意,不论是本地取样计数还是累积取样计数都没有把函数对自身的调用计算在内。函数对自身的调用又被称为递归调用。 最后需要说明的是,图形中的有向线段表示函数之间的调用关系。有向线段旁边的数字为线段起始位置的函数对线段末端位置的函数的调用计数。这里所说的调用计数其实是以函数的累积取样计数为依托的。更具体的讲,如果有一个从函数A到函数B的有向线段且旁边的数字为10,那么就说明在函数B的累加取样计数中有10次计数是由函数A对函数B的直接调用所引起的。也由于这个原因,函数A对函数B的调用计数必定小于等于函数B的累积取样计数。 至此,我们已经对概要图中的所有元素都进行了说明,相信读者已经能够读懂它了。那么,我们怎样通过概要图对程序进行分析呢? 我们可以把概要图中的这个主要图形看成是一张函数调用关系图。在一般情况下,处在非终端节点位置的函数的本地取样计数会非常小,至少会比该函数的累积取样计数小很多。因为它们都是通过对其它函数的调用来实现自身的功能的。进一步说,所有使用Go语言编写的代码的功能最后都需要依托操作系统所提供的API来实现。处在终端节点位置的函数一般都存在于平台相关的源码文件中,甚至有的函数本身就是操作系统的某个API在Go语言中的映射。它们的累积取样计数与本地取样计数是一致的。因此,这类函数的描述信息只有两行,即它的名称和它的累积取样计数。 现在我们已经明确了在概要图中出现的一个函数的本地取样计数、累积取样计数和调用计数的概念和含义以及它们之间的关系。这三个计数是我们分析程序性能的重要依据。 我们可以通过一个函数的累积取样次数计算出执行它所花费的时间。一个函数的累积取样计数越大就说明调用它所花费的CPU时间越多。具体来说,我们可以用CPU取样间隔(10毫秒)乘以函数的累积取样计数得出它所花费的实际时间。虽然这个实际时间只精确到了10毫秒的级别,但是这对于程序性能分析来说已经足够了。即使一个函数的累积取样计数很大,我们也不能判定这个函数本身就是有问题的。我们应该顺藤摸瓜,去寻找这个函数直接或间接调用的函数中最耗费CPU时间的那些函数。其实,这样的查找很容易,因为我们已经有了概要图。在其中的函数调用关系图中,累积取样计数越大的函数就拥有更大的节点(图中的矩形)面积。不过这也有例外,那就是程序的入口函数。广义来讲,在整个函数调用关系中处在初始位置附近且与之相连的有向线段在同一方向上至多只有一个的函数都可以被称作入口函数。无论它们的累积取样计数有多大,其所属的节点的面积都是在函数调用关系图中最小的。由于出现在取样和函数调用关系图中的所有函数几乎都源自入口函数的直接或间接的调用,所以入口函数的累积取样次数必定是它们中最大的。一般情况下,我们并不需要在意入口函数的计数数值,所以在函数调用关系图中也就不需要使用大面积的节点来强调它们。在图5-3中,函数runtime.main和main.main都可以被视为入口函数。另外,在函数调用关系图中,有向线段的粗细也反应了对应的调用计数的大小。 下面,作者总结了根据函数的相关计数来对其进行分析的三个过程: 如果一个处在终端节点位置上的函数的累积取样计数和百分比都很大,就说明它自身花费了过多的CPU时间。这时,需要检查这个函数所实现的功能是否确实需要花费如此多的时间。如果花费的时间超出了我们的估算,则需要通过list命令找出函数体内最耗时的代码并进行进一步分析。如果我们发现这个函数所承担的职责过多,那么可以直接将这个函数拆分成多个拥有不同职责的更小的函数。 如果一个处在非终端节点位置上的函数的累积取样计数和百分比都很大并且超出了我们的估算,那么我们应该首先查看其本地取样计数的大小。如果它的本地取样计数和百分比也很大,我们就需要通过list命令对这个函数体中的代码进行进一步分析。否则,我们就应该把关注点放在其下的分支节点所对应的函数上。如果当前节点下的所有直接分支节点的函数的累积取样计数都不大,但是直接分支节点的数量却非常多(十几甚至几十个),那么大致上可以断定当前节点的函数承担了过多的与流程控制相关的职责,我们需要对它进行拆分甚至重新设计。如果当前节点下的分支节点中包含累积取样计数和百分比很大的函数,那么我们就应该根据这个分支节点的类型(终端节点或非终端节点)来对其进行过程1或过程2的分析。 单从调用计数的角度,我们并不能判断一个函数是否承担了过多的职责或者包含了过多的流程控制逻辑。但是,我们可以把调用计数作为定位问题的一种辅助手段。举个例子,如果根据过程1和过程2中的分析,我们怀疑在函数B及其调用的函数中可能存在性能问题,并且我们还发现函数A对函数B的调用计数也非常大,那么我们就应该想到函数B在取样中的频繁出现也许是由函数A对函数B的频繁调用引起的。在这种情况下,我们就应该先查看函数A中的代码,检查其中是否包含了过多的对函数B的不合理调用。如果存在不合理的调用,我们就应该对这部分代码进行重新设计。除此之外,我们还可以根据调用计数来判定一些小问题。比如,如果一个函数与调用它的所有函数都处于同一个代码包,那么我们就应该考虑把被调用的函数的访问权限设置为包内私有。如果对一个函数的调用都是来自于同一个函数,我们可以考虑在符合单一职责原则的情况下把这两个函数合并。读者可能已经注意到,这与过程1中的一个建议是相互对立的。实际上,这也属于一种推迟优化策略。 在上述几个分析过程中的所有建议都不是绝对的。程序优化是一个复杂的过程,在很多时候都需要在多个指标或多个解决方案之间进行权衡和博弈。 在这几个分析过程的描述中,我们多次提到了list命令。现在我们就来对它进行说明。先来看一个示例: (pprof) list ShowDepStruct Total: 23 samples ROUTINE ====================== main.ShowDepStruct in /home/hc/golang/goc2p /src/helper/pds/showpds.go 0 20 Total samples (flat / cumulative) . . 44: } . . 45: fmt.Printf(\"The dependency structure of package '%s':\\n\", pkgImportPath) . . 46: ShowDepStruct(pn, \"\") . . 47: } . . 48: --- . . 49: func ShowDepStruct(pnode *pkgtool.PkgNode, prefix string) { . . 50: var buf bytes.Buffer . . 51: buf.WriteString(prefix) . . 52: importPath := pnode.ImportPath() . 2 53: buf.WriteString(importPath) . 1 54: deps := pnode.Deps() . . 55: //fmt.Printf(\"P_NODE: '%s', DEP_LEN: %d\\n\", importPath, len(deps)) . . 56: if len(deps) == 0 { . 5 57: fmt.Printf(\"%s\\n\", buf.String()) . . 58: return . . 59: } . . 60: buf.WriteString(ARROWS) . . 61: for _, v := range deps { . 12 62: ShowDepStruct(v, buf.String()) . . 63: } . . 64: } --- . . 65: . . 66: func getPkgImportPath() string { . . 67: if len(pkgImportPathFlag) > 0 { . . 68: return pkgImportPathFlag . . 69: } (pprof) 我们在pprof工具的交互界面中输入了命令list ShowDepStruct之后得到了很多输出信息。其中,ShowDepStruct为参数routine_regexp的值。输出信息的第一行告诉我们CPU概要文件中的取样一共有23个。这与我们之前讲解gv命令时看到的一样。输出信息的第二行显示,与我们提供的程序正则表达式(也就是参数routine_regexp)的值匹配的函数是main.ShowDepStruct,并且这个函数所在的源码文件的绝对路径是/home/hc/golang/goc2p/src/helper/pds/showpds.go。输出信息中的第三行告诉我们,在main.ShowDepStruct函数体中的代码的本地取样计数的总和是0,而累积取样计数的总和是20。在第三行最右边的括号中,flat代表本地取样计数,而cumulative代表累积取样计数。这是对该行最左边的那两个数字(也就是0和20)的含义的提示。从输出信息的第四行开始是对上述源码文件中的代码的截取,其中包含了main.ShowDepStruct函数的源码。list命令在这些代码的左边添加了对应的行号,这让我们查找代码更加容易。另外,在代码行号左边的对应位置上显示了每行代码的本地取样计数和累积取样计数。如果计数为0,则用英文句号“.”代替。这使得我们可以非常方便的找到存在计数值的代码行。 一般情况下,每行代码对应的本地取样计数和累积取样计数都应该与我们用gv命令生成的函数调用关系图中的计数相同。但是,如果一行代码中存在多个函数调用的话,那么在代码行号左边的计数值就会有偏差。比如,在上述示例中,第62行代码ShowDepStruct(v, buf.String())的累积取样计数是12。但是从之前的函数调用关系图中我们得知,函数main.ShowDepStruct的累积取样计数是10。它们之间的偏差是2。实际上,在程序被执行的时候,第62行代码是由两个操作步骤组成的。第一个步骤是执行函数调用buf.String()并获得结果。第二个步骤是,调用函数ShowDepStruct,同时将变量v``和执行第一个步骤所获得的结果作为参数传入。所以,这2个取样计数应该归咎于第62行代码中的函数调用子句buf.String()。也就是说,第62行代码的累积取样计数由两部分组成,即函数main.ShowDepStruct的累积取样计数和函数bytes.(*Buffer).String的累积取样计数。同理,示例中的第57行代码fmt.Printf(\"%s\\n\", buf.String())```的累积取样计数也存在偏差。读者可以试着分析一下原因。 如果读者想验证上面所说的产生偏差的原因的话,可以将上面示例中的第62行代码和第57行代码分别拆成两行,然后再对命令源码文件showpds.go进行编译、运行(记得加入相关标记)并用pprof工具的list命令进行查看。不过,验证这个原因还有一个更简便的方式——使用pprof工具中的disasm命令。我们在下面的示例中执行disasm命令并后跟routine_regexp参数值ShowDepStruct。 bash (pprof) disasm ShowDepStruct Total: 23 samples ROUTINE ====================== main.ShowDepStruct 0 20 samples (flat, cumulative) 87.0% of total -------------------- /home/hc/mybook/goc2p/src/helper/pds/showpds.go . . 49: func ShowDepStruct(pnode *pkgtool.PkgNode, prefix string) { . 10 62: ShowDepStruct(v, buf.String()) . . 80490ce: MOVL main.&buf+3c(SP),AX . . 80490d2: XORL BX,BX . . 80490d4: CMPL BX,AX . . 80490d6: JNE main.ShowDepStruct+0x25f(SB) . . 80490d8: LEAL go.string.*+0x12d4(SB),BX . . 80490de: MOVL 0(BX),CX . . 80490e0: MOVL 4(BX),AX . . 80490e3: MOVL main.v+48(SP),BX . . 80490e7: MOVL BX,0(SP) . . 80490ea: MOVL CX,4(SP) . . 80490ee: MOVL AX,8(SP) . 10 80490f2: CALL main.ShowDepStruct(SB) . . 80490f7: MOVL main.autotmp_0046+44(SP),DX . . 80490fb: MOVL main.autotmp_0048+70(SP),CX . . 61: for _, v := range deps { . . 80490ff: INCL DX . . 8049100: MOVL main.autotmp_0047+2c(SP),BX . . 8049104: CMPL BX,DX . . 8049106: JLT main.ShowDepStruct+0x20b(SB) . . 64: } . . 8049108: ADDL $80,SP . . 804910e: RET . 2 62: ShowDepStruct(v, buf.String()) . . 804910f: MOVL 8(AX),DI . . 8049112: MOVL 4(AX),DX . . 8049115: MOVL c(AX),CX . . 8049118: CMPL CX,DX . . 804911a: JCC main.ShowDepStruct+0x273(SB) . . 804911c: CALL runtime.panicslice(SB) . . 8049121: UD2 . . 8049123: MOVL DX,SI . . 8049125: SUBL CX,SI . . 8049127: MOVL DI,DX . . 8049129: SUBL CX,DX . . 804912b: MOVL 0(AX),BP . . 804912d: ADDL CX,BP . . 804912f: MOVL BP,main.autotmp_0073+74(SP) . . 8049133: MOVL main.autotmp_0073+74(SP),BX . . 8049137: MOVL BX,0(SP) . . 804913a: MOVL SI,4(SP) . . 804913e: MOVL DX,8(SP) . 2 8049142: CALL runtime.slicebytetostring(SB) (pprof) (pprof) 由于篇幅原因,我们只显示了部分输出内容。disasm命令与list命令的输出内容有几分相似。实际上,disasm命令在输出函数main.ShowDepStruct的源码的同时还在每一行代码的下面列出了与这行代码对应的汇编指令。并且,命令还在每一行的最左边的对应位置上标注了该行汇编指令的本地取样计数和累积取样计数,同样以英文句号“.”代表计数为0的情况。另外,在汇编指令的左边且仅与汇编指令以一个冒号相隔的并不是像Go语言代码行中那样的行号,而是汇编指令对应的内存地址。 在上面这个示例中,我们只关注命令源码文件showpds.go中的第62行代码`ShowDepStruct(v, buf.String())所对应的汇编指令。请读者着重查看在累积取样计数的列上有数字的行。像这样的行一共有四个。为了方便起见,我们把这四行摘抄如下: . 10 62: ShowDepStruct(v, buf.String()) . 10 80490f2: CALL main.ShowDepStruct(SB) . 2 62: ShowDepStruct(v, buf.String()) . 2 8049142: CALL runtime.slicebytetostring(SB) 其中的第一行和第三行说明了第62行代码的累积取样计数的组成,而第二行和第四行说明了存在这样的组成的原因。其中,汇编指令CALL main.ShowDepStruct(SB)的累积取样计数为10。也就是说,调用main.ShowDepStruct函数期间分析器进行了10次取样。而汇编指令runtime.slicebytetostring(SB)的累积取样计数为2,意味着在调用函数runtime.slicebytetostring期间分析器进行了2次取样。但是,runtime.slicebytetostring函数又是做什么用的呢?实际上,runtime.slicebytetostring函数正是被函数bytes.(*Buffer).String函数调用的。它实现的功能是把元素类型为byte的切片转换为字符串。综上所述,确实像我们之前说的那样,命令源码文件showpds.go中的第62行代码ShowDepStruct(v, buf.String())的累积取样计数12由函数main.ShowDepStruct的累积取样计数10和函数bytes.(*Buffer).String的累积取样计数2组成。 至此,我们介绍了三个非常有用的命令,它们是gv命令、list命令和disasm命令。我们可以通过gv命令以图像化的方式查看程序中各个函数的本地取样计数、累积取样计数以及它们之间的调用关系和调用计数,并且可以很容易的通过节点面积的大小和有向线段的粗细找到计数值较大的节点。当我们依照之前所描述的分析过程找到可疑的高耗时的函数时,便可以使用list命令来查看函数内部各个代码行的本地取样计数和累积取样计数情况,并能够准确的找到使用了过多的CPU时间的代码。同时,我们还可以使用disasm命令来查看函数中每行代码所对应的汇编指令,并找到代码耗时的根源所在。因此,只要我们适时配合使用上述的这三条命令,就几乎可以在任何情况下理清程序性能问题的来龙去脉。可以说,它们是Go语言为我们提供的用于解决程序性能问题的瑞士军刀。 但是,有时候我们只是想了解哪些函数花费的CPU时间最多。在这种情况下,前面讲到的那几个命令所产生的数据就显得不那么直观了。不过不要担心,pprof工具为此提供了top命令。请看如下示例: bash (pprof) top Total: 23 samples 5 21.7% 21.7% 5 21.7% runtime.findfunc 5 21.7% 43.5% 5 21.7% stkbucket 3 13.0% 56.5% 3 13.0% os.(*File).write 1 4.3% 60.9% 1 4.3% MHeap_AllocLocked 1 4.3% 65.2% 1 4.3% getaddrbucket 1 4.3% 69.6% 2 8.7% runtime.MHeap_Alloc 1 4.3% 73.9% 1 4.3% runtime.SizeToClass 1 4.3% 78.3% 1 4.3% runtime.aeshashbody 1 4.3% 82.6% 1 4.3% runtime.atomicload64 1 4.3% 87.0% 1 4.3% runtime.convT2E (pprof) 在默认情况下,top命令会输出以本地取样计数为顺序的列表。我们可以把这个列表叫做本地取样计数排名列表。列表中的每一行都有六列。我们现在从左到右看,第一列和第二列的含义分别是:函数的本地取样计数和该本地取样计数在总取样计数中所占的比例。第四列和第五列的含义分别是:函数的累积取样计数和该累积取样计数在总取样计数中所占的比例。第五列的含义是左边几列数据所对应的函数的名称。读者应该对它们已经很熟悉了。这里需要重点说明的是第三列。第三列的含义是目前已打印出的函数的本地取样计数之和在总取样计数中所占的百分比。更具体的讲,第三行第三列上的百分比值就是列表前三行的三个本地取样计数的总和13除以总取样计数23而得出的。我们还可以通过将第二行上的百分比值43.5%与第三行第二列上的百分比值13.0%相加得到第三行第三列上的百分比值。第三列的百分比值可以使我们很直观的了解到最耗时的几个函数总共花费掉的CPU时间的比重。我们可以利用这一比重为性能优化任务制定更加多样化的目标。比如,我们的性能优化目标是把前四个函数的总耗时比重占比从60.9%降低到50%,等等。 从上面的示例我们可以看出,本地取样计数较大的函数都属于标准库的代码包或者Go语言内部。所以,我们无法或者不方便对这些函数进行优化。我们在之前提到过,在一般情况下,用户程序中的函数的本地取样计数都会非常低甚至是0。所以,如果我们编写的函数处在本地取样计数排名列表中的前几名的位置上话,就说明这个函数可能存在着性能问题。这时就需要我们通过list命令产生针对于这个函数的数据并仔细进行分析。举个例子,如果我们在函数中加入了一些并发控制代码(不论是同步并发控制还是异步的并发控制)使得这个函数本身的执行时间很长并在本地取样计数排名列表中处于前几名的位置,那么我们就应该仔细查看该函数中各行代码的取样计数以及它们的逻辑合理性。比如,用于同步并发控制的代码中是否存在产生死锁的可能性,或者用于异步并发控制的代码中是否存在协调失衡或者资源分配不均的地方。与编写合理和优秀的并发控制代码有关的内容在本书的第三部分。 在默认情况下,top命令输出的列表中只包含本地取样计数最大的前十个函数。如果我们想自定义这个列表的项数,那么需要在top命令后面紧跟一个项数值。比如:命令top5会输出行数为5的列表,命令top20会输出行数为20的列表,等等。 如果我们在top命令后加入标签--cum,那么输出的列表就是以累积取样计数为顺序的。示例如下: (pprof) top20 --cum Total: 23 samples 0 0.0% 0.0% 23 100.0% gosched0 0 0.0% 0.0% 22 95.7% main.main 0 0.0% 0.0% 22 95.7% runtime.main 0 0.0% 0.0% 16 69.6% runtime.mallocgc 0 0.0% 0.0% 12 52.2% pkgtool.(*PkgNode).Grow 0 0.0% 0.0% 11 47.8% runtime.MProf_Malloc 0 0.0% 0.0% 10 43.5% main.ShowDepStruct 0 0.0% 0.0% 10 43.5% pkgtool.getImportsFromPackage 0 0.0% 0.0% 8 34.8% cnew 0 0.0% 0.0% 8 34.8% makeslice1 0 0.0% 0.0% 8 34.8% runtime.cnewarray 0 0.0% 0.0% 7 30.4% gostringsize 0 0.0% 0.0% 7 30.4% runtime.slicebytetostring 0 0.0% 0.0% 6 26.1% pkgtool.getImportsFromGoSource 0 0.0% 0.0% 6 26.1% runtime.callers 1 4.3% 4.3% 6 26.1% runtime.gentraceback 0 0.0% 4.3% 6 26.1% runtime.makeslice 5 21.7% 26.1% 5 21.7% runtime.findfunc 5 21.7% 47.8% 5 21.7% stkbucket 0 0.0% 47.8% 4 17.4% fmt.Fprintf (pprof) 我们可以把这类列表叫做累积取样计数排名列表。在这个列表中,有命令源码文件showpds.go和代码包pkgtool中的函数上榜。它们都存在于项目goc2p中。在实际场景中,用户程序中的函数一般都处于函数调用关系图的上游。尤其是命令源码文件的入口函数main.main。所以,它们的累积取样计数一般都比较大,即使在累积取样计数排名列表中名列前茅也不足为奇。不过,如果一个函数的累积取样计数和百分比都很大,就应该引起我们的注意了。这在前面讲解gv命令的时候也有所提及。如果我们想在排名列表中过滤掉一些我们不关注的函数,还可以在命令的最后追加一个或多个我们想忽略的函数的名称或相应的正则表达式。像这样: (pprof) top20 --cum -fmt\\..* -os\\..* Ignoring samples in call stacks that match 'fmt\\..*|os\\..*' Total: 23 samples After ignoring 'fmt\\..*|os\\..*': 15 samples of 23 (65.2%) 0 0.0% 0.0% 15 65.2% gosched0 0 0.0% 0.0% 14 60.9% main.main 0 0.0% 0.0% 14 60.9% runtime.main 0 0.0% 0.0% 12 52.2% runtime.mallocgc 0 0.0% 0.0% 8 34.8% pkgtool.(*PkgNode).Grow 0 0.0% 0.0% 7 30.4% gostringsize 0 0.0% 0.0% 7 30.4% pkgtool.getImportsFromPackage 0 0.0% 0.0% 7 30.4% runtime.MProf_Malloc 0 0.0% 0.0% 7 30.4% runtime.slicebytetostring 0 0.0% 0.0% 6 26.1% main.ShowDepStruct 0 0.0% 0.0% 6 26.1% pkgtool.getImportsFromGoSource 0 0.0% 0.0% 5 21.7% cnew 0 0.0% 0.0% 5 21.7% makeslice1 0 0.0% 0.0% 5 21.7% runtime.cnewarray 0 0.0% 0.0% 4 17.4% runtime.callers 1 4.3% 4.3% 4 17.4% runtime.gentraceback 0 0.0% 4.3% 3 13.0% MCentral_Grow 0 0.0% 4.3% 3 13.0% runtime.MCache_Alloc 0 0.0% 4.3% 3 13.0% runtime.MCentral_AllocList 3 13.0% 17.4% 3 13.0% runtime.findfunc (pprof) 在上面的示例中,我们通过命令top20获取累积取样计数最大的20个函数的信息,同时过滤掉了来自代码包fmt和os中的函数。 我们要详细讲解的最后一个命令是callgrind。pprof工具可以将概要转化为强大的Valgrind工具集中的组件Callgrind支持的格式。Valgrind是可运行在Linux操作系统上的用来成分析程序性能及程序中的内存泄露错误的强力工具。而作为其中组件之一的Callgrind的功能是收集程序运行时的一些数据、函数调用关系等信息。由此可知,Callgrind工具的功能基本上与我们之前使用标准库代码包runtime的API对程序运行情况进行取样的操作是一致的。 我们可以通过callgrind命令将概要文件的内容转化为Callgrind工具可识别的格式并保存到文件中。示例如下: (pprof) callgrind cpu.callgrind Writing callgrind file to 'cpu.callgrind'. (pprof) 文件cpu.callgrind是一个普通文本文件,所以我们可以使用任何文本查看器来查看其中的内容。但更方便的是,我们可以使用callgrind命令直接查看到图形化的数据。现在我们来尝试一下: (pprof) callgrind Writing callgrind file to '/tmp/pprof2641.0.callgrind'. Starting 'kcachegrind /tmp/pprof2641.0.callgrind & ' (pprof) sh: 1: kcachegrind: not found 我们没有在callgrind命令后添加任何作为参数的统计文件路径。所以callgrind命令会自行使用kcachegrind工具以可视化的方式显示统计数据。然而,我们的系统中还没有安装kcachegrind工具。 在Debian的Linux发行版及其衍生版下,我们可以直接使用命令sudo apt-get install kcachegrind来安装kcachegrind工具。或者我们可以从其官方网站下载安装包来进行安装。 安装好kcachegrind工具之后,我们再来执行callgrind命令: bash (pprof) callgrind Writing callgrind file to '/tmp/pprof2641.1.callgrind'. Starting 'kcachegrind /tmp/pprof2641.1.callgrind & ' (pprof) 从命令输出的提示信息可以看出,实际上callgrind命令把统计文件保存到了Linux的临时文件夹/tmp中。然后使用kcachegrind工具进行查看。下图为在pprof工具交互模式下执行callgrind命令后弹出的kcachegrind工具界面。 图0-4 使用kcachegrind工具查看概要数据 从上图中我们可以看到,kcachegrind工具对数据的展现非常直观。总体上来说,界面被分为了左右两栏。在左栏中的是概要文件中记录的函数的信息列表。列表一共有五列,从左到右的含义 分别是函数的累积取样计数在总取样计数中的百分比、函数的本地取样计数在总取样计数中的百分比、函数被调用的总次数(包括递归调用)、函数的名称以及函数所在的源码文件的名称。而在界面的右栏,我们查看在左栏中选中的行的详细信息。kcachegrind工具的功能非常强大。不过由于对它的介绍超出了本书的范围,所以我们就此暂告一个段落。 我们刚刚提到过,不加任何参数callgrind命令的执行分为两个步骤——生成统计文件和使用kcachegrind工具查看文件内容。还记得我们在之前已经生成了一个名为统计文件cpu.callgrind吗?其实,我们可以使用命令kcachegrind cpu.callgrind直接对它进行查看。执行这个命令后所弹出的kcachegrind工具界面与我们之前看到的完全一致。 到现在为止,我们又介绍了两个可以更直观的统计和查看概要文件中数据的命令。top命令让我们可以在命令行终端中查看这些统计信息。而callgrind命令使我们通过kcachegrind工具查看概要文件的数据成为了可能。这两个命令都让我们可以宏观的、从不同维度的来查看和分析概要文件。它们都是非常给力的统计辅助工具。 除了上面详细讲述的那些命令之外,pprof工具在交互模式下还支持少许其它的命令。这在表5-23中也有所体现。这些命令有的只是主要命令的另一种形式(比如web命令和weblist命令),而有的只是为了提供辅助功能(比如help命令和quit命令)。 在本小节中,我们只使用go tool pprof命令对CPU概要文件进行了查看和分析。读者可以试着对内存概要文件和程序阻塞概要文件进行分析。 相对于普通的编程方式来讲,并发编程都是复杂的。所以,我们就更需要像pprof这样的工具为我们保驾护航。大家可以将本小节当作一篇该工具的文档,并在需要时随时查看。 © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:09:40 "},"go/cmd/toolcgo.html":{"url":"go/cmd/toolcgo.html","title":"go tool cgo","keywords":"","body":"go tool cgo go tool cgo cgo也是一个Go语言自带的特殊工具。一般情况下,我们使用命令go tool cgo来运行它。这个工具可以使我们创建能够调用C语言代码的Go语言源码文件。这使得我们可以使用Go语言代码去封装一些C语言的代码库,并提供给Go语言代码或项目使用。 在执行go tool cgo命令的时候,我们需要加入作为目标的Go语言源码文件的路径。这个路径可以是绝对路径也可以是相对路径。但是,作者强烈建议在目标源码文件所属的代码包目录下执行go tool cgo命令并以目标源码文件的名字作为参数。因为,go tool cgo命令会在当前目录(也就是我们执行go tool cgo命令的目录)中生成一个名为_obj的子目录。该目录下会包含一些Go源码文件和C源码文件。这个子目录及其包含的文件理应被保存在目标代码包目录之下。至于原因,我们稍后再做解释。 我们现在来看可以作为go tool cgo命令参数的Go语言源码文件。这个源码文件必须要包含一行只针对于代码包C的导入语句。其实,Go语言标准库中并不存在代码包C。代码包C是一个伪造的代码包。导入这个代码包是为了告诉cgo工具在这个源码文件中需要调用C代码,同时也是给予cgo所产生的代码一个专属的命名空间。除此之外,我们还需要在这个代码包导入语句之前加入一些注释,并且在注释行中写出我们真正需要使用的C语言接口文件的名称。像这样: // #include import \"C\" 在Go语言的规范中,把在代码包C导入语句之前的若干注释行叫做序文(preamble)。 在引入了C语言的标准代码库stdlib.h之后,我们就可以在后面的源码中调用这个库中的接口了。像这样: func Random() int { return int(C.rand()) } func Seed(i int) { C.srand(C.uint(i)) } 我们把上述的这些Go语言代码写入Go语言的库源码文件rand.go中,并将这个源码文件保存在goc2项目的代码包basic/cgo/lib的对应目录中。 在Go语言源码文件rand.go中对代码包C有四处引用,分别是三个函数调用语句C.rand、C.srand和C.uint,以及一个导入语句import \"C\"。其中,在Go语言函数Random中调用了C语言标准库代码中的函数rand并返回了它的结果。但是,C语言的rand函数返回的结果的类型是C语言中的int类型。在cgo工具的环境中,C语言中的int类型与C.int相对应。作为一个包装C语言接口的函数,我们必须将代码包C的使用限制在当前代码包内。也就是说,我们必须对当前代码包之外的Go代码隐藏代码包C。这样做也是为了遵循代码隔离原则。我们在设计接口或者接口适配程序的时候经常会用到这种方法。因此,rand函数的结果的类型必须是Go语言的。所以,我们在这里使用函数int对C.int类型的C语言接口的结果进行了转换。当然,为了更清楚的表达,我们也可以将函数Random中的代码return int(C.rand())拆分成两行,像这样: var r C.int = C.rand() return int(r) 而Go语言函数Seed则恰好相反。C语言标准代码库中的函数srand接收一个参数,且这个参数的类型必须为C语言的uint类型,即C.uint。而Go语言函数Seed的参数为Go语言的int类型。为此,我们需要使用代码包C的函数unit对其进行转换。 实际上,标准C语言的数字类型都在cgo工具中有对应的名称,包括:C.char、C.schar(有符号字符类型)、C.uchar(无符号字符类型)、C.short、C.ushort(无符号短整数类型)、C.int、C.uint(无符号整数类型)、C.long、C.ulong(无符号长整数类型)、C.longlong(对应于C语言的类型long long,它是在C语言的C99标准中定义的新整数类型)、C.ulonglong(无符号的long long类型)、C.float和C.double。另外,C语言类型void*对应于Go语言的类型unsafe.Pointer。 如果想直接访问C语言中的struct、union或enum类型的话,就需要在名称前分别加入前缀struct、union或enum。比如,我们需要在Go源码文件中访问C语言代码中的名为command的struct类型的话,就需要这样写:C.struct_command。那么,如果我们想在Go语言代码中访问C语言类型struct中的字段需要怎样做呢?解决方案是,同样以C语言类型struct的实例名以及选择符“.”作为前导,但需要在字段的名称前加入下划线“”。例如,如果command1是名为command的C语言struct类型的实例名,并且这个类型中有一个名为name的字段,那么我们在Go语言代码中访问这个字段的方式就是command1._name。需要注意的是,我们不能在Go的struct类型中嵌入C语言类型的字段。这与我们在前面所说的代码隔离原则具有相同的意义。 在上面展示的库源码文件rand.go中有多处对C语言函数的访问。实际上,任何C语言的函数都可以 被Go语言代码调用。只要在源码文件中导入了代码包C。并且,我们还可以同时取回C语言函数的结果,以及一个作为错误提示信息的变量。这与我们在Go语言中同时获取多个函数结果的方法一样。同样的,我们可以使用下划线“_”直接丢弃取回的值。这在调用无结果的C语言函数时非常有用。请看下面的例子: package cgo /* #cgo LDFLAGS: -lm #include */ import \"C\" func Sqrt(p float32) (float32, error) { n, err := C.sqrt(C.double(p)) return float32(n), err } 上面这段代码被保存在了Go语言库源码文件math.go中,并与源码文件rand.go在同一个代码包目录。在Go语言函数Sqrt中的C.sqrt是一个在C语言标准代码库math.h中的函数。它会返回参数的平方根。但是在第一行代码中,我们接收由函数C.sqrt返回的两个值。其中,第一个值即为C语言函数sqrt的结果。而第二个值就是我们上面所说的那个作为错误提示信息的变量。实际上,这个变量的类型是Go语言的error接口类型。它包装了一个C语言的全局变量errno。这个全局变量被定义在了C语言代码库errno.h中。cgo工具在为我们生成C语言源码文件时会默认引入两个C语言标准代码库,其中一个就是errno.h。所以我们并不用在Go语言源码文件中使用指令符#include显式的引入这个代码库。cgo工具默认为我们引入的另一个是C语言标准代码库string.h。它包含了很多用于字符串处理和内存处理的函数。 在我们以“C.*”的形式调用C语言代码库时,有一点需要特别注意。在C语言中,如果一个函数的参数是一个具有固定尺寸的数组,那么实际上这个函数所需要的是指向这个数组的第一个元素的指针。C编译器能够正确识别和处理这个调用惯例。它可以自行获取到这个指针并传给函数。但是,这在我们使用cgo工具调用C语言代码库时是行不通的。在Go语言中,我们必须显式的将这个指向数组的第一个元素的指针传递给C语言的函数,像这样:``C.func1(&x[0])````。 另一个需要特别注意的地方是,在C语言中没有像Go语言中独立的字符串类型。C语言使用最后一个元素为‘\\0’的字符数组来代表字符串。在Go语言的字符串和C语言的字符串之间进行转换的时候,我们就需要用到代码包C中的C.C.CString、C.GoString和C.GoStringN等函数。这些转换操作是通过对字符串数据的拷贝来完成的。Go语言内存管理器并不能感知此类内存分配操作。因为它们是由C语言代码引发的。所以,我们在使用与C.CString函数类似的会导致内存分配操作的函数之后,需要调用代码包C的free函数以手动的释放内存。这里有一个小技巧,即我们可以把对C.free函数的调用放到defer语句中或者放入在defer之后的匿名函数中。这样就可以保证在退出当前函数之前释放这些被分配的内存了。请看下面这个示例: func Print(s string) { cs := C.CString(s) defer C.free(unsafe.Pointer(cs)) C.myprint(cs) } 上面这段代码被存放在goc2p项目的代码包basic/cgo/lib的库源码文件print.go中。其中的函数C.myprint是我们在该库源码文件的序文中自定义的。关于这种C语言函数定义方式,我们一会儿再解释。在这段代码中,我们首先把Go语言的字符串转换为了C语言的字符串。注意,变量cs的值实际上是指向字符串(在C语言中,字符串由字符数组代表)中第一个字符的指针。在cgo工具对应的上下文环境中,cs变量的类型是*C.Char。然后,我们通过defer语句和C.free函数保证由C语言代码分配的内存得以释放。请注意子语句unsafe.Pointer(cs)。正因为cs变量在C语言中的类型是指针类型,且与之相对应的Go语言类型是unsafe.Pointer。所以,我们需要先将其转换为Go语言可以识别的类型再作为参数传递给函数C.free。最后,我们将这个字符串打印到标准输出。 再次重申,我们在使用C.CString函数将Go语言的字符串转换为C语言字符串后,需要显式的调用C.free函数以释放用于数据拷贝的内存。而最佳实践是,将在defer语句中调用C.free函数。 在前面我们已经提到过,在导入代码包C的语句之上可以加入若干个为cgo工具而写的若干注释行(也被叫做序文)。并且,以#include和一个空格开始的注释行可以用来引入一个C语言的接口文件。我们也把序文中这种形式的字符串叫做指令符。指令符#cgo的用途是为编译器和连接器提供标记。这些标记在编译当前源码文件中涉及到代码包C的那部分代码时会被用到。 标记CFLAGS和LDFLAGS``可以被放在指令符#cgo```之后,并用于定制编译器gcc的行为。gcc(GNU Compiler Collection,GNU编译器套装),是一套由GNU开发的开源的编程语言编译器。它是GNU项目的关键部分,也是类Unix操作系统(也包括Linux操作系统)中的标准编译器。gcc(特别是其中的C语言编译器)也常被认为是跨平台编译器的事实标准。gcc原名为GNU C语言编译器(GNU C Compiler),因为它原本只能处理C语言。不过,gcc变得可以处理更多的语言。现在,gcc中包含了很多针对特定编程语言的编译器。我们在本节第一小节的末尾提及的gccgo就是这款套件中针对Go语言的编译器。标记CFLAGS可以指定用于gcc中的C编译器的选项。它尝尝用于指定头文件(.h文件)的路径。而标记LDFLAGS则可以指定gcc编译器会用到的一些优化参数,也可以用来告诉链接器需要用到的C语言代码库文件的位置。 为了清晰起见,我们可以把这些标记及其值拆分成多个注释行,并均以指令符#cgo作为前缀。另外,在指令符#cgo和标记之间,我们也可以加入一些可选的内容,即环境变量GOOS和GOARCH中的有效值。这样,我们就可以使这些标记只在某些操作系统和/或某些计算架构的环境下起作用了。示例如下: // #cgo CFLAGS: -DPNG_DEBUG=1 // #cgo linux CFLAGS: -DLINUX=1 // #cgo LDFLAGS: -lpng // #include import \"C\" 在上面的示例中,序文由四个注释行组成。第一行注释的含义是预定义一个名为PNG_DEBUG的宏并将它的值设置为1。而第二行注释的意思是,如果在Linux操作系统下,则预定义一个名为LINUX的宏并将它的值设置为1。第三行注释是与链接器有关的。它告诉链接器需要用到一个库名为png的代码库文件。最后,第四行注释引入了C语言的标准代码库png.h。 如果我们有一些在所有场景下都会用到的CFLAGS标记或LDFLAGS标记的值,那么就可以把它们分别作为环境变量CGO_CFLAGS和CGO_LDFLAGS的值。而对于需要针对某个导入了“C”的代码包的标记值就只能连同指令符#cgo一起放入Go语言源码文件的注释行中了。 相信读者对指令符#cgo和#include的用法已经有所了解了。 实际上,我们几乎可以在序文中加入任何C代码。像这样: /* #cgo LDFLAGS: -lsqlite3 #include #include // These wrappers are necessary because SQLITE_TRANSIENT // is a pointer constant, and cgo doesn't translate them correctly. // The definition in sqlite3.h is: // // typedef void (*sqlite3_destructor_type)(void*); // #define SQLITE_STATIC ((sqlite3_destructor_type)0) // #define SQLITE_TRANSIENT ((sqlite3_destructor_type)-1) static int my_bind_text(sqlite3_stmt *stmt, int n, char *p, int np) { return sqlite3_bind_text(stmt, n, p, np, SQLITE_TRANSIENT); } static int my_bind_blob(sqlite3_stmt *stmt, int n, void *p, int np) { return sqlite3_bind_blob(stmt, n, p, np, SQLITE_TRANSIENT); } */ 上面这段代码摘自开源项目gosqlite的Go语言源码文件sqlite.go。gosqlite项目是一个开源数据SQLite的Go语言版本的驱动代码库。实际上,它只是把C语言版本的驱动代码库进行了简单的封装。在Go语言的世界里,这样的封装随处可见,尤其是在Go语言发展早期。因为,这样可以非常方便的重用C语言版本的客户端程序,而大多数软件都早已拥有这类程序了。并且,封装C语言版本的代码库与从头开发一个Go语言版本的客户端程序相比,无论从开发效率还是运行效率上来讲都会是非常迅速的。现在让我们看看在上面的序文中都有些什么。很显然,在上面的序文中直接出现了两个C语言的函数my_bind_text和my_bind_blob。至于为什么要把C语言函数直接写到这里,在它们前面的注释中已经予以说明。大意翻译如下:这些包装函数是必要的,这是因为SQLITE_TRANSIENT是一个指针常量,而cgo并不能正确的翻译它们。看得出来,这是一种备选方案,只有在cgo不能帮我们完成工作时才会被选用。不管怎样,在序文中定义的这两个函数可以直接在当前的Go语言源码文件中被使用。具体的使用方式同样是通过“C.*”的形式来调用。比如源码文件sqlite.go中的代码: rv := C.my_bind_text(s.stmt, C.int(i+1), cstr, C.int(len(str))) 和 rv := C.my_bind_blob(s.stmt, C.int(i+1), unsafe.Pointer(p), C.int(len(v))) 上述示例中涉及到的源码文件可以通过这个网址访问到。有兴趣的读者可以前往查看。 我们再来看看我们之前提到过的库源码文件print.go(位于goc2p项目的代码包basic/cgo/lib之中)的序文: /* #include #include void myprint(char* s) { printf(\"%s\", s); } */ import \"C\" 我们在序文中定义一个名为myprint的函数。在这个函数中调用了C语言的函数printf。自定义函数myprint充当了类似于适配器的角色。之后,我们就可以在后续的代码中直接使用这个自定义的函数了: C.myprint(cs) 关于在序文中嵌入C语言代码的方法我们就介绍到这里。 现在,让我们来使用go tool cgo命令并以rand.go作为参数生成_obj子目录和相关源码文件: hc@ubt:~/golang/goc2p/src/basic/cgo/lib$ go tool cgo rand.go hc@ubt:~/golang/goc2p/src/basic/cgo/lib$ ls _obj rand.go hc@ubt:~/golang/goc2p/src/basic/cgo/lib$ ls _obj _cgo_defun.c _cgo_export.h _cgo_gotypes.go _cgo_.o rand.cgo2.c _cgo_export.c _cgo_flags _cgo_main.c rand.cgo1.go 子目录_obj中一共包含了九个文件。 其中,cgo工具会把作为参数的Go语言源码文件rand.go转换为四个文件。其中包括两个Go语言源码文件rand.cgo1.go和_cgo_gotypes.go,以及两个C语言源码文件_cgo_defun.c和rand.cgo2.c。 文件rand.cgo1.go用于存放cgo工具对原始源码文件rand.go改写后的内容。改写具体细节包括去掉其中的代码包C导入语句,以及替换涉及到代码包C的语句,等等。最后,这些替换后的标识符所对应的Go语言的函数、类型或变量的定义,将会被写入到文件_cgo_gotypes.go中。 需要说明的是,替换涉及到代码包C的语句的具体做法是根据xxx的种类将标识符C.xxx替换为_Cfunc_xxx或者_Ctype_xxx。比如,作为参数的源码文件rand.go中存在如下语句: C.srand(C.uint(i)) cgo工具会把它改写为: _Cfunc_srand(_Ctype_uint(i)) 其中,标识符C.srand被替换为_Cfunc_srand,而标识符C.uint被替换为了_Ctype_uint。并且,新的标识符_Cfunc_srand和_Ctype_uint的定义将会在文件_cgo_gotypes.go中被写明: type _Ctype_uint uint32 type _Ctype_void [0]byte func _Cfunc_srand(_Ctype_uint) _Ctype_void 其中,类型_Ctype_void可以表示空的参数列表或空的结果列表。 文件_cgo_defun.c中包含了相应的C语言函数的定义和实现。例如,C语言函数_Cfunc_srand的实现如下: #pragma cgo_import_static _cgo_54716c7dc6a7_Cfunc_srand void _cgo_54716c7dc6a7_Cfunc_srand(void*); void ·_Cfunc_srand(struct{uint8 x[4];}p) { runtime·cgocall(_cgo_54716c7dc6a7_Cfunc_srand, &p); } 其中,十六进制数“54716c7dc6a7”是cgo工具由于作为参数的源码文件的内容计算得出的哈希值。这个十六进制数作为了函数名称_cgo_54716c7dc6a7_Cfunc_srand的一部分。这样做是为了生成一个唯一的名称以避免冲突。我们看到,在源码文件_cgo_defun.c中只包含了函数_cgo_54716c7dc6a7_Cfunc_srand的定义。而其实现被写入到了另一个C语言源码文件中。这个文件就是rand.cgo2.c。函数_cgo_54716c7dc6a7_Cfunc_srand对应的实现代码如下: void _cgo_f290d3e89fd1_Cfunc_srand(void *v) { struct { unsigned int p0; } __attribute__((__packed__)) *a = v; srand(a->p0); } 这个函数从指向函数_Cfunc_puts的参数帧中抽取参数,并调用系统C语言函数srand,最后将结果存储在帧中并返回。 下面我们对在子目录_obj中存放的其余几个文件进行简要说明: 文件_cgo_flags用于存放CFLAGS标记和LDFLAGS标记的值。 文件_cgo_main.c用于存放一些C语言函数的存根,也可以说是一些函数的空实现。函数的空实现即在函数体重没有任何代码(return语句除外)的实现。其中包括在源码文件_cgo_export.c出现的声明为外部函数的函数。另外,文件_cgo_main.c中还会有一个被用于动态链接处理的main函数。 在文件_cgo_export.h中存放了可以暴露给C语言代码的与Go语言类型相对应的C语言声明语句。 文件_cgo_export.c中则包含了与可以暴露给C语言代码的Go语言函数相对应的C语言函数定义和实现代码。 文件cgo.o是gcc编译器在编译C语言源码文件rand.cgo2.c、_cgo_export.c和_cgo_main.c之后生成的结果文件。 在上述的源码文件中,文件rand.cgo1.go和_cgo_gotypes.go将会在构建代码包时被Go官方Go语言编译器(6g、8g或5g)编译。文件_cgo_defun.c会在构建代码包时被Go官方的C语言的编译器(6c、8c或5c)编译。而文件rand.cgo2.c、_cgo_export.c和_cgo_main.c 则会被gcc编译器编译。 如果我们在执行go tool cgo命令时加入多个Go语言源码文件作为参数,那么在当前目录的_obj子目录下会出现与上述参数数量相同的x.cgo1.go文件和x.cgo2.c文件。其中,x为作为参数的Go语言源码文件主文件名。 通过上面的描述,我们基本了解了由cgo工具生成的文件的内容和用途。 与其它go命令一样,我们在执行go tool cgo命令的时候还可以加入一些标记。如下表。 表0-24 go tool cgo命令可接受的标记 名称 默认值 说明 -cdefs false 将改写后的源码内容以C定义模式打印到标准输出,而不生成相关的源码文件。 -godefs false 将改写后的源码内容以Go定义模式打印到标准输出,而不生成相关的源码文件。 -objdir \"\" gcc编译的目标文件所在的路径。若未自定义则为当前目录下的_obj子目录。 -dynimport \"\" 如果值不为空字符串,则打印为其值所代表的文件生成的动态导入数据到标准输出。 -dynlinker false 记录在dynimport模式下的动态链接器信息。 -dynout \"\" 将-dynimport的输出(如果有的话)写入到其值所代表的文件中。 -gccgo false 生成可供gccgo编译器使用的文件。 -gccgopkgpath \"\" 对应于gccgo编译器的-fgo-pkgpath选项。 -gccgoprefix \"\" 对应于gccgo编译器的-fgo-prefix选项。 -debug-define false 打印相关的指令符#defines及其后续内容到标准输出。 -debug-gcc false 打印gcc调用信息到标准输出。 -import_runtime_cgo true 在生成的代码中加入语句“import runtime/cgo”。 -import_syscall true 在生成的代码中加入语句“import syscall”。 在上表中,我们把标记分为了五类并在它们之间以空行分隔。 在第一类标记中,-cdefs标记和-godefs标记都可以打印相应的代码到标准输出,并且使cgo工具不生成相应的源码文件。cgo工具在获取目标源码文件内容之后会改写其中的内容,包括去掉代码包C的导入语句,以及对代码包C的调用语句中属于代码包C的类型、函数和变量进行等价替换。如果我们加入了标记-cdefs或-godefs,那么cgo工具随后就会把改写后的目标源码打印到标准输出了。需要注意的是,我们不能同时使用这两个标记。使用这两个标记打印出来的源码内容几乎相同,而最大的区别也只是格式方面的。 第二类的三个标记都与动态链接库有关。在类Unix系统下,标记-dynimport的值可以是一个ELF(Executable and Linkable Format)格式或者Mach-O(Mach Object)格式的文件的路径。ELF即可执行链接文件格式。ELF格式的文件保存了足够的系统相关信息,以至于使它能够支持不同平台上的交叉编译和交叉链接,可移植性很强。同时,它在执行中支持动态链接共享库。我们在Linux操作系统下使用go命令生成的命令源码文件的可执行文件就是ELF格式的。而Mach-O是一种用于可执行文件、目标代码、动态链接库和内核转储的文件格式。在Windows下,这个标记的值应该是一个PE(Portable Execute)格式的文件的路径。在Windows操作系统下,使用go命令生成的命令源码文件的可执行文件就是PE格式的。 实质上,加入标记-dynimport的go tool cgo命令相当于一个被构建在cgo工具内部的独立的帮助命令。使用方法如go tool cgo -dynimport='cgo_demo.go'。这个命令会扫描这个标记的值所代表的可执行文件,并将其中记录的与已导入符号和已导入代码库相关的信息打印到标准输出。go build命令程序中有专门为cgo工具制定的规则。这使得它可以在编译直接或间接依赖了代码包C的命令源码文件时可以生成适当的可执行文件。在这个可执行文件中,直接包含了相关的已导入符号和已导入代码库的信息,以供之后使用。这样就无需使链接器复制gcc编译器的所有关于如何寻找已导入的符号以及使用它的位置的专业知识了。下面我们来试用一下go tool cgo -dynimport命令。 首先,我们创建一个命令源码文件cgo_demo.go,并把它存放在goc2p项目的代码包basic/cgo对应的目录下。命令源码文件cgo_demo.go的内容如下: package main import ( cgolib \"basic/cgo/lib\" \"fmt\" ) func main() { input := float32(2.33) output, err := cgolib.Sqrt(input) if err != nil { fmt.Errorf(\"Error: %s\\n\", err) } fmt.Printf(\"The square root of %f is %f.\\n\", input, output) } 在这个命令源码文件中,我们调用了goc2p项目的代码包basic/cgo/lib中的函数Sqrt。这个函数是被保存在库源码文件math.go中的。而在文件math.go中,我们导入了代码包C。也就是说,命令源码文件cgo_demo.go间接的依赖了代码包C。现在,我们使用go build命令将这个命令源码文件编译成ELF格式的可执行文件。然后,我们就能够使用go tool cgo -dynimport命令查看其中的导入信息了。请看如下示例: hc@ubt:~/golang/goc2p/basic/cgo$ go build cgo_demo.go hc@ubt:~/golang/goc2p/basic/cgo$ go tool cgo -dynimport='cgo_demo' #pragma cgo_import_dynamic pthread_attr_init pthread_attr_init#GLIBC_2.1 \"libpthread.so.0\" #pragma cgo_import_dynamic pthread_attr_destroy pthread_attr_destroy#GLIBC_2.0 \"libpthread.so.0\" #pragma cgo_import_dynamic stderr stderr#GLIBC_2.0 \"libc.so.6\" #pragma cgo_import_dynamic sigprocmask sigprocmask#GLIBC_2.0 \"libc.so.6\" #pragma cgo_import_dynamic free free#GLIBC_2.0 \"libc.so.6\" #pragma cgo_import_dynamic fwrite fwrite#GLIBC_2.0 \"libc.so.6\" #pragma cgo_import_dynamic malloc malloc#GLIBC_2.0 \"libc.so.6\" #pragma cgo_import_dynamic strerror strerror#GLIBC_2.0 \"libc.so.6\" #pragma cgo_import_dynamic srand srand#GLIBC_2.0 \"libc.so.6\" #pragma cgo_import_dynamic setenv setenv#GLIBC_2.0 \"libc.so.6\" #pragma cgo_import_dynamic __libc_start_main __libc_start_main#GLIBC_2.0 \"libc.so.6\" #pragma cgo_import_dynamic fprintf fprintf#GLIBC_2.0 \"libc.so.6\" #pragma cgo_import_dynamic pthread_attr_getstacksize pthread_attr_getstacksize#GLIBC_2.1 \"libpthread.so.0\" #pragma cgo_import_dynamic sigfillset sigfillset#GLIBC_2.0 \"libc.so.6\" #pragma cgo_import_dynamic __errno_location __errno_location#GLIBC_2.0 \"libpthread.so.0\" #pragma cgo_import_dynamic sqrt sqrt#GLIBC_2.0 \"libm.so.6\" #pragma cgo_import_dynamic rand rand#GLIBC_2.0 \"libc.so.6\" #pragma cgo_import_dynamic pthread_create pthread_create#GLIBC_2.1 \"libpthread.so.0\" #pragma cgo_import_dynamic abort abort#GLIBC_2.0 \"libc.so.6\" #pragma cgo_import_dynamic _ _ \"libm.so.6\" #pragma cgo_import_dynamic _ _ \"libpthread.so.0\" #pragma cgo_import_dynamic _ _ \"libc.so.6\" 从上面示例的输出信息中,我们可以看到可执行文件cgo_demo所涉及到的所有动态链接库文件以及相关的函数名和代码库版本等信息。 如果我们再加入一个标记-dynlinker,那么在命令的输出信息还会包含动态链接器的信息。示例如下: hc@ubt:~/golang/goc2p/src/basic/cgo$ go tool cgo -dynimport='cgo_demo' -dynlinker #pragma cgo_dynamic_linker \"/lib/ld-linux.so.2\" 如果我们在命令go tool cgo -dynimport后加入标记-dynout,那么命令的输出信息将会写入到指定的文件中,而不是被打印到标准输出。比如命令go tool cgo -dynimport='cgo_demo' -dynlinker -dynout='cgo_demo.di'就会将可执行文件cgo_demo中的导入信息以及动态链接器信息转储到当前目录下的名为“cgo_demo.di”的文件中。 第四类标记包含了-gccgo、-gccgopkgpath和-gccgoprefix。它们都与编译器gccgo有关。标记-gccgo的作用是使cgo工具生成可供gccgo编译器使用的源码文件。这些源码文件会与默认情况下生成的源码文件在内容上有一些不同。实际上,到目前为止,cgo工具还不能很好的与gccgo编译器一同使用。但是,按照gccgo编译器的主要开发者Ian Lance Taylor的话来说,gccgo编译器并不需要cgo工具,也不应该使用gcc工具。不管怎样,这种情况将会在Go语言的1.3版本中得到改善。 第五类标记用于打印调试信息,包括标记-debug-define和-debug-gcc。gcc工具不但会生成新的Go语言源码文件以保存其对目标源码改写后的内容,还会生成若干个C语言源码文件。cgo工具为了编译这些C语言源码文件,就会用到gcc编译器。在加入-debug-gcc标记之后,gcc编译器的输出信息就会被打印到标准输出上。另外,gcc编译器在对C语言源码文件进行编译之后会产生一个结果文件。这个结果文件就是在obj子目录下的名为_cgo.o的文件。 第六类标记的默认值都为true。也就是说,在默认情况下cgo工具生成的obj子目录下的Go语言源码文件_cgo_gotypes.go中会包含代码包导入语句```import \"runtime/cgo\"和import \"syscall\"。代码包导入语句import \"runtime/cgo\"只是引发了代码包runtime/cgo中的初始化函数的执行而没有被分配到一个具体的命名空间上。在这些初始化函数中,包含了对一些C语言的全局变量和函数声明的初始化过程。需要注意的是,只要我们在执行go tool cgo命令的时候加入了标记-gccgo,即使标记-import_runtime_cgo有效,在Go语言源码文件_cgo_gotypes.go中也不会包含import \"runtime/cgo\"```语句。 至此,我们在本小节讨论的都是Go语言代码如果通过cgo工具调用标准C语言编写的函数。其实,我们利用cgo工具还可以把Go语言编写的函数暴露给C语言代码。 Go语言可以使它的函数被C语言代码所用。这是通过使用一个特殊的注释“//export”来实现的。示例如下: package cgo /* #include extern void CFunction1(); */ import \"C\" import \"fmt\" //export GoFunction1 func GoFunction1() { fmt.Println(\"GoFunction1() is called.\") } func CallCFunc() { C.CFunction1() } 在这个示例中,我们使用注释行“//export GoFunction1”把Go语言函数GoFunction1暴露给了C语言代码。注意,注释行中在“//export ”之后的字符串必须与其下一行的那个函数的名字一致。我们也可以把字符串“//export”看成一种指令符,就像#cgo和#include。这里有一个限制,就是只要我们使用了指令符“//export”,在当前源码文件的序文中就不能包含任何C语言定义语句,只可以包含C语言声明语句。上面示例的序文中的extern void CFunction1();就是一个很好的例子。序文中的这一行C语言声明语句会被拷贝到两个不同的cgo工具生成的C语言源码文件中。这也正是其序文中不能包含C语言定义语句的原因。那么C语言函数CFunction1的定义语句我们应该放在哪儿呢?答案是放到在同目录的其它Go语言源码文件的序文中,或者直接写到C语言源码文件中。 我们把上面示例中的内容保存到名为go_export.go的文件中,并放到goc2p项目的basic/cgo/lib代码包中。现在我们使用go tool cgo来处理这个源码文件。如下: hc@ubt:~/golang/goc2p/basic/cgo/lib$ go tool cgo go_export.go 之后,我们会发现在_obj子目录下的C语言头文件_cgo_export.h中包含了这样一行代码: extern void GoFunction1(); 这说明C语言代码已经可以对函数GoFunction1进行调用了。现在我们使用go build命令构建goc2p项目的代码包basic/cgo,如下: hc@ubt:~/golang/goc2p/basic/cgo/lib$ go build # basic/cgo/lib /tmp/go-build477634277/basic/cgo/lib/_obj/go_export.cgo2.o: In function `_cgo_cc103c85817e_Cfunc_CFunction1': ./go_export.go:34: undefined reference to `CFunction1' collect2: ld return 1 构建并没有成功完成。根据错误提示信息我们获知,C语言函数CFunction1未被定义。这个问题的原因是我们并没有在Go语言源码文件go_export.go的序文中写入C语言函数CFunction1的实现,也即未对它进行定义。我们之前说过,在这种情况下,对应函数的定义应该被放到其它Go语言源码文件的序文或者C语言源码文件中。现在,我们在当前目录下创建一个新的Go语言源码文件go_export_def.go。其内容如下: package cgo /* #include void CFunction1() { printf(\"CFunction1() is called.\\n\"); GoFunction1(); } */ import \"C\" 这个文件是专门用于存放C语言函数定义的。注意,由于C语言函数printf来自C语言标准代码库stdio.h,所以我们需要在序文中使用指令符#include将它引入。保存好源码文件go_export_def.go之后,我们重新使用go tool cgo命令处理这两个文件,如下: hc@ubt:~/golang/goc2p/basic/cgo/lib$ go tool cgo go_export.go go_export_def.go 然后,我们再次执行go build命令构建代码包basic/cgo/lib: hc@ubt:~/golang/goc2p/basic/cgo/lib$ go build 显然,这次的构建成功完成。当然单独构建代码包basic/cgo/lib并不是必须的。我们在这里是为了检查该代码包中的代码(包括Go语言代码和C语言代码)是否都能够被正确编译。 还记得goc2p项目的代码包basic/cgo中的命令源码文件cgo_demo.go。现在我们在它的main函数的最后加入一行新代码:cgo.CallCFunc(),即调用在代码包``basic/cgo/lib```中的库源码文件go_export.go的函数。然后,我们运行这个命令源码文件: hc@ubt:~/golang/goc2p/basic/cgo$ go run cgo_demo.go The square root of 2.330000 is 1.526434. ABC CFunction1() is called. GoFunction1() is called. 从输出的信息可以看出,我们定义的C语言函数CFunction1和Go语言函数GoFunction1都已被调用,并且调用顺序正如我们所愿。这个例子也说明,我们可以非常方便的使用cgo工具完成如下几件事: Go语言代码调用标准C语言的代码。这也使得我们可以使用Go语言封装任何已存在的C语言代码库,并提供给其他Go语言代码使用。 可以在Go语言源码文件的序文中自定义任何C语言代码并由Go语言代码使用。这使得我们可以更灵活的对C语言代码进行封装。同时,我们还可以利用这一特性在我们自定义的C语言代码中使用Go语言代码。 通过指令符“//export”,可使C语言代码能够使用Go语言代码。这里所说的C语言代码是指我们在Go语言源码文件的序文中自定义的C语言代码。但是,go tool cgo命令会将序文中的C语言代码声明和定义分别写入到其生成的C语言头文件和C语言源码文件中。所以,从原则上讲,这已经具备了让外部C语言代码使用Go语言代码的能力。 综上所述,cgo工具不但可以使Go语言直接使用现存的非常丰富的C语言代码库,还可以使用Go语言代码扩展现有的C语言代码库。 至此,我们介绍了怎样独立的使用cgo工具。但实际上,我们可以直接使用标准go命令构建、安装和运行导入了代码包C的代码包和源码文件。标准go命令能够认出代码包C的导入语句并自动使用cgo工具进行处理。示例如下: hc@ubt:~/golang/goc2p/src/basic/cgo$ rm -rf lib/_obj hc@ubt:~/golang/goc2p/src/basic/cgo$ go run cgo_demo.go The square root of 2.330000 is 1.526434. ABC CFunction1() is called. GoFunction1() is called. 在上例中,我们首先删除了代码包basic/cgo/lib目录下的子目录_obj,以此来保证原始的测试环境。然后,我们直接运行了命令源码文件cgo_demo.go。在这个源码文件中,包含了对代码包basic/cgo/lib中函数的调用语句,而在这些函数中又包含了对代码包C的引用。从输出信息我们可以看出,命令源码文件cgo_demo.go的运行成功的完成了。这也验证了标准go命令在这方面的功能。不过,有时候我们还是很有必要单独使用go tool cgo命令,比如对相关的Go语言代码和C语言代码的功能进行验证或者需要通过标记定制化运行cgo工具的时候。另外,如果我们通过标准go命令构建或者安装直接或间接导入了代码C的命令源码文件,那么在生成的可执行文件中就会包含动态导入数据和动态链接器信息。我们可以使用go tool cgo命令查看可执行文件中的这些信息。 © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:09:39 "},"go/cmd/env.html":{"url":"go/cmd/env.html","title":"go env","keywords":"","body":"go env go env 命令go env用于打印Go语言的环境信息。其中的一些信息我们在之前已经多次提及,但是却没有进行详细的说明。在本小节,我们会对这些信息进行深入介绍。我们先来看一看go env命令情况下都会打印出哪些Go语言通用环境信息。 表0-25 go env命令可打印出的Go语言通用环境信息 名称 说明 CGO_ENABLED 指明cgo工具是否可用的标识。 GOARCH 程序构建环境的目标计算架构。 GOBIN 存放可执行文件的目录的绝对路径。 GOCHAR 程序构建环境的目标计算架构的单字符标识。 GOEXE 可执行文件的后缀。 GOHOSTARCH 程序运行环境的目标计算架构。 GOOS 程序构建环境的目标操作系统。 GOHOSTOS 程序运行环境的目标操作系统。 GOPATH 工作区目录的绝对路径。 GORACE 用于数据竞争检测的相关选项。 GOROOT Go语言的安装目录的绝对路径。 GOTOOLDIR Go工具目录的绝对路径。 下面我们对这些环境信息进行逐一说明。 CGO_ENABLED 通过上一小节的介绍,相信读者对cgo工具已经很熟悉了。我们提到过,标准go命令可以自动的使用cgo工具对导入了代码包C的代码包和源码文件进行处理。这里所说的“自动”并不是绝对的。因为当环境变量CGO_ENABLED被设置为0时,标准go命令就不能处理导入了代码包C的代码包和源码文件了。请看下面的示例: hc@ubt:~/golang/goc2p/src/basic/cgo$ export CGO_ENABLED=0 hc@ubt:~/golang/goc2p/src/basic/cgo$ go build -x WORK=/tmp/go-build775234613 我们临时把环境变量CGO_ENABLED的值设置为0,然后执行go build命令并加入了标记-x。标记-x会让命令程序将运行期间所有实际执行的命令都打印到标准输出。但是,在执行命令之后没有任何命令被打印出来。这说明对代码包basic/cgo的构建操作并没有被执行。这是因为,构建这个代码包需要用到cgo工具,但cgo工具已经被禁用了。下面,我们再来运行调用了代码包basic/cgo中函数的命令源码文件cgo_demo.go。也就是说,命令源码文件cgo_demo.go间接的导入了代码包C。还记得吗?这个命令源码文件被存放在goc2p项目的代码包basic/cgo中。示例如下: hc@ubt:~/golang/goc2p/src/basic/cgo$ export CGO_ENABLED=0 hc@ubt:~/golang/goc2p/src/basic/cgo$ go run -work cgo_demo.go WORK=/tmp/go-build856581210 # command-line-arguments ./cgo_demo.go:4: can't find import: \"basic/cgo/lib\" 在上面的示例中,我们在执行go run命令时加入了两个标记——-a和-work。标记-a会使命令程序强行重新构建所有的代码包(包括涉及到的标准库),即使它们已经是最新的了。标记-work会使命令程序将临时工作目录的绝对路径打印到标准输出。命令程序输出的错误信息显示,命令程序没有找到代码包basic/cgo。其原因是由于代码包basic/cgo无法被构建。所以,命令程序在临时工作目录和工作区中都找不到代码包basic/cgo对应的归档文件cgo.a。如果我们使用命令ll /tmp/go-build856581210查看临时工作目录,也找不到名为basic的目录。 不过,如果我们在环境变量CGO_ENABLED的值为1的情况下生成代码包basic/cgo对应的归档文件cgo.a,那么无论我们之后怎样改变环境变量CGO_ENABLED的值也都可以正确的运行命令源码文件cgo_demo.go。即使我们在执行go run命令时加入标记-a也是如此。因为命令程序依然可以在工作区中找到之前在我们执行go install命令时生成的归档文件cgo.a。示例如下: hc@ubt:~/golang/goc2p/src/basic/cgo$ export CGO_ENABLED=1 hc@ubt:~/golang/goc2p/src/basic/cgo$ go install ../basic/cgo hc@ubt:~/golang/goc2p/src/basic/cgo$ export CGO_ENABLED=0 hc@ubt:~/golang/goc2p/src/basic/cgo$ go run -a -work cgo_demo.go WORK=/tmp/go-build130612063 The square root of 2.330000 is 1.526434. ABC CFunction1() is called. GoFunction1() is called. 由此可知,只要我们事先成功安装了引用了代码包C的代码包,即生成了对应的代码包归档文件,即使cgo工具在之后被禁用,也不会影响到其它Go语言代码对该代码包的使用。当然,命令程序首先会到临时工作目录中寻找需要的代码包归档文件。 关于cgo工具还有一点需要特别注意,即:当存在交叉编译的情况时,cgo工具一定是不可用的。在标准go命令的上下文环境中,交叉编译意味着程序构建环境的目标计算架构的标识与程序运行环境的目标计算架构的标识不同,或者程序构建环境的目标操作系统的标识与程序运行环境的目标操作系统的标识不同。在这里,我们可以粗略认为交叉编译就是在当前的计算架构和操作系统下编译和构建Go语言代码并生成针对于其他计算架构或/和操作系统的编译结果文件和可执行文件。 GOARCH GOARCH的值的含义是程序构建环境的目标计算架构的标识,也就是程序在构建或安装时所对应的计算架构的名称。在默认情况下,它会与程序运行环境的目标计算架构一致。即它的值会与GOHOSTARCH的值是相同。但如果我们显式的设置了环境变量GOARCH,则它的值就会是这个环境变量的值。 GOBIN GOBIN的值为存放可执行文件的目录的绝对路径。它的值来自环境变量GOBIN。在我们使用go tool install命令安装命令源码文件时生成的可执行文件会存放于这个目录中。 GOCHAR GOCHAR的值是程序构建环境的目标计算架构的单字符标识。它的值会根据GOARCH的值来设置。当GOARCH的值为386时,GOCHAR的值就是8。当GOARCH的值为amd64时GOCHAR的值就是6。而GOCHAR的值5与GOARCH的值arm相对应。 GOCHAR主要有两个用途,列举如下: Go语言官方的平台相关的工具的名称会以它的值为前缀。的名称会以GOCHAR的值为前缀。比如,在amd64计算架构下,用于编译Go语言代码的编译器的名称是6g,链接器的名称是6l。用于编译C语言代码的编译器的名称是6c。而用于编译汇编语言代码的编译器的名称为6a。 Go语言的官方编译器生成的结果文件会以GOCHAR的值作为扩展名。Go语言的官方编译器6g在对命令源码文件编译之后会把结果文件go.6存放到临时工作目录的相应位置中。 GOEXE GOEXE的值会被作为可执行文件的后缀。它的值与GOOS的值存在一定关系,即只有GOOS的值为“windows”时GOEXE的值才会是“.exe”,否则其值就为空字符串“”。这与在各个操作系统下的可执行文件的默认后缀是一致的。 GOHOSTARCH GOHOSTARCH的值的含义是程序运行环境的目标计算架构的标识,也就是程序在运行时所在的计算机系统的计算架构的名称。在通常情况下,它的值不需要被显式的设置。因为用来安装Go语言的二进制分发文件和MSI(Microsoft软件安装)软件包文件都是平台相关的。所以,对于不同计算架构的Go语言环境来说,它都会是一个常量。 GOHOSTOS GOHOSTOS的值的含义是程序运行环境的目标操作系统的标识,也即程序在运行时所在的计算机系统的操作系统的名称。与GOHOSTARCH类似,它的值在不同的操作系统下是固定不变的,同样不需要显式的设置。 GOPATH 这个环境信息我们在之前已经提到过很多次。它的值指明了Go语言工作区目录的绝对路径。我们需要显式的设置环境变量GOPATH。如果有多个工作区,那么多个工作区的绝对路径之间需要用分隔符分隔。在windows操作系统下,这个分隔符为“;”。在其它操作系统下,这个分隔符为“:”。注意,GOPATH的值不能与GOROOT的值相同。 GORACE GORACE的值包含了用于数据竞争检测的相关选项。数据竞争是在并发程序中最常见和最难调试的一类bug。数据竞争会发生在多个Goroutine争相访问相同的变量且至少有一个Goroutine中的程序在对这个变量进行写操作的情况下。 数据竞争检测需要被显式的开启。还记得标记-race吗?我们可以通过在执行一些标准go命令时加入这个标记来开启数据竞争检测。在这种情况下,GORACE的值就会被使用到了。支持-race标记的标准go命令包括:go test命令、go run命令、go build命令和go install命令。 GORACE的值形如“option1=val1 option2=val2”,即:选项名称与选项值之间以等号“=”分隔,多个选项之间以空格“ ”分隔。数据竞争检测的选项包括log_path、exitcode、strip_path_prefix和history_size。为了设置GORACE的值,我们需要设置环境变量GORACE。或者,我们也可以在执行go命令时临时设置它,像这样: hc@ubt:~/golang/goc2p/src/cnet/ctcp$ GORACE=\"log_path=/home/hc/golang/goc2p /race/report strip_path_prefix=home/hc/golang/goc2p/\" go test -race 关于数据竞争检测的更多细节我们将会在本书的第三部分予以说明。 GOROOT GOROOT会是我们在安装Go语言时第一个碰到Go语言环境变量。它的值指明了Go语言的安装目录的绝对路径。但是,只有在非默认情况下我们才需要显式的设置环境变量GOROOT。这里所说的默认情况是指:在Windows操作系统下我们把Go语言安装到c:\\Go目录下,或者在其它操作系统下我们把Go语言安装到/usr/local/go目录下。另外,当我们不是通过二进制分发包来安装Go语言的时候,也不需要设置环境变量GOROOT的值。比如,在Windows操作系统下,我们可以使用MSI软件包文件来安装Go语言。 GOTOOLDIR GOTOOLDIR的值指明了Go工具目录的绝对路径。根据GOROOT、GOHOSTOS和GOHOSTARCH来设置。其值为$GOROOT/pkg/tool/$GOOS_$GOARCH。关于这个目录,我们在之前也提到过多次。 除了上面介绍的这些通用的Go语言环境信息,还两个针对于非Plan 9操作系统的环境信息。它们是CC和GOGCCFLAGS。环境信息CC的值是操作系统默认的C语言编译器的命令名称。环境信息GOGCCFLAGS的值则是Go语言在使用操作系统的默认C语言编译器对C语言代码进行编译时加入的参数。 如果我们要有针对性的查看上述的一个或多个环境信息,可以在go env命令的后面加入它们的名字并执行之。在go env命令和环境信息名称之间需要用空格分隔,多个环境信息名称之间也需要用空格分隔。示例如下: hc@ubt:~$ go env GOARCH GOCHAR 386 8 上例的go env命令的输出信息中,每一行对一个环境信息的值,且其顺序与我们输入的环境信息名称的顺序一致。比如,386为环境信息GOARCH,而8则是环境信息GOCHAR的值。 go env命令能够让我们对当前的Go语言环境进行简要的了解。通过它,我们也可以对当前安装的Go语言的环境设置进行简单的检查。 © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:09:33 "},"tools/":{"url":"tools/","title":"工具资源","keywords":"","body":"工具资源 总结记录下使用过程中值得推荐的软件,正所谓\"授人以鱼不如授人以渔\",有些博主总结的更加全面有用,在此也分享下. 工具资源 快典小报 快典小报,每周不定期精选优质内容! Chrome插件英雄榜 Chrome插件英雄榜, 为优秀的Chrome插件写一本中文说明书, 让Chrome插件英雄们造福人类~ Github星聚弃疗榜(Github星爆沙雕榜) 为Github创意项目写一本推荐书,让Github优秀项目造福人类~ IRM Markdowner | 微信排版编辑器 微信公众号格式化工具 : https://github.com/hadeshe93/irm-markdowner WeChat Format | 微信公众号排版编辑器 微信公众号排版编辑器 : https://github.com/lyricat/wechat-format 微信公众号格式化工具 在线Markdown转换器 : https://github.com/dyc87112/online-markdown/ -图表秀帮助文档 最好用的在线图表制作网站 : https://www.tubiaoxiu.com/ Cocos Creator v2.0 用户手册 gitbook 搭建的 Cocos Creator v2.0 用户手册 https://docs.cocos.com/creator/manual/zh/ OpenFalcon 产品文档 OpenFalcon是一款企业级、高可用、可扩展的开源监控解决方案 : http://book.open-falcon.org/ azk docs/ azk 是本地化开发的开源协调器 : http://docs.azk.io/ Learning Git Branching | 闯关游戏顺便学 git 游戏闯关中学习 git 常用技巧 : https://github.com/pcottle/learnGitBranching carbon | 源码转图片 将源码转成图片分享出去! https://github.com/dawnlabs/carbon GPSspg | 在线地图经度纬度查询 基于Google谷歌地图、百度地图、腾讯地图QQ地图、高德地图、图吧地图在线地图技术,可实现经度纬度查询地名位置、地名查询经度纬度位置、任意地图位置解析经纬度。海拔高度查询。 Regexper | 正则表达式可视化工具 正则表达式可视化工具 : https://gitlab.com/javallone/regexper-static sourceforge | 开源软件托管平台 完全开源和商业软件平台 : https://sourceforge.net/ Pandownload | 网盘下载神器 在线解析百度网盘资源,加速下载不封号 : https://www.baiduwp.com/ convert anything to anything 功能强大且免费的文档转换工具 : https://cloudconvert.com/ 博客主页 八一菜刀 有志者,事竟成!苦心人,天不负!加油吧~!!! Lyric 歌词经理,是一名产品经理. 程序猿DD 《Spring Cloud微服务实战》作者,SpringCloud中文社区创始人(bbs.springcloud.com.cn),Spring4All社区联合发起人(spring4all.com) 大大的微笑 博客最核心的还是内容, 内容为王! 再华丽的简介也只是表象, 有内涵才是关键! 敖小剑的博客 敖小剑,资深码农,十六年软件开发经验,微服务专家,Service Mesh布道师,Servicemesher社区联合创始人。专注于基础架构,Cloud Native 拥护者,敏捷实践者,坚守开发一线打磨匠艺的架构师。曾在亚信、爱立信、唯品会等任职,对基础架构和微服务有过深入研究和实践. 沈煜的博客 我叫沈煜,目前是个码农。上学的时候效仿业界大佬,整了个博客,那时候用的域名还不是现在用的这个,现在看到这个博客内容已经重新开始,不定期会写点什么,内容比较随意。 Zhang Jikai的博客 暂无个人介绍,来源于 gitbook 学习而关注. © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:10:58 "},"tools/mac-install-vmware.html":{"url":"tools/mac-install-vmware.html","title":"给 mac 装个 vmware 虚拟机","keywords":"","body":"给mac装个vmware虚拟机 mac 系统安装虚拟机目前有两种主流软件,一种是 Parallels Desktop ,另一种是 vmware. 本教程选用的是 vmware ,因为我之前 windows 上安装的虚拟机软件就是vmware,所以当然选择熟悉的方式鼓捣 mac 虚拟机了啊! 如果你没听说过虚拟机,可能你走错门了,不太适合看这篇教程.如果你执意要了解一下新知识,那我只能用自己浅薄的知识简单介绍下虚拟机的基本概念,一家之言,仅供参考哟! 正常来说,我们当前使用的电脑一般只有一个系统,比如你买的是 mac 笔记本,那么电脑系统就是 mac 操作系统,肯定不会是 windows 操作系统,所以你在 mac 电脑上看不到 windows 的办公软件三件套(word,excel,ppt). 当然你的电脑如果是 联想,戴尔,华硕,神舟 等等品牌的话 ,操作系统一般都是 windows,自然也不会是看到 mac 电脑上的 xcode 软件. 这一点很好理解,每种电脑与生俱来自带特定的操作系统,但是,竟然有一种方法能够突破这层限制! 什么是虚拟机 虚拟机(Virtual Machine)指通过软件模拟的具有完整硬件系统功能,运行在一个完全隔离环境中的完整计算机系统. 虚拟机,顾名思义是虚拟的机器.虚拟意味着不是真实的,机器意味着功能像是一台机器. 所以,我们能够理解上述百度百科的定义,虚拟机就是通过软件模拟实现真实机器功能. 软件模拟硬件是手段,实现机器功能是目的,既然如此理论上应该能够模拟任何操作系统,从而实现一台真实的物理机可以有多种不同的操作系统! 这样一来是不是很神奇?一台 mac 可以模拟出 Windows 电脑,也可以模拟出 linux 电脑. 换言之,只要有一台真实的物理机,通过软件我们就可以模拟出任意操作系统,这种软件就是我们接下来要介绍的 vmware . 虚拟机的使用场景 作为软件开发者,尽管很多语言支持跨平台运行,但是为了检验真实效果,我们需要运行到不同的环境中,比如windows 和 linux 系统的差异就不是一星半点! 或者为了教程的完整性,需要在各个平台测试运行后才能放心讲解某个知识点,不然别人按照教程发现运行不了,既浪费了别人的时间,又惹得人家不高兴,好心办坏事,大家都不好受. 所以,多个系统是刚需,如果真实环境中能够提供的话,那么自然不需要虚拟机. 只有实际情况下,不能提供真实的多种操作系统的情况下,我们才使用虚拟机技术来模拟不同的操作系统. 为什么是 vmware 通过软件模拟实现虚拟机目标,关键在于软件能力如何,所以选择哪一款软件直接决定了我们的虚拟机性能如何. 市面上,这种软件并不是唯一一家,至少目前我了解的就有 vmware ,Parallels Desktop 和 virtualbox .那么为什么选择 vmware 呢? 没有为什么,因为我之前用过 vmware 而已,对于小白的我,并没研究过三者软件有什么区别,哪一种更好,只要操作足够简单,市面上足够流行就可以了. 快速体验 vmware 本教程使用的是 mac 电脑,利用虚拟机安装了三种不同系统,分别是 win7旗舰版 , centos7.5 和 Ubuntu18. windows 虚拟机 启动 windows7 旗舰版 虚拟机,并且打开 chrome 浏览器测试. centos 虚拟机 启动 centos7.5 虚拟机,并且输入 pwd 命令测试. ubuntu 虚拟机 启动 ubuntu18 虚拟机,并且输入 pwd 命令测试. 如何安装 vmware 需要实现准备好下载工具以及留下足够的内存空间,因为虚拟机和镜像毕竟都挺大,下载挺费时间,安装也比较占内存,毕竟是完整的虚拟机. 下载软件以及序列号生成工具 链接: https://pan.baidu.com/s/1D0LL_muZ_YEbmgS4A6l3pw 提取码: ti8v 复制这段内容后打开百度网盘手机App,操作更方便哦 VMware-Fusion-11.0.3-12992109.dmg [必选] 安装软件 vmware 软件是收费软件,有一段时间的试用期,这里采用序列号激活方式,有条件的话,建议支持下正版. KeyMaker.app [可选] 序列号生成工具 如果是选择官网试用版或者已购买正版,自然不需要序列号生成功能. 安装 VMware-Fusion 软件 双击安装 VMware-Fusion-11.0.3-12992109.dmg 软件,接下来一路允许按照提示操作即可. 双击安装,因为软件源不是从 App Store 下载的,所以苹果默认策略不允许安装第三方来源. 既然询问是否打开软件,当然打开,不然怎么安装呢? 然而,还是太年轻,尽管刚才已经选择打开软件,然而苹果怕是担心我们不小心安装了有害应用吧?还是需要再问我们一遍,你确定要安装吗?我确定!我怎么知道是你本人?你输入管理员密码试试,密码正确我就让你安装. 千呼万呼使出来,你终于相信我是我了,安装进行中... 安装到一定程度时,会让我们输入产品密钥进行激活,否则只能试用30天,到期会再次提醒输入密钥,接下来我们来获取序列号. 打开 KeyMaker 软件 双击运行 KeyMaker.app ,弹出一系列序列号,随意选择某一行的序列号复制到上一步安装VMware-Fusion 的产品密钥并验证. Some good serial numbers.. KGLWE-VA5KZ-D1QHT-2R51Q-ZKQVV VTZMD-ZYTKX-D1ZCR-C6QCZ-QZZEV GQZX9-ZFX3T-Z1Z6Y-AFPCW-ZZ5GZ THQQR-00TZQ-81L0R-10LEG-G2ZTZ P1VXR-GFNGC-R1JJR-JXG3T-PQ7XT ZXYXY-VMTKZ-Y1YCX-7MQ9X-MQQ6V Here another one GK9QC-9KEM4-V1VAQ-P8JEP-MK77V Greets to Corby 随便复制一个序列号,继续正常安装. 输入产品密钥后基本上就是 vmware 用户,除非你不同意它的产品协议,当然同意了! 本以为安装到此结束,没想到还想要获取辅助功能权限,没办法,既然你想要,那我就给你啊,保不齐缺胳膊少腿的. 和安装相同,不是你选择允许苹果就允许,仍然需要你提供管理员密码以此确保主观操作意愿. 授予辅助功能权限,并再次锁定该项操作,可以与想的是,以后有应用想要申请辅助功能,必须经过管理员同意才可以,为了安全需要这么多步骤,好吧. 我只想安安静静使用 vmware 产品,不希望使用数据被上传收集,当然也有点小担心,毕竟也不是正儿八经的用户,所以才不加入体验计划呢! 验证 vmware 软件 在访达或启动台中找到 VMware-Fusion 软件单击启动,测试能否正常运行软件. 小结 本节主要介绍了什么是虚拟机,虚拟机的使用场景以及如何安装虚拟机,下一节我们将介绍如何给虚拟机加点料,让虚拟机派上练武之地! © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:10:27 "},"tools/mac-vmware-install-windows.html":{"url":"tools/mac-vmware-install-windows.html","title":"给 mac 虚拟机装个 windows","keywords":"","body":"给 mac 虚拟机装个 windows 前面我们介绍了如何在 mac 宿主机安装 VMware 虚拟机软件,本节我们将继续介绍如何给虚拟机安装 windows 镜像,切换不同的操作系统. VMware 软件是容器,镜像是内核,这里的镜像指的是操作系统. 下载镜像 windows 操作系统下载: https://msdn.itellyou.cn/ 按照实际需要选择适合自己的操作系统,这里选择的是 win7 旗舰版 ,然后选择详情会弹出下载链接. 一般需要使用迅雷等第三方工具下载种子链接,大小一般在 3g 多,下载时间稍微比较久! ed2k://|file|cn_windows_7_ultimate_x64_dvd_x15-66043.iso|3341268992|7DD7FA757CE6D2DB78B6901F81A6907A|/ 配置镜像 准备好已下载的镜像文件: cn_windows_7_ultimate_x64_dvd_x15-66043.iso 打开 VMware 软件,选择 文件->新建 选项开始安装镜像文件. 弹出安装配置界面,选择 从光盘或镜像中安装 选项,然后将已下载的镜像文件拖动到安装区进行识别. 识别到镜像文件后选中该文件,点击 继续 准备下一步安装. 配置账号信息以及产品密钥等信息,暂时不需要激活的话,也可以不填写产品密钥. 集成方式选择 更加独立 ,然后点击 继续. 提示下载 VMware Tools 工具,如果可以的话,最好还是先下载,也可以安装完毕后再手动下载. 确认配置信息无误后,点击 完成 ,等待镜像安装,,, 安装镜像 人生若只如初见,远远望见熟悉的背影,便确定了你就是我要安装的操作系统. 期待花开,耐心等待你的文件复制进程. 花开花落又是一年,你说重启才能遇到最美的季节,那我便等待你的凤凰涅槃. 见证你的凤凰涅槃,期待你的浴火重生. 浴火重生后的操作系统,还差最后一步就能欣赏你的容颜. 千呼万唤始出来,犹抱琵琶半遮面,正在进行最后的准备桌面. 终于等到你,还好我没放弃! 小结 总体来说,mac 系统安装 windows 镜像配置比较简单,基本上按照默认配置即可. 下载镜像时文件一般比较大,需要利用专门的第三方工具下载,比如本文提供的下载链接是种子文件,选择的第三方工具就是迅雷. © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:10:29 "},"tools/mac-vmware-install-centos.html":{"url":"tools/mac-vmware-install-centos.html","title":"给 mac 虚拟机装个 centos","keywords":"","body":"给 mac 虚拟机装个 centos 前文我们已经讲解了如何在 mac 系统上安装虚拟机软件,这节我们接着讲解如何利用虚拟机安装 centos 镜像. 安装镜像的大致步骤基本相同,只不过是配置项略显不同而已,如果需要安装其他系统镜像,请参考另外两篇教程. 下载镜像 centos 操作系统下载: https://www.centos.org/download/ DVD ISO 和 Minimal ISO 两种类型,普通用户推荐选择前一种标准版,开发用户建议选择后一种最小版. 标准版功能比较齐全,最小版保证最小依赖,后续缺啥填啥,比较灵活节省空间内存. 按照实际需要选择适合自己的操作系统,这里选择的是 centos7.6 ,然后选择合适的下载方式(直接下载或下载种子链接). 建议选择镜像服务器下载,如果直接下载官网的地址,速度感人,时间有点长. 依次选择 list of current mirrors -> http://mirrors.aliyun.com/centos/ -> 7.6.1810/ -> isos/ -> x86_64/ -> CentOS-7-x86_64-Minimal-1810.iso 选择合适的版本点击下载. 配置镜像 准备好已下载的镜像文件: CentOS-7-x86_64-Minimal-1804.iso 打开 VMware 软件,选择 文件->新建 选项开始安装镜像文件. 弹出安装配置界面,选择 从光盘或镜像中安装 选项,然后将已下载的镜像文件拖动到安装区进行识别. 识别到镜像文件后选中该文件,点击 继续 准备下一步安装. 选择固件类型,默认方式 传统 BIOS .然后点击 继续 . 确认配置信息无误后,点击 完成 ,等待镜像安装,,, 安装镜像 只因在人群中看见了 centos ,便确定了你就是我要安装的操作系统. 阅览安装摘要信息,等待继续安装. 设置用户信息,包括设置 root 用户密码和创建初始用户账号信息. 花开花落又是一年,你说重启才能遇到最美的季节,那我便等待你的凤凰涅槃. 终于等到你,还好我没放弃! 按照之前配置的用户信息登录系统,打印出当前路径,证明安装成功. 小结 总体来说,mac 系统安装 centos 镜像配置比较简单,基本上按照默认配置即可. 下载镜像时文件一般比较大,需要利用专门的第三方工具下载,既可以选择下载种子链接也可以直接下载. © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:10:28 "},"tools/mac-vmware-install-ubuntu.html":{"url":"tools/mac-vmware-install-ubuntu.html","title":"给 mac 虚拟机装个 ubuntu","keywords":"","body":"给 mac 虚拟机装个 ubuntu 前文我们已经讲解了如何在 mac 系统上安装虚拟机软件,这节我们接着讲解如何利用虚拟机安装 Ubuntu 镜像. 安装镜像的大致步骤基本相同,只不过是配置项略显不同而已,如果需要安装其他系统镜像,请参考另外两篇教程. 下载镜像 Ubuntu 操作系统下载: https://www.ubuntu.com/download 这里我们选择桌面版(Ubuntu Desktop),接着选择 LTS 长期支持版进行下载安装. 按照实际需要选择适合自己的操作系统,这里选择的是 Ubuntu18 LTS ,然后选择下载. 配置镜像 准备好已下载的镜像文件: ubuntu-18.04.2-desktop-amd64.iso 打开 VMware 软件,选择 文件->新建 选项开始安装镜像文件. 弹出安装配置界面,选择 从光盘或镜像中安装 选项,然后将已下载的镜像文件拖动到安装区进行识别. 识别到镜像文件后选中该文件,点击 继续 准备下一步安装. linux 快捷安装选项中配置用户信息,点击 继续 . 确认配置信息无误后,点击 完成 ,等待镜像安装... 安装镜像 只因在人群中看见了 Ubuntu ,便确定了你就是我要安装的操作系统. 惊鸿一瞥,容颜出现,安装进行时. 熟悉的命令行,成功只差一步. 现在输入之前配置的账号信息,开始登陆系统,见证奇迹的时刻即将来临... 终于等到你,还好我没放弃! 小结 总体来说,mac 系统安装 Ubuntu 镜像配置比较简单,基本上按照默认配置即可. 下载镜像时文件一般比较大,需要利用专门的第三方工具下载,既可以选择下载种子链接也可以直接下载. © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:10:29 "},"tools/windows-install-vmware.html":{"url":"tools/windows-install-vmware.html","title":"给 windows 装个 vmware 虚拟机","keywords":"","body":"给 windows 装个 vmware 虚拟机 如果长时间处于同一种环境,慢慢得我们会觉得有些无聊,所以适当地出去走走看看外面的世界能带给我们不一样的体验. 所以,何不出去走走,看看另一个世界? 然而,平时需要工作很难抽身无所顾忌地潇洒走开,这是不是意味着无法离开,要画地为牢了呢? 既然是工作问题,那么我们就从工作本身开始改变,我们每个人的电脑正常来说都只有一个操作系统,如果有一种方式能够让你切换到另一种操作系统上,岂不是相当于计算机的旅游了吗? 虽然我们本人不能亲身去另外一个地方看看,就让计算机代替我们去体验不同的环境吧! 虚拟机理论上支持任何操作系统,换句话说,Windows 系统可以装 Windows ,也可以装 Mac 和 Linux 等等. 如何换个新环境 Windows 电脑想要体验另一种操作系统,最简单的方式莫过于借助虚拟机方式,何为虚拟机? 虚拟机(Virtual Machine)指通过软件模拟的具有完整硬件系统功能,运行在一个完全隔离环境中的完整计算机系统. 虚拟机,顾名思义就是虚拟的计算机,虚拟意味着并不是真正的,计算机意味着拥有普通电脑的基本功能. 所以虚拟机要表达的意思就是说,创建一台并不是真实的计算机,但这种计算机却拥有普通计算机的基本能力. 正是由于虚拟机概念的提出,使得原本单一的操作系统支持多种不同的操作系统.Windows 计算机可以装Windows ,也可以装 Mac ,当然还有开发人员专用的 Linux. 原来的计算机称之为物理机也叫作宿主机,新产生的计算机就是虚拟机. 只要有明确的目标,虚拟机就能带你的计算机去另外一个世界. 平时不敢在物理机进行的秘密实验,你可以搬到虚拟机去实验; 羡慕键盘如飞的电脑黑客,你可以装个 Linux 虚拟机去体验一把命令行操作的灵活自由; 某些操作只能使用 Mac 电脑完成而苦于身边没有 Mac 电脑,也可以装个 Mac 虚拟机感受一下苹果的优雅. ... 不论是哪一种应用场景,虚拟机基本上都能满足,值得注意的是,虚拟机虽好,不要贪多哟! 只有物理机的性能足够强劲,才建议安装虚拟机,否则的话,病怏怏的身体怎么承受得住活泼好动灵魂的折腾. 安装虚拟机软件 市面上的虚拟机软件可选性有不少,而我主要介绍的是 VMware 软件的解决方案. VMware 软件不仅支持 Windows 宿主机,也支持 Mac 宿主机,而且一直在用也挺好的. 既然应认定了 VMware ,那还等什么,赶紧出来让我们看一眼吧! 百度搜索 vmware 或者直接进入 https://www.vmware.com/cn.html 即可访问 vmware 官网. 如果无法访问,可能需要另辟蹊径,具体原因你猜猜看. 打开 下载 > 免费产品试用版和演示 > Workstation Pro 查看下载页面. 跳转到下载页面后,选择 Windows 版本,点击 立即下载,耐心等待文件下载. Windows 下载链接: https://www.vmware.com/go/getworkstation-win 下载完毕后,双击 VMware-workstation-full-15.0.4-12990004.exe 进行默认安装,安装过程比较简单,以下动图仅供参考. 产品密钥可以使用 KeyGen.exe 自动生成,也可以从下列密钥中随意挑选一个. GF1T2-D8G97-M85MY-LDMNC-PZA96 AV34H-DDG8L-48EXQ-CQZET-ZZUR2 YY51H-FJXEQ-H85YQ-U5M5X-Q38D0 VY74R-FXX81-085PQ-DMMQT-X2AF6 VY10K-8WY03-H808Y-35YZE-NKKV2 YY11K-8UY46-M88MP-VMYEE-MYAF6 AA30K-27ZEL-480DQ-3DZ7C-MQKU4 VV7N8-D2E41-M852Q-8EQEX-ZQRU0 VC190-46W06-08E8P-TGQ5T-MLR8D AZ3NH-DQX9N-488RP-15ZXC-Q68VA 现在 vmware 已经安装成功,接下来我们将创建新的虚拟机,开始真正的计算机换装旅行吧! 在菜单栏依次点击 帮助 > 关于 确认一下是否注册成功. 虽然提供激活码注册方式,但是还是想说有条件的小伙伴请支持正版! 回顾总结 本文主要介绍了什么是虚拟机和虚拟机的应用场景以及如何安装 Vmware 软件从而安装虚拟机. 简单来说,虚拟机就是运行在本机上的一个虚拟独立的计算机,虽然不是真实的物理机,但是却拥有计算机的基本属性,不论是想在新电脑上瞎折腾还是想体验不同的操作系统,虚拟机都可以满足你的需求. 值得注意的是,虚拟机虽好,不要贪多哟,毕竟虚拟机很占资源,如果宿主机本身不给力的话,虚拟机也很难流畅地运行. 下节预告: 给 windows 虚拟机装个 windows 给 windows 虚拟机装个 centos 给 windows 虚拟机装个 ubantu © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:10:29 "},"tools/windows-vmware-install-mac.html":{"url":"tools/windows-vmware-install-mac.html","title":"给 windows 虚拟机装个 mac","keywords":"","body":"给 windows 虚拟机装个 mac 众所周知,Mac 很好但也很贵,对一般大众而言,漂亮简洁高颜值,对软件开发者而言,方便省心有点贵. 好到什么程度? 内置大量常用的开发工具,省去了初学者安装配置环境的麻烦,版本控制工具 svn 默认已安装,web 服务器 apache 默认已安装,编程开发环境 php 默认已安装等等,例子很多就不一一列举了. 除此之外,Mac 系统不同于 Windows 系统,Mac 系统是一种类 Unix 操作系统,命令行工具很好用,相当于提前熟悉 Linux 语法了,这一点是 Windows 望尘莫及的. 贵到什么程度? 随随便便的普通版七八千,如果再稍微挑挑拣拣,起码一两万! 如果预算不够但又想体验一下 Mac 电脑,该怎么办? 答案很简单,直线去去线下苹果体验店啊! 哈哈,我是开玩笑的,言归正传,买不起真实的苹果笔记本也没关系,我们可以像安装普通软件那样,安装一个苹果笔记本! 下面我们将介绍一种方法,让你能够在 Windows 电脑上安装一个苹果笔记本,用起来和真的一样,只不过你我都心知肚明,那并不是真实的机器! 知识扫盲 虚拟机是相对于真实的物理机而言的概念,是在我们当前正在使用的计算机基础上,通过软件或硬件的方式创造的新的计算机. VMware 是常用的虚拟机软件之一,物理机安装好 VMware 软件就可以利用该软件虚拟出任意计算机即虚拟机. VMware 支持 Windows ,Mac 和 Linux 等常见操作系统,是名副其实的跨平台软件. 镜像 是一种特殊格式的文件,文件后缀名一般是 .iso ,但也要例外,比如本文安装的 macOS Mojave 10.14 18A391 Lazy Installer(MD5-CDD5EDA714D8BCC8E799F8272556CF3B).cdr 的后缀名却是 .cdr ,镜像文件就是创建虚拟机的必要程序,有了它 VMware 软件才能创建出虚拟机. 总的来说,VMware 软件加载 xxos.iso 镜像文件创建出 xxos 虚拟机. 所以,阅读教程前请务必准备好 VMware 虚拟机以及相应的镜像文件. 安装准备 虚拟机技术能够虚拟出任何操作系统,并不局限于 Windows 安装 Mac ,也可以安装 Linux 或者 Windows . 同理,虚拟机技术也不局限于 VMware ,也可以是 VirtualBox 或者 Parallels Desktop 等等. 为了避免选择困难症,下面以 VMware 虚拟机安装 Mac 为例,简单演示一下安装流程. 安装 Mac 操作系统需要两个前提条件: Windows 电脑已安装好 VMware 虚拟机. Windows 电脑已下载好 Mac 操作系统镜像. 链接: https://pan.baidu.com/s/1zL7-nB7ukif6nWBQ8KyOMA 提取码: hrgr 给 windows 装个 vmware 虚拟机 给 mac 装个 vmware 虚拟机 如果尚未安装 VMware 虚拟机,请参考上述链接进行安装,如果链接已失效,请私信我补发. macOS Unlocker for VMware v3.0.2.zip 解锁文件,是安装镜像文件的前提. macOS Mojave 10.14 18A391 Lazy Installer(MD5-CDD5EDA714D8BCC8E799F8272556CF3B).cdr 镜像文件,是安装 Mac 操作系统的灵魂. 友情提示,百度云下载大文件限速太厉害,一定要准备好下载方案或者开通超级VIP进行下载. 安装镜像 解锁镜像 打开 VMware 软件,选择 文件-> 创建新的虚拟机 或者在主页中选择 创建新的虚拟机 . 选择已下载好的镜像文件,选择文件时默认后缀名是 .iso 而我们安装的镜像文件是 .cdr ,因此一定要选择全部文件,这样就能选中 macOS Mojave 10.14 18A391 Lazy Installer(MD5-CDD5EDA714D8BCC8E799F8272556CF3B).cdr 镜像文件了! 虽然已经加载镜像文件,但是存在警告信息: 无法检测此光盘镜像中的操作系统. 无关紧要,下一步手动指定安装的镜像文件是 Mac 10.14 操作系统即可! 当我们理所当然点击下一步时,顿时傻眼了,竟然没有 Mac os 操作系统,凭什么?! 客户机操作系统选项没有 Mac 操作系统是不是因为上一步的警告信息呢?还真不是,因为并没有解锁! 解压 macOS Unlocker for VMware v3.0.2.zip 并找到 win-install.cmd 文件,选中该文件右键以管理员身份运行! 测试时运行效果是一闪而过,应该也无碍,只要再次安装镜像时出现 Mac Os 操作系统就是解锁成功. 如果没有解锁成功,打开 windows 任务管理器 并杀死 vmware 相关的全部进程,再次运行win-install.cmd 命令. 继续安装 解锁成功后,再次打开 VMware 软件继续安装镜像文件,此时已经出现 Mac os 操作系统选项了,如果没有出现该选项,请返回上一步. 接下来正常安装,傻瓜式操作均采用默认配置,直接点击下一步,直到安装完成. 安装虚拟机后,有啥秘密试验都可以在虚拟机上进行操作啦,再也不担心会不小心损坏物理机了呢! 开机体验 虚拟机安装完毕后,选择启动该虚拟机,正如物理机按开机按键一样,静静等待传说中的黑苹果! 安装一会接着提示\"无法在更新服务器上找到组件\",这是因为网络不通或者破解软件的原因,可以暂时忽略该错误. 设置语言,使劲往下滑直到最后,然后选择简体中文,紧接着下一步. 开始安装 macOS 操作系统,选择\"继续\". 同意协议并继续下一步. 不好意思,我又指了一条错误的安装道路! 下一步无路可走,只好返回,现在又回到准备安装的界面,看来我们必须准备好安装磁盘才能继续! 选择 实用工具->磁盘工具 开始准备安装磁盘. 打开的新页面左侧有两块磁盘,选择下面以 VMWare 开头的磁盘,编辑好磁盘名称(如 snowdreams1006),然后点击抹掉. 完成后关闭当前页面并返回到开始安装页面,紧接着继续下一步直到上次停留的页面. 现在已经出现了安装磁盘,选择刚刚命名的 snowdreams1006 安装磁盘,继续愉快的下一步! 虽然说还剩 16min ,实测感觉要长多了,幸运的是,接下来的安装步骤没有特别需要注意点,因此整理成动图略过. 终于安装完毕,退出安装光盘,大功告成! 并不是所有的安装版本都是最新版,当初的最新版也不一定是现在的最初版,如果追求最新操作系统,那你可以手动升级啊! 总结 VMware 软件是一款跨平台的虚拟机操作软件,加载到有效的镜像文件就能创造出虚拟机,值得注意的是,由于创造的虚拟机是一个完整的操作系统,占用了物理机一定的资源,因此物理机性能不够强劲的话,不要创建过多的虚拟机. 一两个虚拟机足矣,不使用虚拟机时一定要及时关机,否则电脑卡到怀疑人生! 安装虚拟机的必要文件是各种各种的镜像文件,有个镜像文件就有了相应的虚拟机,一般来说,镜像文件都是网络上别人制作好的或者官方提供的,当然,如果你愿意的话,你也可以制作自己的镜像供别人下载使用. 镜像文件一般都是比较大的,小则 3g ,大则 7g ,因此下载镜像文件比较费时,不同镜像文件安装时间也不尽相同,快则 1h,慢则 3h. 至于虚拟机配置方面,一般来说采用系统默认值即可,除非你有特殊需求或者明白你正在设置的选项含义,否则不要随意更改推荐设置. 安装虚拟机中途可能会多次重启虚拟机,请耐心等待,不要中断安装操作,该完成时自会完成! 最后,感谢你的阅读,希望能够对你有所帮助! © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:10:30 "},"tools/windows-vmware-install-windows.html":{"url":"tools/windows-vmware-install-windows.html","title":"给 windows 虚拟机装个 windows","keywords":"","body":"给 windows 虚拟机装个 windows 前面我们已经介绍了如何在 Windows 宿主机安装 VMware 虚拟机,这节我们将利用安装好的 VMware 软件安装 Windows 系统. 前情回顾 虚拟机是相对于真实的物理机而言的概念,是在我们当前正在使用的计算机基础上,通过软件或硬件的方式创造的新的计算机. 本文主要介绍的是 VMware 虚拟机,下载并安装 VMware 软件再安装操作系统即可模拟出另一台计算机的效果,这种模拟出来的计算机就是虚拟机. VMware 不仅支持 Windows 也支持 Linux ,对于 Mac 系统也是支持的,如需了解 Mac 宿主机如何安装使用虚拟机,可以参考工具资源系列之给mac装个虚拟机. Windows 物理机如何安装虚拟机请参考 给 windows 装个 vmware 虚拟机 下载镜像 VMware 为我们安装虚拟机提供了环境,真正的虚拟机到底是 Windows 系统还是 Linux 系统取决于我们要安装什么操作系统. 安装操作系统最简单便捷的方式就是下载操作系统的镜像文件,VMware 识别到镜像文件后就会一步一步安装操作系统. 所以我们第一步要做的就是寻找镜像文件,正所谓\"知自知彼方能百战不殆\",意味着首先要确定下我们到底要安装哪一个版本的操作系统? Windows 操作系统有很多种,有 Win7 ,Win8 和 Win10 ,还有旗舰版和家庭版. 如果我们购买真实的计算机,那肯定要好好考虑一下,毕竟金钱要花的有价值,然而我们要安装的虚拟机,并不收费,这么多版本我们可以任意挑选安装! 如果有明确的目标,可以按照实际需求自行下载相应的操作系统; 如果没有明确的目标,不妨和我一样安装一个和本机相似的虚拟机,一来操作比较熟悉,而来可以在虚拟机进行任意实验. 所以,首先我要看一下本机的系统版本,因此我决定安装同款 Win7 旗舰版! 选择 我的电脑 > 右键属性 > 查看计算机的相关属性 ,我的电脑是 Windows 7 旗舰版 现在已经明确了虚拟机的操作系统,那我们去哪里下载目标虚拟机的镜像文件呢? 这里推荐一下 https://msdn.itellyou.cn/ 网站,方便使用,好评! Windows 7 Enterprise (x64) - DVD (Chinese-Simplified) : 64 位企业版 Windows 7 Enterprise (x86) - DVD (Chinese-Simplified) : 32 位企业版 Windows 7 Enterprise with Service Pack 1 (x64) - DVD (Chinese-Simplified) : 64 位企业版且带有service package 1 Windows 7 Enterprise with Service Pack 1 (x86) - DVD (Chinese-Simplified) : 32 位企业版且带有service package 1 Windows 7 Home Basic (x86) - DVD (Chinese-Simplified) : 32 位家庭普通版 Windows 7 Home Basic with Service Pack 1 (x86) - DVD (Chinese-Simplified) : 32 位家庭普通版且带有service package 1 Windows 7 Home Premium (x64) - DVD (Chinese-Simplified) : 64 位家庭高级版 Windows 7 Home Premium (x86) - DVD (Chinese-Simplified) : 32 位家庭高级版 Windows 7 Home Premium with Service Pack 1 (x64) - DVD (Chinese-Simplified) : 64 位家庭高级版且带有service package 1 Windows 7 Home Premium with Service Pack 1 (x86) - DVD (Chinese-Simplified) : 32 位家庭高级版且带有service package 1 Windows 7 Professional (x64) - DVD (Chinese-Simplified) : 64 位专业版 Windows 7 Professional (x86) - DVD (Chinese-Simplified) : 32 位专业版 Windows 7 Professional with Service Pack 1 (x64) - DVD (Chinese-Simplified) : 64 位专业版且带有service package 1 Windows 7 Professional with Service Pack 1 (x86) - DVD (Chinese-Simplified) : 32 位专业版且带有service package 1 Windows 7 Professional with Service Pack 1, VL Build (x64) - DVD (Chinese-Simplified) : 64 位专业版且带有service package 1,并基于 VL 进行构建. Windows 7 Professional with Service Pack 1, VL Build (x86) - DVD (Chinese-Simplified) : 32 位专业版且带有service package 1,并基于 VL 进行构建. Windows 7 Professional, VL Build (x64) - DVD (Chinese-Simplified) : 64 位专业版并基于 VL 进行构建. Windows 7 Professional, VL Build (x86) - DVD (Chinese-Simplified) : 32 位专业版并基于 VL 进行构建. Windows 7 Starter (x86) - DVD (Chinese-Simplified) : 32 位初级版 Windows 7 Starter with Service Pack 1 (x86) - DVD (Chinese-Simplified) : 32 位初级版且带有service package 1 Windows 7 Ultimate (x64) - DVD (Chinese-Simplified) : 64 位旗舰版 Windows 7 Ultimate (x86) - DVD (Chinese-Simplified) : 32 位旗舰版 Windows 7 Ultimate with Service Pack 1 (x64) - DVD (Chinese-Simplified) : 64 位旗舰版且带有service package 1 Windows 7 Ultimate with Service Pack 1 (x86) - DVD (Chinese-Simplified) : 32 位旗舰版且带有service package 1 Windows Automated Installation Kit for Windows 7 and Windows Server 2008 R2 (x86, x64, ia64) - DVD (Chinese-Simplified) : Windows 7 and Windows Server 2008 R2 (x86, x64, ia64) 自动安装包套件 Windows Automated Installation Kit for Windows 7 and Windows Server 2008 R2 Service Pack 1 (x86, x64, ia64) - DVD (Chinese-Simplified) : Windows 7 and Windows Server 2008 R2 Service Pack 1 (x86, x64, ia64) 自动安装包套件 上述这么多的版本是不是让人有些眼花缭乱,具体版本之间有何差异以及自己适合哪一种请百度一下再理性分析! 以下以 64 位旗舰版且带有服务包操作系统为例进行演示,主要是创建一个和宿主机一样的操作环境,方便后续进行秘密实验! cn_windows_7_ultimate_with_sp1_x64_dvd_u_677408.iso 镜像文件,其中 cn 表示中文简体语言, Windows_7 表示 Win7 操作系统, ultimate 表示旗舰版,sp1 表示service package 1 ,x64 表示 64 位操作系统,dvd 表示 DVD 安装方式,677408 应该是版本号,.iso 是镜像文件的后缀. ed2k://|file|cn_windows_7_ultimate_with_sp1_x64_dvd_u_677408.iso|3420557312|B58548681854236C7939003B583A8078|/ 由于镜像文件本身比较大,因此推荐使用专业的下载工具进行,这里使用的是迅雷下载磁力链接. 镜像文件: cn_windows_7_ultimate_with_sp1_x64_dvd_u_677408.iso ,其中后缀是 .iso ,千万不要解压! 不要解压! 不要解压! 安装镜像 准备好已下载的镜像文件: cn_windows_7_ultimate_x64_dvd_x15-66043.iso 打开 VMware 软件,选择 文件-> 创建新的虚拟机 或者在主页中选择 创建新的虚拟机 . 总体来说,安装过程比较简单,前面相关配置按照默认值即可,后面真正安装过程可能耗费时间比较长,耐心等待安装完成. VMware 仅能识别出镜像文件基本信息,具体版本信息还是需要手动校准,下载的镜像文件是 Win7 旗舰版 ,因此安装版本也是 Win7 旗舰版 . 一系列安装配置完毕后,还有最后一步配置确认操作,确认无误后点击 完成 就可以真正进行安装虚拟机了! 安装过程中可能要求输入产品密钥以及设置用户,这些操作和新买计算机刚开机时操作一模一样,百度找一下相应版本的产品密钥即可,可以设置登录用户也可以不设置用户. 至此,安装成功! 安装虚拟机后,有啥秘密试验都可以在虚拟机上进行操作啦,再也不担心会不小心损坏物理机了呢! 回忆总结 VMware 软件提供了虚拟机环境,差一个操作系统就能创建出虚拟机,而这种操作系统大部分是 .iso镜像文件. 镜像文件基本上至少 3g ,下载镜像文件也比较耗时,可以利用专业第三方下载工具进行下载. 虚拟机配置比较简单,采用推荐的默认值进行设置即可,除非你有特殊需求或者明白你正在设置的选项含义,否则不要随意更改推荐设置. 安装虚拟机中途可能会多次重启虚拟机,请耐心等待,不要中断安装操作,完成后自会完成! 工具资源系列之给windows装个虚拟机 工具资源系列之给mac装个虚拟机 工具资源系列之给mac虚拟机装个windows 如果觉得本文写的不错,欢迎点赞留言和转发哟! © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:10:30 "},"tools/windows-vmware-install-centos.html":{"url":"tools/windows-vmware-install-centos.html","title":"给 windows 虚拟机装个 centos","keywords":"","body":"给 windows 虚拟机装个 centos 前面我们已经介绍了如何在 Windows 宿主机安装 VMware 虚拟机,这节我们将利用安装好的 VMware 软件安装 centos 系统. 前情回顾 由于大多数人使用的 Windows 电脑而工作中可能需要一台 centos 电脑,如果条件允许的话,一般公司会有相应的测试服务器. 但是,如果是个人使用的话,公司的测试服务器就不能轻易做各种实验了,毕竟测试服务器是大家公用的,万一不小心搞坏了影响了他人的使用就不好交代了. 因此,最好能够有一台私有的 centos 计算机,可以随意鼓捣还不用担心影响到其他人,这种情况下虚拟机提供了很好的解决思路. 虚拟机是相对宿主机而言较为独立的计算机,即使不小心把虚拟机搞崩了也不要紧,重新装下虚拟机就好了,也不会损坏真实的宿主机. 而我们介绍的虚拟机软件是 VMware ,跨平台支持三大主流操作系统,因此无论是 Windows 还是 Mac 或者 Linux 系统都可以借壳生蛋,继续创造出不同的操作系统. 给 windows 装个 vmware 虚拟机 给 mac 装个 vmware 虚拟机 下载镜像 centos 操作系统下载: https://www.centos.org/download/ 安装 centos 操作系统需要镜像文件,寻找镜像文件最简单的方法是 centos 自己的官网,因此我们直接去官方看一下吧! 这里提供了 DVD ISO 和 Minimal ISO 两种类型,普通用户推荐选择前一种标准版,开发用户建议选择后一种最小版. 标准版功能比较齐全,最小版保证最小依赖,后续缺啥填啥,比较灵活节省空间内存. 按照实际需要选择适合自己的操作系统,这里选择的是 centos7.6 ,然后选择合适的下载方式(直接下载或下载种子链接). 建议选择镜像服务器下载,如果直接下载官网的地址,速度感人,时间有点长. 依次选择 list of current mirrors -> http://mirrors.aliyun.com/centos/ -> 7.6.1810/ -> isos/ -> x86_64/ -> CentOS-7-x86_64-Minimal-1810.iso 选择合适的版本点击下载. 镜像文件: CentOS-7-x86_64-Minimal-1810.iso ,其中后缀是 .iso ,千万不要解压! 不要解压! 不要解压! 安装镜像 准备好已下载的镜像文件: CentOS-7-x86_64-Minimal-1810.iso 打开 VMware 软件,选择 文件-> 创建新的虚拟机 或者在主页中选择 创建新的虚拟机 . 总体来说,安装过程比较简单,前面相关配置按照默认值即可,后面真正安装过程可能耗费时间比较长,耐心等待安装完成. 一系列安装配置完毕后,还有最后一步配置确认操作,确认无误后点击 完成 就可以真正进行安装虚拟机了! 安装过程中配置语言,默认是英语,中文简体在最下面,一直往下翻选择简体中文即可. 设置超级管理员密码以及添加用户,用于安装完毕后登陆系统. 至此,安装成功! 安装虚拟机后,有啥秘密试验都可以在虚拟机上进行操作啦,再也不担心会不小心损坏物理机了呢! 回忆总结 VMware 软件提供了虚拟机环境,只要一个操作系统的镜像文件就能轻易创建出虚拟机,认准镜像文件的后缀名是 .iso . VMware 虚拟机配置比较简单,基本上采用默认值进行设置即可,除非你有特殊需求或者明白你正在设置的选项含义,否则不要随意更改推荐设置. 安装虚拟机中途可能会多次重启虚拟机,请耐心等待,不要中断安装操作,完成后自会完成! 工具资源系列之给windows装个虚拟机 工具资源系列之给mac装个虚拟机 工具资源系列之给虚拟机装个centos 如果觉得本文写的不错,欢迎点赞留言和转发哟! © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:10:29 "},"tools/windows-vmware-install-ubuntu.html":{"url":"tools/windows-vmware-install-ubuntu.html","title":"给 windows 虚拟机装个 ubuntu","keywords":"","body":"给 windows 虚拟机装个 ubuntu 前面我们已经介绍了如何在 Windows 宿主机安装 VMware 虚拟机,这节我们将利用安装好的 VMware 软件安装 Ubuntu 系统. 前情回顾 虚拟机为我们在 Windows 宿主机体验别的系统提供了可能,虚拟机的强大之处在于我们可以自由安装任意操作系统,不管是同款 Windows 还是 Linux 都可以! 关于如何安装 VMware 软件可以参考上一篇文章,这里给出 Windows 和 Mac 的安装教程. 给 windows 装个 vmware 虚拟机 给 mac 装个 vmware 虚拟机 下载镜像 Ubuntu 操作系统下载: https://www.ubuntu.com/download 这里我们选择桌面版(Ubuntu Desktop),接着选择 LTS 长期支持版进行下载安装. 按照实际需要选择适合自己的操作系统,这里选择的是 Ubuntu18 LTS ,然后选择下载. 镜像文件: ubuntu-18.04.2-desktop-amd64.iso ,其中后缀是 .iso ,千万不要解压! 不要解压! 不要解压! 安装镜像 准备好已下载的镜像文件: ubuntu-18.04.2-desktop-amd64.iso 打开 VMware 软件,选择 文件-> 创建新的虚拟机 或者在主页中选择 创建新的虚拟机 . 总体来说,安装过程比较简单,前面相关配置按照默认值即可,后面真正安装过程可能耗费时间比较长,耐心等待安装完成. 一系列安装配置完毕后,还有最后一步配置确认操作,确认无误后点击 完成 就可以真正进行安装虚拟机了! 安装过程中需要添加用户,用于安装完毕后登陆系统. 至此,安装成功! 安装虚拟机后,有啥秘密试验都可以在虚拟机上进行操作啦,再也不担心会不小心损坏物理机了呢! 回忆总结 VMware 虚拟机配置比较简单,安装 Ubuntu 镜像文件时耗费时间相当长,慢慢等待一切会自动安装好的. 安装虚拟机过程可能会重启虚拟机,请耐心等待,不要中断安装操作,完成后自会完成! 工具资源系列之给windows装个虚拟机 工具资源系列之给mac装个虚拟机 工具资源系列之给虚拟机装个ubuntu 如果觉得本文写的不错,欢迎点赞留言和转发哟! © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:10:30 "},"tools/badge.html":{"url":"tools/badge.html","title":"github 上各式各样的小徽章从何而来?","keywords":"","body":"github 上各式各样的小徽章从何而来? 前言 平时大家在在逛 github 时或多或少都看到过项目首页各式各样的小徽章,不知道你是否和我一样好奇这些小徽章都是哪来的呢? 首先我们先来一睹为快目前前端开发的三大主流框架: var ,看一看他们的 github 项目首页有哪些小徽章吧! Vue : https://github.com/vuejs/vue Angular : https://github.com/angular/angular React : https://github.com/facebook/react 小结: 前端三大框架的徽章均不相同,由此可见,这应该不是 github 统一分发而是自定义行为! 虽然不是统一分配的,但也不是毫无规律可寻,想要制作专属的小徽章,其实真的很简单! 什么是徽章 徽章是一种小巧精美的小图标,一般配有相关文字进行辅助说明,富有表现力. 不仅出现于 github 项目主页,凡是能够表现图片的地方都可以出现徽章,本质上是一种 svg 格式的矢量图标. 下面以自定义 github-snowdreams1006-brightgreen.svg 徽章为例,简单认识一下徽章. 在线链接 在线链接: github-snowdreams1006-brightgreen.svg https://img.shields.io/badge/github-snowdreams1006-brightgreen.svg 浏览器效果 打开在线链接,并检查当前网页,豁然开朗,徽章是一种 svg 实现的矢量图标. svg VS png 如果说 svg 是矢量图形而 png 却不是,所以不妨将 png 姑且称之为标量图形. svg 是矢量图形,png 是标量图形,两者均能实现类似效果,只不过矢量图形不论怎么方法都能保持原样,并不会像 png 那样会失真而已. 既然两种均能表现相同的效果,现在我们就来演示一下 png 的实现效果. svg 转 png 在线网站: https://cloudconvert.com/svg-to-svg 左侧的 svg 无论放大多少倍,依然保持原样,清晰度保持不变.右侧的 png 一旦放大,立马变得模糊不清. 如何使用徽章 大多数徽章都是 svg 格式,当然也不排除某些徽章是 png 格式,不论怎么说,一律当成图标使用就可以了. 如果你和我一样,希望在 markdown 文件中使用徽章,那么建议使用在线链接,或者引入本地 svg 相关文件. 徽章格式 : [](超链接地址) 即超链接内部嵌套图片 [](https://github.com/snowdreams1006) 如果你是在 html 文件使用徽章,同样先取得在线徽章地址,然后按照 html 语法插入图片即可. 徽章格式 : 即超链接内部嵌套图片 不论是什么语法,最核心最根本的获得到徽章链接,至于不同语言有着各自的语法,按照语言规则手动拼接就好. Badge URL https://img.shields.io/badge/github-snowdreams1006-brightgreen.svg Markdown [](https://github.com/snowdreams1006) HTML Textile !https://img.shields.io/badge/github-snowdreams1006-brightgreen.svg!:https://github.com/snowdreams1006 RDOC {}[https://github.com/snowdreams1006] AsciiDoc image:https://img.shields.io/badge/github-snowdreams1006-brightgreen.svg[\"github\", link=\"https://github.com/snowdreams1006\"] RST .. image:: https://img.shields.io/badge/github-snowdreams1006-brightgreen.svg :target: https://github.com/snowdreams1006 徽章分类 如果以徽章的格式为标准,那么可以分为svg 和 png 两类. svg https://badge.fury.io/js/gitbook-plugin-mygitalk.svg png https://badge.fury.io/js/gitbook-plugin-mygitalk.png 如果以徽章的样式为标准,那么可以分为默认样式和自定义样式两类. 默认样式 https://img.shields.io/github/stars/snowdreams1006/snowdreams1006.github.io.svg?style=social 自定义样式 https://img.shields.io/badge/%E5%BE%AE%E4%BF%A1%E5%85%AC%E4%BC%97%E5%8F%B7-%E9%9B%AA%E4%B9%8B%E6%A2%A6%E6%8A%80%E6%9C%AF%E9%A9%BF%E7%AB%99-brightgreen.svg 如果以徽章的内容数据是否动态为标准,那么可以分为静态数据和动态数据两类. 静态数据意味着数据本身是不变的,只要在线链接不变,那么生成的徽章永远不会改变,而动态数据意味着生成徽章的数据是动态变化的,即使在线链接不变,当数据本身发现变化时,徽章自然随之更新. 静态数据 https://img.shields.io/badge/github-snowdreams1006-brightgreen.svg 动态数据 https://badge.fury.io/js/gitbook-plugin-mygitalk.svg 静态数据示例中 github-snowdreams1006-brightgreen.svg 数据不会更改,自然生成的徽章也不会变.动态数据示例中 gitbook-plugin-mygitalk.svg 是 npm 的版本号,当项目升级后,版本号会发生更改,那么生成的徽章也会随之更新. 如果以徽章的内容数据来源为标准,那么可以有无数多的分类. GitHub https://badgen.net/github/stars/snowdreams1006/gitbook-plugin-mygitalk Npm https://badgen.net/npm/dt/gitbook-plugin-mygitalk Docker https://badgen.net/docker/stars/library/centos 如果以徽章的内容数据用途为标准,那么也可以有无数多的分类. 构建状态 https://img.shields.io/travis/GitbookIO/gitbook.svg 代码覆盖率 https://img.shields.io/codecov/c/github/vuejs/vue.svg 代码分析 https://img.shields.io/github/languages/top/snowdreams1006/snowdreams1006.github.io.svg 徽章来源 徽章有不同的分类,不管是哪种分类,在线徽章最为简单便捷,下面就简单介绍下提供在线生成徽章的网站. https://shields.io/ https://badgen.net/ https://forthebadge.com/ https://badge.fury.io/ https://github.com/boennemann/badges https://shields.io/ 适用于绝大多数情况,默认按照徽章内容分类,Build,Code Coverage,Analysis 等多主题,同时支持自定义徽章和动态徽章. 如果徽章的主题明确,那么根据网站提供的主题对号入座即可在线生成徽章,下面以 gitbook-plugin-mygitalk 为例,简要说明如何获得相应徽章链接. gitbook-plugin-mygitalk 是 gitbook 的一款评论插件. 打开网站后按照分类,选择其中一个主题,点击进去后填写目标信息,即可在线生成徽章. 浏览已支持的主体,选择 License 许可证主题. 浏览已支持的 License 许可证列表,选择 NPM 许可证. 填写好正确的 npm 包信息并实时预览,然后点击按钮复制徽章链接或者或者特定格式的徽章.  按照主题生成徽章真的很简单,首先对号入座,然后按需生成相应徽章即可,唯一的要求就是对号入座! 如果默认提供的徽章主题没有适合自己的徽章,或者想要自定义徽章效果,那么也可以在线制作私人订制徽章. 打开网站后往下拉,找到 Your Badge 区域,准备制作专属徽章. 填写(Label)标签-(Message)信息-(Color)颜色等信息后,点击(Make Badge)生成徽章. 点击生成徽章后默认会在当前标签页面打开该链接,手动复制链接并调整成目标格式即可.  https://badgen.net/ 徽章内容来源种类较多,默认按照平台分类,按照特定规则生成徽章,需要手动拼接在线链接,略显繁琐. https://badgen.net/badge/:subject/:status/:color?icon=github ──┬── ───┬─── ──┬─── ──┬── ────┬────── │ │ │ │ └─ Extra Options (label, list, icon, color) │ │ │ │ │ TEXT TEXT RGB / COLOR_NAME ( optional ) │ \"badge\" - default (static) badge generator 虽然支持颜色,图标以及查询参数等高级用法,但是还是习惯性采用默认设置,下面动手开始制作徽章吧! 切换到默认动态徽章选项卡,选择 GitHUb 徽章. 选择 stars 徽章,将 micromatch 替换成目标信息. /github/stars/micromatch/micromatch 替换成 /stars/snowdreams1006/snowdreams1006.github.io 预览徽章效果并手动修改成目标格式.  除了支持动态徽章,同样也支持静态徽章,切换到 STATIC BADGES 选项卡,一起来生成静态徽章吧!  按照徽章的在线链接规则,应该也支持自定义徽章,再次回顾一下链接规则: 规则 : https://badgen.net/badge/:subject/:status/:color ,如果是自定义动态链接,估计不支持吧!  https://forthebadge.com/ 扁平化的徽章,支持的徽章数量有限,不支持自定义徽章. 网站首页默认提供了一些预览徽章,左侧是复制 image 链接,右侧是复制 markdown 链接. [](https://forthebadge.com) 网站首页默认展示的徽章毕竟有限,如果找不到理想徽章,岂不是白介绍了这个网站,当然不能够! VIEW ALL 查看目前支持的全部徽章,如果还是找不到徽章,那就真的没有. https://badge.fury.io/ 版本徽章,支持各类平台版本,包括 npm ,Ruby,Python,Go 等平台. 选择目标平台并输入包管理信息,即可在线生成各个类型的徽章版本. [](https://badge.fury.io/js/gitbook-plugin-mygitalk) 排版布局 默认 markdown 实现的图片是依次排开的,无法自定义样式,而 markdown 语法同时也兼容 html 语法,因此我们可以用 html 语法实现居中对齐. 抛砖引玉 社交化徽章     自定义徽章 [](https://github.com/snowdreams1006) [](http://weixin.qq.com/r/cy5CWvvE5Kabrb8593th) [](https://www.imooc.com/u/5224488/articles) [](https://www.jianshu.com/u/577b0d76ab87) [](https://blog.csdn.net/weixin_38171180) [](https://www.cnblogs.com/snowdreams1006/) [](https://juejin.im/user/582d5cb667f356006331e586) [](https://segmentfault.com/u/snowdreams1006) [](https://my.oschina.net/snowdreams1006) [](https://cloud.tencent.com/developer/user/2952369/activities) 进度条徽章 [](https://github.com/fehmicansaglam/progressed.io) [](https://github.com/fehmicansaglam/progressed.io) [](https://github.com/fehmicansaglam/progressed.io) [](https://github.com/fehmicansaglam/progressed.io) 参考文档 GitHub 项目徽章的添加和设置 玩转 Github 徽章 为你的Github README生成漂亮的徽章和进度条 给python项目在github贴上build和pypi小徽章 https://github.com/igrigorik/ga-beacon https://github.com/boennemann/badges https://ellerbrock.github.io/open-source-badges/ http://githubbadges.com/ 在线网站 https://shields.io/ https://badgen.net/ https://forthebadge.com/ https://badge.fury.io/ https://ellerbrock.github.io/open-source-badges/ http://githubbadges.com/ © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:10:58 "},"write/":{"url":"write/","title":"如何写作","keywords":"","body":"如何写作 越来越多的人想写个人博客或者打算做自媒体,笔者也是一样. 最近在学习如何写博客,分享下创作经历,避免走弯路. 首先在于定位,不同的定位决定了不同的平台.由于笔者分享的大多是技术类博客,所以主战场是各大主流的技术类博客论坛,当然还搭建了自己的官网. 考虑到前期仅仅是分享技术博客,无需购买专门的服务器,因此寻求免费的解决方案. 笔者采用的是 github 搭建个人项目官网,优点是不花一分钱,就能免费开启 https 服务,缺点是国内访问速度慢. 下面分为两部分介绍博客的创作平台,一部分是个人官网,另一部分是第三方平台. 个人官网 首先注册 github 账号(例如:用户名 snowdreams1006),然后新建特定规则的项目(例如:项目名 snowdreams1006.github.io),最后在项目下创建首页 index.html .现在访问 https://用户名.github.io/ (https://snowdreams1006.github.io/)即可. 个人官网的基本流程和特点如下: 利用 gitbook 技术将 markdown 源码文件输出为 html 静态网页. 将项目按照特定规则上传到 github 网站公开托管,生成免费网站. 源代码更新后再生成输出文件,然后一起上传到 github,个人官网自动更新. 项目源码 snowdreams1006.github.io,项目官网 https://snowdreams1006.github.io/ 点击图片会自动跳转到 https://snowdreams1006.github.io/ 第三方平台 这里列举了常逛的第三方平台,将从新手视觉看待各家平台特点,试图分析各家平台特点从而决定是否适合自身. 一家之言,仅供各位参考. 排名不分顺序,只是笔者文章依次同步的顺序而已. 1. CSDN https://www.csdn.net/ SEO 优化不错,阅读量稳定,网页端阅读体验一般,手机端干净清爽,阅读量相对稳定. 不限制作者本人点赞,重复统计本人浏览记录,突出阅读数,其次是评论数和点赞数. 2. 博客园 https://www.cnblogs.com/ SEO 优化不错,博客开通需审核,支持发布首页,但也可能因质量不达标而被移除.页面风格满满的时代感,目前暂无手机端. 限制作者本人推荐,不统计本人浏览记录,突出推荐数,其次是阅读量和评论数. 3. 开源中国 https://www.oschina.net/ 国内版 github ,高质量文章可能会被推荐至首页,否则阅读量几乎为零. 限制作者本人点赞,不统计本人浏览记录,突出访问量,其次评论数和点赞数. 4. 简书 https://www.jianshu.com/ 文艺范的自媒体平台,简洁优美文艺性十足,SEO 优化不错,日更活动鼓励持续更新,简书钻和简书贝等虚拟货币增添写作乐趣! 不限制作者本人喜欢,不统计本人浏览记录,突出简书钻,其次阅读量,评论数和点赞数. 5. 思否 https://segmentfault.com/ 国内版Stack OverFlow,专注于技术问答,界面风格绿色清新,SEO 优化不错,但忽略阅读量. 限制作者本人点赞,不统计本人浏览记录,首次发布专栏需要审核,突出投票数,其次是收藏数,最后是阅读数. 6. 掘金 https://juejin.im/timeline 异军突起,风头正盛,时间流布局,掘金小册子是一大亮点,但SEO 很差! 不限制作者本人点赞,不重复统计本人浏览记录,突出点赞数,其次是评论数,最后是阅读量. 7. 慕课网手记 https://www.imooc.com/article 丰富的免费教学视频,正所谓\"成也萧何败萧何\",手记模块相比其他专业平台还有着不少的差距,SEO 一般. 不限制作者本人点赞,重复统计本人浏览记录,文章需要审核,手记功能更像是配套教学视频而诞生的笔记,不太像专门博客. 8. 微信公众号 https://mp.weixin.qq.com/ 目前仅支持富文本编辑器,依靠粉丝流量,碎片化阅读体验,SEO 几乎没有. 需要花费精力运营公众号,限定当天阅读量和\"在看\"数,如果没有粉丝,那肯定没有阅读量. 9. B站专栏 https://www.bilibili.com/ 弹幕视频网站,开通专栏投稿,目前仅支持富文本编辑器,不适合博客,SEO 可忽略. 适合视频教程,暂不适合博客且投稿专栏限制较多,毕竟不是专业做博客的平台,谁让我误入了呢! 总结 大多数平台都有阅读量,评论,点赞等维度数据统计,但不同平台有着不同的推荐策略,优缺点如下: csdn : SEO 不错,阅读量稳定,适合新手积累信心. 博客园 : SEO 不错,阅读量有保障,适合新手提高自信. 开源中国 : SEO 一般,阅读量有挑战,适合优质文章博取官方推荐. 简书 : SEO 不错,阅读量很少,日更活动和简书钻奖励等形式鼓励持续创作,适合自我督促. 思否 : SEO 一般,阅读量一般,适合技术问答. 掘金 : SEO 很差,阅读量一般,适合优质文章. 慕课网手记 : SEO 一般,适合教学视频的配套笔记. 微信公众号 : SEO 很差,适合粉丝用户. B站 : SEO 很差,适合教学视频. 从以上分析中可以看出,只要是优质文章无论到哪都受欢迎,然而\"罗马并非一日建成\",优质文章的诞生不在一朝一夕,所以新手期应该选择适合自己的平台发展,积累到一定程度后方能\"春风得意马蹄疾,一日看遍长安花\". 个人建议: 选择简书平台,保持日更,同步到CSDN积累自信,推送到博客园,提高自信. 官网保持更新,运营公众号慢慢积累粉丝,最后再考虑开源中国,掘金和思否. © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:10:58 "},"write/markdownOrRichText.html":{"url":"write/markdownOrRichText.html","title":"markdown 和富文本","keywords":"","body":"markdown和富文本 不知道你是否留意过平时写作时的编辑器,有的是 markdown 编辑器,有的是各种富文本编辑器,到底选择哪一个相信你有自己的判断. 如果只是在某一家平台上写作,哪一种编辑器都无所谓,只要你喜欢就好. 可是如果你需要同时发布到各个平台呢?此时,真的需要停下来思考一下,我该使用哪一种编辑器了? 各家的编辑器的界面设计风格迥然不同,不仅按钮排序顺序不一样,而且最终输出效果也不尽相同. 这就给我们带来了一个问题,明明已经排好版的文章,复制到另外一家平台样式不一样了,或者格式被清除了?! 心中一万只羊驼呼啸而过,尽管如此,还是在心里告诉自己要冷静,要冷静! 既然我们追求的一处编写,到处复制,那么我就有必要郑重向你推荐 markdown 编辑器. 简单地说,markdown 编辑器是一种标记语言,写的是源码,输出的是 html. 所以很多情况下, markdown 更适合技术人员写文章,不用关心排版布局,回归写作本质,而富文本格式适合文学工作者,强调布局美观,重视审美体验. 两者看似相互独立,实际上最终展示效果几乎太大差别, markdown 格式和富文本格式最终都输出 html 格式,毕竟绝大多数阅读媒介还是各种浏览器. markdown 语法支持嵌套 html 语法,从而可以实现较为复杂的排版布局. markdown 格式 如果使用的是 markdown 格式编写文章,首先需要记忆常用的基本语法,半个小时足够入门写博客了,比txt 高级,比 html 简洁,取代 word 地位! 正是因为 markdown 语法规范,所以可以说是跨平台的写作语言,基本上各大主流的博客平台均支持 markdown 格式,保证了\"一处编写,到处复制\"的优良特性. 值得注意的是,不同平台对 markdown 格式的渲染结果稍有差异,甚至语法支持度不同,这要求我们尽量写通用语法或者因地制宜有针对性编写文章. ## markdown 二号标题 - markdown 无序列表1 - markdown 无序列表2 - markdown 无序列表3 **markdown 加粗文字效果** [markdown 超链接文字](https://snowdreams1006.github.io/markdown/)  markdown 快速入门 富文本格式 平常熟悉的 word 编辑器可以理解为一种富文本格式,布局,标题,超链接,图片等均以控件的形式展示,需要填写标题了点一下按钮,需要加粗效果再点一下按钮,效果直观,不需要二次渲染,但不同的平台自然是不同的布局. 一家平台的布局还不一定能够完美复制到另一家平台,虽然适合大多数人,但可移植性差! 如果需要同时发布到多家平台,简直不敢相信,复制不了样式,需要重新排版等问题绝对是一种折磨. 小结 markdown : 拥有编程经验,不关心排版布局,专注写作多家平台发表首选 markdown 编辑器,\"一处编写,到处复制\",可移植性好,最值渲染效果也不错! 富文本格式: 可视化书写文章,无需编程经验的话,首选富文本编辑器,调整鼠标就能搞定页面布局还是很轻松的,同步更新到多家平台时,页面布局格式差强人意,后期维护难度大! © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:11:14 "},"write/markdown2richText.html":{"url":"write/markdown2richText.html","title":"markdown 转富文本","keywords":"","body":"markdown转富文本 正常情况下不太希望你能看到这篇文章,可天不遂人愿, 总有些平台至今不支持 markdown 语言,没办法只能迁就它! 现在遇到的问题是,部分平台仅支持富文本格式,不支持 markdown 格式.既然想要在这些平台上发表文章,不得不按照他们的规定做事. 下面总结下笔者在用的一些方法: 1. markdown转富文本 适合已有 markdown 格式的文章,想要优雅转换成富文本格式,这种情况下我们只要能够获取到渲染后的富文本内容,然后复制到平台的编辑器即可. 不少 markdown 编辑器支持实时预览,一边是 markdown 源码,另一边是 富文本 预览,选中富文本然后复制,相当简单. 或者,将 markdown 源码上传到第三方平台在线转换成富文本格式,推荐 Markdown Here 插件或 在线工具. Markdown Here chrome插件的使用方法: 下载安装 Markdown Here 插件 各大浏览器基本上都有相应插件,如需翻墙,请自行解决. 配置插件并重启浏览器 支持自定义css样式,内嵌多套主题可供选择. 使用快捷键或命令面板转换 默认快捷键CTRL + ALT + M,或者复制到插件的预览窗口,点击Markdown 转换 目标富文本内容拷贝回富文本编辑器 选中渲染后的富文本内容拷贝到目标编辑器 online-markdown 在线工具的使用方法: 复制源 markdown 内容转换成目标富文本格式 将源 markdown 内容复制到左侧代码区,右侧可实时预览富文本效果 目标富文本内容拷贝回富文本编辑器 点击复制后到目标编辑器选择粘贴 2. 截图分享 适合懒癌晚期的作者,第一种方法转换后的富文本可能无法完美移植到某些平台时,而笔者又懒得重新编辑富文本,因此选择截图分享方式发布在该平台,只不过这种体验上稍差些,毕竟图片加载速度比文字相对来说还是慢很多! 简书和开源中国的 app 均支持截长图保存分享,网页端的话滚动截屏插件很多,目前在用fireshot还不错. 3. 重新编辑 适合认真负责的完美主义者,既然不支持 markdown 语言,那就用富文本编辑器重新编辑一份,素材和效果都有参考,再写一遍应该也不至于特别耗费时间,如果需要重写多份的话,那就另当别论了! © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:11:14 "},"write/static-semi-manual-with-csv.html":{"url":"write/static-semi-manual-with-csv.html","title":"csv 半手动数据统计","keywords":"","body":"csv半手动数据统计 背景 作为正在探索如何写作并发表到各大博客平台的新人,目前虽然已基本弄清写作和发表的基本流程,但是离打造个人知名度还差很大很大一段距离. 尤其处于新手阶段,需要的更是自信与外界的积极反馈,看着各平台日益增长的阅读量和粉丝量,心中自然不甚欣喜. 但是,持续的技术输出能否坚持下去很大程度上靠的是外界的积极反馈,如果写的文章基本没人看,或者反映并不理想,估计也很难再坚持创作了. 所以笔者每天晚上都会统计一下各个平台的数据,看一下有哪些收获,只有看得见的数据才能给我安全感和自信心. 下面简单展示一下每日数据统计效果: 每日数据统计 这里列出的平台默认是没有提供数据分析功能,而有些平台已经提供数据分析,说不定还要丰富图表分析功能,自然不用像下面这般复杂操作. 所以,针对没有提供数据分析的平台,只好采用人工方式进行每日数据统计,一开始文章比较少,用肉眼加计算器就能很轻松得到阅读量和粉丝数等数据. 但是,随着文章的每日更新,不断累加的文章越来越多,人工方式简直让我崩溃,比如昨晚在统计慕课网手记相关数据时就意外被一旁的小侄子打断三次! 简直不可忍受,穷则思变,懒则想法偷懒,所以是时候探索新的方式解决纯手动的弊端了! 全网汇总数据 慕课手记 简书 博客园 腾讯云社区 图表渲染效果来自 gitbook 的 chart 插件,详情请参考 官网文档 懒则想法偷懒 回顾操作流程 要想解放重复劳动量,必须先知道问题瓶颈,现在先回顾一下手动操作流程. 登录各大博客平台后台,找到文章列表. 打开计算器按照阅读量等指标累加每篇文章的相关数据. 更新统计页面数据,利用 chart 插件渲染图标. 修改 chart 渲染数据语法,截图渲染效果. 确认渲染效果并推送到 github 网站 本来不必利用截图表示图表的,只是无奈 github 不支持 chart 插件语法,只好用截图代替了. 思考问题瓶颈 分析上述流程后不难发现,最复杂也是最费时费力的便是第二步的数据统计,由于要肉眼统计文章并用计算器累加,简直是手脑并用,只有高度专注才能保证统计数据的准确性和可靠性. 这也就解释了被打断三次后的崩溃了,找到问题的根源了,想办法如何解决吧! 最容易想到的解决办法是手动复制文章列表数据,然后程序分析提取关键数据,最后再统计数据. 又是三步操作,再分解一下步骤,看看目前能够解决哪部分. 手动复制文章列表数据 程序分析提取关键数据 统计指标数据 在这三步中,只有第二步最为关键,也是目前我能做到的事情,因为第一步可能需要爬虫技术或模拟接口调用,总体来说,总体来说还是比较麻烦的,以后再继续优化吧. 梳理操作流程 因此,现在先着手如何将复制后的文章列表转化成程序能够处理的文件格式,进而调用程序统计. 下面以慕课网手记文章为例,简单介绍下处理流程. 手动复制文章 现在文章已复制到文件,应该保存成什么格式呢?这又是一个思考点. 由于文件内容最终需要被程序处理,而程序处理要求数据需要具备一定的格式,因此自然不能是 txt 或 word 这类文档,平常接触比较多的文档数据处理一般就是 excel 或者 json 类型的文档. 这里需要 excel 这种格式文档,但是 excel 比较笨重,还需要相关软件才能打开 excel 文件,好像并不是很适合,怎么办呢? 但是我真的需要这种一行一行的数据格式啊,有没有折中的处理方案? 当然有!轻量级的 csv 格式不是巧合适合简单文档处理吗? csv 和 excel 具有类似的特征,大体上都是一行一行一列一列地存储数据,最适合统计数据了. 看着乱七八糟的文章列表,csv 也无法处理这种复杂数据啊,接下来还是要手动格式化数据,整理一下数据. 程序分析提取 至此,我们已经完成数据分析的第一步了,接下来是如何读取 csv 文件,由于本人是 java 程序员,所以我要看一下 java 如何处理 csv 文件. 需求很简单,编写一个 csv 工具类并实现基本的写入和读取操作即可. 说到工具类当然首选现成的开源工具了,毕竟小小的需求不值得造轮子. 寻求解决方案 说到开源工具,脑海中第一个闪现的是 Apache Commons 工具类,所以先去 maven 上搜一下有没有 csv 相关的工具类. 在线搜索 commons-csv 天不负我!果然有 csv 相关工具类,下面就开始研究如何调用吧! 集成 commons-csv 工具类 org.apache.commons commons-csv 1.6 编写工具类 /** * 写入csv文件 * * @param data 数据内容 * @param filePath 文件路径 * @throws IOException **/ public static void writeCsv(List data, String filePath) throws IOException { FileWriter fw = new FileWriter(new File(filePath)); final CSVPrinter printer = CSVFormat.EXCEL.print(fw); printer.printRecords(data); printer.flush(); printer.close(); } /** * 读取csv文件 * * @param filePath 文件路径 * @return CSVRecord 迭代对象 * @throws IOException **/ public static Iterable readCSV(String filePath) throws IOException { InputStream inputStream = new FileInputStream(filePath); InputStreamReader isr = new InputStreamReader(inputStream); Iterable records = CSVFormat.EXCEL.parse(isr); return records; } /** * 测试写入并读取csv 文件 */ private static void testWriteAndRead() throws IOException { //写入数据 List data = new ArrayList(); data.add(new String[]{\"张三\", \"18\", \"3000\"}); data.add(new String[]{\"李四\", \"20\", \"4000\"}); data.add(new String[]{\"王二\", \"25\", \"5000\"}); //写入文件路径 String path = \"/Users/sunpo/Downloads/testWriteAndRead.csv\"; //写入 csv 文件 writeCsv(data, path); //读取文件 Iterable records = readCSV(path); for (CSVRecord record : records) { for (String string : record) { System.out.print(string); System.out.print(\" \"); } System.out.println(); } } 测试写入并读取功能 测试结果真实可用,工具类基本功能编写完成. 制定解决方案 已经有了 csv 工具类,那么现在就要想办法解决实际问题,再看一下当前慕课网手记的内容格式吧! 148浏览 2推荐 0评论 204浏览 2推荐 0评论 181浏览 2推荐 0评论 分析上述内容格式有以下特点: 内容数据一行一条数据,可能需要换行符问题 每一行数据以空格分割,可分割成数组或列表再处理 已分割后的列表项包括了有效数据和文字说明,可能需要过滤出有效数据 按照上述分析结果,开始 coding 逐个解决,下面展示下关键代码. 按照空格将每一行数据分割成列表 List row = StringTools.splitToListString(string, \" \"); StringTools.splitToListString 方式是笔者封装的分割字符串方法,目的将字符串按照指定分隔符分割成字符串列表 处理分割后字符串列表并过来出有效数据 String readCountWithDescString = row.get(0); String readCountString = StringUtils.substringBefore(readCountWithDescString, \"浏览\"); String recommendCountWithDescString = row.get(1); String recommendCountString = StringUtils.substringBefore(recommendCountWithDescString, \"推荐\"); String commentCountWithDescString = row.get(2); String commentCountString = StringUtils.substringBefore(commentCountWithDescString, \"评论\"); StringUtils.substringBefore 方法也是Apache Commons 工具类,具体来源于 org.apache.commons.lang3 ,下述涉及到的 StringUtils 静态方法 也是,不再单独说明. 最后一步即统计分析 //浏览数 int readCount = 0; //推荐数 int recommendCount = 0; //评论数 int commentCount = 0; readCount += Integer.parseInt(readCountString); recommendCount += Integer.parseInt(recommendCountString); commentCount += Integer.parseInt(commentCountString); 实施解决方案 如此一来,三步均已解决,现在运行以下统计方法,看一下真实效果如何. /** * 统计慕课手记 * * @throws IOException */ private static void countImooc() throws IOException { //昨日统计数据 String yesterday = DateFormatUtils.format(DateUtils.addDays(new Date(), -1), \"yyyyMMdd\"); String path = String.format(\"/Users/sunpo/Documents/workspace/count/imooc-%s.csv\", yesterday); //总行数 int allRows = 0; //有效行数 int allValidRows = 0; //当前行是否有效 boolean isValidRow = true; //浏览数 int readCount = 0; //推荐数 int recommendCount = 0; //评论数 int commentCount = 0; Iterable records = readCSV(path); for (CSVRecord record : records) { allRows++; for (String string : record) { System.out.println(string); if (StringUtils.isBlank(string)) { isValidRow = false; break; } List row = StringTools.splitToListString(string, \" \"); String readCountWithDescString = row.get(0); String readCountString = StringUtils.substringBefore(readCountWithDescString, \"浏览\"); String recommendCountWithDescString = row.get(1); String recommendCountString = StringUtils.substringBefore(recommendCountWithDescString, \"推荐\"); String commentCountWithDescString = row.get(2); String commentCountString = StringUtils.substringBefore(commentCountWithDescString, \"评论\"); readCount += Integer.parseInt(readCountString); recommendCount += Integer.parseInt(recommendCountString); commentCount += Integer.parseInt(commentCountString); } if (isValidRow) { allValidRows++; } isValidRow = true; } System.out.println(); System.out.println(String.format(\"[慕课手记] 一共读取%d行,有效行: allValidRows = %d ,其中浏览数: readCount = %d ,推荐数: recommendCount = %d ,评论数: commentCount = %d\", allRows, allValidRows, readCount, recommendCount, commentCount)); System.out.println(); } 很完美,终于不必再肉眼统计数据了,虽然很长程度上仍然依赖人工整理好 csv 文件,但是目前已经解决了纯手动的弊端. 因此,上述解决方案是半手动的方式,仍然还有很多可以优化的地方,等下次忍受不了这种方案时再解决! 小结 本文主要介绍了纯手工统计报表遇到的诸多问题,寻求一种相对简单的解决方案. 基本流程大致可以分为下述流程: 手动复制文章列表(包括阅读量,评论量和点赞数),并整理成标准的 csv 格式文件. 编写各个平台的 csv 工具处理类,解析并统计 csv 文件内容. 运行工具类得到最终统计数据,大功告成! 本文主要介绍的是解决问题的思路,对于其中涉及到的相关技术点并未深入展开,关键源码已经贴上,如果还想要更详细的完整源码,可以留言回复. © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:11:14 "},"write/static-semi-manual-with-js.html":{"url":"write/static-semi-manual-with-js.html","title":"js 半手动数据统计","keywords":"","body":"js半手动数据统计 在日常文章数据统计的过程中,纯手动方式已经难以应付,于是乎,逐步开始了程序介入方式进行统计. 在上一节中,探索利用 csv 文件格式进行文章数据统计,本来以为能够应付一阵子,没想到仅仅一天我就放弃了. 原因还不是因为我懒,需要复制文章内容,然后整理成特定的 csv 格式,最后利用已编写的 java 工具类进行统计. 在这三步操作中,第一步复制文章内容最简单,第二步整理文章格式最麻烦,第三步编写 csv 工具类最技术. 因此,能不能再简单点?懒癌晚期,必须继续寻求新的解决方案. 关于如何利用 csv 文件处理统计数据,可以参考 https://snowdreams1006.github.io/static-semi-manual-with-csv.html 实现效果 慕课手记 慕课手记 : https://www.imooc.com/u/5224488/articles c3.generate({\"bindto\":\"#plugin-chart-1\",\r \"data\": {\r \"x\": \"x\",\r \"columns\": [\r [\r \"x\",\r \"2019-04-01\",\r \"2019-04-02\",\r \"2019-04-03\",\r \"2019-04-04\",\r \"2019-04-05\",\r \"2019-04-06\",\r \"2019-04-07\",\r \"2019-04-08\",\r \"2019-04-09\",\r \"2019-04-10\",\r \"2019-04-11\",\r \"2019-04-12\"\r ],\r [\r \"粉丝\",\r 8,\r 8,\r 8,\r 9,\r 9,\r 9,\r 9,\r 9,\r 9,\r 9,\r 9,\r 9\r ],\r [\r \"阅读量\",\r 3508,\r 3645,\r 3650,\r 4356,\r 4528,\r 4864,\r 5276,\r 5593,\r 5872,\r 5912,\r 6271,\r 6400\r ],\r [\r \"手记\",\r 32,\r 33,\r 34,\r 36,\r 38,\r 39,\r 40,\r 41,\r 42,\r 42,\r 44,\r 44\r ],\r [\r \"推荐\",\r 36,\r 36,\r 37,\r 39,\r 41,\r 42,\r 48,\r 49,\r 50,\r 50,\r 52,\r 52\r ],\r [\r \"积分\",\r 107,\r 118,\r 118,\r 130,\r 130,\r 141,\r 152,\r 173,\r 173,\r 173,\r 194,\r 195\r ]\r ],\r \"axes\": {\r \"粉丝\": \"y2\"\r },\r \"types\": {\r \"粉丝\": \"bar\"\r }\r },\r \"axis\": {\r \"x\": {\r \"type\": \"timeseries\",\r \"tick\": {\r \"format\": \"%Y-%m-%d\"\r }\r },\r \"y2\": {\r \"show\": \"true\",\r \"label\": { \r \"text\": \"粉丝\",\r \"position\": \"outer-middle\"\r }\r } \r }\r }); 简书 简书 : https://www.jianshu.com/u/577b0d76ab87 c3.generate({\"bindto\":\"#plugin-chart-2\",\r \"data\": {\r \"x\": \"x\",\r \"columns\": [\r [\r \"x\",\r \"2019-04-01\",\r \"2019-04-02\",\r \"2019-04-03\",\r \"2019-04-04\",\r \"2019-04-05\",\r \"2019-04-06\",\r \"2019-04-07\",\r \"2019-04-08\",\r \"2019-04-09\",\r \"2019-04-10\",\r \"2019-04-11\",\r \"2019-04-12\"\r ],\r [\r \"粉丝\",\r 7,\r 7,\r 6,\r 7,\r 6,\r 5,\r 5,\r 5,\r 5,\r 5,\r 5,\r 5\r ],\r [\r \"阅读量\",\r 343,\r 335,\r 342,\r 358,\r 374,\r 443,\r 468,\r 512,\r 548,\r 552,\r 611,\r 624\r ],\r [\r \"文章\",\r 33,\r 34,\r 35,\r 37,\r 39,\r 40,\r 41,\r 42,\r 43,\r 43,\r 46,\r 46\r ],\r [\r \"喜欢\",\r 57,\r 58,\r 59,\r 60,\r 62,\r 64,\r 65,\r 67,\r 68,\r 68,\r 68,\r 71,\r 71\r ],\r [\r \"简书钻\",\r 27,\r 28,\r 28,\r 9,9\r ,9,\r 10,\r 10,\r 10,\r 10,\r 11,\r 11\r ]\r ],\r \"axes\": {\r \"粉丝\": \"y2\"\r },\r \"types\": {\r \"粉丝\": \"bar\"\r }\r },\r \"axis\": {\r \"x\": {\r \"type\": \"timeseries\",\r \"tick\": {\r \"format\": \"%Y-%m-%d\"\r }\r },\r \"y2\": {\r \"show\": \"true\",\r \"label\": { \r \"text\": \"粉丝\",\r \"position\": \"outer-middle\"\r }\r }\r }\r }); 博客园 博客园 : https://www.cnblogs.com/snowdreams1006/ c3.generate({\"bindto\":\"#plugin-chart-3\",\r \"data\": {\r \"x\": \"x\",\r \"columns\": [\r [\r \"x\",\r \"2019-04-01\",\r \"2019-04-02\",\r \"2019-04-03\",\r \"2019-04-04\",\r \"2019-04-05\",\r \"2019-04-06\",\r \"2019-04-07\",\r \"2019-04-08\",\r \"2019-04-09\",\r \"2019-04-10\",\r \"2019-04-11\",\r \"2019-04-12\"\r ],\r [\r \"粉丝\",\r 17,\r 17,\r 17,\r 18,\r 18,\r 18,\r 18,\r 18,\r 18,\r 18,\r 18,\r 18\r ],\r [\r \"阅读数\",\r 3889,\r 4096,\r 4207,\r 4388,\r 4411,\r 4435,\r 4471,\r 4728,\r 4866,\r 4867,\r 5189,\r 5274\r ],\r [\r \"随笔\",\r 31,\r 32,\r 33,\r 34,\r 36,\r 38,\r 39,\r 41,\r 41,\r 41,\r 43,\r 43\r ],\r [\r \"评论数\",\r 16,\r 16,\r 16,\r 16,\r 16,\r 16,\r 16,\r 16,\r 16,\r 16,\r 16,\r 16\r ]\r ],\r \"axes\": {\r \"粉丝\": \"y2\"\r },\r \"types\": {\r \"粉丝\": \"bar\"\r }\r },\r \"axis\": {\r \"x\": {\r \"type\": \"timeseries\",\r \"tick\": {\r \"format\": \"%Y-%m-%d\"\r }\r },\r \"y2\": {\r \"show\": \"true\",\r \"label\": {\r \"text\": \"粉丝\",\r \"position\": \"outer-middle\"\r }\r }\r }\r }); 腾讯云社区 腾讯云社区 : https://cloud.tencent.com/developer/user/2952369/activities c3.generate({\"bindto\":\"#plugin-chart-4\",\r \"data\": {\r \"x\": \"x\",\r \"columns\": [\r [\r \"x\",\r \"2019-04-04\",\r \"2019-04-05\",\r \"2019-04-06\",\r \"2019-04-07\",\r \"2019-04-08\",\r \"2019-04-09\",\r \"2019-04-10\",\r \"2019-04-11\",\r \"2019-04-12\"\r ],\r [\r \"粉丝\",\r 13,\r 13,\r 13,\r 13,\r 13,\r 13,\r 13,\r 13,\r 13\r ],\r [\r \"阅读量\",\r 1192,\r 1561,\r 2131,\r 2144,\r 2149,\r 2158,\r 2159,\r 2163,\r 2165\r ],\r [\r \"文章\",\r 34,\r 34,\r 34,\r 34,\r 34,\r 34,\r 34,\r 34,\r 34\r ],\r [\r \"点赞\",\r 107,\r 108,\r 110,\r 107,\r 107,\r 107,\r 107,\r 107,\r 107\r ]\r ],\r \"axes\": {\r \"粉丝\": \"y2\"\r },\r \"types\": {\r \"粉丝\": \"bar\"\r }\r },\r \"axis\": {\r \"x\": {\r \"type\": \"timeseries\",\r \"tick\": {\r \"format\": \"%Y-%m-%d\"\r }\r },\r \"y2\": {\r \"show\": \"true\",\r \"label\": {\r \"text\": \"粉丝\",\r \"position\": \"outer-middle\"\r }\r }\r }\r }); js 抓取分析数据 下面以 chrome 浏览器为例,说明如何利用默认控制台抓取关键数据,本文需要一定的 jQuery 基础. 慕课手记 在目标页面右键选择检查选项,打开默认开发者控制台,点击最左侧的小鼠标箭头,然后选中关键数据,比如浏览量. 此时,开发者控制台自动滚动到元素(Elements)选项卡,在目标数据上右键点击复制(Copy),接着点击复制选择器(Copy selector),现在已经定位到阅读量的节点. 点击控制台(Console)选项卡,并且将选择器更改成 jQuery 选择器,即$(\"复制的选择器\").text(),现在在控制台直接输出内容,看一下能否抓取到浏览量吧! 现在已经成功定位到指定元素,而我们要统计的是全部文章的阅读量,因此需要定位到全部元素. $(\"#articlesList > div:nth-child(1) > div.item-btm.clearfix > div > div:nth-child(1) > em\").text(); 简单分析下文章结构结合选择器分析,可以得知, 浏览,推荐和评论三者文档基本一致,唯一不同之处就是排列顺序而已,因此想要准确定位到浏览数,需要定位到第一个元素,推荐量则是第二个元素,因此类推. 83浏览 1推荐 0评论 弄清楚基本文档结构后,开始着手改造选择器使其定位到全部文章的浏览量,我们做如下改造. $(\"#articlesList div:nth-child(1) > em\").text(); 仅仅保留头部和尾部,再去掉中间部分 > div:nth-child(1) > div.item-btm.clearfix > div > ,这样就轻松定位到全部元素的浏览量了,是不是很简单? 看到控制台输出结果,心里瞬间踏实了,这不刚好是第一页全部文章的浏览量吗?观察输出内容格式可知,我们需要将整个字符串按照空格分割成字符串数组. 需要注意的是,行首还有一个空格哟,因此在分割成字符串数组前,我们先将行首的空格去除掉. // 去除空格前:\" 83浏览 91浏览 114浏览 150浏览 129浏览 175浏览 222浏览 173浏览 225浏览 200浏览 201浏览 217浏览 291浏览 202浏览 229浏览 184浏览 226浏览 155浏览 153浏览 211浏览\" $(\"#articlesList div:nth-child(1) > em\").text().trim(); // 去除空格后: \"83浏览 91浏览 114浏览 150浏览 129浏览 175浏览 222浏览 173浏览 225浏览 200浏览 201浏览 217浏览 291浏览 202浏览 229浏览 184浏览 226浏览 155浏览 153浏览 211浏览\" 现在我们再将这整个字符串按照空格分割成字符串数组. // 分割字符串前: \"83浏览 91浏览 114浏览 150浏览 129浏览 175浏览 222浏览 173浏览 225浏览 200浏览 201浏览 217浏览 291浏览 202浏览 229浏览 184浏览 226浏览 155浏览 153浏览 211浏览\" $(\"#articlesList div:nth-child(1) > em\").text().trim().split(\" \"); // 分割字符串后: [\"83浏览\", \"91浏览\", \"114浏览\", \"150浏览\", \"129浏览\", \"175浏览\", \"222浏览\", \"173浏览\", \"225浏览\", \"200浏览\", \"201浏览\", \"217浏览\", \"291浏览\", \"202浏览\", \"229浏览\", \"184浏览\", \"226浏览\", \"155浏览\", \"153浏览\", \"211浏览\"] 现在我们已经够将整个字符串分割成一个个小的字符串,下面需要再将83浏览中的浏览去掉,仅仅保留数字83. $.each($(\"#articlesList div:nth-child(1) > em\").text().trim().split(\" \"),function(idx,ele){ console.log(ele.substr(0,ele.lastIndexOf(\"浏览\"))); }); 现在我们已经抓取到真正的浏览量,接下来就比较简单了,直接将这些浏览量进行累加即可,需要注意的是,这里的浏览数还是字符串类型,需要转换成数字类型才能进行累加运算哟! //阅读量 var readCount = 0; $.each($(\"#articlesList div:nth-child(1) > em\").text().trim().split(\" \"),function(idx,ele){ readCount += parseInt(ele.substr(0,ele.lastIndexOf(\"浏览\"))); }); console.log(\"阅读量: \" + readCount); 小结 我们以 chrome 浏览器为例,讲解了如何利用自带的控制台工具抓取关键数据,从页面结构分析入口,一步一个脚印提取有效数据,最终从一条数据变成多条数据,进而实现数据的累加统计. 总体来说,还是比较简单的,并不需要太多的基础知识,但还是稍微总结其中涉及到的 jQuery 知识点吧! 定位到具体元素: $(\"这里是复制的选择器\") 定位到具体元素内容: $(\"这里是复制的选择器\").text() 去除字符串首尾空格: $(\"这里是复制的选择器\").text().trim() 将字符串按照空格分割成字符串数组: $(\"这里是复制的选择器\").text().trim().split(\" \") 截取字符串指定部分: ele.substr(0,ele.lastIndexOf(\"浏览\") 将字符串转化成数字类型: parseInt(ele.substr(0,ele.lastIndexOf(\"浏览\"))); 变量累加求和: readCount += parseInt(ele.substr(0,ele.lastIndexOf(\"浏览\"))); 完整示例: //阅读量 var readCount = 0; $.each($(\"#articlesList div:nth-child(1) > em\").text().trim().split(\" \"),function(idx,ele){ readCount += parseInt(ele.substr(0,ele.lastIndexOf(\"浏览\"))); }); console.log(\"阅读量: \" + readCount); //推荐量 var recommendCount = 0; $.each($(\"#articlesList div:nth-child(2) > em\").text().trim().split(\" \"),function(idx,ele){ recommendCount += parseInt(ele.substr(0,ele.lastIndexOf(\"推荐\"))); }); console.log(\"推荐量: \" + recommendCount); //评论量 var commendCount = 0; $.each($(\"#articlesList div:nth-child(3) > em\").text().trim().split(\" \"),function(idx,ele){ commendCount += parseInt(ele.substr(0,ele.lastIndexOf(\"评论\"))); }); console.log(\"评论量: \" + commendCount); 简书 简书的文章数据不一定很规整,比如有的发布文章还没有简书钻,所以阅读量的排列顺序就是不确定的,这一点不像前面介绍的慕课手记,但是简书的关键数据前面是有小图标的,因此我们可以利用图标定位到旁边的数据. 按照前面介绍的步骤,我们仍然定位到阅读量,然而 #note-44847909 > div > div > a:nth-child(2) > i 却不能直接使用,因为我们刚刚分析了,简书不能利用顺序定位只能用图标辅助定位. 所以,还是先看看文档结构,尝试着直接定位到全部的阅读量小图标. 经过分析文章结构,我们可以很轻松定位到全部阅读小图标,当然这是一个元素数组,并不是字符串数组哟! $(\"#list-container .ic-list-read\") 接下来我们看一下能否正确定位到每一个小图标,进而定位到小图标左侧的阅读量. 现在我们已经能够定位到全部的阅读量小图标,现在思考如何定位到旁边的真正阅读量呢? 0.2 2 0 1 昨天 10:39 分析文章结构,我们发现阅读量是小图标的父节点的内容,这一下就简单了,我们顺藤摸瓜定位到父节点自然就能定位到阅读量了! $(\"#list-container .ic-list-read\").each(function(idx,ele){ console.log($(ele).parent().text().trim()); }); 现在既然已经能够定位到阅读量,那么首先累加求和就很简单了. //阅读量 var readCount = 0; $(\"#list-container .ic-list-read\").each(function(idx,ele){ readCount += parseInt($(ele).parent().text().trim()); }); console.log(\"阅读量: \" + readCount); 小结 首先分析文章基本结构发现,简书的阅读量需要定位到阅读量小图标,进而定位到父节点,然后父节点的内容才是真正的阅读量. 定位到真正的阅读量后,一切问题迎刃而解,总结一下新增 jQuery 知识点. 定位到当前节点的父节点: $(ele).parent() 完整示例: //阅读量 var readCount = 0; $(\"#list-container .ic-list-read\").each(function(idx,ele){ readCount += parseInt($(ele).parent().text().trim()); }); console.log(\"阅读量: \" + readCount); //评论量 var commendCount = 0; $(\"#list-container .ic-list-comments\").each(function(idx,ele){ commendCount += parseInt($(ele).parent().text().trim()); }); console.log(\"评论量: \" + commendCount); //喜欢量 var recommendCount = 0; $(\"#list-container .ic-list-like\").each(function(idx,ele){ recommendCount += parseInt($(ele).parent().text().trim()); }); console.log(\"喜欢量: \" + recommendCount); 博客园 博客园的文章列表比较复古,传统的 table 布局,是这几个平台中最简单的,基本上不同怎么介绍. 复制到阅读量选择器: #post-row-10694598 > td:nth-child(4) 此时再结合文章结构,因此我们可以得到全部文章的阅读量选择器. $(\"#post_list td:nth-child(4)\") 接下来需要遍历数组,看看能否抓取到当前页面全部文章的阅读量. $(\"#post_list td:nth-child(4)\").each(function(idx,ele){ console.log($(ele).text().trim()); }); 成功抓取到阅读量,现在开始累加当前页面全部文章的阅读量. //阅读数 var readCount = 0; $(\"#post_list td:nth-child(4)\").each(function(idx,ele){ readCount += parseInt($(ele).text().trim()); }); console.log(\"阅读数: \" + readCount); 小结 中规中矩的传统 table 布局,只需要顺序定位到具体的元素即可,需要注意的是,博客园文章页面采用了分页,如果需要统计全部文章的阅读量,需要将每页的阅读量手动累加计算. 完整示例: //评论数 var commendCount = 0; $(\"#post_list td:nth-child(3)\").each(function(idx,ele){ commendCount += parseInt($(ele).text().trim()); }); console.log(\"评论数: \" + commendCount); //阅读数 var readCount = 0; $(\"#post_list td:nth-child(4)\").each(function(idx,ele){ readCount += parseInt($(ele).text().trim()); }); console.log(\"阅读数: \" + readCount); 腾讯云社区 大致分析腾讯云社区的文章结构,基本上和简书结构差不多,既可以像简书那种采用图标定位方式,也可以像慕课网和博客园那种直接顺序定位. 为了较为精准的定位,现在采用图标定位方式来获取阅读量. #react-root > div:nth-child(1) > div.J-body.com-body.with-bg > section > div > section > div > div.com-log-list > section:nth-child(1) > section > div > div > span > span 既然要根据图标定位,我们需要分析图标和阅读量的关系. 76 3 因此,我们需要做如下改造才能定位到与阅读量. $(\"#react-root .com-i-view\").each(function(idx,ele){ console.log($(ele).next().text().trim()); }); 定位到阅读量,接下来就是简单的数据累加求和了. //阅读量 var readCount = 0; $(\"#react-root .com-i-view\").each(function(idx,ele){ readCount += parseInt($(ele).next().text().trim()); }); console.log(\"阅读量: \" + readCount); 小结 腾讯云社区和简书一样,采用的分页叠加模式,因此需要统计全部文章的话,只需要一直滚动直到加载出全部文章即可. 总结一下涉及到的新增 jQuery 知识点: 获取当前节点的下一个节点: $(ele).next() 完整示例: //阅读量 var readCount = 0; $(\"#react-root .com-i-view\").each(function(idx,ele){ readCount += parseInt($(ele).next().text().trim()); }); console.log(\"阅读量: \" + readCount); //点赞量 var recommendCount = 0; $(\"#react-root .com-i-like\").each(function(idx,ele){ recommendCount += parseInt($(ele).next().text().trim()); }); console.log(\"点赞量: \" + recommendCount); 小结 本文通过 jQuery 方式直接抓取文章数据,简单方便,学习成本低,能够快速上手. 慕课网和博客园的文章列表存在分页,如果需要统计全部文章浏览量,需要将每一页的文章累加,直到最后一页. 简书和腾讯云社区的文章列表虽然也有分支,但会自动累加,所以统计全部文章时只需要先等全部文章加载完毕,再利用 js 脚本一次性统计即可. 好了,本次分享到此结束,如果你觉得本文对你有所帮助,欢迎分享让更多人看到哦,对了,上一篇文章也是解决统计问题的,不过使用的是 java 读取 csv 文件方式,如果有兴趣,也可以看一看. © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:11:14 "},"write/jianshu-optimize-static.html":{"url":"write/jianshu-optimize-static.html","title":"简述优选文章统计","keywords":"","body":"简述优选文章统计 c3.generate({\"bindto\":\"#plugin-chart-5\", \"data\": { \"x\": \"x\", \"columns\": [ [ \"x\", \"01\", \"02\", \"03\", \"04\", \"05\", \"06\", \"07\", \"08\", \"09\", \"10\", \"11\", \"12\", \"13\", \"14\", \"15\", \"16\", \"17\", \"18\", \"19\", \"20\", \"21\", \"22\", \"23\", \"24\", \"25\", \"26\", \"27\", \"28\", \"29\", \"30\", \"31\", \"32\", \"33\", \"34\", \"35\", \"36\" ], [ \"阅读量\", 14000, \t\t\t\t10000, \t\t\t\t12000, \t\t\t\t9073, \t\t\t\t15000, \t\t\t\t7988, \t\t\t\t18000, \t\t\t\t17036, \t\t\t\t13146, \t\t\t\t16087, \t\t\t\t18788, \t\t\t\t14861, \t\t\t\t19771, \t\t\t\t27232, \t\t\t\t11637, \t\t\t\t12835, \t\t\t\t14173, \t\t\t\t9256, \t\t\t\t31607, \t\t\t\t12552, \t\t\t\t21840, \t\t\t\t10745, \t\t\t\t12573, \t\t\t\t8760, \t\t\t\t10981, \t\t\t\t22641, \t\t\t\t33164, \t\t\t\t28793, \t\t\t\t22393, \t\t\t\t17709, \t\t\t\t16965, \t\t\t\t29429, \t\t\t\t12264, \t\t\t\t25831, \t\t\t\t14984, \t\t\t\t15204 ], [ \"点赞量\", 174, \t\t\t\t70, \t\t\t\t69, \t\t\t\t30, \t\t\t\t191, \t\t\t\t57, \t\t\t\t117, \t\t\t\t124, \t\t\t\t54, \t\t\t\t101, \t\t\t\t101, \t\t\t\t41, \t\t\t\t32, \t\t\t\t83, \t\t\t\t76, \t\t\t\t70, \t\t\t\t61, \t\t\t\t48, \t\t\t\t72, \t\t\t\t74, \t\t\t\t88, \t\t\t\t55, \t\t\t\t28, \t\t\t\t39, \t\t\t\t13, \t\t\t\t36, \t\t\t\t61, \t\t\t\t111, \t\t\t\t28, \t\t\t\t102, \t\t\t\t23, \t\t\t\t55, \t\t\t\t41, \t\t\t\t28, \t\t\t\t31, \t\t\t\t60 ], [ \"评论量\", 57, \t\t\t\t37, \t\t\t\t22, \t\t\t\t43, \t\t\t\t32, \t\t\t\t16, \t\t\t\t57, \t\t\t\t100, \t\t\t\t39, \t\t\t\t67, \t\t\t\t71, \t\t\t\t25, \t\t\t\t59, \t\t\t\t90, \t\t\t\t15, \t\t\t\t96, \t\t\t\t77, \t\t\t\t17, \t\t\t\t37, \t\t\t\t27, \t\t\t\t62, \t\t\t\t17, \t\t\t\t29, \t\t\t\t39, \t\t\t\t11, \t\t\t\t65, \t\t\t\t64, \t\t\t\t41, \t\t\t\t33, \t\t\t\t26, \t\t\t\t27, \t\t\t\t25, \t\t\t\t38, \t\t\t\t36, \t\t\t\t46, \t\t\t\t36 ] ], \"axes\": { \"阅读量\": \"y2\" }, \"types\": { \"阅读量\": \"bar\" } }, \"axis\": { \"y2\": { \"show\": \"true\", \"label\": { \"text\": \"阅读量\", \"position\": \"outer-middle\" } } } }); 阅读量优先 第一名: 27 号 “女生第1次和第5次的区别”,有些东西,只有经历了才懂得 目前为止阅读量最高,33164 次阅读,其中 61 个点赞,评论数也不错,64 个评论,入选精选留言 44. 第二名: 19 号 “叔叔阿姨,那个...能小点声吗?” 阅读量 31607 ,也是不错的成绩,和第一名相比也就差了一两千,但是评论数只有第一名的一半 37,其中入选精选留言数是 34,看样子只要比较正常的留言都入选了,点赞数有 72 比第一名稍微高点. 第三名: 32 号 蔡少芬直言当过小三:老天给了她一手烂牌,她却打出了王炸 阅读量和第二名相差不多,差一点就 3w ,29429 和 31607 相比也是差了一两千,这一点比较有意思了,前三名的阅读量几乎都是差一两千? 评论数有 25 ,入选留言 20 ,点赞数 55 ,和前两名相比中规中矩,没有特别突出之处. 倒数第一名: 6 号 为什么大多数人宁愿吃生活的苦,也不愿吃学习的苦? 7988 次阅读量,排名垫底,评论数是 16 ,入选留言数 12 ,但是点赞数 57 ,数据并不是很差. 倒数第一的阅读量却拥有第三阅读量的点赞数,这一点足以证明该篇文章的优秀,看得人比较少可能是不愿正视自己吧? 倒数第二名: 24 号 “好不容易上个大学,谈什么恋爱啊” 倒数阅读量的标准本身有些歧视,8760 和 倒数第一名的 7988 相差不到一千,39 次评论,入选 33 条留言,并且点赞数 39 ,各项成绩比较平均. 倒数第三名: 4 号 毕业了,谁又不曾有过爱情的遗憾 和倒数第二名的阅读量相差只有一两百,这个差距应该不是很大,至少不是一两千的差距,遗憾的是仍然是倒数的排名. 9073 次阅读,43 条评论,34 条入选留言,30 个点赞. 站在阅读量优先的立场分析前三名与倒数后三名,不难发现这么一个规律: 简述公众号的受众应该偏向中年,娱乐八卦的话题永远不缺观众,校园生活或者毕业季话题虽然有一定的受众,但是远远不是大众化标准. 暗示色情的标题很大程度上吸引人阅读,但是老司机都明白的是,公众号一般不会真正的色情,所以文章一般都是柳暗花明又一村,如果你想歪了,那就证明你不纯洁! 所以这类文章阅读量比较高,有一种上当受骗的感觉不说点什么觉得对不起自己,所以留言也不错,如果能将这类文章转发出去恶搞一下朋友,这是极好的呢! 点赞量优先 第一名: 5 号 越优秀的人,越不安分 目前为止点赞量第一名,191 次的点赞数,阅读量只有 1.5w ,要知道阅读量前三名都是 3w 啊,仅仅用了一半的阅读量就收获了高于近乎 3 倍的点赞数. 可见,文章的阅读量和点赞量没有必然联系,优秀的文章能够令人深思,愿意主动分享出去,同时这篇文章的评论是是 32,入选留言 22 ,数据比较普通,突出之处就是点赞数. 第二名: 1 号 不合群的你,其实也很酷 点赞数 174 ,很第一名相差一二十而已,评论数是 57,入选留言 45 ,阅读量 1.4w ,从三项指标来看,和第一名不差上下,阅读量虽然少一点,但是评论数却多一点. 有意思的是,点赞数前两名都是和个人修养有关的文章,阅读量 1.5w 是最高阅读量 3w 的一半,也就是说虽然没有大众化口味,但是文章本身的内容非常好,愿意积极进取的中青年看过都说好! 第三名: 8 号 你这么内向,那应该找不到对象吧 124 次点赞稳居排行榜第三名,100 条留言,入选 81 条足以证明该话题引发的讨论是多么激烈,可见话题本身具有一定的爆炸性. 1.7w 的阅读量比前两名的阅读量都要高出两三千,但是点赞数却少了五六十,难不成多出的人是纯粹看热闹的吗? 倒数第一名: 25 号 2060年,那个失踪的宇航员回来了...... 13 次点赞创造历史,10981 次阅读仅仅换来 11 条留言,其中 9 条入选精选留言,除了阅读量高,另外两方面都比较地,我猜是不明所以的人点进来看科幻小说,结果失望而归,所以才没太大人气. 倒数第二名: 31 号 微信朋友圈查访客记录:我和我的“朋友圈”爱人 23 次点赞是本次排名的倒数第二,16965 次阅读本可以像 5 号那样收获最高的点赞数,无奈只有 23 个,27 条留言中入选 24 条,说明文章具备一定的话题性,但是质量还不足以让人主动点赞分享. 倒数第三名: 23 号,29 号,34 号 23 富家女离奇死亡,摩天轮内发现惊人一幕 12573 次阅读量还不错,29 条留言中入选 23 条,点赞数是 28 ,平均下来每一条留言就有一人点赞. 典型的编故事,最终效果竟然还不错,有点不理解. 29 我那个从事裸体艺术的前女友去世了... 22393 次阅读,33 条留言中 26 条入选,最终收获的点赞数也是 28 ,比 23 号多了一半的人都是打酱油的. 34 成为谁,也不要成为小s 大致情况和 29 号差不多,25831 次阅读量本身足够优秀,最高的阅读量也就 3w 啊,36 次留言入选了 23 条,最终点赞数只有 28 . 看样子明星的生活有人愿意看,但是不敢苟同你的观点啊. 分析了点赞排名,发现一个有意思现象,点赞量靠前的文章阅读量不一定很高,可能只有最高阅读量的一半左右,但是优秀的文章天生具备传播性,容易让人点赞分享. 前三名文章竟然无一例外都是和个人修养方面有关的,或者反省自身或者看热闹不留名,总而言之,积极地主旋律应该差不了. 然而倒数后三名的情况有些不一样了,标题带有较为明显的虚假故事情节,离奇猎奇的标题一定程度上吸引了更多人阅读,也引发了一定程度上的讨论,但是最终愿意分享点赞的人却寥寥无几,逞一时之快罢了! 评论量优先 第一名: 8 号 你这么内向,那应该找不到对象吧 又见熟悉的 8 号,内向的我找不到女朋友,记得这篇文章的点赞量 124应该是前三名的,没想到评论数 100 竟然是第一名? 100 次留言,入选 81 条,剩下的 19 条难不成是幸灾乐祸的,所以没入选精选留言? 17036 次阅读不仅是评论量第一名还是点赞量的前三名,即使放到阅读量排名中这个成绩也是不错的. 难不成公众号的粉丝都比较内向,不是说好的奋斗的中青年人设吗? 第二名: 16 号 十年工厂生涯,我活成了没有梦想的中年人 96 次评论入选 74 条留言说明关注这类话题的人确实不少,12835 次阅读中有 70 次点赞,各方面的成绩都挺不错的. 中年人压力很大,简述这一类的用户应该也不少,通过阅读量和点赞量分析来看,不难理解这篇文章为什么如何受欢迎了. 第三名: 14 号 谢谢你,给我18厘米的爱情 首先承认我自己不是纯洁的人,谁让标题起得这么露骨,可是点进去发现我错了,90 次的评论,虽然只有 55 条入选,但是 27232 次阅读量不是吹的,而且 83 次点赞也是很不错的成绩. 倒数第一名: 25 号 2060年,那个失踪的宇航员回来了...... 未来的宇航员又见面了,上一次见面好像是点赞量倒数后三名的时候,没想到评论量后三名又一次出现了. 11 次评论,有 9 条留言入选,明明有 10981 次阅读量,至少不是倒数的阅读量可是评论量和点赞量都不是太理想,点赞数只有区区的 13 个. 倒数第二名: 15 号 你现在这么努力,是为了有朝一日“有得选” 15 次评论有 11 个入选精选留言,11637 次阅读如果不能证明文章很好,那 76 个点赞数应该可以自证清白了吧? 或许是这么一个心理: 你说的都对,我不想评论,还是静静的点赞吧! 倒数第三名: 6 号 为什么大多数人宁愿吃生活的苦,也不愿吃学习的苦? 又是熟悉的面孔,虽然从标题上看,有些刺眼,但是自古忠言逆耳,更何况标题给人一种事后诸葛亮的感觉,所以阅读量才 7988 垫底,流失了大批读者后并没有影响到点赞量,可见文章还是有一定的质量的. 最终可惜的是,评论量和阅读量双惨淡,未免让人唏嘘! 如果仅仅考虑评论数的多寡,那么只要是话题性文章或者争议性文章,个个都能收获不少的评论数,如果标题带有一定的性暗示,那么话题自然就来了,阅读量不错,评论量也不错,如果文章也不错的话,那么点赞量也不会差. 如果是自身经历的故事情节,也能唤起很多人的共鸣,可惜的是劝学类文章空有内涵,不受待见,阅读量低,评论量自然也不会太高. 优质文章总结 前面从阅读量,点赞量和评论量三个不同纬度排名了 36 篇简书优质文章,从最终的结果上看,大致发现如下规律: 阅读量高的总体质量不会太差,一般都有着不错的评论量和点赞量. 评论量高的总体质量一般不错,阅读量和点赞量也会相当靠前. 点赞量高的总体质量可能不错,阅读量和评论量都无法保证. 情感类的文章阅读量都不错,因为这方面的文章受众比较广,无论是在校学生还是出入职场菜鸟亦或是油腻的中年,对于这类文章都毫无招架之力,如果文章话题性不错的话,随随便便就几十条留言,质量可以的话,点赞分享更是几十,这类文章堪称王者. 励志类的文章一般能够收获不错的点赞量,人人都需要心灵鸡汤的自我催眠,积极进取是奋斗的主旋律,虽然这类文章可能阅读量没有情感类的那么巨大,评论量也可能不是太多,但这类文章确实占据一席之地. 社会类的文章容易引发共鸣,吸引大量读者留言评论,这类文章阅读量和评论量都很不错,但是点赞量就不那么确定了,考虑到因人而异的社会经历,有一定的探险猎奇心理,也是优质文章的一部分. 优质文章有标准,不那么成功的文章也有案列,以下文章可能就不太适合简书公众号. 25 号 : 2060年,那个失踪的宇航员回来了...... 点赞量和评论量均倒数,阅读量中等偏下,因此可能不太适合简书公众号. 科幻小说,字数有些长,不适合公众号阅读,故事本身结局有些突兀,还没看够呢,感觉要走点击购买电子书的套路? 公众号文章一般都是碎片化阅读,此类小说适合专栏故事形式进行连载,或者购买电子书进行引流. 6 号 : 为什么大多数人宁愿吃生活的苦,也不愿吃学习的苦? 阅读量和评论量均倒数,点赞量中等,因此可能也不太适合. 苦口婆心劝学类文章本不应该太差,可惜标题给人一种事后诸葛亮的说教,但是内容却还不错,文章重点还有加粗标注,点赞量也还可以,所以这篇文章败在标题上,有些遗憾. 数据分析及处理 js 版本数据统计分析示例 var articleStatic = ` 36 https://mp.weixin.qq.com/s/JA_H5y0SHOcS0V-pn8V7Jw 为什么在一线城市待久的人,就很难再回去? 36 23 15204 60 35 https://mp.weixin.qq.com/s/yqcT5VlIcLh9d7Eyz4uyFw 我嫂子——那个“恶女人”的前半生:你哥他...不行 46 30 14984 31 34 https://mp.weixin.qq.com/s/xJRtW340VwRNDU1hxQc0wA 成为谁,也不要成为小s 36 23 25831 28 33 https://mp.weixin.qq.com/s/TTqzmPeof8FN3D1kErSnJQ 你真傻,念念不忘,是没有回响的 38 31 12264 41 32 https://mp.weixin.qq.com/s/TTqzmPeof8FN3D1kErSnJQ 蔡少芬直言当过小三:老天给了她一手烂牌,她却打出了王炸 25 20 29429 55 31 https://mp.weixin.qq.com/s/7aTGbUziPgWijye1xKF7LA 微信朋友圈查访客记录:我和我的“朋友圈”爱人 27 24 16965 23 30 https://mp.weixin.qq.com/s/sNiSuR7acH43kwnP5yox9g 我的好胜心害死了好朋友······ 26 19 17709 102 29 https://mp.weixin.qq.com/s/i7xqmHHbYrmbBHy__aaGfA 我那个从事裸体艺术的前女友去世了... 33 26 22393 28 28 https://mp.weixin.qq.com/s/85tALM-OAaCet78rfQ59Hw 贾玲,你也是活该! 41 28 28793 111 27 https://mp.weixin.qq.com/s/bhKFbcoeXDrVmfDhpyDiUA “女生第1次和第5次的区别”,有些东西,只有经历了才懂得 64 44 33164 61 26 https://mp.weixin.qq.com/s/lG3mGfKAvTrI7UL383D6Kw 我的3次相亲经历,次次都教我做人 65 31 22641 36 25 https://mp.weixin.qq.com/s/bT33bHJcsf_Jpwx9mjswNg 2060年,那个失踪的宇航员回来了...... 11 9 10981 13 24 https://mp.weixin.qq.com/s/FBzJZzmzNrpeNtcQBSY4fQ “好不容易上个大学,谈什么恋爱啊” 39 33 8760 39 23 https://mp.weixin.qq.com/s/1JBaS5FTcrzRnylIBaoAKg 富家女离奇死亡,摩天轮内发现惊人一幕 29 23 12573 28 22 https://mp.weixin.qq.com/s/pfqKkHNI9bQDbhdv4gPAnw 有好教养的人,总是闪闪发光的啊 17 12 10745 55 21 https://mp.weixin.qq.com/s/o6aGhHp5uI3Nquy0usaaww 优质男:姑娘,我凭什么娶你? 62 50 21840 88 20 https://mp.weixin.qq.com/s/PoiHroXm3V1kKjj3u1jnAw 那个改变了我一生的老男人······ 27 20 12552 74 19 https://mp.weixin.qq.com/s/IpXCQ6YPFrq39T1hFu4P-g “叔叔阿姨,那个...能小点声吗?” 37 34 31607 72 18 https://mp.weixin.qq.com/s/8WOH-pBrSCyEyByf8AOi3A “我,23岁,拖延癌晚期,还有救吗?” 17 13 9256 48 17 https://mp.weixin.qq.com/s/74gJMnvGILwajfFMwWFc_g “十二年了,我才没那么想他” 77 71 14173 61 16 https://mp.weixin.qq.com/s/Fb6Ygp07QkaQYFh-O2OBOw 十年工厂生涯,我活成了没有梦想的中年人 96 74 12835 70 15 https://mp.weixin.qq.com/s/n7mTI2dOAPQjG312gXP_8g 你现在这么努力,是为了有朝一日“有得选” 15 11 11637 76 14 https://mp.weixin.qq.com/s/vJpJkwMlqZLLvmf6W342gg 谢谢你,给我18厘米的爱情 90 55 27232 83 13 https://mp.weixin.qq.com/s/TJrq7zNELA48CG_WUJRtnA 我爱上了隔壁那个被家暴的女人 59 40 19771 32 12 https://mp.weixin.qq.com/s/zOQrwG5q7lwvypabYDs27w 你以为是丑拒,其实是...... 25 11 14861 41 11 https://mp.weixin.qq.com/s/jUHhqDS2fjLjTXY551Bn5w 追我那么久,请你放过我 71 64 18788 101 10 https://mp.weixin.qq.com/s/8y2jAt5be1ndl1JNV4H-1A 我犯了一种罪,叫长的不好看 67 34 16087 101 9 https://mp.weixin.qq.com/s/k8uidACpKxz6a8YCg-kwzQ 你都用不起SK-II,还敢熬这么深的夜? 39 34 13146 54 8 https://mp.weixin.qq.com/s/Vt_nd4xx5gEhNXgyzojAcQ 你这么内向,那应该找不到对象吧 100 81 17036 124 7 https://mp.weixin.qq.com/s/SU3CHUNT7l5Hy0P8WWYPAw 想用自律打败低配人生,你配吗? 57 46 1.8w 117 6 https://mp.weixin.qq.com/s/FoKMrZ-gvzn1RWUtsFQzJQ 为什么大多数人宁愿吃生活的苦,也不愿吃学习的苦? 16 12 7988 57 5 https://mp.weixin.qq.com/s/ugG71ETZOAygI-DhMruCEA 越优秀的人,越不安分 32 22 1.5w 191 4 https://mp.weixin.qq.com/s/e1mif6nhCJ-XeRoilhW83A 毕业了,谁又不曾有过爱情的遗憾 43 34 9073 30 3 https://mp.weixin.qq.com/s/nHhoFhuNojiAqqHKkfBYLQ 经营一段亲密关系,难吗? 22 18 1.2w 69 2 https://mp.weixin.qq.com/s/RqrH9kdgUHIiAfS_vxg3QA 婆媳之间,何止是缘分? 37 27 1.0w 70 1 https://mp.weixin.qq.com/s/QMEy489ohNGHiETixElgMQ 不合群的你,其实也很酷 57 45 1.4w 174 `; var untreatedArticles = articleStatic.split('\\n'); var treatedArticles = untreatedArticles.slice(1,untreatedArticles.length-1); var nums = []; var comments = []; var reads = []; var likes = []; for (i = 0; i 简书公众号 36 篇优质文章数据 编号 标题 阅读量 点赞量 评论量 36 为什么在一线城市待久的人,就很难再回去? 15204 60 36 35 我嫂子——那个“恶女人”的前半生:你哥他...不行 14984 31 46 34 成为谁,也不要成为小s 25831 28 36 33 你真傻,念念不忘,是没有回响的 12264 41 38 32 蔡少芬直言当过小三:老天给了她一手烂牌,她却打出了王炸 29429 55 25 31 微信朋友圈查访客记录:我和我的“朋友圈”爱人 16965 23 27 30 我的好胜心害死了好朋友······ 17709 102 26 29 我那个从事裸体艺术的前女友去世了... 22393 28 33 28 贾玲,你也是活该! 28793 111 41 27 “女生第1次和第5次的区别”,有些东西,只有经历了才懂得 33164 61 64 26 我的3次相亲经历,次次都教我做人 22641 36 65 25 2060年,那个失踪的宇航员回来了...... 10981 13 11 24 “好不容易上个大学,谈什么恋爱啊” 8760 39 39 23 富家女离奇死亡,摩天轮内发现惊人一幕 12573 28 29 22 有好教养的人,总是闪闪发光的啊 10745 55 17 21 优质男:姑娘,我凭什么娶你? 21840 88 62 20 那个改变了我一生的老男人······ 12552 74 27 19 “叔叔阿姨,那个...能小点声吗?” 31607 72 37 18 “我,23岁,拖延癌晚期,还有救吗?” 9256 48 17 17 “十二年了,我才没那么想他” 14173 61 77 16 十年工厂生涯,我活成了没有梦想的中年人 12835 70 96 15 你现在这么努力,是为了有朝一日“有得选” 11637 76 15 14 谢谢你,给我18厘米的爱情 27232 83 90 13 我爱上了隔壁那个被家暴的女人 19771 32 59 12 你以为是丑拒,其实是...... 14861 41 25 11 追我那么久,请你放过我 18788 101 71 10 我犯了一种罪,叫长的不好看 16087 101 67 9 你都用不起SK-II,还敢熬这么深的夜? 13146 54 39 8 你这么内向,那应该找不到对象吧 17036 124 100 7 想用自律打败低配人生,你配吗? 1.8w 117 57 6 为什么大多数人宁愿吃生活的苦,也不愿吃学习的苦? 7988 57 16 5 越优秀的人,越不安分 1.5w 191 32 4 毕业了,谁又不曾有过爱情的遗憾 9073 30 43 3 经营一段亲密关系,难吗? 1.2w 69 22 2 婆媳之间,何止是缘分? 1.0w 70 37 1 不合群的你,其实也很酷 1.4w 174 57 36 https://mp.weixin.qq.com/s/JA_H5y0SHOcS0V-pn8V7Jw 为什么在一线城市待久的人,就很难再回去? 36 23 15204 60 35 https://mp.weixin.qq.com/s/yqcT5VlIcLh9d7Eyz4uyFw 我嫂子——那个“恶女人”的前半生:你哥他...不行 46 30 14984 31 34 https://mp.weixin.qq.com/s/xJRtW340VwRNDU1hxQc0wA 成为谁,也不要成为小s 36 23 25831 28 33 https://mp.weixin.qq.com/s/TTqzmPeof8FN3D1kErSnJQ 你真傻,念念不忘,是没有回响的 38 31 12264 41 32 https://mp.weixin.qq.com/s/TTqzmPeof8FN3D1kErSnJQ 蔡少芬直言当过小三:老天给了她一手烂牌,她却打出了王炸 25 20 29429 55 31 https://mp.weixin.qq.com/s/7aTGbUziPgWijye1xKF7LA 微信朋友圈查访客记录:我和我的“朋友圈”爱人 27 24 16965 23 30 https://mp.weixin.qq.com/s/sNiSuR7acH43kwnP5yox9g 我的好胜心害死了好朋友······ 26 19 17709 102 29 https://mp.weixin.qq.com/s/i7xqmHHbYrmbBHy__aaGfA 我那个从事裸体艺术的前女友去世了... 33 26 22393 28 28 https://mp.weixin.qq.com/s/85tALM-OAaCet78rfQ59Hw 贾玲,你也是活该! 41 28 28793 111 27 https://mp.weixin.qq.com/s/bhKFbcoeXDrVmfDhpyDiUA “女生第1次和第5次的区别”,有些东西,只有经历了才懂得 64 44 33164 61 26 https://mp.weixin.qq.com/s/lG3mGfKAvTrI7UL383D6Kw 我的3次相亲经历,次次都教我做人 65 31 22641 36 25 https://mp.weixin.qq.com/s/bT33bHJcsf_Jpwx9mjswNg 2060年,那个失踪的宇航员回来了...... 11 9 10981 13 24 https://mp.weixin.qq.com/s/FBzJZzmzNrpeNtcQBSY4fQ “好不容易上个大学,谈什么恋爱啊” 39 33 8760 39 23 https://mp.weixin.qq.com/s/1JBaS5FTcrzRnylIBaoAKg 富家女离奇死亡,摩天轮内发现惊人一幕 29 23 12573 28 22 https://mp.weixin.qq.com/s/pfqKkHNI9bQDbhdv4gPAnw 有好教养的人,总是闪闪发光的啊 17 12 10745 55 21 https://mp.weixin.qq.com/s/o6aGhHp5uI3Nquy0usaaww 优质男:姑娘,我凭什么娶你? 62 50 21840 88 20 https://mp.weixin.qq.com/s/PoiHroXm3V1kKjj3u1jnAw 那个改变了我一生的老男人······ 27 20 12552 74 19 https://mp.weixin.qq.com/s/IpXCQ6YPFrq39T1hFu4P-g “叔叔阿姨,那个...能小点声吗?” 37 34 31607 72 18 https://mp.weixin.qq.com/s/8WOH-pBrSCyEyByf8AOi3A “我,23岁,拖延癌晚期,还有救吗?” 17 13 9256 48 17 https://mp.weixin.qq.com/s/74gJMnvGILwajfFMwWFc_g “十二年了,我才没那么想他” 77 71 14173 61 16 https://mp.weixin.qq.com/s/Fb6Ygp07QkaQYFh-O2OBOw 十年工厂生涯,我活成了没有梦想的中年人 96 74 12835 70 15 https://mp.weixin.qq.com/s/n7mTI2dOAPQjG312gXP_8g 你现在这么努力,是为了有朝一日“有得选” 15 11 11637 76 14 https://mp.weixin.qq.com/s/vJpJkwMlqZLLvmf6W342gg 谢谢你,给我18厘米的爱情 90 55 27232 83 13 https://mp.weixin.qq.com/s/TJrq7zNELA48CG_WUJRtnA 我爱上了隔壁那个被家暴的女人 59 40 19771 32 12 https://mp.weixin.qq.com/s/zOQrwG5q7lwvypabYDs27w 你以为是丑拒,其实是...... 25 11 14861 41 11 https://mp.weixin.qq.com/s/l5GKBMa0RMYa5Zs_Z-J1bQ 越是难熬的日子,越要有事可做 22 18 13294 101 11 https://mp.weixin.qq.com/s/jUHhqDS2fjLjTXY551Bn5w 追我那么久,请你放过我 71 64 18788 101 10 https://mp.weixin.qq.com/s/8y2jAt5be1ndl1JNV4H-1A 我犯了一种罪,叫长的不好看 67 34 16087 101 9 https://mp.weixin.qq.com/s/k8uidACpKxz6a8YCg-kwzQ 你都用不起SK-II ,还敢熬这么深的夜? 39 34 13146 54 8 https://mp.weixin.qq.com/s/Vt_nd4xx5gEhNXgyzojAcQ 你这么内向,那应该找不到对象吧 100 81 17036 124 7 https://mp.weixin.qq.com/s/SU3CHUNT7l5Hy0P8WWYPAw 想用自律打败低配人生,你配吗? 57 46 1.8w 117 6 https://mp.weixin.qq.com/s/FoKMrZ-gvzn1RWUtsFQzJQ 为什么大多数人宁愿吃生活的苦,也不愿吃学习的苦? 16 12 7988 57 5 https://mp.weixin.qq.com/s/ugG71ETZOAygI-DhMruCEA 越优秀的人,越不安分 32 22 1.5w 191 4 https://mp.weixin.qq.com/s/e1mif6nhCJ-XeRoilhW83A 毕业了,谁又不曾有过爱情的遗憾 43 34 9073 30 3 https://mp.weixin.qq.com/s/nHhoFhuNojiAqqHKkfBYLQ 经营一段亲密关系,难吗? 22 18 1.2w 69 2 https://mp.weixin.qq.com/s/RqrH9kdgUHIiAfS_vxg3QA 婆媳之间,何止是缘分? 37 27 1.0w 70 1 https://mp.weixin.qq.com/s/QMEy489ohNGHiETixElgMQ 不合群的你,其实也很酷 57 45 1.4w 174 © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:11:13 "},"write/faq.html":{"url":"write/faq.html","title":"常见问题","keywords":"","body":"常见问题 The page build failed for the master branch with the following error 问题描述 看到这封邮件,一脸懵逼,本地运行 gitbook 服务是正常渲染的,控制台并没有任何报错,谁知道推送到 github 时就报错了! 登录到 github 网站查看网站源码已经同步过来了,但是静态网站无法同步,本地实在找不到任何报错信息,这让我如何是好? 再看 github 反馈用的邮件中说道,如有问题可以回复邮件(If you have any questions you can contact us by replying to this email.). 然后死马当活马医,尝试阐释了我的问题,请求帮助定位错误日志,没想到当天下午就收到 github 的回复邮件,提供了解决办法! 问题是由于 Liquid Warning: Liquid syntax error (line 334) 错误,然而我确定这部分代码是没有任何问题的,因为这是我改造 gitbook-plugin-tbfed-pagefooter 插件时的一段代码,反复确认后发现并没有复制粘贴出错啊! var moment = require('moment'); module.exports = { book: { assets: './assets', css: [ 'footer.css' ], }, hooks: { 'page:before': function(page) { var _label = '最后更新时间: ', _format = 'YYYY-MM-DD', _copy = 'powered by snowdreams1006' if(this.options.pluginsConfig['tbfed-pagefooter']) { _label = this.options.pluginsConfig['tbfed-pagefooter']['modify_label'] || _label; _format = this.options.pluginsConfig['tbfed-pagefooter']['modify_format'] || _format; var _c = this.options.pluginsConfig['tbfed-pagefooter']['copyright']; _copy = _c ? _c + ' all right reserved,' + _copy : _copy; } var _copy = ''+_copy+''; var str = ' \\n\\n' + _copy + '' + _label + '\\n{{file.mtime | date(\"' + _format + '\")}}\\n'; str += '\\n\\n'+ '\\n\\n'+ '\\n\\n'+ '\\n\\n'; page.content = page.content + str; return page; } }, filters: { date: function(d, format) { return moment(d).format(format) } } }; 来源于 gitbook-plugin-tbfed-pagefooter 插件的 index.js 文件,这里为了兼容 gitalk 插件而集成了相关代码,详情请参考 gitalk 评论插件 解决方案 根据邮件回复,定位到出错代码片段,真的没发现有什么问题啊? 既然已经确定不是我的问题,那很可能就是 github 的问题了,邮件中推荐我使用 Jekyll 进行构建网站,不不不! 既然已经选择 gitbook 搭建静态网站,那就没必要再使用 Jekyll ,我可不想那么麻烦! If you are not using Jekyll you can disable it by including a .nojekyll file in the root of your repository. 所以我不妨试试新增 .nojekyll 文件,说不定就好使了呢! $ touch .nojekyll $ git add .nojekyll $ git commit -m \"add .nojekyll\" $ git push 天不负我!竟然真的好使了,再也没有收到 github 的报错邮件反馈了,源码和网站都正常更新了! 小结 据我推测,可能是 github 误认为我的网站是使用 Jekyll 工具构建的,实际上,是使用 gitbook 构建的! 因此,增加 .nojekyll 文件禁用 Jekyll 工具,自然不会再受相关语法限制而报错了. 所以,遇到问题时,不仅要多思考,更应该寻求官方人员的帮助,即使不回你,你也要尝试一下! © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:11:13 "},"bigDataWave/":{"url":"bigDataWave/","title":"大数据浪潮之巅:新技术商业制胜之道","keywords":"","body":"大数据浪潮之巅:新技术商业制胜之道 这本书讲述了大数据的发展史,描述了一幅波澜壮阔的宏大场面,有的公司迎难而上抓住机遇,有的公司错失良机失之交臂,看到的不是技术而是一系列的历史事件. 富有趣味性,让人爱不释手,通宵看完意犹未尽,值得推荐,好书! 作者简介 徐飞,著名大数据专家.拥有浙江大学本科学位,美国佛罗里达大学计算机博士学位,研究方向为数据库系统. 从事大数据的基础架构研发 10 余年,先后在微软和 Tableau 等知名公司的大数据核心团队工作.担任过首席架构师. 在国际顶尖会议和杂志上发表论文 10 余篇. 2016 年开通微信公众号\"飞总聊 IT\",并原创\"大数据那些事\"系列文章,系统阐述了大数据发展史,以及发展过程中的种种技术和商业决策案例,受到广泛好评. 内容简介 本书以各个企业在大数据浪潮中跌宕起伏的经历为核心来讲述大数据发展史,并分析各个大数据企业迥异的发展历程,探讨在新技术浪潮来临时应该如何应对. 本书主要分为两部分,前半部分讲述谷歌,微软,IBM,雅虎,亚马逊,阿里巴巴等大公司在大数据浪潮中的发展史,后半部分讲述各个大数据创业公司的发展历程和现状. 在每部分的后面,还通过专门的文章分析并总结了各企业在大数据浪潮中的作为和选择所产生的影响. 全书从公司的视角出发为大家呈现了一幅波澜壮阔的大数据领域发展史,读者不仅可以了解大数据技术,更能领略大数据领域的全貌,从各公司的故事中吸取教训,学习思路. 本书适合对大数据技术和商业思维有兴趣的读者阅读. 目录 谷歌的大数据路: 从拥有\"三驾马车\"到丧失先发优势 谷歌的“三驾马车”开启了大数据时代,然而在这个新时代里,谷歌却丧失了先发优势. 这是为什么呢?我认为是谷歌对待开放架构的态度相对保守导致的. 谷歌的大数据路: 一场影响深远的论战 在大数据发展史上,以迈克尔·斯通布雷克为代表的数据库元老级人物,针对MapReduce向谷歌提出了质疑. 这场著名的论战给整个业界带来了动荡,最后诞生了Spark. 谷歌的大数据路: 谷歌的\"黑科技\" 在大数据的上半场,谷歌以\"三驾马车\"引领时代,但后来因为决策失误丧失了先发优势; 而在大数据的下半场,谷歌带着\"黑科技\"Spanner数据库系统闪亮登场,效果如何呢? 如何读懂类似谷歌\"三驾马车\"这样的技术论文 读懂一篇技术论文,首先需要明白\"论文是写给谁看的\"和\"论文是怎么写出来的\"这两个基本问题,然后就可以有针对性地提升自己阅读论文的功力. 雅虎:大数据领域的\"活雷锋\" 雅虎,这个早已淡出我们视线的公司,却是大数据领域的\"活雷锋\",可以说正是它促成了今天的Hadoop生态圈.这篇文章就来说说它的故事. IBM的大数据路——起早贪黑赶了个晚集 作为历史悠久的计算机公司,IBM早早涉足了大数据领域,最终却只能寄希望于比自己的产品起步还要晚的Spark,我们来看看其中发生了什么. 三大社交媒体公司对Hadoop生态圈的贡献 雅虎把Hadoop开源以后,当时著名的三大社交媒体公司Facebook,LinkedIn和Twitter都加入了这个生态圈,并做出了巨大贡献.Hadoop生态圈给我们的启示是,抱团取暖才是生存之道. 微软的大数据发展史: 微软硅谷研究院 微软硅谷研究院曾经在微软的大数据发展历程中扮演了非常特殊的角色,它推出的Dryad和DryadLINQ可以说是两个另类的产品,虽然未曾大受欢迎,却对大数据的发展有着不可磨灭的贡献. 微软的大数据发展史: 必应的Cosmos Cosmos是微软必应搜索引擎下面的团队开发的大数据基础架构,代表了微软在大数据方面的最高成就. 微软的大数据发展史: Azure的发展 微软大数据发展史上的另一个分支是微软云计算平台下的大数据项目Azure. 这个项目产生了HDInsight,Azure Data Lake,CosmosDB三大平台,但最后只有CosmosDB取得成功. 亚马逊的大数据故事: 从先驱者到一味索取者 在大数据技术发展的早期,亚马逊发表了Dynamo系统的论文,成为和谷歌\"三驾马车\"的论文一样具有深远影响的论文. 然而随着大数据的发展和Hadoop生态圈的建立,亚马逊对大数据圈的贡献极少,但亚马逊自己却从中获得了巨大的利益. 亚马逊的大数据故事: 创新和\"拿来\"并存的云服务 亚马逊不仅在Hadoop生态系统里蓬勃发展,还推出了自己的数据分析产品.这些产品有些是亚马逊自己研发的,有些则只是对开源的产品进行了包装.但是,亚马逊一如既往地没有反哺开源项目. 阿里巴巴的大数据故事: 数据分析平台发展史 国内大数据平台做得最好的公司当属阿里巴巴. 本文就来介绍一下阿里巴巴数据分析平台的发展情况: 数据分析平台的叠加开发. 阿里巴巴的大数据故事: 流计算引擎发展史 在阿里巴巴的发展过程中,流数据处理一直是一项十分重要的技术,阿里巴巴也在这方面做了很多有意义的项目. 本文就来介绍一下阿里巴巴的流计算引擎JStorm与Blink的发展史. 大公司的大数据战略得失: 自建\"轮子\"成本高 大公司的大数据平台可分为两类,一类是自己搭的基础架构(自建\"轮子\"),另一类是抱团取暖所形成的Hadoop生态圈,两者各有利弊.本文将分析第1种情况,主要以谷歌,微软,阿里巴巴自己搭建的大数据平台架构为代表. 大公司的大数据战略得失: 抱团取暖的Hadoop生态圈 除了自建\"轮子\"的公司,其他各大公司走向了一条抱团取暖的道路,就是你搭一个模块,我搭一个模块,大家一起开源出来,最后组成了一个叫作Hadoop的生态圈. 其中有为社区积极做贡献的公司,也有以赚钱为目的的公司,还有一味索取的公司. Hadoop三国之\"魏国\"——Cloudera Hadoop领域曾经有三家发行商互相角逐,其中不乏各种战术与谋略,仔细琢磨,你会发现这三家公司的关系与三国时期的魏蜀吴之间的关系非常相似.本文讲述Hadoop三国之\"魏国\"——Cloudera的故事. Hadoop三国之\"吴国\"——MapR Hadoop三国之\"吴国\"MapR,实力强大却很少参与竞争,这篇文章就来说说它特立独行的故事. Hadoop三国之\"蜀国\"——Hortonworks Hadoop三国之\"蜀国\"Hortonworks始终坚持100%开源,本文讲述它的故事. Hadoop及其发行商的未来 Hadoop已诞生十多年,围绕其生态圈诞生了诸多企业,例如前面讲的社交媒体公司,三大发行商,而亚马逊却最终成为最大的受益者. 文档数据库的缔造者MongoDB(上) MongoDB的诞生像一场意外.它是一个文档型数据库,由10gen公司开发,以易用性闻名. 本文就来讲述MongoDB团队的开发重心,商业运作模式和产品盈利方式. 文档数据库的缔造者MongoDB(下) MongoDB的开发团队一向重视用户体验而不重视核心功能,其负面影响终于以一次安全危机的方式暴露. 加上公司曾经获得具有CIA背景的风投公司的投资,这一并引起了很多人的顾虑. 当然,这一切都挡不住MongoDB公司最终的成功上市. 以MongoDB为例,看基础架构类产品创业 作为一款基础架构类产品,MongoDB以其易用性闻名,然而MongoDB的开发者不注重系统的可靠性,只注重可用性,导致很多MongoDB的用户转向了其他产品. 基础架构类产品的创业者应该如何平衡可用性和可靠性?这是一个值得深思的问题. 直面MongoDB,谈微软的N0SQL战略 2013年,MongoDB在数据库市场中的占有率很高,成为很多创业者和初创企业的首选. 微软究竟做了哪些事情,将Cosmos DB变成能与MongoDB竞争的产品的呢? Palantir: 神秘的大数据独角兽公司 Palantir是一家神秘的大数据创业公司,由硅谷著名投资人彼得·蒂尔创办,其主要服务对象是美国政府部门,特情组织和军队,所以外界对其了解甚少. Splunk: 机器日志数据分析帝国 Splunk是大数据圈里少有的盈利并且蓬勃发展的企业. 它主要服务于机器日志数据分析领域,随后又不断拓展业务,演变开发了若干不同类型的软件. 在本文中我们就来好好聊聊Splunk的进阶史. Confluent: Kafka项目背后的公司 Kafka是LinkedIn开发的开源项目,它主要通过日志文件传输的方式在不同的数据源之间同步数据. 而Confluent公司是Kafka开源项目的创始人离开LinkedIn以后所创立的公司,主要致力于Kafka项目的商业化. 在本文中,我们来讲讲这家公司的故事. Powerset: HBase的\"老东家\" Powerset是一家在多年前被微软收购的创业公司,目前在语义搜索方面开疆拓土. 它为开源社区贡献了BigTable的Hadoop版实现. 本文就来讲讲这家公司的发展史. Cassandra和DataStax公司的故事 Cassandra是开源社区仿照Amazon Dynamo开发的产品,它最初由Facebook开发并开源,却又被公司内部弃用. 创业公司DataStax对Cassandra大力支持,造就了今日繁荣的Cassandra社区。 Databricks: Spark的数据\"金砖\"王国 Spark是Hadoop生态圈里大红大紫的项目,它甚至取代了Hadoop MapReduce的地位. Databricks是对这个项目进行商业化的企业. 本文就来聊聊这家企业的故事. Data Artisans和浴火重生的新一代大数据计算引擎Flink Data Artisans是对Flink进行商业化的公司. Apache Flink是一个年轻的新型处理引擎,是Hadoop社区里Spark的主要竞争对手. Flink设计理念先进,但是工程实现方面相对落后. Dremio: 基于Drill和Arrow的大数据公司 Dremio是另外一家大数据创业公司,其创始人是从MapR公司跳槽出来的. Dremio的主要产品就是Dremio项目,它吸收了MapR主导的开源项目Drill的精华,以开源项目Arrow为核心开发. 本文就来讲讲Dremio公司和Dremio平台的来龙去脉. Imply: 基于Druid的大数据分析公司 开源大数据项目Druid由Metamarkets开发. 开始时籍籍无名,后来被一些大公司,尤其是Airbnb使用和推广以后,受到了很多关注. Kyligence: 麒麟背后的大数据公司 麒麟(英文名字是Kylin)是第1个全部由中国人主导的Apache顶级开源项目,Kyligence则是对这个项目进行商业化的公司. 本文就来看看麒麟和Kyligence的故事. Snowflake: 云端的弹性数据仓库 Snowflake是一个构建在云端的弹性数据仓库,它背后的公司与之同名. Snowflake公司的创始人和管理层都有强大的背景,本文就来讲一下Snowflake及其公司的故事. TiDB:一个国产新数据库的创业故事 TiDB是位于北京的一家创业公司PingCAP的产品,它的目标是实现一个开源的类似谷歌Spanner的系统,这个产品非常有特色,本文就来聊聊TiDB和它背后的公司. 大数据创业公司的前景: 红海vs.蓝海 关于创业的市场,通常有红海和蓝海的说法,蓝海容易成功,红海相对艰难. 对大数据创业公司来说,蓝海多半指的是应用软件类的市场,而红海指的则是基础架构软件类的市场. 本文将对比分析一下这两类市场. 如何通过分析企业的技术积累来判断其发展前景 通过分析企业的技术积累,能够有效地判断企业的发展前景如何. 我们需要关注三个方面: 技术适用的场景是否有巨大的盈利空间,技术本身是否有领先和独到之处,以及技术的积累是否足够深和广. © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:07:55 "},"other/":{"url":"other/","title":"其他","keywords":"","body":"其他 © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:10:16 "},"other/transformation.html":{"url":"other/transformation.html","title":"程序员转型之路","keywords":"","body":"程序员转型之路 视频来自腾讯课堂: 程序员转型之路 如何学习?什么叫理财? 01.如何正确认识学习? 知识: 靠记忆 (20%) 技能: 靠练习 (30%) 态度: 靠发心 (50%) 纸上谈兵一万次,不如战场来一遍 02.谈转型 graph LR product((产品)) -.- management((管理)) management -.- freelance((自由职业)) freelance -.- market((市场)) market -.- entrepreneurship((创业)) 03.如何更高效学习与运用 保持持续性学习 学会跳出自己的\"舒适区\" 思维转变,换个角度看世界 04.学习思路 graph LR empty_cup_mentality[空杯心态] --- closing_comments[关闭评论] closing_comments --- know_and_use[知用合一] know_and_use --- timely_output[及时输出] 如何让自己快速拥有10万+流量,并且快速变现 各大平台特点 抖音 今日头条 微信公众号 知乎 新浪博客 简书 大鱼号 百家号 搜狐自媒体 熊掌号 一点资讯 抖音分为商家版和个人版,成立专门团队,越专业越成功! 到底如何写内容 标题的重要性 字数在20字以内 出乎意料 带数字 给人感觉像故事或干货 人格化定位 引导转发和分享 如何把流量快速变现 如何写出可以直接收钱的文案 如何30分钟学会演讲,客服种种心里障碍 如何复制转型成功的程序员称为自有职业者,月收入是5万+ © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:10:20 "},"other/static.html":{"url":"other/static.html","title":"历史统计","keywords":"","body":"历史统计 全网统计 c3.generate({\"bindto\":\"#plugin-chart-6\",\r \"data\": {\r \"x\": \"x\",\r \"columns\": [\r [\r \"x\",\r \"2019-03-31\",\r \"2019-04-30\",\r \"2019-05-31\",\r \"2019-06-30\",\r \"2019-07-31\"\r ],\r [\r \"粉丝\",\r 110,\r 115,\r 115,\r 116,\r 134\r ],\r [\r \"阅读\",\r 49453,\r 53447,\r 53829,\r 54106,\r 71422 \r ]\r ],\r \"axes\": {\r \"粉丝\": \"y2\"\r },\r \"types\": {\r \"粉丝\": \"bar\"\r }\r },\r \"axis\": {\r \"x\": {\r \"type\": \"timeseries\",\r \"tick\": {\r \"format\": \"%Y-%m-%d\"\r }\r },\r \"y2\": {\r \"show\": \"true\",\r \"label\": {\r \"text\": \"粉丝\",\r \"position\": \"outer-middle\"\r }\r }\r }\r }); 慕课手记 慕课手记 : https://www.imooc.com/u/5224488/articles c3.generate({\"bindto\":\"#plugin-chart-7\",\r \"data\": {\r \"x\": \"x\",\r \"columns\": [\r [\r \"x\",\r \"2019-03-31\",\r \"2019-04-30\",\r \"2019-05-31\",\r \"2019-06-30\",\r \"2019-07-31\"\r ],\r [\r \"粉丝\",\r 18,\r 18,\r 18,\r 18,\r 19\r ],\r [\r \"阅读量\",\r 16448,\r 18037,\r 18215,\r 18313,\r 24293\r ],\r [\r \"手记\",\r 61,\r 61,\r 61,\r 61,\r 64\r ],\r [\r \"推荐\",\r 78,\r 79,\r 79,\r 79,\r 85\r ],\r [\r \"积分\",\r 334,\r 341,\r 342,\r 342,\r 421\r ]\r ],\r \"axes\": {\r \"粉丝\": \"y2\"\r },\r \"types\": {\r \"粉丝\": \"bar\"\r }\r },\r \"axis\": {\r \"x\": {\r \"type\": \"timeseries\",\r \"tick\": {\r \"format\": \"%Y-%m-%d\"\r }\r },\r \"y2\": {\r \"show\": \"true\",\r \"label\": { \r \"text\": \"粉丝\",\r \"position\": \"outer-middle\"\r }\r } \r }\r }); 简书 简书 : https://www.jianshu.com/u/577b0d76ab87 c3.generate({\"bindto\":\"#plugin-chart-8\",\r \"data\": {\r \"x\": \"x\",\r \"columns\": [\r [\r \"x\",\r \"2019-03-31\",\r \"2019-04-30\",\r \"2019-05-31\",\r \"2019-06-30\",\r \"2019-07-31\"\r ],\r [\r \"粉丝\",\r 21,\r 24,\r 24,\r 24,\r 28\r ],\r [\r \"阅读量\",\r 2825,\r 3242,\r 3287,\r 3317,\r 4869\r ],\r [\r \"文章\",\r 61,\r 61,\r 61,\r 61,\r 64\r ],\r [\r \"喜欢\",\r 121,\r 125,\r 126,\r 127,\r 135\r ],\r [\r \"简书钻\",\r 107,\r 24,\r 24,\r 24,\r 26\r ]\r ],\r \"axes\": {\r \"粉丝\": \"y2\"\r },\r \"types\": {\r \"粉丝\": \"bar\"\r }\r },\r \"axis\": {\r \"x\": {\r \"type\": \"timeseries\",\r \"tick\": {\r \"format\": \"%Y-%m-%d\"\r }\r },\r \"y2\": {\r \"show\": \"true\",\r \"label\": { \r \"text\": \"粉丝\",\r \"position\": \"outer-middle\"\r }\r }\r }\r }); CSDN CSDN : https://blog.csdn.net/weixin_38171180 c3.generate({\"bindto\":\"#plugin-chart-9\",\r \"data\": {\r \"x\": \"x\",\r \"columns\": [\r [\r \"x\",\r \"2019-03-31\",\r \"2019-04-30\",\r \"2019-05-31\",\r \"2019-06-30\",\r \"2019-07-31\"\r ],\r [\r \"粉丝\",\r 0,\r 0,\r 0,\r 0,\r 0\r ],\r [\r \"访问量\",\r 2872,\r 2846,\r 2858,\r 2858,\r 3306\r ],\r [\r \"原创\",\r 59,\r 60,\r 60,\r 60,\r 62\r ],\r [\r \"喜欢\",\r 55,\r 55,\r 55,\r 55,\r 59\r ], \r [\r \"积分\",\r 669,\r 669,\r 669,\r 669,\r 706\r ]\r ],\r \"axes\": {\r \"粉丝\": \"y2\"\r },\r \"types\": {\r \"粉丝\": \"bar\"\r }\r },\r \"axis\": {\r \"x\": {\r \"type\": \"timeseries\",\r \"tick\": {\r \"format\": \"%Y-%m-%d\"\r }\r },\r \"y2\": {\r \"show\": \"true\",\r \"label\": {\r \"text\": \"粉丝\",\r \"position\": \"outer-middle\"\r }\r }\r }\r }); 博客园 博客园 : https://www.cnblogs.com/snowdreams1006/ c3.generate({\"bindto\":\"#plugin-chart-10\",\r \"data\": {\r \"x\": \"x\",\r \"columns\": [\r [\r \"x\",\r \"2019-03-31\",\r \"2019-04-30\",\r \"2019-05-31\",\r \"2019-06-30\",\r \"2019-07-31\"\r ],\r [\r \"粉丝\",\r 21,\r 21,\r 21,\r 21,\r 21\r ],\r [\r \"阅读数\",\r 7756,\r 8256,\r 8298,\r 8329,\r 11240\r ],\r [\r \"随笔\",\r 61,\r 62,\r 62,\r 62,\r 64\r ]\r ],\r \"axes\": {\r \"粉丝\": \"y2\"\r },\r \"types\": {\r \"粉丝\": \"bar\"\r }\r },\r \"axis\": {\r \"x\": {\r \"type\": \"timeseries\",\r \"tick\": {\r \"format\": \"%Y-%m-%d\"\r }\r },\r \"y2\": {\r \"show\": \"true\",\r \"label\": {\r \"text\": \"粉丝\",\r \"position\": \"outer-middle\"\r }\r }\r }\r }); 掘金 掘金 : https://juejin.im/user/582d5cb667f356006331e586 c3.generate({\"bindto\":\"#plugin-chart-11\",\r \"data\": {\r \"x\": \"x\",\r \"columns\": [\r [\r \"x\",\r \"2019-03-31\",\r \"2019-04-30\",\r \"2019-05-31\",\r \"2019-06-30\",\r \"2019-07-31\"\r ],\r [\r \"关注者\",\r 10,\r 10,\r 10,\r 10,\r 15\r ],\r [\r \"阅读数\",\r 1885,\r 2086,\r 2093,\r 2105,\r 3246\r ],\r [\r \"专栏\",\r 60,\r 60,\r 60,\r 60,\r 63\r ],\r [\r \"点赞数\",\r 89,\r 89,\r 90,\r 91,\r 107\r ],\r [\r \"掘力值\",\r 106,\r 109,\r 110,\r 110,\r 138\r ]\r ],\r \"axes\": {\r \"关注者\": \"y2\"\r },\r \"types\": {\r \"关注者\": \"bar\"\r }\r },\r \"axis\": {\r \"x\": {\r \"type\": \"timeseries\",\r \"tick\": {\r \"format\": \"%Y-%m-%d\"\r }\r },\r \"y2\": {\r \"show\": \"true\",\r \"label\": {\r \"text\": \"关注者\",\r \"position\": \"outer-middle\"\r }\r }\r }\r }); 思否 思否 : https://segmentfault.com/blog/snowdreams1006 c3.generate({\"bindto\":\"#plugin-chart-12\",\r \"data\": {\r \"x\": \"x\",\r \"columns\": [\r [\r \"x\",\r \"2019-03-31\",\r \"2019-04-30\",\r \"2019-05-31\",\r \"2019-06-30\",\r \"2019-07-31\"\r ],\r [\r \"粉丝\",\r 2,\r 2,\r 2,\r 2,\r 3\r ],\r [\r \"阅读量\",\r 6063,\r 6980,\r 7073,\r 7156,\r 10591\r ],\r [\r \"内容数\",\r 62,\r 62,\r 62,\r 62,\r 65\r ],\r [\r \"点赞数\",\r 23,\r 23,\r 23,\r 23,\r 27\r ],\r [\r \"声望\",\r 91,\r 91,\r 91,\r 91,\r 97\r ]\r ],\r \"axes\": {\r \"粉丝\": \"y2\"\r },\r \"types\": {\r \"粉丝\": \"bar\"\r }\r },\r \"axis\": {\r \"x\": {\r \"type\": \"timeseries\",\r \"tick\": {\r \"format\": \"%Y-%m-%d\"\r }\r },\r \"y2\": {\r \"show\": \"true\",\r \"label\": {\r \"text\": \"粉丝\",\r \"position\": \"outer-middle\"\r }\r }\r }\r }); 开源中国 开源中国 : https://my.oschina.net/snowdreams1006 c3.generate({\"bindto\":\"#plugin-chart-13\",\r \"data\": {\r \"x\": \"x\",\r \"columns\": [\r [\r \"x\",\r \"2019-03-31\",\r \"2019-04-30\",\r \"2019-05-31\",\r \"2019-06-30\",\r \"2019-07-31\"\r ],\r [\r \"粉丝\",\r 17,\r 17,\r 17,\r 17,\r 17\r ],\r [\r \"访问量\",\r 6307,\r 6374,\r 6376,\r 6383,\r 6593\r ],\r [\r \"博文\",\r 61,\r 61,\r 61,\r 61,\r 64\r ],\r [\r \"推荐\",\r 13,\r 13,\r 13,\r 13,\r 13\r ],\r [\r \"积分\",\r 13,\r 13,\r 13,\r 13,\r 13\r ]\r ],\r \"axes\": {\r \"粉丝\": \"y2\"\r },\r \"types\": {\r \"粉丝\": \"bar\"\r }\r },\r \"axis\": {\r \"x\": {\r \"type\": \"timeseries\",\r \"tick\": {\r \"format\": \"%Y-%m-%d\"\r }\r },\r \"y2\": {\r \"show\": \"true\",\r \"label\": {\r \"text\": \"粉丝\",\r \"position\": \"outer-middle\"\r }\r }\r }\r }); 腾讯云社区 腾讯云社区 : https://cloud.tencent.com/developer/user/2952369/activities c3.generate({\"bindto\":\"#plugin-chart-14\",\r \"data\": {\r \"x\": \"x\",\r \"columns\": [\r [\r \"x\",\r \"2019-03-31\",\r \"2019-04-30\",\r \"2019-05-31\",\r \"2019-06-30\",\r \"2019-07-31\"\r ],\r [\r \"粉丝\",\r 13,\r 13,\r 13,\r 13,\r 13\r ],\r [\r \"阅读量\",\r 4809,\r 5062,\r 5065,\r 5076,\r 6324\r ],\r [\r \"文章\",\r 62,\r 62,\r 62,\r 62,\r 67\r ],\r [\r \"点赞\",\r 199,\r 199,\r 199,\r 199,\r 215\r ],\r [\r \"订阅\",\r 11,\r 12,\r 12,\r 12,\r 12\r ]\r ],\r \"axes\": {\r \"粉丝\": \"y2\"\r },\r \"types\": {\r \"粉丝\": \"bar\"\r }\r },\r \"axis\": {\r \"x\": {\r \"type\": \"timeseries\",\r \"tick\": {\r \"format\": \"%Y-%m-%d\"\r }\r },\r \"y2\": {\r \"show\": \"true\",\r \"label\": {\r \"text\": \"粉丝\",\r \"position\": \"outer-middle\"\r }\r }\r }\r }); B站专栏 B站专栏 : https://member.bilibili.com/v2#/upload-manager/text c3.generate({\"bindto\":\"#plugin-chart-15\",\r \"data\": {\r \"x\": \"x\",\r \"columns\": [\r [\r \"x\",\r \"2019-03-31\",\r \"2019-04-30\",\r \"2019-05-31\",\r \"2019-06-30\",\r \"2019-07-31\"\r ],\r [\r \"粉丝\",\r 10,\r 10,\r 10,\r 11,\r 18\r ],\r [\r \"阅读量\",\r 490,\r 564,\r 564,\r 569,\r 960\r ],\r [\r \"投稿\",\r 56,\r 56,\r 56,\r 56,\r 59\r ],\r [\r \"点赞\",\r 65,\r 65,\r 65,\r 65,\r 70\r ],\r [\r \"收藏\",\r 16,\r 22,\r 22,\r 23,\r 35\r ],\r [\r \"投币\",\r 1,\r 1,\r 1,\r 1,\r 1\r ]\r ],\r \"axes\": {\r \"粉丝\": \"y2\"\r },\r \"types\": {\r \"粉丝\": \"bar\"\r }\r },\r \"axis\": {\r \"x\": {\r \"type\": \"timeseries\",\r \"tick\": {\r \"format\": \"%Y-%m-%d\"\r }\r },\r \"y2\": {\r \"show\": \"true\",\r \"label\": {\r \"text\": \"粉丝\",\r \"position\": \"outer-middle\"\r }\r }\r }\r }); © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:10:20 "},"other/me.html":{"url":"other/me.html","title":"关于作者","keywords":"","body":"关于作者 申请认证慕课网作者 个人简介 慕课网昵称是雪之梦技术驿站,真实姓名孙坡,目前工作于浙江省宁波市慈溪市,16年本科毕业距今近3年,就职于宁波米联物联科技有限公司(慈溪赛科软件有限公司)公司担任java 后台软件开发工程师,负责最物流系列产品研发管理. 联系方式 微信: vae5731 (枫林无归) 开发或者设计经验 入职初期主要负责手机 App 开发,采用 Dcloud 公司的Html5+ 技术实现一套代码跨平台运行,包括 android 和 ios 两大主流平台. 入门前端开发后逐步向后端开发方向演进,由最初单纯为手机端编写 api 接口到搭建后台项目,再到独当一面重新设计后台框架,逐步演变成全栈工程师. 作为前端工程师: 擅长 js,css,html5 等前端基础,熟练使用 vue.js,webpack 等技术栈,能够独立编写跨多端运行的 app. 作为后端工程师: 擅长 java 技术栈,了解 php 和 go 等其他后端语言,熟练使用 ssm 框架完成企业级项目开发流程,同时熟悉 springboot 和 springcloud 等最新技术. 去年开始,致力于项目框架的升级重构,由原来的 ssm 组合重构成 springboot 自由搭配第三方框架,实现技术迭代的大跨步,引入一系列的新功能特定弥补了旧框架的不足,简化了开发流程,提高了开发效率,经过一年多的努力,框架已经成熟,目前已推广到全公司各个项目团队一起使用,进入版本升级维护阶段! 前端和后端的同时推进让我具备一定的全栈开发能力,系统的重构为我的下一次架构师设定了目标. 写作经验介绍 今年3月份刚刚打算尝试写作,主要想整理下这一段时间的收获和自己的一些思考,当时刚巧看到简书有日更活动便开始了写作,今天刚好是日更的第20天,因此暂时没有取得比较大的成就. 主要的投稿平台的历史统计如下(每天更新): 简书 : 0粉丝,24文章,23850字数,27收获喜欢,24简书钻. CSDN : 23原创,1粉丝,23喜欢,0评论,1151访问,255积分,36万+排名. 博客园 : 22随笔,0文章,10粉丝,14评论. 掘金 : 21原创,4关注者,25点赞数,361阅读数,0评论数. 开源中国 : 22博文,2推荐,4粉丝,2积分,34访问. segmentfault : 22文章,0粉丝,11声望,747阅读量,3点赞数. 慕课手记 : 21手记,41835经验,19积分,2粉丝. B站专栏 : 86阅读量+4,0评论0,20点赞0,3收藏+0,0投币0. 简书 基本上没什么阅读量,偶尔一两个阅读点赞,创作的动力在于日更活动督促自己不要断更,还有就是每天的简书钻收益提醒见证自己的成长. csdn 阅读量稳定,积累到一定数量的原创文章后可开通专栏,创作动力在于搜索引擎优化很多,增添自信. 博客园 默认自动发表到首页但会有审核,一旦质量不过关随时可能被移出首页,并且有推荐和反对两种方式,保证了质量的同时,带来了一两百的阅读量,粉丝增长明显,已经10个粉丝啦! 掘金 markdown 编辑器很漂亮,页面风格像是朋友圈一样,比简书一个等级但稍微好点,4 个粉丝. 开源中国 文章质量普遍较高,刚开始没自信自荐故而无缘首页,后来见其他平台的几百阅读量还不错,因此鼓起勇气自荐. 一旦自荐通过审核,上了博客首页,阅读量至少上百,而且还能顺便吸引几个粉丝和收藏,目前保持在两天一推荐. 思否 和掘金一个等级,重点是技术问答社区,专栏文章被收藏后会增加威望值,目前3篇被收藏. 慕课手记 零起点入门教程,生于斯死于斯,和csdn一个等级,没它seo做得好,粉丝增加了2个呢! B站专栏 纯属意外发现B站竟然有专栏,毕竟不是做技术的就不苛刻了,不支持markdown,阅读量和掘金,思否一个等级,至今没有粉丝. 微信公众号 闭环没流量,没必要做统计,权当自娱自乐了! 以上平台的账号几乎都是雪之梦技术驿站(snowdreams1006) 个人主页 昵称: 雪之梦技术驿站 主页: 5224488 © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:10:20 "},"other/donate.html":{"url":"other/donate.html","title":"捐赠支持","keywords":"","body":"捐赠支持 © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:10:16 "},"todo/":{"url":"todo/","title":"TODO清单","keywords":"","body":"TODO清单 nodejs-操作文件系统读取写入文件 10分钟教你撸一个nodejs爬虫系统 NodeJs+Request+Cheerio 采集数据 nodejs模拟登录-request模块 GitHub Wiki 页面的添加和设置 Kingfisher © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-08-17 14:10:27 "},"GLOSSARY.html":{"url":"GLOSSARY.html","keywords":"","body":"markdown 简洁优雅的排版语言,简化版的 HTML,加强版的 TXT,详情请参考 https://snowdreams1006.github.io/markdown/ git 分布式版本控制系统,详情请参考 https://snowdreams1006.github.io/git/ © snowdreams1006 all right reserved,powered by Gitbook文件修订时间: 2019-04-28 10:07:12 "}}