大呜

大呜 查看完整档案

深圳编辑  |  填写毕业院校  |  填写所在公司/组织 www.texixi.com 编辑
编辑

放下杂念,幸福天天

个人动态

大呜 发布了文章 · 2019-03-25

Terry-Ye/im 系统使用zookeeper

项目地址

https://github.com/Terry-Ye/im

Terry-Ye/im 基本流程说明

以下主要是给没用过zookeeper的说明应用场景
图片描述

例上图:logic层如因业务发展需要扩展web机器,新增的机器可以直接启动logic 服务,向zookeeper注册。
而comet 层不需要修改任何配置信息,comet层rpc 调用logic层时,zookeeper 还可以充当负载均衡的角色,获取其中logic层的一台机器给comet层调用。

注:Terry-Ye/im 系统里面,使用的rpc服务支持zookeeper

zookeeper背景

https://www.hollischuang.com/...

优缺点

  1. 优点 http://www.chaozh.com/whats-g...
  2. 缺点 https://www.sohu.com/a/234858...

设计特点与适用场景

https://www.cnblogs.com/qingy...

技术的原理和关键实现

https://www.cnblogs.com/qingy...

ZooKeeper、Eureka对比

https://www.cnblogs.com/jieqi...

mac 可视化工具

zooInspector https://blog.csdn.net/uisoul/...

查看原文

赞 0 收藏 0 评论 0

大呜 评论了文章 · 2019-01-29

题库分库分表架构方案

个人博客地址 https://www.texixi.com/2019/0...

方案

项目背景

在现在题库架构下,针对新购买的1300W多道数据进行整合,不影响现有功能。由于数据量偏多,需要进行数据的切分

目标场景

  1. 兼容旧的功能
  2. 对1300多W数据进行分库分表
  3. 需要对旧的数据进行整合
  4. 老师端选题组卷 可以根据 学段、学科、知识点、难度、题型 来筛选
  5. 学生端根据老师端所选题目获取对应的题目
  6. 对3年内以后扩展的增量数据预留数量空间

数据样例

学段数据量
小学1285336
初中6655780
高中6144072
学段学科数据量
初中数学1869524
初中化学1356224
初中英语288440

切分方案一

  1. 切分为3个库, 分别是小学、初中、高中 数据占比如上
  2. 每个库切分10个表 根据 (学科+首级知识点)%10
  3. 每个库一个总表

缺点:例:用到不同知识点时,需要多表获取数据

优点:数据分布较为平均

切分方案二 (采用)

  1. 切分为3个库, 分别是小学、初中、高中 数据占比如上
  2. 每个库切分10个表(全部10个学科) 根据 学科区分, 例: 数学表、物理表
  3. 每个库一个总表

缺点:数据不大平均, 数据量多的例数学有186W多、英语28W多

优点:当有用到组卷等需要筛选多知识点题目时,不用多表查询

数据id 自增区间划分

  1. 小学 1-2亿
  2. 中学 2-3亿
  3. 高中 3亿起

关联关系图

图片描述

根据知识点获取题目流程

图片描述

自增id

  1. 对原有的id区间段不做处理
  2. 对切分后的id自增段进行规划

兼容旧功能

解决的问题
  1. 新旧数据有重复的知识点、题目
  2. 新旧数据的结构不一样
  3. 对旧的题库功能代码的修改
  4. 两套题库合并主键冲突问题
兼容旧功能 方案一 (个人推荐)
  1. 有操作的旧的数据洗入新的结构,旧的数据只为兼容原有的功能数据,不做显示。

优点:
不用变动数据结构,最新的购买的数据结构较为清晰。 易维护扩展,因为目前旧的数据已经整合了两套数据

缺点:
需要修改全部旧有的功能代码(针对新的数据结构)

兼容旧功能 方案二

  1. 把新购买的数据整合进老的数据结构,同时保留三批数据,需要处理所有表的主键冲突、三批各表数据去重

优点:

  1. 旧有代码只修改数据结构切分的部分,不用全部修改功能代码

缺点:

  1. 数据较乱,三套不同的数据同时存在数据库
  2. 需要处理新的结构整合进旧的数据结构,同时需要处理主键冲突,
  3. 代码上需要处理对应的数据

问题点

  1. 测试环境和正式环境图片存放在那里?100多G,上传cdn需要几十天时间,有4000多W张,目前cdn不支持打包上传

解决方案:购买单独服务器,主备,存放图片

  1. 测试db 正式db 1300多w 目前占用100G左右, 需要存放空间

解决方案:测试环境新加硬盘,新加db实例端口3307,正式环境db存放在图片服务器

代码设计模式

  1. 采用适配器模式(原先的代码结构不变)
  2. 类图

调研内容

中间件MYCAT(未使用)

什么是MYCAT
  1. 一个彻底开源的,面向企业应用开发的大数据库集群
  2. 支持事务、ACID、可以替代MySQL的加强版数据库
  3. 一个可以视为MySQL集群的企业级数据库,用来替代昂贵的Oracle集群
  4. 一个融合内存缓存技术、NoSQL技术、HDFS大数据的新型SQL Server
  5. 结合传统数据库和新型分布式数据仓库的新一代企业级数据库产品
  6. 一个新颖的数据库中间件产品
MYCAT特性
  1. ==支持库内分表(1.6)==
  2. ==支持单库内部任意join,支持跨库2表join,甚至基于caltlet的多表join==
  3. 支持全局序列号,解决分布式下的主键生成问题。
  4. ==分片规则丰富==,插件化开发,易于扩展。
  5. 基于Nio实现,有效管理线程,解决高并发问题。
  6. ==支持通过全局表,ER关系的分片策略,实现了高效的多表join查询==
  7. 支持分布式事务(弱xa)。
  8. 支持SQL黑名单、sql注入攻击拦截
  9. ==支持MySQL、Oracle、DB2、SQL Server、PostgreSQL等DB的常见SQL语法==
  10. ==遵守Mysql原生协议==,跨语言,跨平台,跨数据库的通用中间件代理。
  11. ==基于心跳的自动故障切换,支持读写分离,支持MySQL主从,==以及galera cluster集群。
  12. 可以大幅降低开发难度,提升开发速度
  13. 具体看 mycat 官网
Mycat 注意事项
  1. 全局表一致性检测 1.6版本开始支持(一致性的定时检测)
  2. 分片 join(尽量避免使用 Left join 或 Right join,而用 Inner join)
Mycat 原理
  1. 应用要面对很多个数据库的时候,这个时候就需要对数据库层做一个抽象,来管理这些数据库,而最上面的应用只需要面对一个数据库层的抽象或者说数据库中间件就好了,这就是Mycat的核心作用。
  2. 分片分析、路由分析、读写分离分析、缓存分析等,然后将此SQL发往后端的真实数据库,并将返回的结果做适当的处理,最终再返回给用户。
Mycat 应用场景
  1. 读写分离,配置简单
  2. 分表分库,对于超过1000万的表进行分片,最大支持1000亿的单表分片
  3. 报表系统,借助于Mycat的分表能力,处理大规模报表的统计
文章整理
  1. 应用场景 那些适合,那些不适合 https://www.cnblogs.com/barry...
  2. 使用说明 https://juejin.im/post/59c325...

总表使用mysql MERGE 引擎(不考虑)

  1. 合并的表使用的必须是MyISAM引擎
  2. 表的结构必须一致,包括索引、字段类型、引擎和字符集
  3. 对于增删改查,直接操作总表即可。

数据切分原则

  1. 能不切分尽量不要切分。
  2. 如果要切分一定要选择合适的切分规则,提前规划好。
  3. 数据切分尽量通过数据冗余或表分组(Table Group)来降低跨库 Join 的可能。
  4. 由于数据库中间件对数据 Join 实现的优劣难以把握,而且实现高性能难度极大,业务读取尽量少使用多表 Join。
  5. 尽可能的比较均匀分布数据到各个节点上
  6. 该业务字段是最频繁的或者最重要的查询条件。
查看原文

大呜 发布了文章 · 2019-01-29

题库分库分表架构方案

个人博客地址 https://www.texixi.com/2019/0...

方案

项目背景

在现在题库架构下,针对新购买的1300W多道数据进行整合,不影响现有功能。由于数据量偏多,需要进行数据的切分

目标场景

  1. 兼容旧的功能
  2. 对1300多W数据进行分库分表
  3. 需要对旧的数据进行整合
  4. 老师端选题组卷 可以根据 学段、学科、知识点、难度、题型 来筛选
  5. 学生端根据老师端所选题目获取对应的题目
  6. 对3年内以后扩展的增量数据预留数量空间

数据样例

学段数据量
小学1285336
初中6655780
高中6144072
学段学科数据量
初中数学1869524
初中化学1356224
初中英语288440

切分方案一

  1. 切分为3个库, 分别是小学、初中、高中 数据占比如上
  2. 每个库切分10个表 根据 (学科+首级知识点)%10
  3. 每个库一个总表

缺点:例:用到不同知识点时,需要多表获取数据

优点:数据分布较为平均

切分方案二 (采用)

  1. 切分为3个库, 分别是小学、初中、高中 数据占比如上
  2. 每个库切分10个表(全部10个学科) 根据 学科区分, 例: 数学表、物理表
  3. 每个库一个总表

缺点:数据不大平均, 数据量多的例数学有186W多、英语28W多

优点:当有用到组卷等需要筛选多知识点题目时,不用多表查询

数据id 自增区间划分

  1. 小学 1-2亿
  2. 中学 2-3亿
  3. 高中 3亿起

关联关系图

图片描述

根据知识点获取题目流程

图片描述

自增id

  1. 对原有的id区间段不做处理
  2. 对切分后的id自增段进行规划

兼容旧功能

解决的问题
  1. 新旧数据有重复的知识点、题目
  2. 新旧数据的结构不一样
  3. 对旧的题库功能代码的修改
  4. 两套题库合并主键冲突问题
兼容旧功能 方案一 (个人推荐)
  1. 有操作的旧的数据洗入新的结构,旧的数据只为兼容原有的功能数据,不做显示。

优点:
不用变动数据结构,最新的购买的数据结构较为清晰。 易维护扩展,因为目前旧的数据已经整合了两套数据

缺点:
需要修改全部旧有的功能代码(针对新的数据结构)

兼容旧功能 方案二

  1. 把新购买的数据整合进老的数据结构,同时保留三批数据,需要处理所有表的主键冲突、三批各表数据去重

优点:

  1. 旧有代码只修改数据结构切分的部分,不用全部修改功能代码

缺点:

  1. 数据较乱,三套不同的数据同时存在数据库
  2. 需要处理新的结构整合进旧的数据结构,同时需要处理主键冲突,
  3. 代码上需要处理对应的数据

问题点

  1. 测试环境和正式环境图片存放在那里?100多G,上传cdn需要几十天时间,有4000多W张,目前cdn不支持打包上传

解决方案:购买单独服务器,主备,存放图片

  1. 测试db 正式db 1300多w 目前占用100G左右, 需要存放空间

解决方案:测试环境新加硬盘,新加db实例端口3307,正式环境db存放在图片服务器

代码设计模式

  1. 采用适配器模式(原先的代码结构不变)
  2. 类图

调研内容

中间件MYCAT(未使用)

什么是MYCAT
  1. 一个彻底开源的,面向企业应用开发的大数据库集群
  2. 支持事务、ACID、可以替代MySQL的加强版数据库
  3. 一个可以视为MySQL集群的企业级数据库,用来替代昂贵的Oracle集群
  4. 一个融合内存缓存技术、NoSQL技术、HDFS大数据的新型SQL Server
  5. 结合传统数据库和新型分布式数据仓库的新一代企业级数据库产品
  6. 一个新颖的数据库中间件产品
MYCAT特性
  1. ==支持库内分表(1.6)==
  2. ==支持单库内部任意join,支持跨库2表join,甚至基于caltlet的多表join==
  3. 支持全局序列号,解决分布式下的主键生成问题。
  4. ==分片规则丰富==,插件化开发,易于扩展。
  5. 基于Nio实现,有效管理线程,解决高并发问题。
  6. ==支持通过全局表,ER关系的分片策略,实现了高效的多表join查询==
  7. 支持分布式事务(弱xa)。
  8. 支持SQL黑名单、sql注入攻击拦截
  9. ==支持MySQL、Oracle、DB2、SQL Server、PostgreSQL等DB的常见SQL语法==
  10. ==遵守Mysql原生协议==,跨语言,跨平台,跨数据库的通用中间件代理。
  11. ==基于心跳的自动故障切换,支持读写分离,支持MySQL主从,==以及galera cluster集群。
  12. 可以大幅降低开发难度,提升开发速度
  13. 具体看 mycat 官网
Mycat 注意事项
  1. 全局表一致性检测 1.6版本开始支持(一致性的定时检测)
  2. 分片 join(尽量避免使用 Left join 或 Right join,而用 Inner join)
Mycat 原理
  1. 应用要面对很多个数据库的时候,这个时候就需要对数据库层做一个抽象,来管理这些数据库,而最上面的应用只需要面对一个数据库层的抽象或者说数据库中间件就好了,这就是Mycat的核心作用。
  2. 分片分析、路由分析、读写分离分析、缓存分析等,然后将此SQL发往后端的真实数据库,并将返回的结果做适当的处理,最终再返回给用户。
Mycat 应用场景
  1. 读写分离,配置简单
  2. 分表分库,对于超过1000万的表进行分片,最大支持1000亿的单表分片
  3. 报表系统,借助于Mycat的分表能力,处理大规模报表的统计
文章整理
  1. 应用场景 那些适合,那些不适合 https://www.cnblogs.com/barry...
  2. 使用说明 https://juejin.im/post/59c325...

总表使用mysql MERGE 引擎(不考虑)

  1. 合并的表使用的必须是MyISAM引擎
  2. 表的结构必须一致,包括索引、字段类型、引擎和字符集
  3. 对于增删改查,直接操作总表即可。

数据切分原则

  1. 能不切分尽量不要切分。
  2. 如果要切分一定要选择合适的切分规则,提前规划好。
  3. 数据切分尽量通过数据冗余或表分组(Table Group)来降低跨库 Join 的可能。
  4. 由于数据库中间件对数据 Join 实现的优劣难以把握,而且实现高性能难度极大,业务读取尽量少使用多表 Join。
  5. 尽可能的比较均匀分布数据到各个节点上
  6. 该业务字段是最频繁的或者最重要的查询条件。
查看原文

赞 35 收藏 25 评论 4

大呜 发布了文章 · 2018-12-19

纯golang im即时通讯系统(支持分布式)

简介

纯go实现的im即时通讯系统,各层可单独部署,之间通过rpc通讯,支持集群,github地址 https://github.com/Terry-Ye/im , 学习于goim, 总分三层,

  1. comet(用户连接层),可以直接部署多个节点,每个节点保证serverId 唯一,在配置文件comet.toml
  2. logic(业务逻辑层),无状态,各层通过rpc通讯,容易扩展,支持http接口来接收消息
  3. job(任务推送层)通过redsi 订阅发布功能进行推送到comet层。

系统架构图

图片描述

时序图

以下Comet 层,Logic 层,Job层都可以灵活扩展机器

图片描述

特性

  1. 分布式,可拓扑的架构
  2. 支持单个,房间推送
  3. 心跳支持(gorilla/websocket内置)
  4. 基于redis 做消息推送
  5. 轻量级
  6. 持续迭代...

部署

  1. 安装
go get -u github.com/Terry-Ye/im
mv $GOPATH/src/github.com/Terry-Ye/im $GOPATH/src/im
cd $GOPATH/src/im
go get ./...

golang.org 包拉不下来的情况,例

package golang.org/x/net/ipv4: unrecognized import path "golang.org/x/net/ipv4" (https fetch: Get https://golang.org/x/net/ipv4?go-get=1: dial tcp 216.239.37.1:443: i/o timeout)

从github 拉下来,再移动位置


git clone https://github.com/golang/net.git
mkdir -p golang.org/x/

mv net $GOPATH/src/golang.org/x/
  1. 部署im

安装comet、logic、job模块

cd $GOPATH/src/im/comet
go install
cd ../logic/
go install
cd ../job
go install

nohup $GOPATH/bin/logic -d $GOPATH/src/im/logic/ 2>&1 > /data/log/im/logic.log &

nohup $GOPATH/bin/comet -d $GOPATH/src/im/comet/ 2>&1 > /data/log/im/comet.log &

nohup $GOPATH/bin/job -d $GOPATH/src/im/job/ 2>&1 > /data/log/im/job.log &
  1. im_api 是im系统中使用的接口,需要像demo那样整体跑起来需要完整的部署

部署注意事项

  1. 部署服务器注意防火墙是否开放对应的端口(本地不需要,具体需要的端口在各层的配置文件)

demo

聊天室:http://www.texixi.com:1999/

使用的包

  • log: github.com/sirupsen/logrus
  • rpc: github.com/smallnest/rpcx
  • websocket: github.com/gorilla/websocket
  • 配置文件:github.com/spf13/viper

后续计划

  1. 在线列表
  2. 支持wss
  3. 聊天机器人
查看原文

赞 27 收藏 18 评论 2

大呜 赞了文章 · 2018-12-19

localStorage、sessionStorage、Cookie的区别及用法

localStorage、sessionStorage、Cookie的区别及用法

图片描述

webstorage

webstorage是本地存储,存储在客户端,包括localStorage和sessionStorage。

localStorage

localStorage生命周期是永久,这意味着除非用户显示在浏览器提供的UI上清除localStorage信息,否则这些信息将永远存在。存放数据大小为一般为5MB,而且它仅在客户端(即浏览器)中保存,不参与和服务器的通信。

sessionStorage

sessionStorage仅在当前会话下有效,关闭页面或浏览器后被清除。存放数据大小为一般为5MB,而且它仅在客户端(即浏览器)中保存,不参与和服务器的通信。源生接口可以接受,亦可再次封装来对Object和Array有更好的支持。

localStorage和sessionStorage使用时使用相同的API:

 localStorage.setItem("key","value");//以“key”为名称存储一个值“value”

    localStorage.getItem("key");//获取名称为“key”的值

    localStorage.removeItem("key");//删除名称为“key”的信息。

    localStorage.clear();​//清空localStorage中所有信息

简单的举个例子来了解一下他们的用法

仿一下京东官网顶部的广告关闭,效果为第一次进入官网会出现广告,然后点击关闭,刷新网页不会再显示广告,但是当清除localStorage存入的数据,刷新网页会再显示广告。
html代码

<div class="header">
    <div class="header-a">
        <a href=""></a>
        <i class="close">x</i>
    </div>
</div>    

css代码

.header{
    width:100%;
    height:80px;
    background:#000;
}
.header-a{
    width:1190px;
    margin:0 auto;
    position:relative;
    background:url("images/1.jpg") no-repeat;
}
.header-a a{
    width:100%;
    height:80px;
    display:block;
}
.close{
    cursor:pointer;
    color:#fff;
    position:absolute;
    top:5px;
    right:5px;
    background:rgb(129, 117, 117);
    width: 20px;
    text-align: center;
    line-height: 20px;
}    

js代码

//localStorage方法
<script data-original="../js/jquery.min.js"></script>
function haxi(){
        //判断localStorage里有没有isClose
        if(localStorage.getItem("isClose")){             
            $(".header").hide();
        }else{
            $(".header").show();
        }
        //点击关闭隐藏图片存取数据
        $(".close").click(function(){
            $(".header").fadeOut(1000);

            localStorage.setItem("isClose", "1"); 
        })
    }
    haxi();

作用域不同

不同浏览器无法共享localStorage或sessionStorage中的信息。相同浏览器的不同页面间可以共享相同的 localStorage(页面属于相同域名和端口),但是不同页面或标签页间无法共享sessionStorage的信息。这里需要注意的是,页面及标 签页仅指顶级窗口,如果一个标签页包含多个iframe标签且他们属于同源页面,那么他们之间是可以共享sessionStorage的。

Cookie

生命期为只在设置的cookie过期时间之前一直有效,即使窗口或浏览器关闭。 存放数据大小为4K左右 。有个数限制(各浏览器不同),一般不能超过20个。与服务器端通信:每次都会携带在HTTP头中,如果使用cookie保存过多数据会带来性能问题。但Cookie需要程序员自己封装,源生的Cookie接口不友好(http://www.jb51.net/article/6...
)。
js代码

//Cookie方法
<script data-original="../js/cookie.js"></script>//Cookie函数自己封装引入
function haxi(){
        if(getCookie("isClose")){             
            $(".header").hide();
        }else{
            $(".header").show();
        }
        
        $(".close").click(function(){
            $(".header").fadeOut(1000);

            setCookie("isClose", "1","s10");
        })
    }
    haxi();

cookie的优点:具有极高的扩展性和可用性

1.通过良好的编程,控制保存在cookie中的session对象的大小。
2.通过加密和安全传输技术,减少cookie被破解的可能性。
3.只有在cookie中存放不敏感的数据,即使被盗取也不会有很大的损失。
4.控制cookie的生命期,使之不会永远有效。这样的话偷盗者很可能拿到的就   是一个过期的cookie。

cookie的缺点:

1.cookie的长度和数量的限制。每个domain最多只能有20条cookie,每个cookie长度不能超过4KB。否则会被截掉。
2.安全性问题。如果cookie被人拦掉了,那个人就可以获取到所有session信息。加密的话也不起什么作用。
3.有些状态不可能保存在客户端。例如,为了防止重复提交表单,我们需要在服务端保存一个计数器。若吧计数器保存在客户端,则起不到什么作用。

localStorage、sessionStorage、Cookie共同点:都是保存在浏览器端,且同源的。

查看原文

赞 96 收藏 100 评论 0

大呜 收藏了文章 · 2018-10-31

基于Docker搭建Jumpserver堡垒机操作实践

一、背景

笔者最近想起此前公司使用过的堡垒机系统,觉得用的很方便,而现在的公司并没有搭建此类系统,想着以后说不定可以用上;而且最近也有点时间,因此来了搭建堡垒机系统的兴趣,在搭建过程中参考了比较多的文档,其中最详细的还是官方文档,地址如下所示:

  1. Jumpserver 文档

二、操作概要

1. 系统运行
2. 配置入门
3. 测试验证

三、系统运行

在官方文档中安装堡垒机有很多种方法,这让笔者有些纠结,另外而且在不同系统中安装方法也不一致,不过正在徘徊不定时,发现一种通用的安装方法,便是采用docker进行安装,因此本文中笔者将以docker安装为例

3.1 下载镜像

在docker官方镜像库当中并没有收录jumpserver,因此下载镜像命令如下所示:

docker pull registry.jumpserver.org/public/jumpserver:1.0.0

下载过程可能比较慢,笔者大约花费了14分钟才将其下载完成,下载完成后结果如下所示

1.0.0: Pulling from public/jumpserver
af4b0a2388c6: Pull complete
aa66a3d10fd2: Pull complete
1d4c6a27f2ac: Pull complete
2490267572de: Pull complete
b00f1599768d: Pull complete
398fc903cdc3: Pull complete
f8490bbfc09a: Pull complete
86d238b365f5: Pull complete
2cd3b1ef59b2: Pull complete
4a21434eeb73: Pull complete
ae8cf3e909e0: Pull complete
7c440776471a: Pull complete
0a5e895f91af: Pull complete
b86672241685: Pull complete
af16a4945f95: Pull complete
0374e723cd6c: Pull complete
e18b86849df9: Pull complete
648aa832cb74: Pull complete
b52364a5c704: Pull complete
Digest: sha256:0f26e439c492ac52cbc1926aa950a59730607c947c79557ab3da51bfc2c7b5d4
Status: Downloaded newer image for registry.jumpserver.org/public/jumpserver:1.0.0

3.2 运行镜像

下载之后笔者需要将下载下来的容器运行起来,为了防止80端口被宿主机其他进程所占用,因此将容器端口映射到宿主机的8011上,运行命令如下所示:

docker run --name jms_server -d -p 8011:80 -p 2222:2222 registry.jumpserver.org/public/jumpserver:1.0.0

在参数当中因为有加入后台运行参数-d,容器运行之后终端不会进入容器bash中,而且当命令执行成功之后,docker将会返回容器ID,如果返回信息则可能出现了异常错误,正常返回结果如下所示

4709a7d85af28bf05a63fb3e42541a41c30edda6668fd54a446cfab006c35b9e

3.3 运行检查

容器运行之后,笔者需要对其进行检测确保运行成功,检查方式有两个,首先观察容器是否正常运行,然后是检查堡垒机是否能被浏览器所访问

首先通过如下命令可以查看当前正在运行的容器

docker ps

如果容器正常运行将会出现刚在笔者所运行的堡垒机容器ID,正常返回结果参考如下

CONTAINER ID        IMAGE                                             COMMAND               CREATED             STATUS              PORTS                                                   NAMES
4709a7d85af2        registry.jumpserver.org/public/jumpserver:1.0.0   "/opt/start_jms.sh"   8 minutes ago       Up 8 minutes        443/tcp, 0.0.0.0:2222->2222/tcp, 0.0.0.0:8011->80/tcp   jms_server

在返回结果当中可以看到之前docker返回的容器ID正处于运行状态,便可以确定容器运行正常,接着笔者还需要通过浏览器来检测是否运行成功,使用浏览器打开如下地址

http://127.0.0.1:8011/

当浏览器出现如下界面时,则基本代表成功

image

四、配置入门

在确定系统正常运行之后,接下来就可以对系统进行一些配置,堡垒机配置比较简单,下面的配置是将是使用堡垒机最为基础的一些配置,配置主要是添加一些资产进行管理,这便需要添加管理用户、系统普通用户、账户授权等操作。

4.1 登录系统

在前面的检验运行的截图当中可以看到需要登录,而账号和密码笔者并没有在官方文档中所看到,笔者随手一尝试,发现用户名和密码分别是adminadmin,如下图所示

image

登录成功之后,进入系统看到的界面如下图所示

image

4.2 管理用户

接下来笔者需要添加一些资产,添加资产的前提条件是有一个管理用户,这个管理用户是资产的最高权限账户,堡垒机之后会使用此账户来登录并管理资产,和获取一些统计信息,笔者在资产管理->管理用户列表中点击创建系统用户按钮,便来到了创建管理用户的页面,如下图所示

image

在表单中可以看见必须填写用户名,和认证所用的密码或私钥,按照真实情况去填写,比如笔者的资产最高权限账户是song,密码123456Ab,那么就如实填写上去。

4.3 资产管理

在添加管理用户之后,便可以添加资产了,添加资产也非常的简单,在资产列表点击创建资产按钮,便来到了添加资产的页面,如下图所示

image

添加资产需要填写,资产的IP地址,以及ssh的端口号,以及选择资产的操作系统类型,并且选择用哪一个管理用户

4.4 系统用户

在资产管理下还有一个系统用户管理,这个系统用户的使用场景是,有时候需要在很多个目标资产中创建一个普通账户,这时候肯定是十分麻烦;此时便可以通过堡垒机上的系统用户管理来创建一个系统用户;然后下发到目标资产中,这样一来就不需要去目标主机一个个登录然后去创建,因此非常方便,添加系统用户如下图所示

image

创建系统用户需输入需要创建的账号,以及选择认证的方式,默认为秘钥方式,也可以将选择框选中去掉,通过密码来认证。

五、测试验证

在前面的配置步骤操作完毕后,便可以进行一些常规功能验证,以此来加深对jumpserver系统的了解,这些功能测试点有 资产连接测试、用户授权、Web终端、在线会话、命令记录等功能。

5.1 连接测试

连接测试的目的是检查资产是否可以被堡垒机所访问,可以在资产列表点击资产名称,便可以进入资产详情页面,右侧有两个按钮,点击刷新按钮,正确配置的参考效果如下图所示

image

如果能看到左侧的硬件信息发生了变更,就代表此前配置的管理用户没有问题,否则会弹出错误提示框;

5.2 用户授权

当配置资产后,如果想在堡垒机中直接连接终端就还需要给用户授权,授权分为两个步骤,第一步是给web终端账户授权,在会话管理->终端管理,如下图所示

image

第二步则是给用户自己本身授权,在授权管理->资产权限->创建权限规则中做好相应配置,如下图所示

image

5.3 web终端

当给用户授权之后,用户便可以会话管理->Web终端中与系统进行交互,如下图所示

image

5.4 在线会话

有些时候想看谁在操作服务器,可以很轻松的通过在线会话功能来查看当前有哪些用户在操作终端,在会话管理->在线会话列表中进行查看,如下图所示
image

5.5 命令记录

笔者觉得堡垒机最大的作用之一便是审计,如果想知道某个用户在系统中执行了那些命令,可以很方便的在会话管理->命令记录中进行查看,如下图所示

image

六、 图书推荐

如果对笔者的实战文章较为感兴趣,可以关注笔者新书《PHP Web安全开发实战》,现已在各大平台上架销售,封面如下图所示

image

作者:汤青松

微信:songboy8888

日期:2018-10-30

查看原文

大呜 收藏了文章 · 2018-09-09

PHP面试:尽可能多的说出你知道的排序算法

预警

本文适合对于排序算法不太了解的新手同学观看,大佬直接忽略即可。因为考虑到连贯性,所以篇幅较长。老铁们看完需要大概一个小时,但是从入门到完全理解可能需要10个小时(哈哈哈,以我自己的经历来计算的),所以各位老铁可以先收藏下来,同步更新在Github,本文引用到的所有算法的实现在这个地址,每天抽点时间理解一个排序算法即可。

排序和他们的类型

我们的数据大多数情况下是未排序的,这意味着我们需要一种方法来进行排序。我们通过将不同元素相互比较并提高一个元素的排名来完成排序。在大多数情况下,如果没有比较,我们就无法决定需要排序的部分。在比较之后,我们还需要交换元素,以便我们可以对它们进行重新排序。良好的排序算法具有进行最少的比较和交换的特征。除此之外,还存在基于非比较的排序,这类排序不需要比较数据来进行排序。我们将在这篇文章中为各位老铁介绍这些算法。以下是本篇文章中我们将要讨论的一些排序算法:

  • Bubble sort
  • Insertion sort
  • Selection sort
  • Quick sort
  • Merge sort
  • Bucket sort

以上的排序可以根据不同的标准进行分组和分类。例如简单排序,高效排序,分发排序等。我们现在将探讨每个排序的实现和复杂性分析,以及它们的优缺点。

时间空间复杂度以及稳定性

我们先看下本文提到的各类排序算法的时间空间复杂度以及稳定性。各位老铁可以点击这里了解更多。

clipboard.png

冒泡排序

冒泡排序是编程世界中最常讨论的一个排序算法,大多数开发人员学习排序的第一个算法。冒泡排序是一个基于比较的排序算法,被认为是效率最低的排序算法之一。冒泡排序总是需要最大的比较次数,平均复杂度和最坏复杂度都是一样的。

冒泡排序中,每一个待排的项目都会和剩下的项目做比较,并且在需要的时候进行交换。下面是冒泡排序的伪代码。

procedure bubbleSort(A: list of sortable items)
n = length(A)
for i = 0 to n inclusive do
 for j = 0 to n - 1 inclusive do
    if A[j] > A[j + 1] then
        swap(A[j + 1], A[j])
    end if
  end for
end for
end procedure

正如我们从前面的伪代码中看到的那样,我们首先运行一个外循环以确保我们迭代每个数字,内循环确保一旦我们指向某个项目,我们就会将该数字与数据集合中的其他项目进行比较。下图显示了对列表中的一个项目进行排序的单次迭代。假设我们的数据包含以下项目:20,45,93,67,10,97,52,88,33,92。第一次迭代将会是以下步骤:

clipboard.png

有背景颜色的项目显示的是我们正在比较的两个项目。我们可以看到,外部循环的第一次迭代导致最大的项目存储在列表的最顶层位置。然后继续,直到我们遍历列表中的每个项目。现在让我们使用PHP实现冒泡排序算法。

我们可以使用PHP数组来表示未排序的数字列表。由于数组同时具有索引和值,我们根据位置轻松迭代每个项目,并将它们交换到合适的位置。

function bubbleSort(&$arr) : void
{
    $swapped = false;
    for ($i = 0, $c = count($arr); $i < $c; $i++) {
        for ($j = 0; $j < $c - 1; $j ++) {
            if ($arr[$j + 1] < $arr[$j]) {
                list($arr[$j], $arr[$j + 1]) = array($arr[$j + 1], $arr[$j]);
            }
        }
    }
}

冒泡排序的复杂度分析

对于第一遍,在最坏的情况下,我们必须进行n-1比较和交换。 对于第2次遍历,在最坏的情况下,我们需要n-2比较和交换。 所以,如果我们一步一步地写它,那么我们将看到:复杂度= n-1 + n-2 + ..... + 2 + 1 = n *(n-1)/ 2 = O(n2)。因此,冒泡排序的复杂性是O(n2)。 分配临时变量,交换,遍历内部循环等需要一些恒定的时间,但是我们可以忽略它们,因为它们是不变的。以下是冒泡排序的时间复杂度表,适用于最佳,平均和最差情况:

best time complexityΩ(n)
worst time complexityO(n2)
average time complexityΘ(n2)
space complexity (worst case)O(1)

尽管冒泡排序的时间复杂度是O(n2),但是我们可以使用一些改进的手段来减少排序过程中对数据的比较和交换次数。最好的时间复杂度是O(n)是因为我们至少要一次内部循环才可以确定数据已经是排好序的状态。

冒泡排序的改进

冒泡排序最重要的一个方面是,对于外循环中的每次迭代,都会有至少一次交换。如果没有交换,则列表已经排序。我们可以利用它改进我们的伪代码

procedure bubbleSort(A: list of sortable items)
    n = length(A)
    for i = 1 to n inclusive do
        swapped = false
        for j = i to n - 1 inclusive do
            if A[j] > A[j + 1] then
                swap(A[j], A[j + 1])
                swapped = true
            endif
        end for
        if swapped = false
            break
        endif
    end for
end procedure
    

正如我们所看到的,我们现在为每个迭代设置了一个标志为false,我们期望在内部迭代中,标志将被设置为true。如果内循环完成后标志仍然为假,那么我们可以打破外循环。

function bubbleSort(&$arr) : void
{
    for ($i = 0, $c = count($arr); $i < $c; $i++) {
        $swapped = false;
        for ($j = 0; $j < $c - 1; $j++) {
            if ($arr[$j + 1] < $arr[$j]) {
                list($arr[$j], $arr[$j + 1]) = array($arr[$j + 1], $arr[$j]);
                $swapped = true;
            }
        }

        if (!$swapped) break; //没有发生交换,算法结束
    }
}

我们还发现,在第一次迭代中,最大项放置在数组的右侧。在第二个循环,第二大的项将位于数组右侧的第二个。我们可以想象出来在每次迭代之后,第i个单元已经存储了已排序的项目,不需要访问该索引和
做比较。因此,我们可以从内部迭代减少迭代次数并减少比较。这是我们的第二个改进的伪代码

procedure bubbleSort(A: list of sortable items)
    n = length(A)
    for i = 1 to n inclusive do
        swapped = false
        for j = 1 to n - i - 1 inclusive do
            if A[j] > A[j + 1] then
                swap(A[j], A[j + 1])
                swapped = true
            endif
        end for
        if swapped = false
            break
        end if
    end for
end procedure
   

下面的是PHP的实现

function bubbleSort(&$arr) : void
{
    
    for ($i = 0, $c = count($arr); $i < $c; $i++) {
        $swapped = false;
        for ($j = 0; $j < $c - $i - 1; $j++) {
            if ($arr[$j + 1] < $arr[$j]) {
                list($arr[$j], $arr[$j + 1]) = array($arr[$j + 1], $arr[$j]);
                $swapped = true;
            }

            if (!$swapped) break; //没有发生交换,算法结束
        }
    }
}

我们查看代码中的内循环,唯一的区别是$j < $c - $i - 1;其他部分与第一次改进一样。因此,对于20、45、93、67、10、97、52、88、33、92, 我们可以很认为,在第一次迭代之后,顶部数字97将不被考虑用于第二次迭代比较。同样的情况也适用于93,将不会被考虑用于第三次迭代。

clipboard.png

我们看看前面的图,脑海中应该马上想到的问题是“92不是已经排序了吗?我们是否需要再次比较所有的数字?是的,这是一个好的问题。我们完成了内循环中的最后一次交换后可以知道在哪一个位置,之后的数组已经被排序。因此,我们可以为下一个循环设置一个界限,伪代码是这样的:

procedure bubbleSort(A: list of sortable items)
    n = length(A)
    bound = n - 1
    for i = 1 to n inclusive do
        swapped = false
        bound = 0
        for j = 1 to bound inclusive do
            if A[j] > A[j + 1] then
                swap(A[j], A[j + 1])
                swapped = true
                newbound = j
            end if
        end for
        bound = newbound
        if swapped = false
            break
        endif
    end for
end procedure
   

这里,我们在每个内循环完成之后设定边界,并且确保我们没有不必要的迭代。下面是PHP代码:

function bubbleSort(&$arr) : void
{
    $swapped = false;
    $bound = count($arr) - 1;
    for ($i = 0, $c = count($arr); $i < $c; $i++) {
        for ($j = 0; $j < $bound; $j++) {
            if ($arr[$j + 1] < $arr[$j]) {
                list($arr[$j], $arr[$j + 1]) = array($arr[$j + 1], $arr[$j]);
                $swapped = true;
                $newBound = $j;
            }
        }
        $bound = $newBound;
        if (!$swapped) break; //没有发生交换,算法结束
    }
}

选择排序

选择排序是另一种基于比较的排序算法,它类似于冒泡排序。最大的区别是它比冒泡排序需要更少的交换。在选择排序中,我们首先找到数组的最小/最大项并将其放在第一位。如果我们按降序排序,那么我们将从数组中获取的是最大值。对于升序,我们获取的是最小值。在第二次迭代中,我们将找到数组的第二个最大值或最小值,并将其放在第二位。持续到我们把每个数字放在正确的位置。这就是所谓的选择排序,选择排序的伪代码如下:

procedure  selectionSort( A : list of sortable items)
    n = length(A)
    for i = 1 to n inclusive do
        min  =  i
        for j = i + 1 to n inclusive do
            if  A[j] < A[min] then
                min = j 
            end if
        end  for

        if min != i
            swap(a[i], a[min])
        end if
    end  for
end procedure

看上面的算法,我们可以发现,在外部循环中的第一次迭代之后,第一个最小项被存储在第一个位置。在第一次迭代中,我们选择第一个项目,然后从剩下的项目(从2到n)找到最小值。我们假设第一个项目是最小值。我们找到另一个最小值,我们将标记它的位置,直到我们扫描了剩余的列表并找到新的最小最小值。如果没有找到最小值,那么我们的假设是正确的,这确实是最小值。如下图:

clipboard.png

正如我们在前面的图中看到的,我们从列表中的第一个项目开始。然后,我们从数组的其余部分中找到最小值10。在第一次迭代结束时,我们只交换了两个地方的值(用箭头标记)。因此,在第一次迭代结束时,我们得到了的数组中得到最小值。然后,我们指向下一个数字45,并开始从其位置的右侧找到下一个最小的项目,我们从剩下的项目中找到了20(如两个箭头所示)。在第二次迭代结束时,我们将第二个位置的值和从列表的剩余部分新找到的最小位置交换。这个操作一直持续到最后一个元素,在过程结束时,我们得到了一个排序的列表,下面是PHP代码的实现。

function selectionSort(&$arr)
{
    $count = count($arr);

    //重复元素个数-1次
    for ($j = 0; $j <= $count - 1; $j++) {
        //把第一个没有排过序的元素设置为最小值
        $min = $arr[$j];
        //遍历每一个没有排过序的元素
        for ($i = $j + 1; $i < $count; $i++) {
            //如果这个值小于最小值
            if ($arr[$i] < $min) {
                //把这个元素设置为最小值
                $min = $arr[$i];
                //把最小值的位置设置为这个元素的位置
                $minPos = $i;
            }
        }
        //内循环结束把最小值和没有排过序的元素交换
        list($arr[$j], $arr[$minPos]) = [$min, $arr[$j]];
    }
    
}

选择排序的复杂度

选择排序看起来也类似于冒泡排序,它有两个for循环,从0到n。冒泡排序和选择排序的区别在于,在最坏的情况下,选择排序使交换次数达到最大n - 1,而冒泡排序可以需要 n * n 次交换。在选择排序中,最佳情况、最坏情况和平均情况具有相似的时间复杂度。

best time complexityΩ(n2)
worst time complexityO(n2)
average time complexityΘ(n2)
space complexity (worst case)O(1)

插入排序

到目前为止,我们已经看到了两种基于比较的排序算法。现在,我们将探索另一个排序算法——插入排序。与刚才看到的其他两个排序算法相比,它有最简单的实现。如果项目的数量较小,插入排序优于冒泡排序和选择排序。如果数据集很大,就像冒泡排序一样就变得效率低下。插入排序的工作原理是将数字插入到已排序列表的正确位置。它从数组的第二项开始,并判断该项是否小于当前值。如果是这样,它将项目转移,并将较小的项目存储在其正确的位置。然后,它移动到下一项,并且相同的原理继续下去,直到整个数组被排序。

procedure insertionSort(A: list of sortable items)
    n length(A)
    for i=1 to n inclusive do
        key = A[i]
        j = i - 1
        while j >= 0 and A[j] > key do
            A[j+1] = A[j]
            j--
        end while
        A[j + 1] = key
    end for
end procedure

假如我们有下列数组,元素是:20 45 93 67 10 97 52 88 33 92。我们从第二个项目45开始。现在我们将从45的左边第一个项目开始,然后到数组的开头,看看左边是否有大于45的值。由于只有20,所以不需要插入,目前两项(20, 45)被排序。现在我们将指针移到93,从它再次开始,比较从45开始,由于45不大于93,我们停止。现在,前三项(20, 45, 93)已排序。接下来,对于67,我们从数字的左边开始比较。左边的第一个数字是93,它较大,所以必须移动一个位置。我们移动93到67的位置。然后,我们移动到它左边的下一个项目45。45小于67,不需要进一步的比较。现在,我们先将93移动到67的位置,然后我们插入67的到93的位置。继续如上操作直到整个数组被排序。下图说明在每个步骤中使用插入排序的直到完全排序过程。

clipboard.png

function insertionSort(array &$arr)
{
    $len = count($arr);
    for ($i = 1; $i < $len; $i++) {
        $key = $arr[$i];
        $j = $i - 1;

        while ($j >= 0 && $arr[$j] > $key) {
            $arr[$j + 1] = $arr[$j];
            $j--;
        }
        $arr[$j + 1] = $key;
    }
}

插入排序的复杂度

插入排序具有与冒泡排序相似的时间复杂度。与冒泡排序的区别是交换的数量远低于冒泡排序。

best time complexityΩ(n)
worst time complexityO(n2)
average time complexityΘ(n2)
space complexity (worst case)O(1)

排序中的分治思想

到目前为止,我们已经了解了每次对完整列表进行排序的一些排序算法。我们每次都需要应对一个比较大的数字集合。我们可以设法使数据集合更小,从而解决这个问题。分治思想对我们有很大帮助。用这种方法,我们将一个问题分成两个或多个子问题或集合,然后在组合子问题的所有结果以获得最终结果。这就是所谓的分而治之方法,分而治之方法可以让我们有效地解决排序问题,并降低算法的复杂度。最流行的两种排序算法是合并排序和快速排序,它们应用分治算法对数据进行排序,因此被认为是最好的排序算法。

归并排序

正如我们已经知道的,归并排序应用分治方法来解决排序问题,我们用法两个过程来解决这个问题。第一个是将问题集划分为足够小的问题,以便容易地求解,然后将这些结果结合起来。我们将用递归方法来完成分治部分。下面的图显示了如何采用分治的方法。

clipboard.png

基于前面的图像,我们现在可以开始准备我们的代码,它将有两个部分。


/**
 * 归并排序
 * 核心:两个有序子序列的归并(function merge)
 * 时间复杂度任何情况下都是 O(nlogn)
 * 空间复杂度 O(n)
 * 发明人: 约翰·冯·诺伊曼
 * 速度仅次于快速排序,为稳定排序算法,一般用于对总体无序,但是各子项相对有序的数列
 * 一般不用于内(内存)排序,一般用于外排序
 */

function mergeSort($arr)
{
    $lenght = count($arr); 
    if ($lenght == 1) return $arr;
    $mid = (int)($lenght / 2);

    //把待排序数组分割成两半
    $left = mergeSort(array_slice($arr, 0, $mid));
    $right = mergeSort(array_slice($arr, $mid));

    return merge($left, $right);
}

function merge(array $left, array $right)
{
    //初始化两个指针
    $leftIndex = $rightIndex = 0;
    $leftLength = count($left);
    $rightLength = count($right);
    //临时空间
    $combine = [];

    //比较两个指针所在的元素
    while ($leftIndex < $leftLength && $rightIndex < $rightLength) {
        //如果左边的元素大于右边的元素,就将右边的元素放在单独的数组,并将右指针向后移动
        if ($left[$leftIndex] > $right[$rightIndex]) {
            $combine[] = $right[$rightIndex];
            $rightIndex++;
        } else {
            //如果右边的元素大于左边的元素,就将左边的元素放在单独的数组,并将左指针向后移动
            $combine[] = $left[$leftIndex];
            $leftIndex++;
        }
    }

    //右边的数组全部都放入到了返回的数组,然后把左边数组的值放入返回的数组
    while ($leftIndex < $leftLength) {
        $combine[] = $left[$leftIndex];
        $leftIndex++;
    }

    //左边的数组全部都放入到了返回的数组,然后把右边数组的值放入返回的数组
    while ($rightIndex < $rightLength) {
        $combine[] = $right[$rightIndex];
        $rightIndex++;
    }

    return $combine;
}

我们划分数组,直到它达到1的大小。然后,我们开始使用合并函数合并结果。在合并函数中,我们有一个数组来存储合并的结果。正因为如此,合并排序实际上比我们迄今所看到的其他算法具有更大的空间复杂度。

归并排序的复杂度

由于归并排序遵循分而治之的方法,所以我们必须解决这两个复杂问题。对于n个大小的数组,我们首先需要将数组分成两个部分,然后合并它们以得到n个大小的数组。我们可以看下面的示意图

clipboard.png

解决每一层子问题需要的时间都是cn,假设一共有l层,那么总的时间复杂度会是ln。因为一共有logn + 1
层,那么结果就是 cn(logn + 1)。我们删除常数阶和线性阶,最后的结果可以得出时间复杂度就是O(nlog2n)。

best time complexityΩ(nlogn)
worst time complexityO(nlogn)
average time complexityΘ(nlogn)
space complexity (worst case)O(n)

快速排序

快速排序是对冒泡排序的一种改进。它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

function qSort(array &$arr, int $p, int $r)
{
    if ($p < $r) {
        $q = partition($arr, $p, $r);
        qSort($arr, $p, $q);
        qSort($arr, $q + 1, $r);
    }
}

function partition(array &$arr, int $p, int $r)
{
    $pivot = $arr[$p];
    $i = $p - 1;
    $j = $r + 1;

    while (true) {
        do {
            $i++;
        } while ($arr[$i] < $pivot);

        do {
            $j--;
        } while ($arr[$j] > $pivot);

        if ($i < $j) {
            list($arr[$i], $arr[$j]) = [$arr[$j], $arr[$i]];
        } else {
            return $j;
        }

    }
}

快速排序的复杂度

最坏情况下快速排序具有与冒泡排序相同的时间复杂度,pivot的选取非常重要。下面是快速排序的复杂度分析。

best time complexityΩ(nlogn)
worst time complexityO(n2)
average time complexityΘ(nlogn)
space complexity (worst case)O(logn)

对于快速排序的优化,有兴趣的老铁可以点击这里查看。

桶排序

桶排序 (Bucket sort)或所谓的箱排序,工作的原理是将数组分到有限数量的桶里。每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)。

/**
 * 桶排序
 * 不是一种基于比较的排序
 * T(N, M) = O(M + N) N是带排序的数据的个数,M是数据值的数量
 * 当 M >> N 时,需要考虑使用基数排序
 */

function bucketSort(array &$data)
{
    $bucketLen = max($data) - min($data) + 1;
    $bucket = array_fill(0, $bucketLen, []);

    for ($i = 0; $i < count($data); $i++) {
        array_push($bucket[$data[$i] - min($data)], $data[$i]);
    }

    $k = 0;

    for ($i = 0; $i < $bucketLen; $i++) {
        $currentBucketLen = count($bucket[$i]);

        for ($j = 0; $j < $currentBucketLen; $j++) {
            $data[$k] = $bucket[$i][$j];
            $k++;
        }
    }
}

基数排序的PHP实现,有兴趣的同学同样可以访问这个页面来查看。

快速排序的复杂度

桶排序的时间复杂度优于其他基于比较的排序算法。以下是桶排序的复杂性

best time complexityΩ(n+k)
worst time complexityO(n2)
average time complexityΘ(n+k)
space complexity (worst case)O(n)

PHP内置的排序算法

PHP有丰富的预定义函数库,也包含不同的排序函数。它有不同的功能来排序数组中的项目,你可以选择按值还是按键/索引进行排序。在排序时,我们还可以保持数组值与它们各自的键的关联。下面是这些函数的总结

函数名功能
sort()升序排列数组。value/key关联不保留
rsort()按反向/降序排序数组。index/key关联不保留
asort()在保持索引关联的同时排序数组
arsort()对数组进行反向排序并维护索引关联
ksort()按关键字排序数组。它保持数据相关性的关键。这对于关联数组是有用的
krsort()按顺序对数组按键排序
natsort()使用自然顺序算法对数组进行排序,并保持value/key关联
natcasesort()使用不区分大小写的“自然顺序”算法对数组进行排序,并保持value/key关联。
usort()使用用户定义的比较函数按值对数组进行排序,并且不维护value/key关联。第二个参数是用于比较的可调用函数
uksort()使用用户定义的比较函数按键对数组进行排序,并且不维护value/key关联。第二个参数是用于比较的可调用函数
uasort()使用用户定义的比较函数按值对数组进行排序,并且维护value/key关联。第二个参数是用于比较的可调用函数

对于sort()、rsort()、ksort()、krsort()、asort()以及 arsort()下面的常量可以使用

  • SORT_REGULAR - 正常比较单元(不改变类型)
  • SORT_NUMERIC - 单元被作为数字来比较
  • SORT_STRING - 单元被作为字符串来比较
  • SORT_LOCALE_STRING - 根据当前的区域(locale)设置来把单元当作字符串比较,可以用 setlocale() 来改变。
  • SORT_NATURAL - 和 natsort() 类似对每个单元以“自然的顺序”对字符串进行排序。 PHP 5.4.0 中新增的。
  • SORT_FLAG_CASE - 能够与 SORT_STRING 或 SORT_NATURAL 合并(OR 位运算),不区分大小写排序字符串。

完整内容

本文引用到的所有算法的实现在这个地址,主要内容是使用PHP语法总结基础的数据结构和算法。欢迎各位老铁收藏~

查看原文

大呜 收藏了文章 · 2018-07-13

php-fpm进程数管理

PHP-FPM

先来了解一些名词概念:

CGICommon Gateway Interface(通用网管协议),用于让交互程序和Web服务器通信的协议。它负责处理URL的请求,启动一个进程,将客户端发送的数据作为输入,由Web服务器收集程序的输出并加上合适的头部,再发送回客户端。

FastCGI是基于CGI的增强版本的协议,不同于创建新的进程来服务请求,使用持续的进程和创建的子进程来处理一连串的进程,这些进程由FastCGI服务器管理,开销更小,效率更高。

PHP-FPMPHP实现的FastCGI Process Manager(FastCGI进程管理器), 用于替换PHP FastCGI的大部分附加功能,适用于高负载网站。支持的功能如:

  1. 平滑停止/启动的高级进程管理功能
  2. 慢日志记录脚本
  3. 动态/静态子进程产生
  4. 基于php.ini的配置文件

PHP-FPM在5.4之后已经整合进入PHP源代码中,提供更好的PHP进程管理方式,可以有效控制内存和进程,平滑重载PHP配置。如果需要使用,在./configure的时候带上-enable-fpm参数即可,使用PHP-FPM来控制FastCGI进程:

// 支持start/stop/quit/restart/reload/logrotate参数
// quit/reload是平滑终止和平滑重新加载,即等现有的服务完成
./php-fpm --start

PHP-FPM 配置

PHP-FPM配置文件为php-fpm.conf,在这个配置文件中我们需要了解一些参数。下面所有的子进程均指php-fpm进程,可以在终端通过ps aux | grep php查看到。

  • 显示php-fpm: pool www的代表work子进程(实际处理请求)
  • 显示php-fpm: process master的代表master主进程(负责管理work子进程)

全局配置

先看PHP-FPM最重要的全局配置部分:

emergency_restart_threshold

如果在emergency_restart_interval设定的时间内收到该参数设定次数的SIGSEGVSIGBUS退出的信号,则FPM会重新启动。默认值为0,表示关闭该功能。

emergency_restart_interval

设定平滑重启的间隔时间,有助于解决加速器中共享内存的使用问题。可用单位s(默认)/m/h/d,默认值为0, 表示关闭。

process.max

FPM能够创建的最大子进程数量,它在使用多个pm = dynamic配置的php-fpm pool进程池的时候,控制全局的子进程数量。默认值为0,代表着无限制。

进程池配置

PHP-FPM的配置其余部分是一个名为Pool Definitions的区域,这个区域的配置设置每个PHP-FPM进程池,进程池中是一系列相关的子进程。这部分开头都是[进程池名称],如[www]

此时可以解释看到ps aux | grep php中显示的是php-fpm: pool www

pm

pm指的是process manager,指定进程管理器如何控制子进程的数量,它为必填项,支持3个值:

  • static: 使用固定的子进程数量,由pm.max_children指定
  • dynamic:基于下面的参数动态的调整子进程的数量,至少有一个子进程

    • pm.max_chidren: 可以同时存活的子进程的最大数量
    • pm.start_servers: 启动时创建的子进程数量,默认值为min_spare_servers + max_spare_servers - min_spare_servers) / 2
    • pm.min_spare_servers: 空闲状态的子进程的最小数量,如果不足,新的子进程会被自动创建
    • pm.max_spare_servers: 空闲状态的子进程的最大数量,如果超过,一些子进程会被杀死
  • ondemand: 启动时不会创建子进程,当新的请求到达时才创建。会使用下面两个参数:

    • pm.max_children
    • pm.process_idle_timeout 子进程的空闲超时时间,如果超时时间到没有新的请求可以服务,则会被杀死

pm.max_requests

每一个子进程的最大请求服务数量,如果超过了这个值,该子进程会被自动重启。在解决第三方库的内存泄漏问题时,这个参数会很有用。默认值为0,指子进程可以持续不断的服务请求。

PHP-FPM配置优化

PHP-FPM管理的方式是一个master主进程,多个pool进程池,多个worker子进程。其中每个进程池监听一个socket套接字。具体的图示:

图片描述

其中的worker子进程实际处理连接请求,master主进程负责管理子进程:

1. `master`进程,设置1s定时器,通过`socket`文件监听
2. 在`pm=dynamic`时,如果`idle worker`数量<`pm.min_spare_servers`,创建新的子进程
3. 在`pm=dynamic`时,如果`idle worker`数量>`pm.max_spare_servers`,杀死多余的空闲子进程
4. 在`pm=ondemand`时,如果`idle worker`空闲时间>`pm.process_idle_timeout`,杀死该空闲进程
5. 当连接到达时,检测如果`worker`数量>`pm.max_children`,打印`warning`日志,退出;如果无异常,使用`idle worker`服务,或者新建`worker`服务

保障基本安全

我们为了避免PHP-FPM主进程由于某些糟糕的PHP代码挂掉,需要设置重启的全局配置:

; 如果在1min内有10个子进程被中断失效,重启主进程
emergency_restart_threshold = 10
emergency_restart_interval = 1m

进程数调优

每一个子进程同时只能服务一次连接,所以控制同时存在多少个进程数就很重要,如果过少会导致很多不必要的重建和销毁的开销,如果过多又会占用过多的内存,影响其他服务使用。

我们应该测试自己的PHP进程使用多少内存,一般来说刚启动时是8M左右,运行一段时间由于内存泄漏和缓存会上涨到30M左右,所以你需要根据自己的预期内存大小设定进程的数量。同时根据进程池的数量来看一个进程管理器的子进程数量限制。

测试平均PHP子进程占用的内存:

$ps auxf | grep php | grep -v grep
work     26829  0.0  0.0 715976  4712 ?        Ss   Jul11   0:00 php-fpm: master process (./etc/php-fpm.conf)
work     21889  0.0  0.0 729076 29668 ?        S    03:12   0:20  \_ php-fpm: pool www         
work     21273  0.0  0.0 728928 31380 ?        S    03:25   0:21  \_ php-fpm: pool www         
work     15114  0.0  0.0 728052 29084 ?        S    03:40   0:19  \_ php-fpm: pool www         
work     17072  0.0  0.0 728800 34240 ?        S    03:54   0:22  \_ php-fpm: pool www         
work     22763  0.0  0.0 727904 20352 ?        S    11:29   0:04  \_ php-fpm: pool www         
work     38545  0.0  0.0 727796 19484 ?        S    12:34   0:01  \_ php-fpm: pool www

// 共占用的内存数量
$ps auxf | grep php | grep -v grep | grep -v master | awk '{sum+=$6} END {print sum}'
162712

// 所有的子进程数量
$ ps auxf | grep php | grep -v grep | grep -v master | wc -l 
6

可以看到第6列,每一个子进程的内存占用大概在19-34M之间(单位为KB)。平均的内存占用为162712KB/6 = 27.1M

查看服务器总的内存大小

$ free -g
             total       used       free     shared    buffers     cached
Mem:           157        141         15          0          4        123
-/+ buffers/cache:         13        143
Swap:            0          0          0

可以看出我的服务器总得内存大小是157G(-g采用了G的单位)。

进程数限制

此时如果我们分配全部的内存给PHP-FPM使用,那么进程数可以限制在157000/27 = 5814,但是由于我的服务器同时服务了很多内容,所以我们可以向下调整到512个进程数:

process.max = 512
pm = dynamic
pm.max_children = 512
pm.start_servers = 16
pm.min_spare_servers = 8
pm.max_spare_serveres = 30

防止内存泄漏

由于糟糕的插件和库,内存泄漏时有发生,所以我们需要对每一个子进程服务的请求数量做限制,防止无限制的内存泄漏:

pm.max_requests = 1000

重启

如果上面的配置都按照你的实际需求和环境配置好了,不要忘记重启PHP-FPM服务。

参考资料

  1. PHP手册-FastCGI: http://php.net/manual/zh/inst...
  2. 维基百科-CGI:https://zh.wikipedia.org/wiki...
  3. 维基百科-FastCGI:https://zh.wikipedia.org/wiki...
  4. 博客园 php-fpm进程数优化:https://www.cnblogs.com/52fhy...
  5. 简书 php-fpm进程管理:https://www.jianshu.com/p/c9a...
  6. PHP手册 php-fpm.conf:http://php.net/manual/zh/inst...
  7. 《Modern PHP》 第七章 PHP-FPM
查看原文

大呜 收藏了文章 · 2018-05-31

调试 Nginx 的配置

调试 Nginx 的配置

标签(空格分隔): Nginx 调试 Debuging 配置


注:该文原文是 Debugging Nginx Configuration

默认,Nginx 仅仅记录标准错误日志到 Nginx 默认的 error 文件中,或是被 error_log 指令指定的文件中。

我们可以控制许多方面的错误日志,这将帮助我们调试我们 Nginx 配置文件。

重要:对于 Nginx 配置文件的任何改变,你都必须测试和重载 Nginx 的配置文件来让变更生效。在 Ubuntu 系统,你可以简单的运行 nginx -t && service nginx reload 命令。

在我们继续进行之前

在你复制粘贴任何 Nginx 配置之前,确保你移除了你不想要的代码。并且,每次你升级 Nginx,也请使用最新版 Nginx 提供的更新你的配置文件。

在我们开始之前,请详细阅读这些官方文章:通常的 Nginx 陷阱if 是恶魔location 指令Nginx 请求过程。你可能会单独使用它们来解决你的问题。

注:5 星推荐上面的几篇官方文章。

好吧,看起来你需要一些严谨的调试,让我们开始!

仅仅调试 rewrite 规则

大部分时间,你仅仅需要这个,特别是当你看见 404 或是不是期望的页面的时候。

server {
        #other config
        error_log    /var/logs/nginx/example.com.error.log;
        rewrite_log on;
        #other config
}

rewrite_log 仅仅是一个标志。当打开它,它将发送所有的 rewrite 相关的日志信息到 error_log 文件中,使用 [notice] 级别。

因此,一旦你打开了它,在 error_log 中查看日志信息。

设置 Nginx 日志的调试级别

下面的示例增加了 debug 级别,记录在指定的路径。

server {
        #other config
        error_log    /var/logs/nginx/example.com.error.log debug;
        #other config
}

debug 将记录最大的消息。你可以在这里看到其他值

注意:在一个高流量的网站,不要忘记恢复 error_log 日志的调试级别,error_log 可能会吃光你所有的可用磁盘空间,并引起服务器 crash。

设置 Nginx 仅仅记录来自于你的 IP 的错误

当你设置日志级别成 debug,如果你在调试一个在线的高流量网站的话,你的错误日志可能会记录每个请求的很多消息,这样会变得毫无意义。

为了促使 Nginx 记录仅仅来自于你的 IP 的错误日志,添加以下行到配置文件 /etc/nginx/nginx.confevents{..}

确保使用你自己的公网 IP 替换 1.2.3.4,你可以在这里找到你的公网 IP

events {
        debug_connection 1.2.3.4;
}

你可以在这里查看更多的细节

Nginx 的 Location 指定错误日志

在 Nginx 中,我们使用 location{..}

为了调试一个应用的部分,你可以在一个或多个 location{..} 中指定 error_log 指令。

server {
        #other config
        error_log    /var/logs/nginx/example.com.error.log;
        location /admin/ { 
        error_log /var/logs/nginx/admin-error.log debug; 
    }         
    #other config
}

以上将仅仅调试你应用的 /admin/ 部分,错误日志将被记录到一个不同的文件中。

你可以组合使用 debug_connectionerror_log 来获取更多的控制调试日志。

使用 Nginx 的 HttpEchoModule 模块调试

HttpEchoModule 是一个独立的 Nginx 模块,它可以帮助你完全以不同的方式调试。这个模块默认没有被 Nginx 绑定。

你需要重新编译 Nginx 来使用这个模块。对于 Ubuntu 用户,这是一个快速启动仓库

我最近在使用它,我还用它来调试项目。当我做完的时候,我将写一篇文章详细讲述。

为 Nginx 的配置使用 Perl/Lua 语言

如果你仍然有困难的时间,并且你定期配置你的 Nginx,应该考虑使用其他语言来处理你的 Nginx 配置。

这里有关于 Perl 语言Lua 语言的 Nginx 模块。

我非常不善于学习新的语言,不会有太多的机会写更多关于这方面的东西,但是如果你知道或是可以非常容易学习 Perl/Lua,这会非常有趣。

扩展阅读

  • How Nginx’s location-if works!
  • Maintaining, Optimizing & Debugging WordPress-Nginx Setup
查看原文

大呜 赞了文章 · 2018-05-31

调试 Nginx 的配置

调试 Nginx 的配置

标签(空格分隔): Nginx 调试 Debuging 配置


注:该文原文是 Debugging Nginx Configuration

默认,Nginx 仅仅记录标准错误日志到 Nginx 默认的 error 文件中,或是被 error_log 指令指定的文件中。

我们可以控制许多方面的错误日志,这将帮助我们调试我们 Nginx 配置文件。

重要:对于 Nginx 配置文件的任何改变,你都必须测试和重载 Nginx 的配置文件来让变更生效。在 Ubuntu 系统,你可以简单的运行 nginx -t && service nginx reload 命令。

在我们继续进行之前

在你复制粘贴任何 Nginx 配置之前,确保你移除了你不想要的代码。并且,每次你升级 Nginx,也请使用最新版 Nginx 提供的更新你的配置文件。

在我们开始之前,请详细阅读这些官方文章:通常的 Nginx 陷阱if 是恶魔location 指令Nginx 请求过程。你可能会单独使用它们来解决你的问题。

注:5 星推荐上面的几篇官方文章。

好吧,看起来你需要一些严谨的调试,让我们开始!

仅仅调试 rewrite 规则

大部分时间,你仅仅需要这个,特别是当你看见 404 或是不是期望的页面的时候。

server {
        #other config
        error_log    /var/logs/nginx/example.com.error.log;
        rewrite_log on;
        #other config
}

rewrite_log 仅仅是一个标志。当打开它,它将发送所有的 rewrite 相关的日志信息到 error_log 文件中,使用 [notice] 级别。

因此,一旦你打开了它,在 error_log 中查看日志信息。

设置 Nginx 日志的调试级别

下面的示例增加了 debug 级别,记录在指定的路径。

server {
        #other config
        error_log    /var/logs/nginx/example.com.error.log debug;
        #other config
}

debug 将记录最大的消息。你可以在这里看到其他值

注意:在一个高流量的网站,不要忘记恢复 error_log 日志的调试级别,error_log 可能会吃光你所有的可用磁盘空间,并引起服务器 crash。

设置 Nginx 仅仅记录来自于你的 IP 的错误

当你设置日志级别成 debug,如果你在调试一个在线的高流量网站的话,你的错误日志可能会记录每个请求的很多消息,这样会变得毫无意义。

为了促使 Nginx 记录仅仅来自于你的 IP 的错误日志,添加以下行到配置文件 /etc/nginx/nginx.confevents{..}

确保使用你自己的公网 IP 替换 1.2.3.4,你可以在这里找到你的公网 IP

events {
        debug_connection 1.2.3.4;
}

你可以在这里查看更多的细节

Nginx 的 Location 指定错误日志

在 Nginx 中,我们使用 location{..}

为了调试一个应用的部分,你可以在一个或多个 location{..} 中指定 error_log 指令。

server {
        #other config
        error_log    /var/logs/nginx/example.com.error.log;
        location /admin/ { 
        error_log /var/logs/nginx/admin-error.log debug; 
    }         
    #other config
}

以上将仅仅调试你应用的 /admin/ 部分,错误日志将被记录到一个不同的文件中。

你可以组合使用 debug_connectionerror_log 来获取更多的控制调试日志。

使用 Nginx 的 HttpEchoModule 模块调试

HttpEchoModule 是一个独立的 Nginx 模块,它可以帮助你完全以不同的方式调试。这个模块默认没有被 Nginx 绑定。

你需要重新编译 Nginx 来使用这个模块。对于 Ubuntu 用户,这是一个快速启动仓库

我最近在使用它,我还用它来调试项目。当我做完的时候,我将写一篇文章详细讲述。

为 Nginx 的配置使用 Perl/Lua 语言

如果你仍然有困难的时间,并且你定期配置你的 Nginx,应该考虑使用其他语言来处理你的 Nginx 配置。

这里有关于 Perl 语言Lua 语言的 Nginx 模块。

我非常不善于学习新的语言,不会有太多的机会写更多关于这方面的东西,但是如果你知道或是可以非常容易学习 Perl/Lua,这会非常有趣。

扩展阅读

  • How Nginx’s location-if works!
  • Maintaining, Optimizing & Debugging WordPress-Nginx Setup
查看原文

赞 7 收藏 25 评论 0

大呜 赞了文章 · 2018-04-27

使用 caddy 作为微服务的 API gateway

背景

大家都知道,Docker这些年让IT界产生了深刻的变革,
从开发到测试到运维,处处都有它的身影。
它同时也和微服务架构相互促进,并肩前行。

在最新版的 Docker(CE 17.03) 里,随着 swarm mode 的成熟,
在较简单的场景里已经可以不再需要专门的基础设施管理
服务编排服务发现健康检查负载均衡等等。

但是API gateway还是需要一个的。或许再加上一个日志收集
你的微服务架构就五脏俱全了。
我们知道Nginx Plus是可以很好的胜任 API gateway 的工作的,
但它是商业软件。Nginx我们不说认证啊限流啊统计啊之类的功能,
单就请求转发这一点最基本的就出了问题。

我们知道Docker是用DNS的方式,均衡同一名称的服务请求到不同的node,
但是Nginx为了速度,在反向代理的时候会有一个不可取消的 DNS Cache,
这样我们Docker在根据容器的扩展或收缩动态的更新DNS,可Nginx却不为所动,
坚持把请求往固定的IP上发,不说均衡,这个IP甚至可能已经失效了呢。

有一个配置文件上的小Hack可以实现Nginx每次去查询DNS,我本来准备写一篇文章来着,
现在看来不用了,我们找到了更优雅的API gateway, Caddy
我上篇文章也写了一个它的简介。

接下来的所有代码,都在这个demo中,
你可以clone下来玩,也能在此基础上做自己的实验。

应用

我们先用golang写一个最简单的HTTP API,你可以用你会的任何语言写出来,
它为GET请求返回 Hello World 加自己的 hostname .

package main

import (
    "io"
    "log"
    "net/http"
    "os"
)

// HelloServer the web server
func HelloServer(w http.ResponseWriter, req *http.Request) {
    hostname, _ := os.Hostname()
    log.Println(hostname)
    io.WriteString(w, "Hello, world! I am "+hostname+" :)\n")
}

func main() {
    http.HandleFunc("/", HelloServer)
    log.Fatal(http.ListenAndServe(":12345", nil))
}

Docker 化

我们需要把上面的应用做成一个docker镜像,暴露端口12345
接着才有可能使用Docker Swarm启动成集群。
本来做镜像特别简单,但我为了让大家直接拉镜像测试时快一点,用了两步构建,
先编译出应用,然后添加到比较小的alpine镜像中。大家可以不必在意这些细节。
我们还是先来看看最终的docker-compose.yml编排文件吧。

version: '3'
services:
    app:
        image: muninn/caddy-microservice:app
        deploy:
            replicas: 3
    gateway:
        image: muninn/caddy-microservice:gateway
        ports:
            - 2015:2015
        depends_on:
            - app
        deploy:
            replicas: 1
            placement:
                constraints: [node.role == manager]

这是最新版本的docker-compose文件,不再由docker-compose命令启动,而是要用docker stack deploy命令。
总之现在这个版本在编排方面还没有完全整合好,有点晕,不过能用。现在我们看到编排中有两个镜像:

  • muninn/caddy-microservice:app 这是我们上一节说的app镜像,我们将启动3个实例,测试上层的负载均衡。

  • muninn/caddy-microservice:gateway 这是我们接下来要讲的gateway了,它监听2015端口并将请求转发给app。

用 caddy 当作 gateway

为了让caddy当作gateway,我们主要来看一下Caddyfile:

:2015 {
    proxy / app:12345
}

好吧,它太简单了。它监听本机的2015端口,将所有的请求都转发到 app:12345 。
这个app,其实是一个域名,在docker swarm的网络中,它会被解析到这个名字服务随机的一个实例。

将来如果有很多app,将不同的请求前缀转发到不同的app就好啦。
所以记得写规范的时候让一个app的endpoint前缀尽量用一样的。

然后caddy也需要被容器化,感兴趣的可以看看Dockerfile.gateway .

运行服务端

理解了上面的内容,就可以开始运行服务端了。直接用我上传到云端的镜像就可以。本文用到的三个镜像下载时总计26M左右,不大。
clone我背景章节提到的库进入项目目录,或者仅仅复制上文提到的compose文件存成docker-compose.yml,然后执行如下命令。

docker-compose pull
docker stack deploy -c docker-compose.yml caddy

啊,对了,第二个stack命令需要你已经将docker切到了swarm模式,如果没有会自动出来提示,根据提示切换即可。
如果成功了,我们检查下状态:

docker stack ps caddy

如果没问题,我们能看到已经启动了3个app和一个gateway。然后我们来测试这个gateway是否能将请求分配到三个后端。

测试

我们是可以通过访问http://{your-host-ip}:2015来测试服务是不是通的,用浏览器或者curl。
然后你会发现,怎么刷新内容都不变啊,并没有像想象中的那样会访问到随机的后端。

不要着急,这个现象并非因为caddy像nginx那样缓存了dns导致均衡失败,而是另一个原因。
caddy为了反向代理的速度,会和后端保持一个连接池。当只有一个客户端的时候,用到总是那第一个连接呢。
为了证明这一点,我们需要并发的访问我们的服务,再看看是否符合我们的预期。

同样的,测试我也为大家准备了镜像,可以直接通过docker使用。

docker run --rm -it muninn/caddy-microservice:client

感兴趣的人可以看client文件夹里的代码,它同时发起了30个请求,并且打印出了3个后端被命中的次数。

另外我还做了一个shell版本,只需要sh test.sh就可以,不过只能看输出拉,没有自动检查结果。

好了,现在我们可以知道,caddy可以很好的胜任微服务架构中的 API Gateway 了。

API Gateway

什么?你说没看出来这是个 API Gateway 啊。我们前边只是解决了容器项目中 API Gateway 和DNS式服务发现配合的一个难题,
接下来就简单了啊,我们写n个app,每个app是一个微服务,在gateway中把不同的url路由到不同的app就好了啊。

进阶

caddy还可以轻松的顺便把认证中心做了,微服务建议用jwt做认证,将权限携带在token中,caddy稍微配置下就可以。
我后续也会给出教程和demo 。auth2.0我认为并不适合微服务架构,但依然是有个复杂的架构方案的,这个主题改天再说。

caddy还可以做API状态监控,缓存,限流等API gateway的职责,不过这些就要你进行一些开发了。
你还有什么更多的想法吗?欢迎留言。

查看原文

赞 7 收藏 8 评论 1

大呜 分享了头条 · 2018-04-11

文章内容干货多,值得花时间一看

赞 2 收藏 6 评论 1

大呜 收藏了文章 · 2018-03-22

Golang 新手可能会踩的 50 个坑

译文:Golang 新手可能会踩的 50 个坑
原文:50 Shades of Go: Traps, Gotchas, and Common Mistakes
翻译已获作者授权,转载请注明来源。

不久前发现在知乎这篇质量很高的文章,打算加上自己的理解翻译一遍。文章分为三部分:初级篇 1-34,中级篇 35-50,高级篇 51-57

前言

Go 是一门简单有趣的编程语言,与其他语言一样,在使用时不免会遇到很多坑,不过它们大多不是 Go 本身的设计缺陷。如果你刚从其他语言转到 Go,那这篇文章里的坑多半会踩到。

如果花时间学习官方 doc、wiki、讨论邮件列表Rob Pike 的大量文章以及 Go 的源码,会发现这篇文章中的坑是很常见的,新手跳过这些坑,能减少大量调试代码的时间。

初级篇:1-34

1. 左大括号 { 不能单独放一行

在其他大多数语言中,{ 的位置你自行决定。Go 比较特别,遵守分号注入规则(automatic semicolon injection):编译器会在每行代码尾部特定分隔符后加 ; 来分隔多条语句,比如会在 ) 后加分号:

// 错误示例
func main()                    
{
    println("hello world")
}

// 等效于
func main();    // 无函数体                    
{
    println("hello world")
}
./main.go: missing function body
./main.go: syntax error: unexpected semicolon or newline before {
// 正确示例
func main() {
    println("hello world")
}     

2. 未使用的变量

如果在函数体代码中有未使用的变量,则无法通过编译,不过全局变量声明但不使用是可以的。

即使变量声明后为变量赋值,依旧无法通过编译,需在某处使用它:

// 错误示例
var gvar int     // 全局变量,声明不使用也可以

func main() {
    var one int     // error: one declared and not used
    two := 2    // error: two declared and not used
    var three int    // error: three declared and not used
    three = 3        
}


// 正确示例
// 可以直接注释或移除未使用的变量
func main() {
    var one int
    _ = one
    
    two := 2
    println(two)
    
    var three int
    one = three

    var four int
    four = four
}

3. 未使用的 import

如果你 import 一个包,但包中的变量、函数、接口和结构体一个都没有用到的话,将编译失败。

可以使用 _ 下划线符号作为别名来忽略导入的包,从而避免编译错误,这只会执行 package 的 init()

// 错误示例
import (
    "fmt"    // imported and not used: "fmt"
    "log"    // imported and not used: "log"
    "time"    // imported and not used: "time"
)

func main() {
}


// 正确示例
// 可以使用 goimports 工具来注释或移除未使用到的包
import (
    _ "fmt"
    "log"
    "time"
)

func main() {
    _ = log.Println
    _ = time.Now
}

4. 简短声明的变量只能在函数内部使用

// 错误示例
myvar := 1    // syntax error: non-declaration statement outside function body
func main() {
}


// 正确示例
var  myvar = 1
func main() {
}

5. 使用简短声明来重复声明变量

不能用简短声明方式来单独为一个变量重复声明, := 左侧至少有一个新变量,才允许多变量的重复声明:

// 错误示例
func main() {  
    one := 0
    one := 1 // error: no new variables on left side of :=
}


// 正确示例
func main() {
    one := 0
    one, two := 1, 2    // two 是新变量,允许 one 的重复声明。比如 error 处理经常用同名变量 err
    one, two = two, one    // 交换两个变量值的简写
}

6. 不能使用简短声明来设置字段的值

struct 的变量字段不能使用 := 来赋值以使用预定义的变量来避免解决:

// 错误示例
type info struct {
    result int
}

func work() (int, error) {
    return 3, nil
}

func main() {
    var data info
    data.result, err := work()    // error: non-name data.result on left side of :=
    fmt.Printf("info: %+v\n", data)
}


// 正确示例
func main() {
    var data info
    var err error    // err 需要预声明

    data.result, err = work()
    if err != nil {
        fmt.Println(err)
        return
    }

    fmt.Printf("info: %+v\n", data)
}

7. 不小心覆盖了变量

对从动态语言转过来的开发者来说,简短声明很好用,这可能会让人误会 := 是一个赋值操作符。

如果你在新的代码块中像下边这样误用了 :=,编译不会报错,但是变量不会按你的预期工作:

func main() {
    x := 1
    println(x)        // 1
    {
        println(x)    // 1
        x := 2
        println(x)    // 2    // 新的 x 变量的作用域只在代码块内部
    }
    println(x)        // 1
}

这是 Go 开发者常犯的错,而且不易被发现。

可使用 vet 工具来诊断这种变量覆盖,Go 默认不做覆盖检查,添加 -shadow 选项来启用:

> go tool vet -shadow main.go
main.go:9: declaration of "x" shadows declaration at main.go:5

注意 vet 不会报告全部被覆盖的变量,可以使用 go-nyet 来做进一步的检测:

> $GOPATH/bin/go-nyet main.go
main.go:10:3:Shadowing variable `x`

8. 显式类型的变量无法使用 nil 来初始化

nil 是 interface、function、pointer、map、slice 和 channel 类型变量的默认初始值。但声明时不指定类型,编译器也无法推断出变量的具体类型。

// 错误示例
func main() {
    var x = nil    // error: use of untyped nil
    _ = x
}


// 正确示例
func main() {
    var x interface{} = nil
    _ = x
}    

9. 直接使用值为 nil 的 slice、map

允许对值为 nil 的 slice 添加元素,但对值为 nil 的 map 添加元素则会造成运行时 panic

// map 错误示例
func main() {
    var m map[string]int
    m["one"] = 1        // error: panic: assignment to entry in nil map
    // m := make(map[string]int)// map 的正确声明,分配了实际的内存
}    


// slice 正确示例
func main() {
    var s []int
    s = append(s, 1)
}

10. map 容量

在创建 map 类型的变量时可以指定容量,但不能像 slice 一样使用 cap() 来检测分配空间的大小:

// 错误示例
func main() {
    m := make(map[string]int, 99)
    println(cap(m))     // error: invalid argument m1 (type map[string]int) for cap  
}    

11. string 类型的变量值不能为 nil

对那些喜欢用 nil 初始化字符串的人来说,这就是坑:

// 错误示例
func main() {
    var s string = nil    // cannot use nil as type string in assignment
    if s == nil {    // invalid operation: s == nil (mismatched types string and nil)
        s = "default"
    }
}


// 正确示例
func main() {
    var s string    // 字符串类型的零值是空串 ""
    if s == "" {
        s = "default"
    }
}

12. Array 类型的值作为函数参数

在 C/C++ 中,数组(名)是指针。将数组作为参数传进函数时,相当于传递了数组内存地址的引用,在函数内部会改变该数组的值。

在 Go 中,数组是值。作为参数传进函数时,传递的是数组的原始值拷贝,此时在函数内部是无法更新该数组的:

// 数组使用值拷贝传参
func main() {
    x := [3]int{1,2,3}

    func(arr [3]int) {
        arr[0] = 7
        fmt.Println(arr)    // [7 2 3]
    }(x)
    fmt.Println(x)            // [1 2 3]    // 并不是你以为的 [7 2 3]
}

如果想修改参数数组:

  • 直接传递指向这个数组的指针类型:
// 传址会修改原数据
func main() {
    x := [3]int{1,2,3}

    func(arr *[3]int) {
        (*arr)[0] = 7    
        fmt.Println(arr)    // &[7 2 3]
    }(&x)
    fmt.Println(x)    // [7 2 3]
}
  • 直接使用 slice:即使函数内部得到的是 slice 的值拷贝,但依旧会更新 slice 的原始数据(底层 array)
// 会修改 slice 的底层 array,从而修改 slice
func main() {
    x := []int{1, 2, 3}
    func(arr []int) {
        arr[0] = 7
        fmt.Println(x)    // [7 2 3]
    }(x)
    fmt.Println(x)    // [7 2 3]
}

13. range 遍历 slice 和 array 时混淆了返回值

与其他编程语言中的 for-inforeach 遍历语句不同,Go 中的 range 在遍历时会生成 2 个值,第一个是元素索引,第二个是元素的值:

// 错误示例
func main() {
    x := []string{"a", "b", "c"}
    for v := range x {
        fmt.Println(v)    // 1 2 3
    }
}


// 正确示例
func main() {
    x := []string{"a", "b", "c"}
    for _, v := range x {    // 使用 _ 丢弃索引
        fmt.Println(v)
    }
}

14. slice 和 array 其实是一维数据

看起来 Go 支持多维的 array 和 slice,可以创建数组的数组、切片的切片,但其实并不是。

对依赖动态计算多维数组值的应用来说,就性能和复杂度而言,用 Go 实现的效果并不理想。

可以使用原始的一维数组、“独立“ 的切片、“共享底层数组”的切片来创建动态的多维数组。

  1. 使用原始的一维数组:要做好索引检查、溢出检测、以及当数组满时再添加值时要重新做内存分配。
  2. 使用“独立”的切片分两步:
  • 创建外部 slice

    • 对每个内部 slice 进行内存分配

      注意内部的 slice 相互独立,使得任一内部 slice 增缩都不会影响到其他的 slice

// 使用各自独立的 6 个 slice 来创建 [2][3] 的动态多维数组
func main() {
    x := 2
    y := 4
    
    table := make([][]int, x)
    for i  := range table {
        table[i] = make([]int, y)
    }
}
  1. 使用“共享底层数组”的切片
  • 创建一个存放原始数据的容器 slice
  • 创建其他的 slice
  • 切割原始 slice 来初始化其他的 slice
func main() {
    h, w := 2, 4
    raw := make([]int, h*w)

    for i := range raw {
        raw[i] = i
    }

    // 初始化原始 slice
    fmt.Println(raw, &raw[4])    // [0 1 2 3 4 5 6 7] 0xc420012120 
    
    table := make([][]int, h)
    for i := range table {
        
        // 等间距切割原始 slice,创建动态多维数组 table
        // 0: raw[0*4: 0*4 + 4]
        // 1: raw[1*4: 1*4 + 4]
        table[i] = raw[i*w : i*w + w]
    }

    fmt.Println(table, &table[1][0])    // [[0 1 2 3] [4 5 6 7]] 0xc420012120
}

更多关于多维数组的参考

go-how-is-two-dimensional-arrays-memory-representation

what-is-a-concise-way-to-create-a-2d-slice-in-go

15. 访问 map 中不存在的 key

和其他编程语言类似,如果访问了 map 中不存在的 key 则希望能返回 nil,比如在 PHP 中:

> php -r '$v = ["x"=>1, "y"=>2]; @var_dump($v["z"]);'
NULL

Go 则会返回元素对应数据类型的零值,比如 nil''false 和 0,取值操作总有值返回,故不能通过取出来的值来判断 key 是不是在 map 中。

检查 key 是否存在可以用 map 直接访问,检查返回的第二个参数即可:

// 错误的 key 检测方式
func main() {
    x := map[string]string{"one": "2", "two": "", "three": "3"}
    if v := x["two"]; v == "" {
        fmt.Println("key two is no entry")    // 键 two 存不存在都会返回的空字符串
    }
}

// 正确示例
func main() {
    x := map[string]string{"one": "2", "two": "", "three": "3"}
    if _, ok := x["two"]; !ok {
        fmt.Println("key two is no entry")
    }
}

16. string 类型的值是常量,不可更改

尝试使用索引遍历字符串,来更新字符串中的个别字符,是不允许的。

string 类型的值是只读的二进制 byte slice,如果真要修改字符串中的字符,将 string 转为 []byte 修改后,再转为 string 即可:

// 修改字符串的错误示例
func main() {
    x := "text"
    x[0] = "T"        // error: cannot assign to x[0]
    fmt.Println(x)
}


// 修改示例
func main() {
    x := "text"
    xBytes := []byte(x)
    xBytes[0] = 'T'    // 注意此时的 T 是 rune 类型
    x = string(xBytes)
    fmt.Println(x)    // Text
}

注意: 上边的示例并不是更新字符串的正确姿势,因为一个 UTF8 编码的字符可能会占多个字节,比如汉字就需要 3~4 个字节来存储,此时更新其中的一个字节是错误的。

更新字串的正确姿势:将 string 转为 rune slice(此时 1 个 rune 可能占多个 byte),直接更新 rune 中的字符

func main() {
    x := "text"
    xRunes := []rune(x)
    xRunes[0] = '我'
    x = string(xRunes)
    fmt.Println(x)    // 我ext
}

17. string 与 byte slice 之间的转换

当进行 string 和 byte slice 相互转换时,参与转换的是拷贝的原始值。这种转换的过程,与其他编程语的强制类型转换操作不同,也和新 slice 与旧 slice 共享底层数组不同。

Go 在 string 与 byte slice 相互转换上优化了两点,避免了额外的内存分配:

  • map[string] 中查找 key 时,使用了对应的 []byte,避免做 m[string(key)] 的内存分配
  • 使用 for range 迭代 string 转换为 []byte 的迭代:for i,v := range []byte(str) {...}

雾:参考原文

18. string 与索引操作符

对字符串用索引访问返回的不是字符,而是一个 byte 值。

这种处理方式和其他语言一样,比如 PHP 中:

> php -r '$name="中文"; var_dump($name);'    # "中文" 占用 6 个字节
string(6) "中文"

> php -r '$name="中文"; var_dump($name[0]);' # 把第一个字节当做 Unicode 字符读取,显示 U+FFFD
string(1) "�"    

> php -r '$name="中文"; var_dump($name[0].$name[1].$name[2]);'
string(3) "中"
func main() {
    x := "ascii"
    fmt.Println(x[0])        // 97
    fmt.Printf("%T\n", x[0])// uint8
}

如果需要使用 for range 迭代访问字符串中的字符(unicode code point / rune),标准库中有 "unicode/utf8" 包来做 UTF8 的相关解码编码。另外 utf8string 也有像 func (s *String) At(i int) rune 等很方便的库函数。

19. 字符串并不都是 UTF8 文本

string 的值不必是 UTF8 文本,可以包含任意的值。只有字符串是文字字面值时才是 UTF8 文本,字串可以通过转义来包含其他数据。

判断字符串是否是 UTF8 文本,可使用 "unicode/utf8" 包中的 ValidString() 函数:

func main() {
    str1 := "ABC"
    fmt.Println(utf8.ValidString(str1))    // true

    str2 := "A\xfeC"
    fmt.Println(utf8.ValidString(str2))    // false

    str3 := "A\\xfeC"
    fmt.Println(utf8.ValidString(str3))    // true    // 把转义字符转义成字面值
}

20. 字符串的长度

在 Python 中:

data = u'♥'  
print(len(data)) # 1

然而在 Go 中:

func main() {
    char := "♥"
    fmt.Println(len(char))    // 3
}

Go 的内建函数 len() 返回的是字符串的 byte 数量,而不是像 Python 中那样是计算 Unicode 字符数。

如果要得到字符串的字符数,可使用 "unicode/utf8" 包中的 RuneCountInString(str string) (n int)

func main() {
    char := "♥"
    fmt.Println(utf8.RuneCountInString(char))    // 1
}

注意:RuneCountInString 并不总是返回我们看到的字符数,因为有的字符会占用 2 个 rune:

func main() {
    char := "é"
    fmt.Println(len(char))    // 3
    fmt.Println(utf8.RuneCountInString(char))    // 2
    fmt.Println("cafe\u0301")    // café    // 法文的 cafe,实际上是两个 rune 的组合
}

参考:normalization

21. 在多行 array、slice、map 语句中缺少 ,

func main() {
    x := []int {
        1,
        2    // syntax error: unexpected newline, expecting comma or }
    }
    y := []int{1,2,}    
    z := []int{1,2}    
    // ...
}

声明语句中 } 折叠到单行后,尾部的 , 不是必需的。

22. log.Fatallog.Panic 不只是 log

log 标准库提供了不同的日志记录等级,与其他语言的日志库不同,Go 的 log 包在调用 Fatal*()Panic*() 时能做更多日志外的事,如中断程序的执行等:

func main() {
    log.Fatal("Fatal level log: log entry")        // 输出信息后,程序终止执行
    log.Println("Nomal level log: log entry")
}

23. 对内建数据结构的操作并不是同步的

尽管 Go 本身有大量的特性来支持并发,但并不保证并发的数据安全,用户需自己保证变量等数据以原子操作更新。

goroutine 和 channel 是进行原子操作的好方法,或使用 "sync" 包中的锁。

24. range 迭代 string 得到的值

range 得到的索引是字符值(Unicode point / rune)第一个字节的位置,与其他编程语言不同,这个索引并不直接是字符在字符串中的位置。

注意一个字符可能占多个 rune,比如法文单词 café 中的 é。操作特殊字符可使用norm 包。

for range 迭代会尝试将 string 翻译为 UTF8 文本,对任何无效的码点都直接使用 0XFFFD rune(�)UNicode 替代字符来表示。如果 string 中有任何非 UTF8 的数据,应将 string 保存为 byte slice 再进行操作。

func main() {
    data := "A\xfe\x02\xff\x04"
    for _, v := range data {
        fmt.Printf("%#x ", v)    // 0x41 0xfffd 0x2 0xfffd 0x4    // 错误
    }

    for _, v := range []byte(data) {
        fmt.Printf("%#x ", v)    // 0x41 0xfe 0x2 0xff 0x4    // 正确
    }
}

25. range 迭代 map

如果你希望以特定的顺序(如按 key 排序)来迭代 map,要注意每次迭代都可能产生不一样的结果。

Go 的运行时是有意打乱迭代顺序的,所以你得到的迭代结果可能不一致。但也并不总会打乱,得到连续相同的 5 个迭代结果也是可能的,如:

func main() {
    m := map[string]int{"one": 1, "two": 2, "three": 3, "four": 4}
    for k, v := range m {
        fmt.Println(k, v)
    }
}

如果你去 Go Playground 重复运行上边的代码,输出是不会变的,只有你更新代码它才会重新编译。重新编译后迭代顺序是被打乱的:

26. switch 中的 fallthrough 语句

switch 语句中的 case 代码块会默认带上 break,但可以使用 fallthrough 来强制执行下一个 case 代码块。

func main() {
    isSpace := func(char byte) bool {
        switch char {
        case ' ':    // 空格符会直接 break,返回 false // 和其他语言不一样
        // fallthrough    // 返回 true
        case '\t':
            return true
        }
        return false
    }
    fmt.Println(isSpace('\t'))    // true
    fmt.Println(isSpace(' '))    // false
}

不过你可以在 case 代码块末尾使用 fallthrough,强制执行下一个 case 代码块。

也可以改写 case 为多条件判断:

func main() {
    isSpace := func(char byte) bool {
        switch char {
        case ' ', '\t':
            return true
        }
        return false
    }
    fmt.Println(isSpace('\t'))    // true
    fmt.Println(isSpace(' '))    // true
}

27. 自增和自减运算

很多编程语言都自带前置后置的 ++-- 运算。但 Go 特立独行,去掉了前置操作,同时 ++ 只作为运算符而非表达式。

// 错误示例
func main() {
    data := []int{1, 2, 3}
    i := 0
    ++i            // syntax error: unexpected ++, expecting }
    fmt.Println(data[i++])    // syntax error: unexpected ++, expecting :
}


// 正确示例
func main() {
    data := []int{1, 2, 3}
    i := 0
    i++
    fmt.Println(data[i])    // 2
}

28. 按位取反

很多编程语言使用 ~ 作为一元按位取反(NOT)操作符,Go 重用 ^ XOR 操作符来按位取反:

// 错误的取反操作
func main() {
    fmt.Println(~2)        // bitwise complement operator is ^
}


// 正确示例
func main() {
    var d uint8 = 2
    fmt.Printf("%08b\n", d)        // 00000010
    fmt.Printf("%08b\n", ^d)    // 11111101
}

同时 ^ 也是按位异或(XOR)操作符。

一个操作符能重用两次,是因为一元的 NOT 操作 NOT 0x02,与二元的 XOR 操作 0x22 XOR 0xff 是一致的。

Go 也有特殊的操作符 AND NOT &^ 操作符,不同位才取1。

func main() {
    var a uint8 = 0x82
    var b uint8 = 0x02
    fmt.Printf("%08b [A]\n", a)
    fmt.Printf("%08b [B]\n", b)

    fmt.Printf("%08b (NOT B)\n", ^b)
    fmt.Printf("%08b ^ %08b = %08b [B XOR 0xff]\n", b, 0xff, b^0xff)

    fmt.Printf("%08b ^ %08b = %08b [A XOR B]\n", a, b, a^b)
    fmt.Printf("%08b & %08b = %08b [A AND B]\n", a, b, a&b)
    fmt.Printf("%08b &^%08b = %08b [A 'AND NOT' B]\n", a, b, a&^b)
    fmt.Printf("%08b&(^%08b)= %08b [A AND (NOT B)]\n", a, b, a&(^b))
}
10000010 [A]
00000010 [B]
11111101 (NOT B)
00000010 ^ 11111111 = 11111101 [B XOR 0xff]
10000010 ^ 00000010 = 10000000 [A XOR B]
10000010 & 00000010 = 00000010 [A AND B]
10000010 &^00000010 = 10000000 [A 'AND NOT' B]
10000010&(^00000010)= 10000000 [A AND (NOT B)]

29. 运算符的优先级

除了位清除(bit clear)操作符,Go 也有很多和其他语言一样的位操作符,但优先级另当别论。

func main() {
    fmt.Printf("0x2 & 0x2 + 0x4 -> %#x\n", 0x2&0x2+0x4)    // & 优先 +
    //prints: 0x2 & 0x2 + 0x4 -> 0x6
    //Go:    (0x2 & 0x2) + 0x4
    //C++:    0x2 & (0x2 + 0x4) -> 0x2

    fmt.Printf("0x2 + 0x2 << 0x1 -> %#x\n", 0x2+0x2<<0x1)    // << 优先 +
    //prints: 0x2 + 0x2 << 0x1 -> 0x6
    //Go:     0x2 + (0x2 << 0x1)
    //C++:   (0x2 + 0x2) << 0x1 -> 0x8

    fmt.Printf("0xf | 0x2 ^ 0x2 -> %#x\n", 0xf|0x2^0x2)    // | 优先 ^
    //prints: 0xf | 0x2 ^ 0x2 -> 0xd
    //Go:    (0xf | 0x2) ^ 0x2
    //C++:    0xf | (0x2 ^ 0x2) -> 0xf
}

优先级列表:

Precedence    Operator
    5             *  /  %  <<  >>  &  &^
    4             +  -  |  ^
    3             ==  !=  <  <=  >  >=
    2             &&
    1             ||

30. 不导出的 struct 字段无法被 encode

以小写字母开头的字段成员是无法被外部直接访问的,所以 struct 在进行 json、xml、gob 等格式的 encode 操作时,这些私有字段会被忽略,导出时得到零值:

func main() {
    in := MyData{1, "two"}
    fmt.Printf("%#v\n", in)    // main.MyData{One:1, two:"two"}

    encoded, _ := json.Marshal(in)
    fmt.Println(string(encoded))    // {"One":1}    // 私有字段 two 被忽略了

    var out MyData
    json.Unmarshal(encoded, &out)
    fmt.Printf("%#v\n", out)     // main.MyData{One:1, two:""}
}

31. 程序退出时还有 goroutine 在执行

程序默认不等所有 goroutine 都执行完才退出,这点需要特别注意:

// 主程序会直接退出
func main() {
    workerCount := 2
    for i := 0; i < workerCount; i++ {
        go doIt(i)
    }
    time.Sleep(1 * time.Second)
    fmt.Println("all done!")
}

func doIt(workerID int) {
    fmt.Printf("[%v] is running\n", workerID)
    time.Sleep(3 * time.Second)        // 模拟 goroutine 正在执行 
    fmt.Printf("[%v] is done\n", workerID)
}

如下,main() 主程序不等两个 goroutine 执行完就直接退出了:

常用解决办法:使用 "WaitGroup" 变量,它会让主程序等待所有 goroutine 执行完毕再退出。

如果你的 goroutine 要做消息的循环处理等耗时操作,可以向它们发送一条 kill 消息来关闭它们。或直接关闭一个它们都等待接收数据的 channel:

// 等待所有 goroutine 执行完毕
// 进入死锁
func main() {
    var wg sync.WaitGroup
    done := make(chan struct{})

    workerCount := 2
    for i := 0; i < workerCount; i++ {
        wg.Add(1)
        go doIt(i, done, wg)
    }

    close(done)
    wg.Wait()
    fmt.Println("all done!")
}

func doIt(workerID int, done <-chan struct{}, wg sync.WaitGroup) {
    fmt.Printf("[%v] is running\n", workerID)
    defer wg.Done()
    <-done
    fmt.Printf("[%v] is done\n", workerID)
}

执行结果:

看起来好像 goroutine 都执行完了,然而报错:

fatal error: all goroutines are asleep - deadlock!

为什么会发生死锁?goroutine 在退出前调用了 wg.Done() ,程序应该正常退出的。

原因是 goroutine 得到的 "WaitGroup" 变量是 var wg WaitGroup 的一份拷贝值,即 doIt() 传参只传值。所以哪怕在每个 goroutine 中都调用了 wg.Done(), 主程序中的 wg 变量并不会受到影响。

// 等待所有 goroutine 执行完毕
// 使用传址方式为 WaitGroup 变量传参
// 使用 channel 关闭 goroutine

func main() {
    var wg sync.WaitGroup
    done := make(chan struct{})
    ch := make(chan interface{})

    workerCount := 2
    for i := 0; i < workerCount; i++ {
        wg.Add(1)
        go doIt(i, ch, done, &wg)    // wg 传指针,doIt() 内部会改变 wg 的值
    }

    for i := 0; i < workerCount; i++ {    // 向 ch 中发送数据,关闭 goroutine
        ch <- i
    }

    close(done)
    wg.Wait()
    close(ch)
    fmt.Println("all done!")
}

func doIt(workerID int, ch <-chan interface{}, done <-chan struct{}, wg *sync.WaitGroup) {
    fmt.Printf("[%v] is running\n", workerID)
    defer wg.Done()
    for {
        select {
        case m := <-ch:
            fmt.Printf("[%v] m => %v\n", workerID, m)
        case <-done:
            fmt.Printf("[%v] is done\n", workerID)
            return
        }
    }
}

运行效果:

32. 向无缓冲的 channel 发送数据,只要 receiver 准备好了就会立刻返回

只有在数据被 receiver 处理时,sender 才会阻塞。因运行环境而异,在 sender 发送完数据后,receiver 的 goroutine 可能没有足够的时间处理下一个数据。如:

func main() {
    ch := make(chan string)

    go func() {
        for m := range ch {
            fmt.Println("Processed:", m)
            time.Sleep(1 * time.Second)    // 模拟需要长时间运行的操作
        }
    }()

    ch <- "cmd.1"
    ch <- "cmd.2" // 不会被接收处理
}

运行效果:

33. 向已关闭的 channel 发送数据会造成 panic

从已关闭的 channel 接收数据是安全的:

接收状态值 okfalse 时表明 channel 中已没有数据可以接收了。类似的,从有缓冲的 channel 中接收数据,缓存的数据获取完再没有数据可取时,状态值也是 false

向已关闭的 channel 中发送数据会造成 panic:

func main() {
    ch := make(chan int)
    for i := 0; i < 3; i++ {
        go func(idx int) {
            ch <- idx
        }(i)
    }

    fmt.Println(<-ch)        // 输出第一个发送的值
    close(ch)            // 不能关闭,还有其他的 sender
    time.Sleep(2 * time.Second)    // 模拟做其他的操作
}

运行结果:

针对上边有 bug 的这个例子,可使用一个废弃 channel done 来告诉剩余的 goroutine 无需再向 ch 发送数据。此时 <- done 的结果是 {}

func main() {
    ch := make(chan int)
    done := make(chan struct{})

    for i := 0; i < 3; i++ {
        go func(idx int) {
            select {
            case ch <- (idx + 1) * 2:
                fmt.Println(idx, "Send result")
            case <-done:
                fmt.Println(idx, "Exiting")
            }
        }(i)
    }

    fmt.Println("Result: ", <-ch)
    close(done)
    time.Sleep(3 * time.Second)
}

运行效果:

34. 使用了值为 nil 的 channel

在一个值为 nil 的 channel 上发送和接收数据将永久阻塞:

func main() {
    var ch chan int // 未初始化,值为 nil
    for i := 0; i < 3; i++ {
        go func(i int) {
            ch <- i
        }(i)
    }

    fmt.Println("Result: ", <-ch)
    time.Sleep(2 * time.Second)
}

runtime 死锁错误:

fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive (nil chan)]

利用这个死锁的特性,可以用在 select 中动态的打开和关闭 case 语句块:

func main() {
    inCh := make(chan int)
    outCh := make(chan int)

    go func() {
        var in <-chan int = inCh
        var out chan<- int
        var val int

        for {
            select {
            case out <- val:
                println("--------")
                out = nil
                in = inCh
            case val = <-in:
                println("++++++++++")
                out = outCh
                in = nil
            }
        }
    }()

    go func() {
        for r := range outCh {
            fmt.Println("Result: ", r)
        }
    }()

    time.Sleep(0)
    inCh <- 1
    inCh <- 2
    time.Sleep(3 * time.Second)
}

运行效果:

34. 若函数 receiver 传参是传值方式,则无法修改参数的原有值

方法 receiver 的参数与一般函数的参数类似:如果声明为值,那方法体得到的是一份参数的值拷贝,此时对参数的任何修改都不会对原有值产生影响。

除非 receiver 参数是 map 或 slice 类型的变量,并且是以指针方式更新 map 中的字段、slice 中的元素的,才会更新原有值:

type data struct {
    num   int
    key   *string
    items map[string]bool
}

func (this *data) pointerFunc() {
    this.num = 7
}

func (this data) valueFunc() {
    this.num = 8
    *this.key = "valueFunc.key"
    this.items["valueFunc"] = true
}

func main() {
    key := "key1"

    d := data{1, &key, make(map[string]bool)}
    fmt.Printf("num=%v  key=%v  items=%v\n", d.num, *d.key, d.items)

    d.pointerFunc()    // 修改 num 的值为 7
    fmt.Printf("num=%v  key=%v  items=%v\n", d.num, *d.key, d.items)

    d.valueFunc()    // 修改 key 和 items 的值
    fmt.Printf("num=%v  key=%v  items=%v\n", d.num, *d.key, d.items)
}

运行结果:

中级篇:35-50

35. 关闭 HTTP 的响应体

使用 HTTP 标准库发起请求、获取响应时,即使你不从响应中读取任何数据或响应为空,都需要手动关闭响应体。新手很容易忘记手动关闭,或者写在了错误的位置:

// 请求失败造成 panic
func main() {
    resp, err := http.Get("https://api.ipify.org?format=json")
    defer resp.Body.Close()    // resp 可能为 nil,不能读取 Body
    if err != nil {
        fmt.Println(err)
        return
    }

    body, err := ioutil.ReadAll(resp.Body)
    checkError(err)

    fmt.Println(string(body))
}

func checkError(err error) {
    if err != nil{
        log.Fatalln(err)
    }
}

上边的代码能正确发起请求,但是一旦请求失败,变量 resp 值为 nil,造成 panic:

panic: runtime error: invalid memory address or nil pointer dereference

应该先检查 HTTP 响应错误为 nil,再调用 resp.Body.Close() 来关闭响应体:

// 大多数情况正确的示例
func main() {
    resp, err := http.Get("https://api.ipify.org?format=json")
    checkError(err)
    
    defer resp.Body.Close()    // 绝大多数情况下的正确关闭方式
    body, err := ioutil.ReadAll(resp.Body)
    checkError(err)

    fmt.Println(string(body))
}

输出:

Get https://api.ipify.org?format=...: x509: certificate signed by unknown authority

绝大多数请求失败的情况下,resp 的值为 nilerrnon-nil。但如果你得到的是重定向错误,那它俩的值都是 non-nil,最后依旧可能发生内存泄露。2 个解决办法:

  • 可以直接在处理 HTTP 响应错误的代码块中,直接关闭非 nil 的响应体。
  • 手动调用 defer 来关闭响应体:
// 正确示例
func main() {
    resp, err := http.Get("http://www.baidu.com")
    
    // 关闭 resp.Body 的正确姿势
    if resp != nil {
        defer resp.Body.Close()
    }

    checkError(err)
    defer resp.Body.Close()

    body, err := ioutil.ReadAll(resp.Body)
    checkError(err)

    fmt.Println(string(body))
}

resp.Body.Close() 早先版本的实现是读取响应体的数据之后丢弃,保证了 keep-alive 的 HTTP 连接能重用处理不止一个请求。但 Go 的最新版本将读取并丢弃数据的任务交给了用户,如果你不处理,HTTP 连接可能会直接关闭而非重用,参考在 Go 1.5 版本文档。

如果程序大量重用 HTTP 长连接,你可能要在处理响应的逻辑代码中加入:

_, err = io.Copy(ioutil.Discard, resp.Body)    // 手动丢弃读取完毕的数据

如果你需要完整读取响应,上边的代码是需要写的。比如在解码 API 的 JSON 响应数据:

json.NewDecoder(resp.Body).Decode(&data)  

36. 关闭 HTTP 连接

一些支持 HTTP1.1 或 HTTP1.0 配置了 connection: keep-alive 选项的服务器会保持一段时间的长连接。但标准库 "net/http" 的连接默认只在服务器主动要求关闭时才断开,所以你的程序可能会消耗完 socket 描述符。解决办法有 2 个,请求结束后:

  • 直接设置请求变量的 Close 字段值为 true,每次请求结束后就会主动关闭连接。
  • 设置 Header 请求头部选项 Connection: close,然后服务器返回的响应头部也会有这个选项,此时 HTTP 标准库会主动断开连接。
// 主动关闭连接
func main() {
    req, err := http.NewRequest("GET", "http://golang.org", nil)
    checkError(err)

    req.Close = true
    //req.Header.Add("Connection", "close")    // 等效的关闭方式

    resp, err := http.DefaultClient.Do(req)
    if resp != nil {
        defer resp.Body.Close()
    }
    checkError(err)

    body, err := ioutil.ReadAll(resp.Body)
    checkError(err)

    fmt.Println(string(body))
}

你可以创建一个自定义配置的 HTTP transport 客户端,用来取消 HTTP 全局的复用连接:

func main() {
    tr := http.Transport{DisableKeepAlives: true}
    client := http.Client{Transport: &tr}

    resp, err := client.Get("https://golang.google.cn/")
    if resp != nil {
        defer resp.Body.Close()
    }
    checkError(err)

    fmt.Println(resp.StatusCode)    // 200

    body, err := ioutil.ReadAll(resp.Body)
    checkError(err)

    fmt.Println(len(string(body)))
}

根据需求选择使用场景:

  • 若你的程序要向同一服务器发大量请求,使用默认的保持长连接。
  • 若你的程序要连接大量的服务器,且每台服务器只请求一两次,那收到请求后直接关闭连接。或增加最大文件打开数 fs.file-max 的值。

37. 将 JSON 中的数字解码为 interface 类型

在 encode/decode JSON 数据时,Go 默认会将数值当做 float64 处理,比如下边的代码会造成 panic:

func main() {
    var data = []byte(`{"status": 200}`)
    var result map[string]interface{}

    if err := json.Unmarshal(data, &result); err != nil {
        log.Fatalln(err)
    }

    fmt.Printf("%T\n", result["status"])    // float64
    var status = result["status"].(int)    // 类型断言错误
    fmt.Println("Status value: ", status)
}
panic: interface conversion: interface {} is float64, not int

如果你尝试 decode 的 JSON 字段是整型,你可以:

  • 将 int 值转为 float 统一使用
  • 将 decode 后需要的 float 值转为 int 使用
// 将 decode 的值转为 int 使用
func main() {
    var data = []byte(`{"status": 200}`)
    var result map[string]interface{}

    if err := json.Unmarshal(data, &result); err != nil {
        log.Fatalln(err)
    }

    var status = uint64(result["status"].(float64))
    fmt.Println("Status value: ", status)
}
  • 使用 Decoder 类型来 decode JSON 数据,明确表示字段的值类型
// 指定字段类型
func main() {
    var data = []byte(`{"status": 200}`)
    var result map[string]interface{}
    
    var decoder = json.NewDecoder(bytes.NewReader(data))
    decoder.UseNumber()

    if err := decoder.Decode(&result); err != nil {
        log.Fatalln(err)
    }

    var status, _ = result["status"].(json.Number).Int64()
    fmt.Println("Status value: ", status)
}

 // 你可以使用 string 来存储数值数据,在 decode 时再决定按 int 还是 float 使用
 // 将数据转为 decode 为 string
 func main() {
     var data = []byte({"status": 200})
      var result map[string]interface{}
      var decoder = json.NewDecoder(bytes.NewReader(data))
      decoder.UseNumber()
      if err := decoder.Decode(&result); err != nil {
          log.Fatalln(err)
      }
    var status uint64
      err := json.Unmarshal([]byte(result["status"].(json.Number).String()), &status);
    checkError(err)
       fmt.Println("Status value: ", status)
}

​- 使用 struct 类型将你需要的数据映射为数值型

// struct 中指定字段类型
func main() {
      var data = []byte(`{"status": 200}`)
      var result struct {
          Status uint64 `json:"status"`
      }

      err := json.NewDecoder(bytes.NewReader(data)).Decode(&result)
      checkError(err)
    fmt.Printf("Result: %+v", result)
}
  • 可以使用 struct 将数值类型映射为 json.RawMessage 原生数据类型

    适用于如果 JSON 数据不着急 decode 或 JSON 某个字段的值类型不固定等情况:

// 状态名称可能是 int 也可能是 string,指定为 json.RawMessage 类型
func main() {
    records := [][]byte{
        []byte(`{"status":200, "tag":"one"}`),
        []byte(`{"status":"ok", "tag":"two"}`),
    }

    for idx, record := range records {
        var result struct {
            StatusCode uint64
            StatusName string
            Status     json.RawMessage `json:"status"`
            Tag        string          `json:"tag"`
        }

        err := json.NewDecoder(bytes.NewReader(record)).Decode(&result)
        checkError(err)

        var name string
        err = json.Unmarshal(result.Status, &name)
        if err == nil {
            result.StatusName = name
        }

        var code uint64
        err = json.Unmarshal(result.Status, &code)
        if err == nil {
            result.StatusCode = code
        }

        fmt.Printf("[%v] result => %+v\n", idx, result)
    }
}

38. struct、array、slice 和 map 的值比较

可以使用相等运算符 == 来比较结构体变量,前提是两个结构体的成员都是可比较的类型:

type data struct {
    num     int
    fp      float32
    complex complex64
    str     string
    char    rune
    yes     bool
    events  <-chan string
    handler interface{}
    ref     *byte
    raw     [10]byte
}

func main() {
    v1 := data{}
    v2 := data{}
    fmt.Println("v1 == v2: ", v1 == v2)    // true
}

如果两个结构体中有任意成员是不可比较的,将会造成编译错误。注意数组成员只有在数组元素可比较时候才可比较。

type data struct {
    num    int
    checks [10]func() bool        // 无法比较
    doIt   func() bool        // 无法比较
    m      map[string]string    // 无法比较
    bytes  []byte            // 无法比较
}

func main() {
    v1 := data{}
    v2 := data{}

    fmt.Println("v1 == v2: ", v1 == v2)
}
invalid operation: v1 == v2 (struct containing [10]func() bool cannot be compared)

Go 提供了一些库函数来比较那些无法使用 == 比较的变量,比如使用 "reflect" 包的 DeepEqual()

// 比较相等运算符无法比较的元素
func main() {
    v1 := data{}
    v2 := data{}
    fmt.Println("v1 == v2: ", reflect.DeepEqual(v1, v2))    // true

    m1 := map[string]string{"one": "a", "two": "b"}
    m2 := map[string]string{"two": "b", "one": "a"}
    fmt.Println("v1 == v2: ", reflect.DeepEqual(m1, m2))    // true

    s1 := []int{1, 2, 3}
    s2 := []int{1, 2, 3}
       // 注意两个 slice 相等,值和顺序必须一致
    fmt.Println("v1 == v2: ", reflect.DeepEqual(s1, s2))    // true
}

这种比较方式可能比较慢,根据你的程序需求来使用。DeepEqual() 还有其他用法:

func main() {
    var b1 []byte = nil
    b2 := []byte{}
    fmt.Println("b1 == b2: ", reflect.DeepEqual(b1, b2))    // false
}

注意:

  • DeepEqual() 并不总适合于比较 slice
func main() {
    var str = "one"
    var in interface{} = "one"
    fmt.Println("str == in: ", reflect.DeepEqual(str, in))    // true

    v1 := []string{"one", "two"}
    v2 := []string{"two", "one"}
    fmt.Println("v1 == v2: ", reflect.DeepEqual(v1, v2))    // false

    data := map[string]interface{}{
        "code":  200,
        "value": []string{"one", "two"},
    }
    encoded, _ := json.Marshal(data)
    var decoded map[string]interface{}
    json.Unmarshal(encoded, &decoded)
    fmt.Println("data == decoded: ", reflect.DeepEqual(data, decoded))    // false
}

如果要大小写不敏感来比较 byte 或 string 中的英文文本,可以使用 "bytes" 或 "strings" 包的 ToUpper()ToLower() 函数。比较其他语言的 byte 或 string,应使用 bytes.EqualFold()strings.EqualFold()

如果 byte slice 中含有验证用户身份的数据(密文哈希、token 等),不应再使用 reflect.DeepEqual()bytes.Equal()bytes.Compare()。这三个函数容易对程序造成 timing attacks,此时应使用 "crypto/subtle" 包中的 subtle.ConstantTimeCompare() 等函数

  • reflect.DeepEqual() 认为空 slice 与 nil slice 并不相等,但注意 byte.Equal() 会认为二者相等:
func main() {
    var b1 []byte = nil
    b2 := []byte{}

    // b1 与 b2 长度相等、有相同的字节序
    // nil 与 slice 在字节上是相同的
    fmt.Println("b1 == b2: ", bytes.Equal(b1, b2))    // true
}

39. 从 panic 中恢复

在一个 defer 延迟执行的函数中调用 recover() ,它便能捕捉 / 中断 panic

// 错误的 recover 调用示例
func main() {
    recover()    // 什么都不会捕捉
    panic("not good")    // 发生 panic,主程序退出
    recover()    // 不会被执行
    println("ok")
}

// 正确的 recover 调用示例
func main() {
    defer func() {
        fmt.Println("recovered: ", recover())
    }()
    panic("not good")
}

从上边可以看出,recover() 仅在 defer 执行的函数中调用才会生效。

// 错误的调用示例
func main() {
    defer func() {
        doRecover()
    }()
    panic("not good")
}

func doRecover() {
    fmt.Println("recobered: ", recover())
}
recobered: <nil> panic: not good

40. 在 range 迭代 slice、array、map 时通过更新引用来更新元素

在 range 迭代中,得到的值其实是元素的一份值拷贝,更新拷贝并不会更改原来的元素,即是拷贝的地址并不是原有元素的地址:

func main() {
    data := []int{1, 2, 3}
    for _, v := range data {
        v *= 10        // data 中原有元素是不会被修改的
    }
    fmt.Println("data: ", data)    // data:  [1 2 3]
}

如果要修改原有元素的值,应该使用索引直接访问:

func main() {
    data := []int{1, 2, 3}
    for i, v := range data {
        data[i] = v * 10    
    }
    fmt.Println("data: ", data)    // data:  [10 20 30]
}

如果你的集合保存的是指向值的指针,需稍作修改。依旧需要使用索引访问元素,不过可以使用 range 出来的元素直接更新原有值:

func main() {
    data := []*struct{ num int }{{1}, {2}, {3},}
    for _, v := range data {
        v.num *= 10    // 直接使用指针更新
    }
    fmt.Println(data[0], data[1], data[2])    // &{10} &{20} &{30}
}

41. slice 中隐藏的数据

从 slice 中重新切出新 slice 时,新 slice 会引用原 slice 的底层数组。如果跳了这个坑,程序可能会分配大量的临时 slice 来指向原底层数组的部分数据,将导致难以预料的内存使用。

func get() []byte {
    raw := make([]byte, 10000)
    fmt.Println(len(raw), cap(raw), &raw[0])    // 10000 10000 0xc420080000
    return raw[:3]    // 重新分配容量为 10000 的 slice
}

func main() {
    data := get()
    fmt.Println(len(data), cap(data), &data[0])    // 3 10000 0xc420080000
}

可以通过拷贝临时 slice 的数据,而不是重新切片来解决:

func get() (res []byte) {
    raw := make([]byte, 10000)
    fmt.Println(len(raw), cap(raw), &raw[0])    // 10000 10000 0xc420080000
    res = make([]byte, 3)
    copy(res, raw[:3])
    return
}

func main() {
    data := get()
    fmt.Println(len(data), cap(data), &data[0])    // 3 3 0xc4200160b8
}

42. Slice 中数据的误用

举个简单例子,重写文件路径(存储在 slice 中)

分割路径来指向每个不同级的目录,修改第一个目录名再重组子目录名,创建新路径:

// 错误使用 slice 的拼接示例
func main() {
    path := []byte("AAAA/BBBBBBBBB")
    sepIndex := bytes.IndexByte(path, '/') // 4
    println(sepIndex)

    dir1 := path[:sepIndex]
    dir2 := path[sepIndex+1:]
    println("dir1: ", string(dir1))        // AAAA
    println("dir2: ", string(dir2))        // BBBBBBBBB

    dir1 = append(dir1, "suffix"...)
       println("current path: ", string(path))    // AAAAsuffixBBBB
    
    path = bytes.Join([][]byte{dir1, dir2}, []byte{'/'})
    println("dir1: ", string(dir1))        // AAAAsuffix
    println("dir2: ", string(dir2))        // uffixBBBB

    println("new path: ", string(path))    // AAAAsuffix/uffixBBBB    // 错误结果
}

拼接的结果不是正确的 AAAAsuffix/BBBBBBBBB,因为 dir1、 dir2 两个 slice 引用的数据都是 path 的底层数组,第 13 行修改 dir1 同时也修改了 path,也导致了 dir2 的修改

解决方法:

  • 重新分配新的 slice 并拷贝你需要的数据
  • 使用完整的 slice 表达式:input[low:high:max],容量便调整为 max - low
// 使用 full slice expression
func main() {

    path := []byte("AAAA/BBBBBBBBB")
    sepIndex := bytes.IndexByte(path, '/') // 4
    dir1 := path[:sepIndex:sepIndex]        // 此时 cap(dir1) 指定为4, 而不是先前的 16
    dir2 := path[sepIndex+1:]
    dir1 = append(dir1, "suffix"...)

    path = bytes.Join([][]byte{dir1, dir2}, []byte{'/'})
    println("dir1: ", string(dir1))        // AAAAsuffix
    println("dir2: ", string(dir2))        // BBBBBBBBB
    println("new path: ", string(path))    // AAAAsuffix/BBBBBBBBB
}

第 6 行中第三个参数是用来控制 dir1 的新容量,再往 dir1 中 append 超额元素时,将分配新的 buffer 来保存。而不是覆盖原来的 path 底层数组

43. 旧 slice

当你从一个已存在的 slice 创建新 slice 时,二者的数据指向相同的底层数组。如果你的程序使用这个特性,那需要注意 "旧"(stale) slice 问题。

某些情况下,向一个 slice 中追加元素而它指向的底层数组容量不足时,将会重新分配一个新数组来存储数据。而其他 slice 还指向原来的旧底层数组。

// 超过容量将重新分配数组来拷贝值、重新存储
func main() {
    s1 := []int{1, 2, 3}
    fmt.Println(len(s1), cap(s1), s1)    // 3 3 [1 2 3 ]

    s2 := s1[1:]
    fmt.Println(len(s2), cap(s2), s2)    // 2 2 [2 3]

    for i := range s2 {
        s2[i] += 20
    }
    // 此时的 s1 与 s2 是指向同一个底层数组的
    fmt.Println(s1)        // [1 22 23]
    fmt.Println(s2)        // [22 23]

    s2 = append(s2, 4)    // 向容量为 2 的 s2 中再追加元素,此时将分配新数组来存

    for i := range s2 {
        s2[i] += 10
    }
    fmt.Println(s1)        // [1 22 23]    // 此时的 s1 不再更新,为旧数据
    fmt.Println(s2)        // [32 33 14]
}

44. 类型声明与方法

从一个现有的非 interface 类型创建新类型时,并不会继承原有的方法:

// 定义 Mutex 的自定义类型
type myMutex sync.Mutex

func main() {
    var mtx myMutex
    mtx.Lock()
    mtx.UnLock()
}
mtx.Lock undefined (type myMutex has no field or method Lock)...

如果你需要使用原类型的方法,可将原类型以匿名字段的形式嵌到你定义的新 struct 中:

// 类型以字段形式直接嵌入
type myLocker struct {
    sync.Mutex
}

func main() {
    var locker myLocker
    locker.Lock()
    locker.Unlock()
}

interface 类型声明也保留它的方法集:

type myLocker sync.Locker

func main() {
    var locker myLocker
    locker.Lock()
    locker.Unlock()
}

45. 跳出 for-switch 和 for-select 代码块

没有指定标签的 break 只会跳出 switch/select 语句,若不能使用 return 语句跳出的话,可为 break 跳出标签指定的代码块:

// break 配合 label 跳出指定代码块
func main() {
loop:
    for {
        switch {
        case true:
            fmt.Println("breaking out...")
            //break    // 死循环,一直打印 breaking out...
            break loop
        }
    }
    fmt.Println("out...")
}

goto 虽然也能跳转到指定位置,但依旧会再次进入 for-switch,死循环。

46. for 语句中的迭代变量与闭包函数

for 语句中的迭代变量在每次迭代中都会重用,即 for 中创建的闭包函数接收到的参数始终是同一个变量,在 goroutine 开始执行时都会得到同一个迭代值:

func main() {
    data := []string{"one", "two", "three"}

    for _, v := range data {
        go func() {
            fmt.Println(v)
        }()
    }

    time.Sleep(3 * time.Second)
    // 输出 three three three
}

最简单的解决方法:无需修改 goroutine 函数,在 for 内部使用局部变量保存迭代值,再传参:

func main() {
    data := []string{"one", "two", "three"}

    for _, v := range data {
        vCopy := v
        go func() {
            fmt.Println(vCopy)
        }()
    }

    time.Sleep(3 * time.Second)
    // 输出 one two three
}

另一个解决方法:直接将当前的迭代值以参数形式传递给匿名函数:

func main() {
    data := []string{"one", "two", "three"}

    for _, v := range data {
        go func(in string) {
            fmt.Println(in)
        }(v)
    }

    time.Sleep(3 * time.Second)
    // 输出 one two three
}

注意下边这个稍复杂的 3 个示例区别:

type field struct {
    name string
}

func (p *field) print() {
    fmt.Println(p.name)
}

// 错误示例
func main() {
    data := []field{{"one"}, {"two"}, {"three"}}
    for _, v := range data {
        go v.print()
    }
    time.Sleep(3 * time.Second)
    // 输出 three three three 
}


// 正确示例
func main() {
    data := []field{{"one"}, {"two"}, {"three"}}
    for _, v := range data {
        v := v
        go v.print()
    }
    time.Sleep(3 * time.Second)
    // 输出 one two three
}

// 正确示例
func main() {
    data := []*field{{"one"}, {"two"}, {"three"}}
    for _, v := range data {    // 此时迭代值 v 是三个元素值的地址,每次 v 指向的值不同
        go v.print()
    }
    time.Sleep(3 * time.Second)
    // 输出 one two three
}

47. defer 函数的参数值

对 defer 延迟执行的函数,它的参数会在声明时候就会求出具体值,而不是在执行时才求值:

// 在 defer 函数中参数会提前求值
func main() {
    var i = 1
    defer fmt.Println("result: ", func() int { return i * 2 }())
    i++
}
result: 2

48. defer 函数的执行时机

对 defer 延迟执行的函数,会在调用它的函数结束时执行,而不是在调用它的语句块结束时执行,注意区分开。

比如在一个长时间执行的函数里,内部 for 循环中使用 defer 来清理每次迭代产生的资源调用,就会出现问题:

// 命令行参数指定目录名
// 遍历读取目录下的文件
func main() {

    if len(os.Args) != 2 {
        os.Exit(1)
    }

    dir := os.Args[1]
    start, err := os.Stat(dir)
    if err != nil || !start.IsDir() {
        os.Exit(2)
    }

    var targets []string
    filepath.Walk(dir, func(fPath string, fInfo os.FileInfo, err error) error {
        if err != nil {
            return err
        }

        if !fInfo.Mode().IsRegular() {
            return nil
        }

        targets = append(targets, fPath)
        return nil
    })

    for _, target := range targets {
        f, err := os.Open(target)
        if err != nil {
            fmt.Println("bad target:", target, "error:", err)    //error:too many open files
            break
        }
        defer f.Close()    // 在每次 for 语句块结束时,不会关闭文件资源
        
        // 使用 f 资源
    }
}

先创建 10000 个文件:

#!/bin/bash
for n in {1..10000}; do
    echo content > "file${n}.txt"
done

运行效果:

解决办法:defer 延迟执行的函数写入匿名函数中:

// 目录遍历正常
func main() {
    // ...

    for _, target := range targets {
        func() {
            f, err := os.Open(target)
            if err != nil {
                fmt.Println("bad target:", target, "error:", err)
                return    // 在匿名函数内使用 return 代替 break 即可
            }
            defer f.Close()    // 匿名函数执行结束,调用关闭文件资源
            
            // 使用 f 资源
        }()
    }
}

当然你也可以去掉 defer,在文件资源使用完毕后,直接调用 f.Close() 来关闭。

49. 失败的类型断言

在类型断言语句中,断言失败则会返回目标类型的“零值”,断言变量与原来变量混用可能出现异常情况:

// 错误示例
func main() {
    var data interface{} = "great"

    // data 混用
    if data, ok := data.(int); ok {
        fmt.Println("[is an int], data: ", data)
    } else {
        fmt.Println("[not an int], data: ", data)    // [isn't a int], data:  0
    }
}


// 正确示例
func main() {
    var data interface{} = "great"

    if res, ok := data.(int); ok {
        fmt.Println("[is an int], data: ", res)
    } else {
        fmt.Println("[not an int], data: ", data)    // [not an int], data:  great
    }
}

50. 阻塞的 gorutinue 与资源泄露

在 2012 年 Google I/O 大会上,Rob Pike 的 Go Concurrency Patterns 演讲讨论 Go 的几种基本并发模式,如 完整代码 中从数据集中获取第一条数据的函数:

func First(query string, replicas []Search) Result {
    c := make(chan Result)
    replicaSearch := func(i int) { c <- replicas[i](query) }
    for i := range replicas {
        go replicaSearch(i)
    }
    return <-c
}

在搜索重复时依旧每次都起一个 goroutine 去处理,每个 goroutine 都把它的搜索结果发送到结果 channel 中,channel 中收到的第一条数据会直接返回。

返回完第一条数据后,其他 goroutine 的搜索结果怎么处理?他们自己的协程如何处理?

First() 中的结果 channel 是无缓冲的,这意味着只有第一个 goroutine 能返回,由于没有 receiver,其他的 goroutine 会在发送上一直阻塞。如果你大量调用,则可能造成资源泄露。

为避免泄露,你应该确保所有的 goroutine 都能正确退出,有 2 个解决方法:

  • 使用带缓冲的 channel,确保能接收全部 goroutine 的返回结果:
func First(query string, replicas ...Search) Result {  
    c := make(chan Result,len(replicas))    
    searchReplica := func(i int) { c <- replicas[i](query) }
    for i := range replicas {
        go searchReplica(i)
    }
    return <-c
}
  • 使用 select 语句,配合能保存一个缓冲值的 channel default 语句:

    default 的缓冲 channel 保证了即使结果 channel 收不到数据,也不会阻塞 goroutine

func First(query string, replicas ...Search) Result {  
    c := make(chan Result,1)
    searchReplica := func(i int) { 
        select {
        case c <- replicas[i](query):
        default:
        }
    }
    for i := range replicas {
        go searchReplica(i)
    }
    return <-c
}
  • 使用特殊的废弃(cancellation) channel 来中断剩余 goroutine 的执行:
func First(query string, replicas ...Search) Result {  
    c := make(chan Result)
    done := make(chan struct{})
    defer close(done)
    searchReplica := func(i int) { 
        select {
        case c <- replicas[i](query):
        case <- done:
        }
    }
    for i := range replicas {
        go searchReplica(i)
    }

    return <-c
}

Rob Pike 为了简化演示,没有提及演讲代码中存在的这些问题。不过对于新手来说,可能会不加思考直接使用。

高级篇:51-57

51. 使用指针作为方法的 receiver

只要值是可寻址的,就可以在值上直接调用指针方法。即是对一个方法,它的 receiver 是指针就足矣。

但不是所有值都是可寻址的,比如 map 类型的元素、通过 interface 引用的变量:

type data struct {
    name string
}

type printer interface {
    print()
}

func (p *data) print() {
    fmt.Println("name: ", p.name)
}

func main() {
    d1 := data{"one"}
    d1.print()    // d1 变量可寻址,可直接调用指针 receiver 的方法

    var in printer = data{"two"}
    in.print()    // 类型不匹配

    m := map[string]data{
        "x": data{"three"},
    }
    m["x"].print()    // m["x"] 是不可寻址的    // 变动频繁
}
cannot use data literal (type data) as type printer in assignment:

data does not implement printer (print method has pointer receiver)

cannot call pointer method on m["x"]
cannot take the address of m["x"]

52. 更新 map 字段的值

如果 map 一个字段的值是 struct 类型,则无法直接更新该 struct 的单个字段:

// 无法直接更新 struct 的字段值
type data struct {
    name string
}

func main() {
    m := map[string]data{
        "x": {"Tom"},
    }
    m["x"].name = "Jerry"
}
cannot assign to struct field m["x"].name in map

因为 map 中的元素是不可寻址的。需区分开的是,slice 的元素可寻址:

type data struct {
    name string
}

func main() {
    s := []data{{"Tom"}}
    s[0].name = "Jerry"
    fmt.Println(s)    // [{Jerry}]
}

注意:不久前 gccgo 编译器可更新 map struct 元素的字段值,不过很快便修复了,官方认为是 Go1.3 的潜在特性,无需及时实现,依旧在 todo list 中。

更新 map 中 struct 元素的字段值,有 2 个方法:

  • 使用局部变量
// 提取整个 struct 到局部变量中,修改字段值后再整个赋值
type data struct {
    name string
}

func main() {
    m := map[string]data{
        "x": {"Tom"},
    }
    r := m["x"]
    r.name = "Jerry"
    m["x"] = r
    fmt.Println(m)    // map[x:{Jerry}]
}
  • 使用指向元素的 map 指针
func main() {
    m := map[string]*data{
        "x": {"Tom"},
    }
    
    m["x"].name = "Jerry"    // 直接修改 m["x"] 中的字段
    fmt.Println(m["x"])    // &{Jerry}
}

但是要注意下边这种误用:

func main() {
    m := map[string]*data{
        "x": {"Tom"},
    }
    m["z"].name = "what???"     
    fmt.Println(m["x"])
}
panic: runtime error: invalid memory address or nil pointer dereference

53. nil interface 和 nil interface 值

虽然 interface 看起来像指针类型,但它不是。interface 类型的变量只有在类型和值均为 nil 时才为 nil

如果你的 interface 变量的值是跟随其他变量变化的(雾),与 nil 比较相等时小心:

func main() {
    var data *byte
    var in interface{}

    fmt.Println(data, data == nil)    // <nil> true
    fmt.Println(in, in == nil)    // <nil> true

    in = data
    fmt.Println(in, in == nil)    // <nil> false    // data 值为 nil,但 in 值不为 nil
}

如果你的函数返回值类型是 interface,更要小心这个坑:

// 错误示例
func main() {
    doIt := func(arg int) interface{} {
        var result *struct{} = nil
        if arg > 0 {
            result = &struct{}{}
        }
        return result
    }

    if res := doIt(-1); res != nil {
        fmt.Println("Good result: ", res)    // Good result:  <nil>
        fmt.Printf("%T\n", res)            // *struct {}    // res 不是 nil,它的值为 nil
        fmt.Printf("%v\n", res)            // <nil>
    }
}


// 正确示例
func main() {
    doIt := func(arg int) interface{} {
        var result *struct{} = nil
        if arg > 0 {
            result = &struct{}{}
        } else {
            return nil    // 明确指明返回 nil
        }
        return result
    }

    if res := doIt(-1); res != nil {
        fmt.Println("Good result: ", res)
    } else {
        fmt.Println("Bad result: ", res)    // Bad result:  <nil>
    }
}

54. 堆栈变量

你并不总是清楚你的变量是分配到了堆还是栈。

在 C++ 中使用 new 创建的变量总是分配到堆内存上的,但在 Go 中即使使用 new()make() 来创建变量,变量为内存分配位置依旧归 Go 编译器管。

Go 编译器会根据变量的大小及其 "escape analysis" 的结果来决定变量的存储位置,故能准确返回本地变量的地址,这在 C/C++ 中是不行的。

在 go build 或 go run 时,加入 -m 参数,能准确分析程序的变量分配位置:

55. GOMAXPROCS、Concurrency(并发)and Parallelism(并行)

Go 1.4 及以下版本,程序只会使用 1 个执行上下文 / OS 线程,即任何时间都最多只有 1 个 goroutine 在执行。

Go 1.5 版本将可执行上下文的数量设置为 runtime.NumCPU() 返回的逻辑 CPU 核心数,这个数与系统实际总的 CPU 逻辑核心数是否一致,取决于你的 CPU 分配给程序的核心数,可以使用 GOMAXPROCS 环境变量或者动态的使用 runtime.GOMAXPROCS() 来调整。

误区:GOMAXPROCS 表示执行 goroutine 的 CPU 核心数,参考文档

GOMAXPROCS 的值是可以超过 CPU 的实际数量的,在 1.5 中最大为 256

func main() {
    fmt.Println(runtime.GOMAXPROCS(-1))    // 4
    fmt.Println(runtime.NumCPU())    // 4
    runtime.GOMAXPROCS(20)
    fmt.Println(runtime.GOMAXPROCS(-1))    // 20
    runtime.GOMAXPROCS(300)
    fmt.Println(runtime.GOMAXPROCS(-1))    // Go 1.9.2 // 300
}

56. 读写操作的重新排序

Go 可能会重排一些操作的执行顺序,可以保证在一个 goroutine 中操作是顺序执行的,但不保证多 goroutine 的执行顺序:

var _ = runtime.GOMAXPROCS(3)

var a, b int

func u1() {
    a = 1
    b = 2
}

func u2() {
    a = 3
    b = 4
}

func p() {
    println(a)
    println(b)
}

func main() {
    go u1()    // 多个 goroutine 的执行顺序不定
    go u2()    
    go p()
    time.Sleep(1 * time.Second)
}

运行效果:

如果你想保持多 goroutine 像代码中的那样顺序执行,可以使用 channel 或 sync 包中的锁机制等。

57. 优先调度

你的程序可能出现一个 goroutine 在运行时阻止了其他 goroutine 的运行,比如程序中有一个不让调度器运行的 for 循环:

func main() {
    done := false

    go func() {
        done = true
    }()

    for !done {
    }

    println("done !")
}

for 的循环体不必为空,但如果代码不会触发调度器执行,将出现问题。

调度器会在 GC、Go 声明、阻塞 channel、阻塞系统调用和锁操作后再执行,也会在非内联函数调用时执行:

func main() {
    done := false

    go func() {
        done = true
    }()

    for !done {
        println("not done !")    // 并不内联执行
    }

    println("done !")
}

可以添加 -m 参数来分析 for 代码块中调用的内联函数:

你也可以使用 runtime 包中的 Gosched() 来 手动启动调度器:

func main() {
    done := false

    go func() {
        done = true
    }()

    for !done {
        runtime.Gosched()
    }

    println("done !")
}

运行效果:

总结

感谢原作者 kcqon 总结的这篇博客,让我受益匪浅。

由于译者水平有限,不免出现理解失误,望读者在下评论区指出,不胜感激。

后续再更新类似高质量文章的翻译 ?

查看原文

大呜 赞了文章 · 2018-03-22

Golang 新手可能会踩的 50 个坑

译文:Golang 新手可能会踩的 50 个坑
原文:50 Shades of Go: Traps, Gotchas, and Common Mistakes
翻译已获作者授权,转载请注明来源。

不久前发现在知乎这篇质量很高的文章,打算加上自己的理解翻译一遍。文章分为三部分:初级篇 1-34,中级篇 35-50,高级篇 51-57

前言

Go 是一门简单有趣的编程语言,与其他语言一样,在使用时不免会遇到很多坑,不过它们大多不是 Go 本身的设计缺陷。如果你刚从其他语言转到 Go,那这篇文章里的坑多半会踩到。

如果花时间学习官方 doc、wiki、讨论邮件列表Rob Pike 的大量文章以及 Go 的源码,会发现这篇文章中的坑是很常见的,新手跳过这些坑,能减少大量调试代码的时间。

初级篇:1-34

1. 左大括号 { 不能单独放一行

在其他大多数语言中,{ 的位置你自行决定。Go 比较特别,遵守分号注入规则(automatic semicolon injection):编译器会在每行代码尾部特定分隔符后加 ; 来分隔多条语句,比如会在 ) 后加分号:

// 错误示例
func main()                    
{
    println("hello world")
}

// 等效于
func main();    // 无函数体                    
{
    println("hello world")
}
./main.go: missing function body
./main.go: syntax error: unexpected semicolon or newline before {
// 正确示例
func main() {
    println("hello world")
}     

2. 未使用的变量

如果在函数体代码中有未使用的变量,则无法通过编译,不过全局变量声明但不使用是可以的。

即使变量声明后为变量赋值,依旧无法通过编译,需在某处使用它:

// 错误示例
var gvar int     // 全局变量,声明不使用也可以

func main() {
    var one int     // error: one declared and not used
    two := 2    // error: two declared and not used
    var three int    // error: three declared and not used
    three = 3        
}


// 正确示例
// 可以直接注释或移除未使用的变量
func main() {
    var one int
    _ = one
    
    two := 2
    println(two)
    
    var three int
    one = three

    var four int
    four = four
}

3. 未使用的 import

如果你 import 一个包,但包中的变量、函数、接口和结构体一个都没有用到的话,将编译失败。

可以使用 _ 下划线符号作为别名来忽略导入的包,从而避免编译错误,这只会执行 package 的 init()

// 错误示例
import (
    "fmt"    // imported and not used: "fmt"
    "log"    // imported and not used: "log"
    "time"    // imported and not used: "time"
)

func main() {
}


// 正确示例
// 可以使用 goimports 工具来注释或移除未使用到的包
import (
    _ "fmt"
    "log"
    "time"
)

func main() {
    _ = log.Println
    _ = time.Now
}

4. 简短声明的变量只能在函数内部使用

// 错误示例
myvar := 1    // syntax error: non-declaration statement outside function body
func main() {
}


// 正确示例
var  myvar = 1
func main() {
}

5. 使用简短声明来重复声明变量

不能用简短声明方式来单独为一个变量重复声明, := 左侧至少有一个新变量,才允许多变量的重复声明:

// 错误示例
func main() {  
    one := 0
    one := 1 // error: no new variables on left side of :=
}


// 正确示例
func main() {
    one := 0
    one, two := 1, 2    // two 是新变量,允许 one 的重复声明。比如 error 处理经常用同名变量 err
    one, two = two, one    // 交换两个变量值的简写
}

6. 不能使用简短声明来设置字段的值

struct 的变量字段不能使用 := 来赋值以使用预定义的变量来避免解决:

// 错误示例
type info struct {
    result int
}

func work() (int, error) {
    return 3, nil
}

func main() {
    var data info
    data.result, err := work()    // error: non-name data.result on left side of :=
    fmt.Printf("info: %+v\n", data)
}


// 正确示例
func main() {
    var data info
    var err error    // err 需要预声明

    data.result, err = work()
    if err != nil {
        fmt.Println(err)
        return
    }

    fmt.Printf("info: %+v\n", data)
}

7. 不小心覆盖了变量

对从动态语言转过来的开发者来说,简短声明很好用,这可能会让人误会 := 是一个赋值操作符。

如果你在新的代码块中像下边这样误用了 :=,编译不会报错,但是变量不会按你的预期工作:

func main() {
    x := 1
    println(x)        // 1
    {
        println(x)    // 1
        x := 2
        println(x)    // 2    // 新的 x 变量的作用域只在代码块内部
    }
    println(x)        // 1
}

这是 Go 开发者常犯的错,而且不易被发现。

可使用 vet 工具来诊断这种变量覆盖,Go 默认不做覆盖检查,添加 -shadow 选项来启用:

> go tool vet -shadow main.go
main.go:9: declaration of "x" shadows declaration at main.go:5

注意 vet 不会报告全部被覆盖的变量,可以使用 go-nyet 来做进一步的检测:

> $GOPATH/bin/go-nyet main.go
main.go:10:3:Shadowing variable `x`

8. 显式类型的变量无法使用 nil 来初始化

nil 是 interface、function、pointer、map、slice 和 channel 类型变量的默认初始值。但声明时不指定类型,编译器也无法推断出变量的具体类型。

// 错误示例
func main() {
    var x = nil    // error: use of untyped nil
    _ = x
}


// 正确示例
func main() {
    var x interface{} = nil
    _ = x
}    

9. 直接使用值为 nil 的 slice、map

允许对值为 nil 的 slice 添加元素,但对值为 nil 的 map 添加元素则会造成运行时 panic

// map 错误示例
func main() {
    var m map[string]int
    m["one"] = 1        // error: panic: assignment to entry in nil map
    // m := make(map[string]int)// map 的正确声明,分配了实际的内存
}    


// slice 正确示例
func main() {
    var s []int
    s = append(s, 1)
}

10. map 容量

在创建 map 类型的变量时可以指定容量,但不能像 slice 一样使用 cap() 来检测分配空间的大小:

// 错误示例
func main() {
    m := make(map[string]int, 99)
    println(cap(m))     // error: invalid argument m1 (type map[string]int) for cap  
}    

11. string 类型的变量值不能为 nil

对那些喜欢用 nil 初始化字符串的人来说,这就是坑:

// 错误示例
func main() {
    var s string = nil    // cannot use nil as type string in assignment
    if s == nil {    // invalid operation: s == nil (mismatched types string and nil)
        s = "default"
    }
}


// 正确示例
func main() {
    var s string    // 字符串类型的零值是空串 ""
    if s == "" {
        s = "default"
    }
}

12. Array 类型的值作为函数参数

在 C/C++ 中,数组(名)是指针。将数组作为参数传进函数时,相当于传递了数组内存地址的引用,在函数内部会改变该数组的值。

在 Go 中,数组是值。作为参数传进函数时,传递的是数组的原始值拷贝,此时在函数内部是无法更新该数组的:

// 数组使用值拷贝传参
func main() {
    x := [3]int{1,2,3}

    func(arr [3]int) {
        arr[0] = 7
        fmt.Println(arr)    // [7 2 3]
    }(x)
    fmt.Println(x)            // [1 2 3]    // 并不是你以为的 [7 2 3]
}

如果想修改参数数组:

  • 直接传递指向这个数组的指针类型:
// 传址会修改原数据
func main() {
    x := [3]int{1,2,3}

    func(arr *[3]int) {
        (*arr)[0] = 7    
        fmt.Println(arr)    // &[7 2 3]
    }(&x)
    fmt.Println(x)    // [7 2 3]
}
  • 直接使用 slice:即使函数内部得到的是 slice 的值拷贝,但依旧会更新 slice 的原始数据(底层 array)
// 会修改 slice 的底层 array,从而修改 slice
func main() {
    x := []int{1, 2, 3}
    func(arr []int) {
        arr[0] = 7
        fmt.Println(x)    // [7 2 3]
    }(x)
    fmt.Println(x)    // [7 2 3]
}

13. range 遍历 slice 和 array 时混淆了返回值

与其他编程语言中的 for-inforeach 遍历语句不同,Go 中的 range 在遍历时会生成 2 个值,第一个是元素索引,第二个是元素的值:

// 错误示例
func main() {
    x := []string{"a", "b", "c"}
    for v := range x {
        fmt.Println(v)    // 1 2 3
    }
}


// 正确示例
func main() {
    x := []string{"a", "b", "c"}
    for _, v := range x {    // 使用 _ 丢弃索引
        fmt.Println(v)
    }
}

14. slice 和 array 其实是一维数据

看起来 Go 支持多维的 array 和 slice,可以创建数组的数组、切片的切片,但其实并不是。

对依赖动态计算多维数组值的应用来说,就性能和复杂度而言,用 Go 实现的效果并不理想。

可以使用原始的一维数组、“独立“ 的切片、“共享底层数组”的切片来创建动态的多维数组。

  1. 使用原始的一维数组:要做好索引检查、溢出检测、以及当数组满时再添加值时要重新做内存分配。
  2. 使用“独立”的切片分两步:
  • 创建外部 slice

    • 对每个内部 slice 进行内存分配

      注意内部的 slice 相互独立,使得任一内部 slice 增缩都不会影响到其他的 slice

// 使用各自独立的 6 个 slice 来创建 [2][3] 的动态多维数组
func main() {
    x := 2
    y := 4
    
    table := make([][]int, x)
    for i  := range table {
        table[i] = make([]int, y)
    }
}
  1. 使用“共享底层数组”的切片
  • 创建一个存放原始数据的容器 slice
  • 创建其他的 slice
  • 切割原始 slice 来初始化其他的 slice
func main() {
    h, w := 2, 4
    raw := make([]int, h*w)

    for i := range raw {
        raw[i] = i
    }

    // 初始化原始 slice
    fmt.Println(raw, &raw[4])    // [0 1 2 3 4 5 6 7] 0xc420012120 
    
    table := make([][]int, h)
    for i := range table {
        
        // 等间距切割原始 slice,创建动态多维数组 table
        // 0: raw[0*4: 0*4 + 4]
        // 1: raw[1*4: 1*4 + 4]
        table[i] = raw[i*w : i*w + w]
    }

    fmt.Println(table, &table[1][0])    // [[0 1 2 3] [4 5 6 7]] 0xc420012120
}

更多关于多维数组的参考

go-how-is-two-dimensional-arrays-memory-representation

what-is-a-concise-way-to-create-a-2d-slice-in-go

15. 访问 map 中不存在的 key

和其他编程语言类似,如果访问了 map 中不存在的 key 则希望能返回 nil,比如在 PHP 中:

> php -r '$v = ["x"=>1, "y"=>2]; @var_dump($v["z"]);'
NULL

Go 则会返回元素对应数据类型的零值,比如 nil''false 和 0,取值操作总有值返回,故不能通过取出来的值来判断 key 是不是在 map 中。

检查 key 是否存在可以用 map 直接访问,检查返回的第二个参数即可:

// 错误的 key 检测方式
func main() {
    x := map[string]string{"one": "2", "two": "", "three": "3"}
    if v := x["two"]; v == "" {
        fmt.Println("key two is no entry")    // 键 two 存不存在都会返回的空字符串
    }
}

// 正确示例
func main() {
    x := map[string]string{"one": "2", "two": "", "three": "3"}
    if _, ok := x["two"]; !ok {
        fmt.Println("key two is no entry")
    }
}

16. string 类型的值是常量,不可更改

尝试使用索引遍历字符串,来更新字符串中的个别字符,是不允许的。

string 类型的值是只读的二进制 byte slice,如果真要修改字符串中的字符,将 string 转为 []byte 修改后,再转为 string 即可:

// 修改字符串的错误示例
func main() {
    x := "text"
    x[0] = "T"        // error: cannot assign to x[0]
    fmt.Println(x)
}


// 修改示例
func main() {
    x := "text"
    xBytes := []byte(x)
    xBytes[0] = 'T'    // 注意此时的 T 是 rune 类型
    x = string(xBytes)
    fmt.Println(x)    // Text
}

注意: 上边的示例并不是更新字符串的正确姿势,因为一个 UTF8 编码的字符可能会占多个字节,比如汉字就需要 3~4 个字节来存储,此时更新其中的一个字节是错误的。

更新字串的正确姿势:将 string 转为 rune slice(此时 1 个 rune 可能占多个 byte),直接更新 rune 中的字符

func main() {
    x := "text"
    xRunes := []rune(x)
    xRunes[0] = '我'
    x = string(xRunes)
    fmt.Println(x)    // 我ext
}

17. string 与 byte slice 之间的转换

当进行 string 和 byte slice 相互转换时,参与转换的是拷贝的原始值。这种转换的过程,与其他编程语的强制类型转换操作不同,也和新 slice 与旧 slice 共享底层数组不同。

Go 在 string 与 byte slice 相互转换上优化了两点,避免了额外的内存分配:

  • map[string] 中查找 key 时,使用了对应的 []byte,避免做 m[string(key)] 的内存分配
  • 使用 for range 迭代 string 转换为 []byte 的迭代:for i,v := range []byte(str) {...}

雾:参考原文

18. string 与索引操作符

对字符串用索引访问返回的不是字符,而是一个 byte 值。

这种处理方式和其他语言一样,比如 PHP 中:

> php -r '$name="中文"; var_dump($name);'    # "中文" 占用 6 个字节
string(6) "中文"

> php -r '$name="中文"; var_dump($name[0]);' # 把第一个字节当做 Unicode 字符读取,显示 U+FFFD
string(1) "�"    

> php -r '$name="中文"; var_dump($name[0].$name[1].$name[2]);'
string(3) "中"
func main() {
    x := "ascii"
    fmt.Println(x[0])        // 97
    fmt.Printf("%T\n", x[0])// uint8
}

如果需要使用 for range 迭代访问字符串中的字符(unicode code point / rune),标准库中有 "unicode/utf8" 包来做 UTF8 的相关解码编码。另外 utf8string 也有像 func (s *String) At(i int) rune 等很方便的库函数。

19. 字符串并不都是 UTF8 文本

string 的值不必是 UTF8 文本,可以包含任意的值。只有字符串是文字字面值时才是 UTF8 文本,字串可以通过转义来包含其他数据。

判断字符串是否是 UTF8 文本,可使用 "unicode/utf8" 包中的 ValidString() 函数:

func main() {
    str1 := "ABC"
    fmt.Println(utf8.ValidString(str1))    // true

    str2 := "A\xfeC"
    fmt.Println(utf8.ValidString(str2))    // false

    str3 := "A\\xfeC"
    fmt.Println(utf8.ValidString(str3))    // true    // 把转义字符转义成字面值
}

20. 字符串的长度

在 Python 中:

data = u'♥'  
print(len(data)) # 1

然而在 Go 中:

func main() {
    char := "♥"
    fmt.Println(len(char))    // 3
}

Go 的内建函数 len() 返回的是字符串的 byte 数量,而不是像 Python 中那样是计算 Unicode 字符数。

如果要得到字符串的字符数,可使用 "unicode/utf8" 包中的 RuneCountInString(str string) (n int)

func main() {
    char := "♥"
    fmt.Println(utf8.RuneCountInString(char))    // 1
}

注意:RuneCountInString 并不总是返回我们看到的字符数,因为有的字符会占用 2 个 rune:

func main() {
    char := "é"
    fmt.Println(len(char))    // 3
    fmt.Println(utf8.RuneCountInString(char))    // 2
    fmt.Println("cafe\u0301")    // café    // 法文的 cafe,实际上是两个 rune 的组合
}

参考:normalization

21. 在多行 array、slice、map 语句中缺少 ,

func main() {
    x := []int {
        1,
        2    // syntax error: unexpected newline, expecting comma or }
    }
    y := []int{1,2,}    
    z := []int{1,2}    
    // ...
}

声明语句中 } 折叠到单行后,尾部的 , 不是必需的。

22. log.Fatallog.Panic 不只是 log

log 标准库提供了不同的日志记录等级,与其他语言的日志库不同,Go 的 log 包在调用 Fatal*()Panic*() 时能做更多日志外的事,如中断程序的执行等:

func main() {
    log.Fatal("Fatal level log: log entry")        // 输出信息后,程序终止执行
    log.Println("Nomal level log: log entry")
}

23. 对内建数据结构的操作并不是同步的

尽管 Go 本身有大量的特性来支持并发,但并不保证并发的数据安全,用户需自己保证变量等数据以原子操作更新。

goroutine 和 channel 是进行原子操作的好方法,或使用 "sync" 包中的锁。

24. range 迭代 string 得到的值

range 得到的索引是字符值(Unicode point / rune)第一个字节的位置,与其他编程语言不同,这个索引并不直接是字符在字符串中的位置。

注意一个字符可能占多个 rune,比如法文单词 café 中的 é。操作特殊字符可使用norm 包。

for range 迭代会尝试将 string 翻译为 UTF8 文本,对任何无效的码点都直接使用 0XFFFD rune(�)UNicode 替代字符来表示。如果 string 中有任何非 UTF8 的数据,应将 string 保存为 byte slice 再进行操作。

func main() {
    data := "A\xfe\x02\xff\x04"
    for _, v := range data {
        fmt.Printf("%#x ", v)    // 0x41 0xfffd 0x2 0xfffd 0x4    // 错误
    }

    for _, v := range []byte(data) {
        fmt.Printf("%#x ", v)    // 0x41 0xfe 0x2 0xff 0x4    // 正确
    }
}

25. range 迭代 map

如果你希望以特定的顺序(如按 key 排序)来迭代 map,要注意每次迭代都可能产生不一样的结果。

Go 的运行时是有意打乱迭代顺序的,所以你得到的迭代结果可能不一致。但也并不总会打乱,得到连续相同的 5 个迭代结果也是可能的,如:

func main() {
    m := map[string]int{"one": 1, "two": 2, "three": 3, "four": 4}
    for k, v := range m {
        fmt.Println(k, v)
    }
}

如果你去 Go Playground 重复运行上边的代码,输出是不会变的,只有你更新代码它才会重新编译。重新编译后迭代顺序是被打乱的:

26. switch 中的 fallthrough 语句

switch 语句中的 case 代码块会默认带上 break,但可以使用 fallthrough 来强制执行下一个 case 代码块。

func main() {
    isSpace := func(char byte) bool {
        switch char {
        case ' ':    // 空格符会直接 break,返回 false // 和其他语言不一样
        // fallthrough    // 返回 true
        case '\t':
            return true
        }
        return false
    }
    fmt.Println(isSpace('\t'))    // true
    fmt.Println(isSpace(' '))    // false
}

不过你可以在 case 代码块末尾使用 fallthrough,强制执行下一个 case 代码块。

也可以改写 case 为多条件判断:

func main() {
    isSpace := func(char byte) bool {
        switch char {
        case ' ', '\t':
            return true
        }
        return false
    }
    fmt.Println(isSpace('\t'))    // true
    fmt.Println(isSpace(' '))    // true
}

27. 自增和自减运算

很多编程语言都自带前置后置的 ++-- 运算。但 Go 特立独行,去掉了前置操作,同时 ++ 只作为运算符而非表达式。

// 错误示例
func main() {
    data := []int{1, 2, 3}
    i := 0
    ++i            // syntax error: unexpected ++, expecting }
    fmt.Println(data[i++])    // syntax error: unexpected ++, expecting :
}


// 正确示例
func main() {
    data := []int{1, 2, 3}
    i := 0
    i++
    fmt.Println(data[i])    // 2
}

28. 按位取反

很多编程语言使用 ~ 作为一元按位取反(NOT)操作符,Go 重用 ^ XOR 操作符来按位取反:

// 错误的取反操作
func main() {
    fmt.Println(~2)        // bitwise complement operator is ^
}


// 正确示例
func main() {
    var d uint8 = 2
    fmt.Printf("%08b\n", d)        // 00000010
    fmt.Printf("%08b\n", ^d)    // 11111101
}

同时 ^ 也是按位异或(XOR)操作符。

一个操作符能重用两次,是因为一元的 NOT 操作 NOT 0x02,与二元的 XOR 操作 0x22 XOR 0xff 是一致的。

Go 也有特殊的操作符 AND NOT &^ 操作符,不同位才取1。

func main() {
    var a uint8 = 0x82
    var b uint8 = 0x02
    fmt.Printf("%08b [A]\n", a)
    fmt.Printf("%08b [B]\n", b)

    fmt.Printf("%08b (NOT B)\n", ^b)
    fmt.Printf("%08b ^ %08b = %08b [B XOR 0xff]\n", b, 0xff, b^0xff)

    fmt.Printf("%08b ^ %08b = %08b [A XOR B]\n", a, b, a^b)
    fmt.Printf("%08b & %08b = %08b [A AND B]\n", a, b, a&b)
    fmt.Printf("%08b &^%08b = %08b [A 'AND NOT' B]\n", a, b, a&^b)
    fmt.Printf("%08b&(^%08b)= %08b [A AND (NOT B)]\n", a, b, a&(^b))
}
10000010 [A]
00000010 [B]
11111101 (NOT B)
00000010 ^ 11111111 = 11111101 [B XOR 0xff]
10000010 ^ 00000010 = 10000000 [A XOR B]
10000010 & 00000010 = 00000010 [A AND B]
10000010 &^00000010 = 10000000 [A 'AND NOT' B]
10000010&(^00000010)= 10000000 [A AND (NOT B)]

29. 运算符的优先级

除了位清除(bit clear)操作符,Go 也有很多和其他语言一样的位操作符,但优先级另当别论。

func main() {
    fmt.Printf("0x2 & 0x2 + 0x4 -> %#x\n", 0x2&0x2+0x4)    // & 优先 +
    //prints: 0x2 & 0x2 + 0x4 -> 0x6
    //Go:    (0x2 & 0x2) + 0x4
    //C++:    0x2 & (0x2 + 0x4) -> 0x2

    fmt.Printf("0x2 + 0x2 << 0x1 -> %#x\n", 0x2+0x2<<0x1)    // << 优先 +
    //prints: 0x2 + 0x2 << 0x1 -> 0x6
    //Go:     0x2 + (0x2 << 0x1)
    //C++:   (0x2 + 0x2) << 0x1 -> 0x8

    fmt.Printf("0xf | 0x2 ^ 0x2 -> %#x\n", 0xf|0x2^0x2)    // | 优先 ^
    //prints: 0xf | 0x2 ^ 0x2 -> 0xd
    //Go:    (0xf | 0x2) ^ 0x2
    //C++:    0xf | (0x2 ^ 0x2) -> 0xf
}

优先级列表:

Precedence    Operator
    5             *  /  %  <<  >>  &  &^
    4             +  -  |  ^
    3             ==  !=  <  <=  >  >=
    2             &&
    1             ||

30. 不导出的 struct 字段无法被 encode

以小写字母开头的字段成员是无法被外部直接访问的,所以 struct 在进行 json、xml、gob 等格式的 encode 操作时,这些私有字段会被忽略,导出时得到零值:

func main() {
    in := MyData{1, "two"}
    fmt.Printf("%#v\n", in)    // main.MyData{One:1, two:"two"}

    encoded, _ := json.Marshal(in)
    fmt.Println(string(encoded))    // {"One":1}    // 私有字段 two 被忽略了

    var out MyData
    json.Unmarshal(encoded, &out)
    fmt.Printf("%#v\n", out)     // main.MyData{One:1, two:""}
}

31. 程序退出时还有 goroutine 在执行

程序默认不等所有 goroutine 都执行完才退出,这点需要特别注意:

// 主程序会直接退出
func main() {
    workerCount := 2
    for i := 0; i < workerCount; i++ {
        go doIt(i)
    }
    time.Sleep(1 * time.Second)
    fmt.Println("all done!")
}

func doIt(workerID int) {
    fmt.Printf("[%v] is running\n", workerID)
    time.Sleep(3 * time.Second)        // 模拟 goroutine 正在执行 
    fmt.Printf("[%v] is done\n", workerID)
}

如下,main() 主程序不等两个 goroutine 执行完就直接退出了:

常用解决办法:使用 "WaitGroup" 变量,它会让主程序等待所有 goroutine 执行完毕再退出。

如果你的 goroutine 要做消息的循环处理等耗时操作,可以向它们发送一条 kill 消息来关闭它们。或直接关闭一个它们都等待接收数据的 channel:

// 等待所有 goroutine 执行完毕
// 进入死锁
func main() {
    var wg sync.WaitGroup
    done := make(chan struct{})

    workerCount := 2
    for i := 0; i < workerCount; i++ {
        wg.Add(1)
        go doIt(i, done, wg)
    }

    close(done)
    wg.Wait()
    fmt.Println("all done!")
}

func doIt(workerID int, done <-chan struct{}, wg sync.WaitGroup) {
    fmt.Printf("[%v] is running\n", workerID)
    defer wg.Done()
    <-done
    fmt.Printf("[%v] is done\n", workerID)
}

执行结果:

看起来好像 goroutine 都执行完了,然而报错:

fatal error: all goroutines are asleep - deadlock!

为什么会发生死锁?goroutine 在退出前调用了 wg.Done() ,程序应该正常退出的。

原因是 goroutine 得到的 "WaitGroup" 变量是 var wg WaitGroup 的一份拷贝值,即 doIt() 传参只传值。所以哪怕在每个 goroutine 中都调用了 wg.Done(), 主程序中的 wg 变量并不会受到影响。

// 等待所有 goroutine 执行完毕
// 使用传址方式为 WaitGroup 变量传参
// 使用 channel 关闭 goroutine

func main() {
    var wg sync.WaitGroup
    done := make(chan struct{})
    ch := make(chan interface{})

    workerCount := 2
    for i := 0; i < workerCount; i++ {
        wg.Add(1)
        go doIt(i, ch, done, &wg)    // wg 传指针,doIt() 内部会改变 wg 的值
    }

    for i := 0; i < workerCount; i++ {    // 向 ch 中发送数据,关闭 goroutine
        ch <- i
    }

    close(done)
    wg.Wait()
    close(ch)
    fmt.Println("all done!")
}

func doIt(workerID int, ch <-chan interface{}, done <-chan struct{}, wg *sync.WaitGroup) {
    fmt.Printf("[%v] is running\n", workerID)
    defer wg.Done()
    for {
        select {
        case m := <-ch:
            fmt.Printf("[%v] m => %v\n", workerID, m)
        case <-done:
            fmt.Printf("[%v] is done\n", workerID)
            return
        }
    }
}

运行效果:

32. 向无缓冲的 channel 发送数据,只要 receiver 准备好了就会立刻返回

只有在数据被 receiver 处理时,sender 才会阻塞。因运行环境而异,在 sender 发送完数据后,receiver 的 goroutine 可能没有足够的时间处理下一个数据。如:

func main() {
    ch := make(chan string)

    go func() {
        for m := range ch {
            fmt.Println("Processed:", m)
            time.Sleep(1 * time.Second)    // 模拟需要长时间运行的操作
        }
    }()

    ch <- "cmd.1"
    ch <- "cmd.2" // 不会被接收处理
}

运行效果:

33. 向已关闭的 channel 发送数据会造成 panic

从已关闭的 channel 接收数据是安全的:

接收状态值 okfalse 时表明 channel 中已没有数据可以接收了。类似的,从有缓冲的 channel 中接收数据,缓存的数据获取完再没有数据可取时,状态值也是 false

向已关闭的 channel 中发送数据会造成 panic:

func main() {
    ch := make(chan int)
    for i := 0; i < 3; i++ {
        go func(idx int) {
            ch <- idx
        }(i)
    }

    fmt.Println(<-ch)        // 输出第一个发送的值
    close(ch)            // 不能关闭,还有其他的 sender
    time.Sleep(2 * time.Second)    // 模拟做其他的操作
}

运行结果:

针对上边有 bug 的这个例子,可使用一个废弃 channel done 来告诉剩余的 goroutine 无需再向 ch 发送数据。此时 <- done 的结果是 {}

func main() {
    ch := make(chan int)
    done := make(chan struct{})

    for i := 0; i < 3; i++ {
        go func(idx int) {
            select {
            case ch <- (idx + 1) * 2:
                fmt.Println(idx, "Send result")
            case <-done:
                fmt.Println(idx, "Exiting")
            }
        }(i)
    }

    fmt.Println("Result: ", <-ch)
    close(done)
    time.Sleep(3 * time.Second)
}

运行效果:

34. 使用了值为 nil 的 channel

在一个值为 nil 的 channel 上发送和接收数据将永久阻塞:

func main() {
    var ch chan int // 未初始化,值为 nil
    for i := 0; i < 3; i++ {
        go func(i int) {
            ch <- i
        }(i)
    }

    fmt.Println("Result: ", <-ch)
    time.Sleep(2 * time.Second)
}

runtime 死锁错误:

fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive (nil chan)]

利用这个死锁的特性,可以用在 select 中动态的打开和关闭 case 语句块:

func main() {
    inCh := make(chan int)
    outCh := make(chan int)

    go func() {
        var in <-chan int = inCh
        var out chan<- int
        var val int

        for {
            select {
            case out <- val:
                println("--------")
                out = nil
                in = inCh
            case val = <-in:
                println("++++++++++")
                out = outCh
                in = nil
            }
        }
    }()

    go func() {
        for r := range outCh {
            fmt.Println("Result: ", r)
        }
    }()

    time.Sleep(0)
    inCh <- 1
    inCh <- 2
    time.Sleep(3 * time.Second)
}

运行效果:

34. 若函数 receiver 传参是传值方式,则无法修改参数的原有值

方法 receiver 的参数与一般函数的参数类似:如果声明为值,那方法体得到的是一份参数的值拷贝,此时对参数的任何修改都不会对原有值产生影响。

除非 receiver 参数是 map 或 slice 类型的变量,并且是以指针方式更新 map 中的字段、slice 中的元素的,才会更新原有值:

type data struct {
    num   int
    key   *string
    items map[string]bool
}

func (this *data) pointerFunc() {
    this.num = 7
}

func (this data) valueFunc() {
    this.num = 8
    *this.key = "valueFunc.key"
    this.items["valueFunc"] = true
}

func main() {
    key := "key1"

    d := data{1, &key, make(map[string]bool)}
    fmt.Printf("num=%v  key=%v  items=%v\n", d.num, *d.key, d.items)

    d.pointerFunc()    // 修改 num 的值为 7
    fmt.Printf("num=%v  key=%v  items=%v\n", d.num, *d.key, d.items)

    d.valueFunc()    // 修改 key 和 items 的值
    fmt.Printf("num=%v  key=%v  items=%v\n", d.num, *d.key, d.items)
}

运行结果:

中级篇:35-50

35. 关闭 HTTP 的响应体

使用 HTTP 标准库发起请求、获取响应时,即使你不从响应中读取任何数据或响应为空,都需要手动关闭响应体。新手很容易忘记手动关闭,或者写在了错误的位置:

// 请求失败造成 panic
func main() {
    resp, err := http.Get("https://api.ipify.org?format=json")
    defer resp.Body.Close()    // resp 可能为 nil,不能读取 Body
    if err != nil {
        fmt.Println(err)
        return
    }

    body, err := ioutil.ReadAll(resp.Body)
    checkError(err)

    fmt.Println(string(body))
}

func checkError(err error) {
    if err != nil{
        log.Fatalln(err)
    }
}

上边的代码能正确发起请求,但是一旦请求失败,变量 resp 值为 nil,造成 panic:

panic: runtime error: invalid memory address or nil pointer dereference

应该先检查 HTTP 响应错误为 nil,再调用 resp.Body.Close() 来关闭响应体:

// 大多数情况正确的示例
func main() {
    resp, err := http.Get("https://api.ipify.org?format=json")
    checkError(err)
    
    defer resp.Body.Close()    // 绝大多数情况下的正确关闭方式
    body, err := ioutil.ReadAll(resp.Body)
    checkError(err)

    fmt.Println(string(body))
}

输出:

Get https://api.ipify.org?format=...: x509: certificate signed by unknown authority

绝大多数请求失败的情况下,resp 的值为 nilerrnon-nil。但如果你得到的是重定向错误,那它俩的值都是 non-nil,最后依旧可能发生内存泄露。2 个解决办法:

  • 可以直接在处理 HTTP 响应错误的代码块中,直接关闭非 nil 的响应体。
  • 手动调用 defer 来关闭响应体:
// 正确示例
func main() {
    resp, err := http.Get("http://www.baidu.com")
    
    // 关闭 resp.Body 的正确姿势
    if resp != nil {
        defer resp.Body.Close()
    }

    checkError(err)
    defer resp.Body.Close()

    body, err := ioutil.ReadAll(resp.Body)
    checkError(err)

    fmt.Println(string(body))
}

resp.Body.Close() 早先版本的实现是读取响应体的数据之后丢弃,保证了 keep-alive 的 HTTP 连接能重用处理不止一个请求。但 Go 的最新版本将读取并丢弃数据的任务交给了用户,如果你不处理,HTTP 连接可能会直接关闭而非重用,参考在 Go 1.5 版本文档。

如果程序大量重用 HTTP 长连接,你可能要在处理响应的逻辑代码中加入:

_, err = io.Copy(ioutil.Discard, resp.Body)    // 手动丢弃读取完毕的数据

如果你需要完整读取响应,上边的代码是需要写的。比如在解码 API 的 JSON 响应数据:

json.NewDecoder(resp.Body).Decode(&data)  

36. 关闭 HTTP 连接

一些支持 HTTP1.1 或 HTTP1.0 配置了 connection: keep-alive 选项的服务器会保持一段时间的长连接。但标准库 "net/http" 的连接默认只在服务器主动要求关闭时才断开,所以你的程序可能会消耗完 socket 描述符。解决办法有 2 个,请求结束后:

  • 直接设置请求变量的 Close 字段值为 true,每次请求结束后就会主动关闭连接。
  • 设置 Header 请求头部选项 Connection: close,然后服务器返回的响应头部也会有这个选项,此时 HTTP 标准库会主动断开连接。
// 主动关闭连接
func main() {
    req, err := http.NewRequest("GET", "http://golang.org", nil)
    checkError(err)

    req.Close = true
    //req.Header.Add("Connection", "close")    // 等效的关闭方式

    resp, err := http.DefaultClient.Do(req)
    if resp != nil {
        defer resp.Body.Close()
    }
    checkError(err)

    body, err := ioutil.ReadAll(resp.Body)
    checkError(err)

    fmt.Println(string(body))
}

你可以创建一个自定义配置的 HTTP transport 客户端,用来取消 HTTP 全局的复用连接:

func main() {
    tr := http.Transport{DisableKeepAlives: true}
    client := http.Client{Transport: &tr}

    resp, err := client.Get("https://golang.google.cn/")
    if resp != nil {
        defer resp.Body.Close()
    }
    checkError(err)

    fmt.Println(resp.StatusCode)    // 200

    body, err := ioutil.ReadAll(resp.Body)
    checkError(err)

    fmt.Println(len(string(body)))
}

根据需求选择使用场景:

  • 若你的程序要向同一服务器发大量请求,使用默认的保持长连接。
  • 若你的程序要连接大量的服务器,且每台服务器只请求一两次,那收到请求后直接关闭连接。或增加最大文件打开数 fs.file-max 的值。

37. 将 JSON 中的数字解码为 interface 类型

在 encode/decode JSON 数据时,Go 默认会将数值当做 float64 处理,比如下边的代码会造成 panic:

func main() {
    var data = []byte(`{"status": 200}`)
    var result map[string]interface{}

    if err := json.Unmarshal(data, &result); err != nil {
        log.Fatalln(err)
    }

    fmt.Printf("%T\n", result["status"])    // float64
    var status = result["status"].(int)    // 类型断言错误
    fmt.Println("Status value: ", status)
}
panic: interface conversion: interface {} is float64, not int

如果你尝试 decode 的 JSON 字段是整型,你可以:

  • 将 int 值转为 float 统一使用
  • 将 decode 后需要的 float 值转为 int 使用
// 将 decode 的值转为 int 使用
func main() {
    var data = []byte(`{"status": 200}`)
    var result map[string]interface{}

    if err := json.Unmarshal(data, &result); err != nil {
        log.Fatalln(err)
    }

    var status = uint64(result["status"].(float64))
    fmt.Println("Status value: ", status)
}
  • 使用 Decoder 类型来 decode JSON 数据,明确表示字段的值类型
// 指定字段类型
func main() {
    var data = []byte(`{"status": 200}`)
    var result map[string]interface{}
    
    var decoder = json.NewDecoder(bytes.NewReader(data))
    decoder.UseNumber()

    if err := decoder.Decode(&result); err != nil {
        log.Fatalln(err)
    }

    var status, _ = result["status"].(json.Number).Int64()
    fmt.Println("Status value: ", status)
}

 // 你可以使用 string 来存储数值数据,在 decode 时再决定按 int 还是 float 使用
 // 将数据转为 decode 为 string
 func main() {
     var data = []byte({"status": 200})
      var result map[string]interface{}
      var decoder = json.NewDecoder(bytes.NewReader(data))
      decoder.UseNumber()
      if err := decoder.Decode(&result); err != nil {
          log.Fatalln(err)
      }
    var status uint64
      err := json.Unmarshal([]byte(result["status"].(json.Number).String()), &status);
    checkError(err)
       fmt.Println("Status value: ", status)
}

​- 使用 struct 类型将你需要的数据映射为数值型

// struct 中指定字段类型
func main() {
      var data = []byte(`{"status": 200}`)
      var result struct {
          Status uint64 `json:"status"`
      }

      err := json.NewDecoder(bytes.NewReader(data)).Decode(&result)
      checkError(err)
    fmt.Printf("Result: %+v", result)
}
  • 可以使用 struct 将数值类型映射为 json.RawMessage 原生数据类型

    适用于如果 JSON 数据不着急 decode 或 JSON 某个字段的值类型不固定等情况:

// 状态名称可能是 int 也可能是 string,指定为 json.RawMessage 类型
func main() {
    records := [][]byte{
        []byte(`{"status":200, "tag":"one"}`),
        []byte(`{"status":"ok", "tag":"two"}`),
    }

    for idx, record := range records {
        var result struct {
            StatusCode uint64
            StatusName string
            Status     json.RawMessage `json:"status"`
            Tag        string          `json:"tag"`
        }

        err := json.NewDecoder(bytes.NewReader(record)).Decode(&result)
        checkError(err)

        var name string
        err = json.Unmarshal(result.Status, &name)
        if err == nil {
            result.StatusName = name
        }

        var code uint64
        err = json.Unmarshal(result.Status, &code)
        if err == nil {
            result.StatusCode = code
        }

        fmt.Printf("[%v] result => %+v\n", idx, result)
    }
}

38. struct、array、slice 和 map 的值比较

可以使用相等运算符 == 来比较结构体变量,前提是两个结构体的成员都是可比较的类型:

type data struct {
    num     int
    fp      float32
    complex complex64
    str     string
    char    rune
    yes     bool
    events  <-chan string
    handler interface{}
    ref     *byte
    raw     [10]byte
}

func main() {
    v1 := data{}
    v2 := data{}
    fmt.Println("v1 == v2: ", v1 == v2)    // true
}

如果两个结构体中有任意成员是不可比较的,将会造成编译错误。注意数组成员只有在数组元素可比较时候才可比较。

type data struct {
    num    int
    checks [10]func() bool        // 无法比较
    doIt   func() bool        // 无法比较
    m      map[string]string    // 无法比较
    bytes  []byte            // 无法比较
}

func main() {
    v1 := data{}
    v2 := data{}

    fmt.Println("v1 == v2: ", v1 == v2)
}
invalid operation: v1 == v2 (struct containing [10]func() bool cannot be compared)

Go 提供了一些库函数来比较那些无法使用 == 比较的变量,比如使用 "reflect" 包的 DeepEqual()

// 比较相等运算符无法比较的元素
func main() {
    v1 := data{}
    v2 := data{}
    fmt.Println("v1 == v2: ", reflect.DeepEqual(v1, v2))    // true

    m1 := map[string]string{"one": "a", "two": "b"}
    m2 := map[string]string{"two": "b", "one": "a"}
    fmt.Println("v1 == v2: ", reflect.DeepEqual(m1, m2))    // true

    s1 := []int{1, 2, 3}
    s2 := []int{1, 2, 3}
       // 注意两个 slice 相等,值和顺序必须一致
    fmt.Println("v1 == v2: ", reflect.DeepEqual(s1, s2))    // true
}

这种比较方式可能比较慢,根据你的程序需求来使用。DeepEqual() 还有其他用法:

func main() {
    var b1 []byte = nil
    b2 := []byte{}
    fmt.Println("b1 == b2: ", reflect.DeepEqual(b1, b2))    // false
}

注意:

  • DeepEqual() 并不总适合于比较 slice
func main() {
    var str = "one"
    var in interface{} = "one"
    fmt.Println("str == in: ", reflect.DeepEqual(str, in))    // true

    v1 := []string{"one", "two"}
    v2 := []string{"two", "one"}
    fmt.Println("v1 == v2: ", reflect.DeepEqual(v1, v2))    // false

    data := map[string]interface{}{
        "code":  200,
        "value": []string{"one", "two"},
    }
    encoded, _ := json.Marshal(data)
    var decoded map[string]interface{}
    json.Unmarshal(encoded, &decoded)
    fmt.Println("data == decoded: ", reflect.DeepEqual(data, decoded))    // false
}

如果要大小写不敏感来比较 byte 或 string 中的英文文本,可以使用 "bytes" 或 "strings" 包的 ToUpper()ToLower() 函数。比较其他语言的 byte 或 string,应使用 bytes.EqualFold()strings.EqualFold()

如果 byte slice 中含有验证用户身份的数据(密文哈希、token 等),不应再使用 reflect.DeepEqual()bytes.Equal()bytes.Compare()。这三个函数容易对程序造成 timing attacks,此时应使用 "crypto/subtle" 包中的 subtle.ConstantTimeCompare() 等函数

  • reflect.DeepEqual() 认为空 slice 与 nil slice 并不相等,但注意 byte.Equal() 会认为二者相等:
func main() {
    var b1 []byte = nil
    b2 := []byte{}

    // b1 与 b2 长度相等、有相同的字节序
    // nil 与 slice 在字节上是相同的
    fmt.Println("b1 == b2: ", bytes.Equal(b1, b2))    // true
}

39. 从 panic 中恢复

在一个 defer 延迟执行的函数中调用 recover() ,它便能捕捉 / 中断 panic

// 错误的 recover 调用示例
func main() {
    recover()    // 什么都不会捕捉
    panic("not good")    // 发生 panic,主程序退出
    recover()    // 不会被执行
    println("ok")
}

// 正确的 recover 调用示例
func main() {
    defer func() {
        fmt.Println("recovered: ", recover())
    }()
    panic("not good")
}

从上边可以看出,recover() 仅在 defer 执行的函数中调用才会生效。

// 错误的调用示例
func main() {
    defer func() {
        doRecover()
    }()
    panic("not good")
}

func doRecover() {
    fmt.Println("recobered: ", recover())
}
recobered: <nil> panic: not good

40. 在 range 迭代 slice、array、map 时通过更新引用来更新元素

在 range 迭代中,得到的值其实是元素的一份值拷贝,更新拷贝并不会更改原来的元素,即是拷贝的地址并不是原有元素的地址:

func main() {
    data := []int{1, 2, 3}
    for _, v := range data {
        v *= 10        // data 中原有元素是不会被修改的
    }
    fmt.Println("data: ", data)    // data:  [1 2 3]
}

如果要修改原有元素的值,应该使用索引直接访问:

func main() {
    data := []int{1, 2, 3}
    for i, v := range data {
        data[i] = v * 10    
    }
    fmt.Println("data: ", data)    // data:  [10 20 30]
}

如果你的集合保存的是指向值的指针,需稍作修改。依旧需要使用索引访问元素,不过可以使用 range 出来的元素直接更新原有值:

func main() {
    data := []*struct{ num int }{{1}, {2}, {3},}
    for _, v := range data {
        v.num *= 10    // 直接使用指针更新
    }
    fmt.Println(data[0], data[1], data[2])    // &{10} &{20} &{30}
}

41. slice 中隐藏的数据

从 slice 中重新切出新 slice 时,新 slice 会引用原 slice 的底层数组。如果跳了这个坑,程序可能会分配大量的临时 slice 来指向原底层数组的部分数据,将导致难以预料的内存使用。

func get() []byte {
    raw := make([]byte, 10000)
    fmt.Println(len(raw), cap(raw), &raw[0])    // 10000 10000 0xc420080000
    return raw[:3]    // 重新分配容量为 10000 的 slice
}

func main() {
    data := get()
    fmt.Println(len(data), cap(data), &data[0])    // 3 10000 0xc420080000
}

可以通过拷贝临时 slice 的数据,而不是重新切片来解决:

func get() (res []byte) {
    raw := make([]byte, 10000)
    fmt.Println(len(raw), cap(raw), &raw[0])    // 10000 10000 0xc420080000
    res = make([]byte, 3)
    copy(res, raw[:3])
    return
}

func main() {
    data := get()
    fmt.Println(len(data), cap(data), &data[0])    // 3 3 0xc4200160b8
}

42. Slice 中数据的误用

举个简单例子,重写文件路径(存储在 slice 中)

分割路径来指向每个不同级的目录,修改第一个目录名再重组子目录名,创建新路径:

// 错误使用 slice 的拼接示例
func main() {
    path := []byte("AAAA/BBBBBBBBB")
    sepIndex := bytes.IndexByte(path, '/') // 4
    println(sepIndex)

    dir1 := path[:sepIndex]
    dir2 := path[sepIndex+1:]
    println("dir1: ", string(dir1))        // AAAA
    println("dir2: ", string(dir2))        // BBBBBBBBB

    dir1 = append(dir1, "suffix"...)
       println("current path: ", string(path))    // AAAAsuffixBBBB
    
    path = bytes.Join([][]byte{dir1, dir2}, []byte{'/'})
    println("dir1: ", string(dir1))        // AAAAsuffix
    println("dir2: ", string(dir2))        // uffixBBBB

    println("new path: ", string(path))    // AAAAsuffix/uffixBBBB    // 错误结果
}

拼接的结果不是正确的 AAAAsuffix/BBBBBBBBB,因为 dir1、 dir2 两个 slice 引用的数据都是 path 的底层数组,第 13 行修改 dir1 同时也修改了 path,也导致了 dir2 的修改

解决方法:

  • 重新分配新的 slice 并拷贝你需要的数据
  • 使用完整的 slice 表达式:input[low:high:max],容量便调整为 max - low
// 使用 full slice expression
func main() {

    path := []byte("AAAA/BBBBBBBBB")
    sepIndex := bytes.IndexByte(path, '/') // 4
    dir1 := path[:sepIndex:sepIndex]        // 此时 cap(dir1) 指定为4, 而不是先前的 16
    dir2 := path[sepIndex+1:]
    dir1 = append(dir1, "suffix"...)

    path = bytes.Join([][]byte{dir1, dir2}, []byte{'/'})
    println("dir1: ", string(dir1))        // AAAAsuffix
    println("dir2: ", string(dir2))        // BBBBBBBBB
    println("new path: ", string(path))    // AAAAsuffix/BBBBBBBBB
}

第 6 行中第三个参数是用来控制 dir1 的新容量,再往 dir1 中 append 超额元素时,将分配新的 buffer 来保存。而不是覆盖原来的 path 底层数组

43. 旧 slice

当你从一个已存在的 slice 创建新 slice 时,二者的数据指向相同的底层数组。如果你的程序使用这个特性,那需要注意 "旧"(stale) slice 问题。

某些情况下,向一个 slice 中追加元素而它指向的底层数组容量不足时,将会重新分配一个新数组来存储数据。而其他 slice 还指向原来的旧底层数组。

// 超过容量将重新分配数组来拷贝值、重新存储
func main() {
    s1 := []int{1, 2, 3}
    fmt.Println(len(s1), cap(s1), s1)    // 3 3 [1 2 3 ]

    s2 := s1[1:]
    fmt.Println(len(s2), cap(s2), s2)    // 2 2 [2 3]

    for i := range s2 {
        s2[i] += 20
    }
    // 此时的 s1 与 s2 是指向同一个底层数组的
    fmt.Println(s1)        // [1 22 23]
    fmt.Println(s2)        // [22 23]

    s2 = append(s2, 4)    // 向容量为 2 的 s2 中再追加元素,此时将分配新数组来存

    for i := range s2 {
        s2[i] += 10
    }
    fmt.Println(s1)        // [1 22 23]    // 此时的 s1 不再更新,为旧数据
    fmt.Println(s2)        // [32 33 14]
}

44. 类型声明与方法

从一个现有的非 interface 类型创建新类型时,并不会继承原有的方法:

// 定义 Mutex 的自定义类型
type myMutex sync.Mutex

func main() {
    var mtx myMutex
    mtx.Lock()
    mtx.UnLock()
}
mtx.Lock undefined (type myMutex has no field or method Lock)...

如果你需要使用原类型的方法,可将原类型以匿名字段的形式嵌到你定义的新 struct 中:

// 类型以字段形式直接嵌入
type myLocker struct {
    sync.Mutex
}

func main() {
    var locker myLocker
    locker.Lock()
    locker.Unlock()
}

interface 类型声明也保留它的方法集:

type myLocker sync.Locker

func main() {
    var locker myLocker
    locker.Lock()
    locker.Unlock()
}

45. 跳出 for-switch 和 for-select 代码块

没有指定标签的 break 只会跳出 switch/select 语句,若不能使用 return 语句跳出的话,可为 break 跳出标签指定的代码块:

// break 配合 label 跳出指定代码块
func main() {
loop:
    for {
        switch {
        case true:
            fmt.Println("breaking out...")
            //break    // 死循环,一直打印 breaking out...
            break loop
        }
    }
    fmt.Println("out...")
}

goto 虽然也能跳转到指定位置,但依旧会再次进入 for-switch,死循环。

46. for 语句中的迭代变量与闭包函数

for 语句中的迭代变量在每次迭代中都会重用,即 for 中创建的闭包函数接收到的参数始终是同一个变量,在 goroutine 开始执行时都会得到同一个迭代值:

func main() {
    data := []string{"one", "two", "three"}

    for _, v := range data {
        go func() {
            fmt.Println(v)
        }()
    }

    time.Sleep(3 * time.Second)
    // 输出 three three three
}

最简单的解决方法:无需修改 goroutine 函数,在 for 内部使用局部变量保存迭代值,再传参:

func main() {
    data := []string{"one", "two", "three"}

    for _, v := range data {
        vCopy := v
        go func() {
            fmt.Println(vCopy)
        }()
    }

    time.Sleep(3 * time.Second)
    // 输出 one two three
}

另一个解决方法:直接将当前的迭代值以参数形式传递给匿名函数:

func main() {
    data := []string{"one", "two", "three"}

    for _, v := range data {
        go func(in string) {
            fmt.Println(in)
        }(v)
    }

    time.Sleep(3 * time.Second)
    // 输出 one two three
}

注意下边这个稍复杂的 3 个示例区别:

type field struct {
    name string
}

func (p *field) print() {
    fmt.Println(p.name)
}

// 错误示例
func main() {
    data := []field{{"one"}, {"two"}, {"three"}}
    for _, v := range data {
        go v.print()
    }
    time.Sleep(3 * time.Second)
    // 输出 three three three 
}


// 正确示例
func main() {
    data := []field{{"one"}, {"two"}, {"three"}}
    for _, v := range data {
        v := v
        go v.print()
    }
    time.Sleep(3 * time.Second)
    // 输出 one two three
}

// 正确示例
func main() {
    data := []*field{{"one"}, {"two"}, {"three"}}
    for _, v := range data {    // 此时迭代值 v 是三个元素值的地址,每次 v 指向的值不同
        go v.print()
    }
    time.Sleep(3 * time.Second)
    // 输出 one two three
}

47. defer 函数的参数值

对 defer 延迟执行的函数,它的参数会在声明时候就会求出具体值,而不是在执行时才求值:

// 在 defer 函数中参数会提前求值
func main() {
    var i = 1
    defer fmt.Println("result: ", func() int { return i * 2 }())
    i++
}
result: 2

48. defer 函数的执行时机

对 defer 延迟执行的函数,会在调用它的函数结束时执行,而不是在调用它的语句块结束时执行,注意区分开。

比如在一个长时间执行的函数里,内部 for 循环中使用 defer 来清理每次迭代产生的资源调用,就会出现问题:

// 命令行参数指定目录名
// 遍历读取目录下的文件
func main() {

    if len(os.Args) != 2 {
        os.Exit(1)
    }

    dir := os.Args[1]
    start, err := os.Stat(dir)
    if err != nil || !start.IsDir() {
        os.Exit(2)
    }

    var targets []string
    filepath.Walk(dir, func(fPath string, fInfo os.FileInfo, err error) error {
        if err != nil {
            return err
        }

        if !fInfo.Mode().IsRegular() {
            return nil
        }

        targets = append(targets, fPath)
        return nil
    })

    for _, target := range targets {
        f, err := os.Open(target)
        if err != nil {
            fmt.Println("bad target:", target, "error:", err)    //error:too many open files
            break
        }
        defer f.Close()    // 在每次 for 语句块结束时,不会关闭文件资源
        
        // 使用 f 资源
    }
}

先创建 10000 个文件:

#!/bin/bash
for n in {1..10000}; do
    echo content > "file${n}.txt"
done

运行效果:

解决办法:defer 延迟执行的函数写入匿名函数中:

// 目录遍历正常
func main() {
    // ...

    for _, target := range targets {
        func() {
            f, err := os.Open(target)
            if err != nil {
                fmt.Println("bad target:", target, "error:", err)
                return    // 在匿名函数内使用 return 代替 break 即可
            }
            defer f.Close()    // 匿名函数执行结束,调用关闭文件资源
            
            // 使用 f 资源
        }()
    }
}

当然你也可以去掉 defer,在文件资源使用完毕后,直接调用 f.Close() 来关闭。

49. 失败的类型断言

在类型断言语句中,断言失败则会返回目标类型的“零值”,断言变量与原来变量混用可能出现异常情况:

// 错误示例
func main() {
    var data interface{} = "great"

    // data 混用
    if data, ok := data.(int); ok {
        fmt.Println("[is an int], data: ", data)
    } else {
        fmt.Println("[not an int], data: ", data)    // [isn't a int], data:  0
    }
}


// 正确示例
func main() {
    var data interface{} = "great"

    if res, ok := data.(int); ok {
        fmt.Println("[is an int], data: ", res)
    } else {
        fmt.Println("[not an int], data: ", data)    // [not an int], data:  great
    }
}

50. 阻塞的 gorutinue 与资源泄露

在 2012 年 Google I/O 大会上,Rob Pike 的 Go Concurrency Patterns 演讲讨论 Go 的几种基本并发模式,如 完整代码 中从数据集中获取第一条数据的函数:

func First(query string, replicas []Search) Result {
    c := make(chan Result)
    replicaSearch := func(i int) { c <- replicas[i](query) }
    for i := range replicas {
        go replicaSearch(i)
    }
    return <-c
}

在搜索重复时依旧每次都起一个 goroutine 去处理,每个 goroutine 都把它的搜索结果发送到结果 channel 中,channel 中收到的第一条数据会直接返回。

返回完第一条数据后,其他 goroutine 的搜索结果怎么处理?他们自己的协程如何处理?

First() 中的结果 channel 是无缓冲的,这意味着只有第一个 goroutine 能返回,由于没有 receiver,其他的 goroutine 会在发送上一直阻塞。如果你大量调用,则可能造成资源泄露。

为避免泄露,你应该确保所有的 goroutine 都能正确退出,有 2 个解决方法:

  • 使用带缓冲的 channel,确保能接收全部 goroutine 的返回结果:
func First(query string, replicas ...Search) Result {  
    c := make(chan Result,len(replicas))    
    searchReplica := func(i int) { c <- replicas[i](query) }
    for i := range replicas {
        go searchReplica(i)
    }
    return <-c
}
  • 使用 select 语句,配合能保存一个缓冲值的 channel default 语句:

    default 的缓冲 channel 保证了即使结果 channel 收不到数据,也不会阻塞 goroutine

func First(query string, replicas ...Search) Result {  
    c := make(chan Result,1)
    searchReplica := func(i int) { 
        select {
        case c <- replicas[i](query):
        default:
        }
    }
    for i := range replicas {
        go searchReplica(i)
    }
    return <-c
}
  • 使用特殊的废弃(cancellation) channel 来中断剩余 goroutine 的执行:
func First(query string, replicas ...Search) Result {  
    c := make(chan Result)
    done := make(chan struct{})
    defer close(done)
    searchReplica := func(i int) { 
        select {
        case c <- replicas[i](query):
        case <- done:
        }
    }
    for i := range replicas {
        go searchReplica(i)
    }

    return <-c
}

Rob Pike 为了简化演示,没有提及演讲代码中存在的这些问题。不过对于新手来说,可能会不加思考直接使用。

高级篇:51-57

51. 使用指针作为方法的 receiver

只要值是可寻址的,就可以在值上直接调用指针方法。即是对一个方法,它的 receiver 是指针就足矣。

但不是所有值都是可寻址的,比如 map 类型的元素、通过 interface 引用的变量:

type data struct {
    name string
}

type printer interface {
    print()
}

func (p *data) print() {
    fmt.Println("name: ", p.name)
}

func main() {
    d1 := data{"one"}
    d1.print()    // d1 变量可寻址,可直接调用指针 receiver 的方法

    var in printer = data{"two"}
    in.print()    // 类型不匹配

    m := map[string]data{
        "x": data{"three"},
    }
    m["x"].print()    // m["x"] 是不可寻址的    // 变动频繁
}
cannot use data literal (type data) as type printer in assignment:

data does not implement printer (print method has pointer receiver)

cannot call pointer method on m["x"]
cannot take the address of m["x"]

52. 更新 map 字段的值

如果 map 一个字段的值是 struct 类型,则无法直接更新该 struct 的单个字段:

// 无法直接更新 struct 的字段值
type data struct {
    name string
}

func main() {
    m := map[string]data{
        "x": {"Tom"},
    }
    m["x"].name = "Jerry"
}
cannot assign to struct field m["x"].name in map

因为 map 中的元素是不可寻址的。需区分开的是,slice 的元素可寻址:

type data struct {
    name string
}

func main() {
    s := []data{{"Tom"}}
    s[0].name = "Jerry"
    fmt.Println(s)    // [{Jerry}]
}

注意:不久前 gccgo 编译器可更新 map struct 元素的字段值,不过很快便修复了,官方认为是 Go1.3 的潜在特性,无需及时实现,依旧在 todo list 中。

更新 map 中 struct 元素的字段值,有 2 个方法:

  • 使用局部变量
// 提取整个 struct 到局部变量中,修改字段值后再整个赋值
type data struct {
    name string
}

func main() {
    m := map[string]data{
        "x": {"Tom"},
    }
    r := m["x"]
    r.name = "Jerry"
    m["x"] = r
    fmt.Println(m)    // map[x:{Jerry}]
}
  • 使用指向元素的 map 指针
func main() {
    m := map[string]*data{
        "x": {"Tom"},
    }
    
    m["x"].name = "Jerry"    // 直接修改 m["x"] 中的字段
    fmt.Println(m["x"])    // &{Jerry}
}

但是要注意下边这种误用:

func main() {
    m := map[string]*data{
        "x": {"Tom"},
    }
    m["z"].name = "what???"     
    fmt.Println(m["x"])
}
panic: runtime error: invalid memory address or nil pointer dereference

53. nil interface 和 nil interface 值

虽然 interface 看起来像指针类型,但它不是。interface 类型的变量只有在类型和值均为 nil 时才为 nil

如果你的 interface 变量的值是跟随其他变量变化的(雾),与 nil 比较相等时小心:

func main() {
    var data *byte
    var in interface{}

    fmt.Println(data, data == nil)    // <nil> true
    fmt.Println(in, in == nil)    // <nil> true

    in = data
    fmt.Println(in, in == nil)    // <nil> false    // data 值为 nil,但 in 值不为 nil
}

如果你的函数返回值类型是 interface,更要小心这个坑:

// 错误示例
func main() {
    doIt := func(arg int) interface{} {
        var result *struct{} = nil
        if arg > 0 {
            result = &struct{}{}
        }
        return result
    }

    if res := doIt(-1); res != nil {
        fmt.Println("Good result: ", res)    // Good result:  <nil>
        fmt.Printf("%T\n", res)            // *struct {}    // res 不是 nil,它的值为 nil
        fmt.Printf("%v\n", res)            // <nil>
    }
}


// 正确示例
func main() {
    doIt := func(arg int) interface{} {
        var result *struct{} = nil
        if arg > 0 {
            result = &struct{}{}
        } else {
            return nil    // 明确指明返回 nil
        }
        return result
    }

    if res := doIt(-1); res != nil {
        fmt.Println("Good result: ", res)
    } else {
        fmt.Println("Bad result: ", res)    // Bad result:  <nil>
    }
}

54. 堆栈变量

你并不总是清楚你的变量是分配到了堆还是栈。

在 C++ 中使用 new 创建的变量总是分配到堆内存上的,但在 Go 中即使使用 new()make() 来创建变量,变量为内存分配位置依旧归 Go 编译器管。

Go 编译器会根据变量的大小及其 "escape analysis" 的结果来决定变量的存储位置,故能准确返回本地变量的地址,这在 C/C++ 中是不行的。

在 go build 或 go run 时,加入 -m 参数,能准确分析程序的变量分配位置:

55. GOMAXPROCS、Concurrency(并发)and Parallelism(并行)

Go 1.4 及以下版本,程序只会使用 1 个执行上下文 / OS 线程,即任何时间都最多只有 1 个 goroutine 在执行。

Go 1.5 版本将可执行上下文的数量设置为 runtime.NumCPU() 返回的逻辑 CPU 核心数,这个数与系统实际总的 CPU 逻辑核心数是否一致,取决于你的 CPU 分配给程序的核心数,可以使用 GOMAXPROCS 环境变量或者动态的使用 runtime.GOMAXPROCS() 来调整。

误区:GOMAXPROCS 表示执行 goroutine 的 CPU 核心数,参考文档

GOMAXPROCS 的值是可以超过 CPU 的实际数量的,在 1.5 中最大为 256

func main() {
    fmt.Println(runtime.GOMAXPROCS(-1))    // 4
    fmt.Println(runtime.NumCPU())    // 4
    runtime.GOMAXPROCS(20)
    fmt.Println(runtime.GOMAXPROCS(-1))    // 20
    runtime.GOMAXPROCS(300)
    fmt.Println(runtime.GOMAXPROCS(-1))    // Go 1.9.2 // 300
}

56. 读写操作的重新排序

Go 可能会重排一些操作的执行顺序,可以保证在一个 goroutine 中操作是顺序执行的,但不保证多 goroutine 的执行顺序:

var _ = runtime.GOMAXPROCS(3)

var a, b int

func u1() {
    a = 1
    b = 2
}

func u2() {
    a = 3
    b = 4
}

func p() {
    println(a)
    println(b)
}

func main() {
    go u1()    // 多个 goroutine 的执行顺序不定
    go u2()    
    go p()
    time.Sleep(1 * time.Second)
}

运行效果:

如果你想保持多 goroutine 像代码中的那样顺序执行,可以使用 channel 或 sync 包中的锁机制等。

57. 优先调度

你的程序可能出现一个 goroutine 在运行时阻止了其他 goroutine 的运行,比如程序中有一个不让调度器运行的 for 循环:

func main() {
    done := false

    go func() {
        done = true
    }()

    for !done {
    }

    println("done !")
}

for 的循环体不必为空,但如果代码不会触发调度器执行,将出现问题。

调度器会在 GC、Go 声明、阻塞 channel、阻塞系统调用和锁操作后再执行,也会在非内联函数调用时执行:

func main() {
    done := false

    go func() {
        done = true
    }()

    for !done {
        println("not done !")    // 并不内联执行
    }

    println("done !")
}

可以添加 -m 参数来分析 for 代码块中调用的内联函数:

你也可以使用 runtime 包中的 Gosched() 来 手动启动调度器:

func main() {
    done := false

    go func() {
        done = true
    }()

    for !done {
        runtime.Gosched()
    }

    println("done !")
}

运行效果:

总结

感谢原作者 kcqon 总结的这篇博客,让我受益匪浅。

由于译者水平有限,不免出现理解失误,望读者在下评论区指出,不胜感激。

后续再更新类似高质量文章的翻译 ?

查看原文

赞 197 收藏 217 评论 13

大呜 回答了问题 · 2018-03-20

关于laravel-echo

可以参考一下应用场景的第3点 php使用swoole的应用场景

关注 4 回答 3

大呜 发布了文章 · 2018-03-19

二维码被扫实时返回方案

来自个人博客 二维码被扫实时返回方案

场景

  1. 需要在小程序二维码扫码功能
  2. 被扫码成功后跳转到成功页面
  3. 不想使用websocket 通讯,想通过接口的方法实现

方案

  1. 用户扫码二维码成功后 会入库,并入redis 队列
  2. 前端请求扫码状态接口,后端使用redis 取队列方法BRPOP 阻塞25秒,有则返回成功状态,没有则继续等待,超过25秒发状态码让前端重新请求接口。

优点

  1. 避免前端多次的轮询,减少服务器压力
  2. 可以不用websocket实时的知道被扫码情况

流程图

图片描述

查看原文

赞 0 收藏 2 评论 0

大呜 赞了文章 · 2018-03-16

SegmentFault 讲堂一周岁:Keep learning

图片描述

一转眼,我入职 SegmentFault 快接近一年。再回想一下,SegmentFault 讲堂也一周岁了,是时候捋一捋我们这一年都干了些啥,来和我一起回顾下你与讲堂的交集吧~

SegmentFault 讲堂成长轨迹

2017 年 3 月,讲堂正式上线。

2017 年 3 月 14 日,帅气的歪果仁讲师,直播了第一场 Live 讲座。

2017 年 4 月 18 日,讲座折扣券和免费券功能上线。(偷笑脸,可以省点💰了)

2017 年 4 月 27 日,讲座邀请好友获得分成功能上线,每成功邀请一人,你将从讲师所得中获取 30% 的分成。(这回不光可以省💰,还可以赚💰)

2017 年 6 月 20 日,讲座评分和收藏功能上线。(滴!一键收藏我喜欢的讲座,为我支持的讲师打 call )

2017 年 8 月 26 日,老猫发起了第一场视频讲座。

2017 年 9 月 17 日,小马哥发起了第一个系列讲座。

2017 年 9 月 22 日,讲座免费试看功能上线,所有已生成录播的讲座,你均可在购买前试看。(剁手前终于可以瞅瞅讲座的质量啦!)

2017 年 11 月 11 日 - 11 月 13 日,我们搞了个事情:讲堂优惠活动。

2018 年 1 月 2 日 - 1 月 3 日,上线了三堂免费公开课。

以上罗列了一些较为重要的成长节点,产品上的优化还有很多啦 ^O^

你的学习轨迹

这一年里,你可能学习过:

基于 Vue.js 2.x 的 iView 组件开发实践

Java 微服务实践 - Spring Boot 系列(一)初体验

深入剖析 iOS 编译 Clang / LLVM

1 个人如何运维年交易额 30 亿的金融平台

云计算,大数据,人工智能的相遇,相识,相知

如何“四两拨千斤”做好项目管理

......

SegmentFault 讲堂和讲师收到的

SF 讲堂上线以来,讲堂团队收到许多 SFer 的宝贵建议,每一条建议,讲堂 PM 都在思考,也在不断地打磨产品以达到 SFer 的期待。而讲师们,在为大家传道授业时,也收到了许多学员的肯定与支持。

图片描述

图片描述

图片描述

图片描述

你从讲堂中获得的

这一年里,你可能 get 了:

求职面试的奇技淫巧

网站架构思路和设计理念

正确而优雅的撸码姿势

某些技术原理或概念

迅速找到 Bug 并解决的能力

......

一年讲座盘点

这一年,SF 讲堂一共上线了 300 多堂讲座,技术领域上涵盖前端开发、后端开发、移动端开发、运维、大数据等。讲座从内容类型上也分成了四大类:知识体系、项目实战、职业规划、综合。以下,我将一部分优秀讲座分类成专题形式,给大家盘点下。

求职&面试

程序员价值最大化 - 如何在面试中脱颖而出(限时优惠中) @周梦康

硬实力让大家的能力得到提升,软实力让大家的 价值 (工资)得到提升。💪
面试失败了不要怪面试官不识货,因为面试+笔试是面试官唯一能认定你能力的途径。
最近收到过不少简历,也面试了不少人,之前自己也面过不少公司,觉得很有必要把一些经验分享给大家,避免大家走弯路,走错路。

前端面试攻略:避免求职中的“非战斗减员” @Meathill

“非战斗减员”指的就是在未发生战斗的情况下,因为地形、后勤、疫病、自然灾害等导致的部队减员的情况。所谓“出师未捷身先死,长使英雄泪满襟”。在面试求职的时候,有些岗位我们达不到对方的要求,被刷下来很正常;但也有一些机会,明明自己能力是够的,但是连面试的机会都没拿到,我就称之为“非战斗减员”。

造成求职中“非战斗减员”的因素比较多,有些是因为求职者本人比较懒,有些是大家对招聘本身不了解。不过结果都一样,稍不留神,机会就会从手边溜走。这次分享,将介绍我筛选简历、面试别人和我自己作为候选人的经验,帮助大家尽可能避免踩进这些坑。

亚马逊资深面试官教你如何面试 @凯威的讲堂

技术面试是大家很熟悉的过程,相信每个人面试前总是有点紧张。面对心仪的公司,很怕面挂了被关到小黑屋,一年或者半年之内都不能再面试。

有的时候觉得面试官出题不可理喻,有时候觉得自己答得挺好却挂了。到底怎么样才能与面试官愉快地面试?本期讲座,我就将和大家分享帮你面试通关的独门诀窍。

前端面试攻略:肉老师的面试题详解 @Meathill

我的面试题由日常积累而来,包含HTML、CSS、布局、JS、框架、优化、开发习惯等等方面。可深可浅,根据招聘需求来实时调整。我用这套面试题面试了大约200人,有现场也有电话,对它的覆盖面基本满意。事后基本也验证得到验证。

为了照顾初段同学,这次还会分享我对面试的理解,简单的博弈论如零和博弈多和博弈等,方便大家在技术之外提升自己。

PHP笔试面试题精选(一) @纸牌屋弗兰克

本次课程主要围绕 PHP 面试和笔试中经常会出现的一些知识点,但是面试官会在笔试题基础上深入扩展,那么你知道如何更好的回答让面试官满意吗?

面试题目收集自腾讯,迅雷,美图等公司的笔试面试题,以及本人面试经历中印象中的知识点,同时也分享一些面试的经验,相信对你一定有很大的参考价值。本期题目重点涉及基础知识,安全,跨域,及两个简单的设计模式。

实战开发

python爬虫之实战花瓣网 @kimg1234

花瓣网爬虫的实战,主要介绍:1.如何爬取异步加载的网页;2.如何解析请求中的参数;3.headers中的Accept如何应用;4.如何优雅的获取JavaScript中的内容;5.如何解决爬取网页过程中遇到的问题。

Vue实战:打造属于你的博客发布系统 @jrainlau

本次讲座主要针对具有一定Vue.js开发基础的同学。

相信大家已经看过不少关于Vue.js的相关介绍,但可能一直没有灵感或机会去深入尝试。这次讲座将会从0开始,一步一步教你如何通过Vue.js去打造一款先进的博客发布系统。

相比于制作一个“博客页面”,我更倾向于从“工程化”的角度去阐述一个完整的Vue.js项目。从功能设计,环境搭建,编码规范,到具体的项目开发,每一步都值得我们关注。

【前端工程化】玩转Webpack配置 @jrainlau

本次讲座将会从实际项目出发,使用主流的“三个配置文件”的办法,从零开始教你如何进行webpack配置,最终搭建一套完整的开发/生产构建环境。
讲座难度适中,对新手友好,更适合对前端工程化感兴趣,想要加深对webpack理解的同学。

Spring Boot + Redis 实现 论坛系统 @拿客_三产

本系列课程虽然是从实战出发来实现一个论坛系统,但是受限于课时、受众水平不一等原因,课程讲述的内容还是局限于单机 Web 应用,对高并发、集群等内容涉及较少。但是本系列课程的初衷并不是完全手把手交给大家论坛系统的实现,主要侧重点其实是为大家介绍我在学习实践过程中总结出的一套学习技术的思路。

课程知识点:Spring Boot、Spring data redis、Spring Security、Druid 数据库连接池、Mybatis、Kotlin。

被three.js玩坏的地球(限时优惠中) @Chaos

该教程为three.js 可视化入门,讲解了three.js 最常用的可视化领域,也就是制作一个地球,包括绘制点,飞线,以及柱图的绘制。

前端人成长之路

前端工程师的自我修养(限时优惠中) @小胡子哥

这些年,前端领域尘土飞扬,有的公司开始宣扬「大前端」理念;也有公司合并前端和客户端更名为「端团队」;工程师们也在追求着「全栈」的名号……
前端在变,如何在变化中寻求不变,立身于前端的不败之地?小胡子哥将和大家聊一聊前端工程师的自我修养。

前端程序员应该懂点 V8 知识 @justjavac

对于每个前端程序员来讲都有一个终极理想,那就是搞懂 javascript 引擎是如何工作的。javascript 性能经过了两次飞跃:第 1 次飞跃是 2008 年 V8 发布,第 2 次则是 2017 年的 WebAssembly。不过WebAssembly 到底能不能掀起前端的波澜还是未知数,但是 V8 对前端的贡献大家都有目共睹。

讲座主要内容:1.我为什么要研究V8;;2.V8 为什么这么快?;3.动态语言如何进行快速算数运算;4.如何编写高性能的 JS 代码;5.ES 新特质以及 V8 对 ES 新特性的支持;6.可读性 VS 高性能。

Web前端职业技能与规划 @碧青_Kwok

本次分享是总结一下自己从一个前端小白,历经近年前端的快速发展,期间有效学习与实践的经验,并分享对前端这个行业的冷静思考,与对新“入坑”的同学提出一些建议。内容分两大块,分别是前端开发的技能体系和我眼中的前端职业素养与规划。

前端工程师应掌握的网络知识 @碧青_Kwok

网络协议是 Web 技术的基础设施,虽然大多数前端工程师不用直接面向 HTTP、TCP 这些协议编程,但在问题排查、性能优化等方面的能力,必须建立在对网络知识熟练的掌握和理解的基础上。

本课程将讲解前后端通信回路的各项关键网络环节,分析协议及策略(缓存、安全等)等性能及影响,提出优化建议与最佳实践。内容受众:适合网络基础比较薄弱,或在网络性能方面有深入了解意愿的同学参加。

前端知识巩固

JavaScript 异步编程 @王顶

异步编程对于网站前端开发来说,重要性可能还不是太明显,毕竟前端页面的逻辑相对比较简单,也就是 AJAX 应用涉及到远程资源的请求,用到一些异步编程的技术。但是,对于 JavaScript 结合 Node.js,做服务器端编程来说,异步编程就是必须要掌握的了。如果不掌握 JavaScript 异步编程,基本上 Node.js 开发是玩不转的。也就是说,不掌握 JavaScript 异步编程,Node.js 不算入门。本讲座主要介绍四种异步编程的方法以及三种流程控制的实现方式。

前端面试攻略:JavaScript 排序与搜索 @Meathill

从事前端开发的同学很多从页面仔入门,比如说我,自学比例很大,有些时候会无意中忽视一些基础,比如算法、数据结构。这些欠缺在某些时候就会显得很致命,比如说面试,或者处理大量数据的场景。所以希望这样的一场分享能够帮助大家夯实原本不太扎实的基础,将来的开发之路更加顺畅。

这次分享的主要内容有:排序、搜索、例题解析。内容受众:初级前端程序员,有编程基础,能阅读 JS。

javascript面向对象必知必会 @ghostwu

内容包括javascript面向对象常见知识:1,变量提升(也叫词法解释);2,this详解;3,图解对象;4,原型对象(prototype) 与 隐式原型(__proto__)详解;5,原型链查找规则;6,图解3种引用类型( 函数,对象,数组 );7,函数表达式,立即表达式,闭包,模块化开发。

写 CSS 也要开脑洞:万能的 :checked + label @Meathill

你可能不知道,网上那些看起来高大上的表单控件,实现的机制都是 :checked + label。这一对 CSS3 新增的选择器帮助我们将纯 CSS 组件的版图拓展出去一大块。再复合其它的元素和选择器,比如 flexbox、~ 、动画,我们可以开发出更多又好看又好用兼容性又好的表单控件。

通过学习本次分享,您将学会:1.CSS 预处理工具 Stylus 的使用;2.了解到 CSS3 若干新增元素;3.CSS 动画基础。

深入理解布局神器 flexbox @一歩

将一个属性作为一个主题是不是太夸张了?

No,No,No。flexbox 布局相关属性不是一般的多,概念看了一遍又一遍,到实际操作还是无从下手。

本次课程主要向大家讲解 flex 布局的方方面面,从概念到实战。理论和实践相结合,讲解概念的同时进行代码演示。

彻底掌握 JS 异步处理 Promise 和 Async-Await @一歩

本课程旨在让大家快速地学会Promise、Async-Await的使用,脱离ES5时代的回调地狱。
适用人群:前端切图仔、nodejs 业务仔、没事闲的想体验一下ES6 ES7的新特性的。课程风格:撸码+理论。

来,我们一起实现一个 Promise @充电大喵

本次分享将带大家实现一个能够通过所有测试的 Promise/A+ 类,同时也会讲解标准中的一些设定,深入你对 Promise/A+ 标准的理解。

在分享中,你将学习到如下内容:1.Promise 的实现;Promise 标准中一些设计的原因;2.为什么不同的 Promise 库可以交互(即相互调用而不会出错);3.Promise 中常用 helper 函数的实现(如 race,all,catch 等)4.如何测试你自己实现的 Promise 库;5.Promise 与 Deferred 对象的区别及联系;6.其它与 Promise 相关的知识点。

Promise 的 N 种用法 @Meathill

现在大部分浏览器和 Node.js 都已原生支持 Promise,很多类库也开始返回 Promise 对象,即使面对 IE,也有各种降级适配策略。如果您现在还不会使用 Promise,那么我建议您尽快学习一下。

本次分享我准备结合近期的一些开发经验,总结一下 Promise 常见用法,介绍一下我踩过的坑。分享大纲如下:1.什么是 Promise;2.为什么要用 Promise;3.Promise 详解;4.简单范例;5.复杂加载过程;6.改进代码可读性;7.常见错误。

[公益]学习 Vue 你需要知道的 webpack 知识 @KingMario

学习 Vue,诚如其作者尤雨溪在《新手向:Vue 2.0 的建议学习顺序》中突出强调的,了解前端生态/工程化,了解 Webpack 的概念和配置相当重要,本讲座根据在 SegmentFault 回答的各种实际项目中遇到的问题进行归纳和总结,介绍学习 Vue 你需要知道的 webpack 知识,同时也会介绍 Vue-cli 命令行使用 webpack 项目模板所创建项目的配置相关知识、概念和技巧。

组合火力的威力——Vue Dropdown 组件开发示例 @KingMario

本次讲座通过一个 Dropdown 组件开发的演练,展示 Vue 框架在类绑定语法、数据、响应、事件、组件内容、父子组件间通信以及生命周期钩子等方面多种组合火力的威力,解决组件开发中遭遇的常见问题。

面向人群:1.有一定 Vue 开发基础,熟悉其声明式模板语法,了解实例数据、计算属性、watcher……概念和使用方法,了解事件绑定方法及常用修饰符。2.了解 Vue 组件开发,了解组件 props 选项、父子组件间通信方式、通过 slot 进行内容分发。3.对于开发通用 UI 组件感兴趣,或者工作中有基于现有 UI 样式重新造轮子的需求。

Node.js 应用开发系列

Node.js 是 JavaScript 语言的服务器运行环境。Node.js 提供的 API 可以帮助我们快速、高效的构建服务器应用程序。当然,前提是我们能熟练使用 JavaScript 编程语言。本系列讲座由 王顶 讲授,目前已更新至第 14 节。

王顶老师:河北师范大学软件学院讲师,河北师范大学物联网研究院技术总监,拥有微软认证 MCSE、MCP、MCT。

Node.js 应用开发系列(01):Node.js 简介

Node.js 应用开发系列(02):全局对象编程入门

Node.js 应用开发系列(03):Buffer 编程入门

Node.js 应用开发系列(04):模块管理入门

Node.js 应用开发系列(05):事件编程入门

Node.js 应用开发系列(06):流操作入门

Node.js 应用开发系列(07):文件 I/O 操作入门

Node.js 应用开发系列(08):网络编程入门

Node.js 应用开发系列(09):子进程操作入门

Node.js 应用开发系列(10):web 应用开发(上)

Node.js 应用开发系列(10):web 应用开发(下)

Node.js 应用开发系列(11):单元测试入门

Node.js 应用开发系列(12):调试程序入门

Node.js 应用开发系列(14):压缩与解压缩

Java 微服务实践系列

SegmentFault 讲堂里最火的系列讲座之一。讲师:小马哥,一线互联网公司技术专家,十余年 Java EE 从业经验,架构师、微服务布道师。目前主要负责微服务技术实施、架构衍进、基础设施构建等。重点关注云计算、微服务以及软件架构等领域。通过SUN Java(SCJP、SCWCD、SCBCD)以及Oracle OCA等认证。

Spring Boot 为系列讲座,二十节专题直播,时长高达50个小时,包括目前最流行技术,深入源码分析,授人以渔的方式,帮助初学者深入浅出地掌握,为高阶从业人员抛砖引玉。

Spring Cloud 系列课程致力于以实战的方式覆盖所有功能特性,结合小马哥十余年的学习方法和工作经验,体会作者设计意图。结合源码加深理解,最终达到形成系统性的知识和技术体系的目的。

学员评价:相对于世面上的快餐视频、快餐书籍来说,小马哥讲得很入微,好的不仅仅是能帮你找工作,而且是帮你找一个好的工作。——铁拳阿牛

Java 微服务实践 - Spring Boot / Spring Cloud(限时优惠中)

Java 微服务实践 - Spring Boot 系列(限时优惠中)

Java 微服务实践 - Spring Cloud 系列(限时优惠中)

PHPer 进阶之路

PHP单元测试与测试驱动开发 @vimac

这次讲座将分享 PHPUnit 来编写单元测试, 以及通过单元测试的方式来进行测试驱动开发。
内容介绍:1.单元测试是什么;2.为什么要进行单元测试;3.单元测试怎么做;4.如何通过单元测试来进行测试驱动开发。

PHP 进阶之路 - 零基础构建自己的服务治理框架(上)(限时优惠中) @周梦康

PHP 进阶之路 - 零基础构建自己的服务治理框架(下)(限时优惠中) @周梦康

什么是服务治理?
总是听别人分享他们大项目中总会用到服务器治理框架?
大概明白,又不太明白,总有种雾里看花的感觉?
面试的时候老问,深入了又答不上来?
那么这堂课将为你揭开这些困惑!

PHP 进阶之路(限时优惠中) @周梦康

从简单重复的业务中跳出来,看一看架构师是如何工作的,你有多久没有投资自己了。
本系列从大中型项目的架构梳理,到性能提升实战,然后在更大体系的系统下,构造并使用服务治理框架。最后不要拘泥于一门语言,使用 java 快速构建一套 api 服务。

PHP开发者轻松掌握composer三部曲 @阿北

这个系列从composer的安装、使用、命令以及发布各个角度讲解composer的相关知识,提高开发速度。一包烟、一门知识,它们同样重要。

玩转yii2的rbac系列课程 @阿北

作为一个后端,rbac是必须要学的,很多框架都内置了这个权限管理的机制,我们的yii2也一样。
本系列从yii2的acf到rbac,将yii2中的权限管理进行了全面的讲解,同时最后为你提供一个当前最稳定的yii2-admin rbac扩展,让你理念实战两不误。

后端知识巩固

后端工程师必备知识 — 索引(上) @王子亭

后端工程师必备知识 — 索引(下) @王子亭

这个系列分为上下两集,介绍了各种类型的索引能够加速怎样的查询,帮助后端开发者更好地利用索引改进查询性能。上半部分包括对于索引的基本原理介绍、由单个字段构成的索引,以及区分度这个概念。下半部分包括多个字段构成的复合索引、常见的慢查询、数据库性能优化的思路。

Learn Clojure: The Easy Way @jiacai2050

我是 2013年 从 SICP 开始接触 Lisp,之后一直在不断探索这门古老但富有生命力的语言,现在的工作也是以 Clojure 为技术栈的后端开发,深深被其优雅、强大的表达力所吸引,Clojure 作为 21 世纪的 Lisp 方言,除了具有原始 Lisp 的优势,其设计之初就把并发作为一重要特性,不可变的数据结构,STM 都是十分优秀的设计,我已经等不及向大家展示这门语言了。

这应该是国内第一套介绍 Clojure 的视频,我尽了最大能力去整理资料,涵盖 Clojure 语言的方方面面,做到知其然知其所以然,希望为各位学习 Clojure 提供些许帮助。

Redis 系列讲座合集 @拿客_三产

为什么要学习Redis?

Redis 最为目前炙手可热的 Key-Value 数据库,常用做缓存、Session共享中间件,分布式锁等等。

很多企业都要求要熟悉 Redis 的使用。所以学会使用 Redis 可以使你更具竞争力,Java、PHP、Python等主流编程语言开发的项目中 Redis 都有普遍应用,学习 Redis 可以在企业眼中更具吸引力。虽然 Redis 受到开发者和企业的喜爱,但是在实际应用中却局限于缓存等常见场景,并且大多数开发人员对 Redis 的使用场景以及调优一知半解。

本系列课程主要由浅及深为大家提供更多 Redis 应用场景以及相关调优方法。

容器技术

本系列课程主要面向一线的开发和运维人员,帮助开发和运维掌握 Kubernetes 的使用和维护,了解Kubernetes的架构,了解如何扩展 Kubernetes。本系列讲座由 青云QingCloud 讲授,目前已更新至第 5 节。

讲师:王渊命,青云 QingCloud 知行学院讲师,青云 QingCloud 容器平台负责人,曾任新浪微博架构师、微米技术总监、Grouk 技术负责人,他是云与容器的深度实践者,重度工具控。目前在青云 QingCloud 负责容器平台的相关开发,目标是让各种容器平台更好地运行在 QingCloud 之上。

预备课:深入理解 Docker 内部原理及网络配置

第一课:10个小时,深入掌握Kubernetes以及Kubernetes应用实践

第二课:Kubernetes 的安装和运维

第三课:Kubernetes 的网络和存储

第四课:Kubernetes 的 API Spec 以及安全机制

Android 开发必修课

本系列课程主要面向 Android 初学者,旨在帮助大家搞懂 Android 开发中的方方面面。本系列讲座由 阿里巴巴千牛安卓 讲授,讲师们均为阿里巴巴资深无线开发工程师,目前已更新至第 4 节。

Android 资源文件那些事儿

Android 线程同步那些事儿

Android 开发之Activity那些事儿

Android 进程保活那些事儿

如需观看更多讲座 >>> 请乘坐电梯直达

写在最后

这一年,感谢你陪伴着 SegmentFault 讲堂一起成长,看着技术哥哥们修复一个个八阿哥,看着 PM 优化一个个功能点。同时,我们欢迎大家给 SF 讲堂提出更多改进的建议,你的发声是我们前进的动力。

讲师招募令:我们欢迎更多资深的技术人士来 SF 讲堂分享自己的技术知识与心得。如果你具有三年以上的技术从业资历,并在某一技术领域有一定沉淀,可申请成为 SF 讲师,给大家分享你的所思所得。

PS:正值求职季,祝愿跳槽的童鞋们都能找到一个钱多、顺心的工作 ↖(^ω^)↗

查看原文

赞 213 收藏 67 评论 72

大呜 发布了文章 · 2018-03-14

php 面试题目整理(持续更新)

来自 AT博客
整理于面试别人或被别人面试的一些题目(持续更新),答案网上基本都有,不一一列举。希望能帮到需要换工作的你。

数据库

  1. mysql 索引的理解
  2. mysql b-tree 与hash 索引的区别
  3. mysql 索引的优化
  4. mysql 存储引擎的理解,例 MyISAM与InnoDB的区别
  5. 除了mysql 还用过其它数据库吗? 有那些,应用的场景,优缺点
  6. mysql主从配置原理

安全方面

  1. 防sql注入的方法
  2. XSS攻击是什么? 如何预防
  3. 常见的web攻击有那几种 ? DoS攻击,跨站请求伪造攻击(CSRF),跨站脚本攻击(XSS),SQL注入等

服务器相关

  1. 说出或画出你之前项目的服务器架构
  2. php,nginx 重启命令
  3. linux下查看当前系统负载信息的一些方法。
  4. nginx,apache 各的优缺点
  5. nginx是怎么调用php
  6. CGI、FastCGI、PHP-CGI、PHP-FPM的关系。 CGI、FastCGI、PHP-CGI、PHP-FPM 关系简单分析
  7. 有了解过负载均衡吗?之前使用那一种

基础题

  1. 熟悉的数据结构有那些?简单的说一种
  2. http tcp udp的关系区别,分别属于那个层的。
  3. 基本的排序算法?
  4. 排序算法有那些?说出你理解的思路实现,时间复杂度是多少
  5. session与cookie 的区别
  6. http 协议结构,能手动写出来
  7. 数组与链表的数据结构的区别
  8. cdn的原理

php 相关

  1. require 与 include 的区别
  2. 有那些魔术方法? 你常用的是那些
  3. 有用过php加速器吗? APC、XCache、eAccelerator、Zend Opcache等
  4. php 的垃圾回收机制是怎样的
  5. php 是引擎是?
  6. 有没有了解过RPC框架?
  7. 你熟悉那几种框架?
  8. php对一次请求处理过程或生命周期
  9. 接口与抽象有什么区别
  10. php各版本区别

缓存方面

  1. 用过那种缓存技术,分别的业务场景是什么
  2. redis的应用场景 有那些
  3. 深入理解 Memcached 内存管理机制等
  4. 有用过队列吗? 用的业务场景是?

http

  1. 浏览器工作原理详解
  2. http 各种状态码
  3. 三次握手的意思?
  4. 四次挥手的意思?
  5. http tcp udp的关系区别,属于那个层的
  6. http 协议结构

其它题目

  1. 项目中遇到过那些深刻的问题,如何解决
  2. 说一个能体现你技术深度的项目
  3. 针对小组成员你是如何进行codeview
  4. 最近半年左右时间,你印象最深刻的学习的新技术或解决的技术难题是什么
  5. 你最近涉及的项目中,最大的技术挑战是什么?你们如何解决这个挑战的
  6. 介绍一下你所熟悉或认可的团队合作流程
  7. 如何保持和跟踪项目的进度和质量
  8. PHP算法逻辑。例:有36个人去游玩,需要买水,商店活动买3瓶赠送一瓶。请问题目至少需要买多少瓶饮料才可以人手一瓶?

前端方面

  1. yahoo前端性能团队总结的35条黄金定律说出几条
查看原文

赞 61 收藏 145 评论 5

大呜 发布了文章 · 2018-03-08

go程序设计语言练习题

来自 go程序设计语言 一书

源博客地址go程序设计语言练习题

练习题3.10 编写一个非递归的comma函数,运用bytes.Buffer,而不是简单的字符串拼接

package main

import (
    "bytes"
    "fmt"
)

func main() {

    fmt.Println(comma("1234567889988"))

}

func comma(s string) string {
    var newByte byte = ','
    n := len(s)
    buf := bytes.NewBuffer([]byte{})

    if n <= 3 {
        return s
    }

    for i := 0; i < n; i++ {

        if (n-i)%3 == 0 && i != 0 {
            buf.WriteByte(newByte)

        }
        buf.WriteByte(s[i])
    }
    return buf.String()

}

练习4.3 重写函数reverse,使用数组指针作为参数而不是slice

package main

import (
    "fmt"
)

func main() {
    var arr [7]int = [7]int{1, 2, 3, 6, 48, 299, 4990}
    reverse(&arr)
    fmt.Println("In main(), arr values:", arr)

}

func reverse(arr *[7]int) {
    for i, j := 0, len(*arr)-1; i < j; i, j = i+1, j-1 {
        (*arr)[i], (*arr)[j] = (*arr)[j], (*arr)[i]
    }
}

练习4.5,编写一个就地处理函数,用于去 除[]string slice 中相邻的重复字符串元素

package main

import (
    "fmt"
)

func main() {
    x := []int{1, 1, 2, 3, 4, 4, 4, 5, 6, 6, 7, 7}
    x = remove(x)
    fmt.Printf("%d", x)
}

func remove(slice []int) []int {
    for i := range slice {
        if i > len(slice)-1 {
            return slice
        }
        fmt.Printf("%d\n", slice)
        if i < len(slice)-1 && slice[i] == slice[i+1] {

            copy(slice[i:], slice[i+1:])
            slice = slice[:len(slice)-1]
            fmt.Printf("%d \n", slice)
            return remove(slice)
        }
    }
    return slice

}
查看原文

赞 3 收藏 0 评论 0

大呜 赞了文章 · 2018-02-11

Swoole 2.1 正式版发布,协程+通道带来全新的 PHP 编程模式

PHP的异步、并行、高性能网络通信引擎 Swoole 已发布 2.1.0 版本。新版本提供了全新的短名 API,完整支持了协程(Coroutine)+通道(Channel)特性,为 PHP 语言带来了全新的编程模式。Swoole 2.1API借鉴至Go语言,在此向Go语言开发组致敬。

Coroutine

go(function () {
    co::sleep(0.5);
    echo "hello";
});
go("test");
go([$object, "method"]);

Channel

$chan = new chan(128);
$chan->push(1234);
$chan->push(1234.56);
$chan->push("hello world");
$chan->push(["hello world"]);
$chan->push(new stdclass);
$chan->push(fopen("test.txt", "r+"));
while($chan->pop());

Go语言的chan不同,由于PHP是动态语言,所以可以向通道内投递任意类型的变量。

Channel Select

$c1 = new chan(3);
$c2 = new chan(2);
$c3 = new chan(2);
$c4 = new chan(2);

$c3->push(3);
$c3->push(3.1415);

$c4->push(3);
$c4->push(3.1415);

go(function () use ($c1, $c2, $c3, $c4) {
    echo "select\n";
    for ($i = 0; $i < 1; $i++)
    {
        $read_list = [$c1, $c2];
        $write_list = [$c3, $c4];
        // $write_list = null;
        $result = chan::select($read_list, $write_list, 5);
        var_dump($result, $read_list, $write_list);

        foreach($read_list as $ch)
        {
            var_dump($ch->pop());
        }

        foreach($write_list as $ch)
        {
            var_dump($ch->push(666));
        }
        echo "exit\n";
    }
});

go(function () use ($c3, $c4) {
    echo "producer\n";
    co::sleep(1);
    $data = $c3->pop();
    echo "pop[1]\n";
    var_dump($data);
});

go(function () {
    co::sleep(10);
});

go(function () use ($c1, $c2) {

    co::sleep(1);
    $c1->push("resume");
    $c2->push("hello");
});

MySQL Client

go(function () {
    $db = new Co\MySQL();
    $server = array(
        'host' => '127.0.0.1',
        'user' => 'root',
        'password' => 'root',
        'database' => 'test',
    );

    $db->connect($server);

    $result = $db->query('SELECT * FROM userinfo WHERE id = 3');
    var_dump($result);
});

Redis Client

go(function () {
    $redis = new Co\Redis;
    $res = $redis->connect('127.0.0.1', 6379);
    $ret = $redis->set('key', 'value');
    var_dump($redis->get('key'));
});

Http Client

go(function () {
    $http = new Co\Http\Client("www.google.com", 443, true);
    $http->setHeaders(function () {
        
    });
    $ret = $http->get('/');
    var_dump($http->body);
});

Http2 Client

go(function () {
    $http = new Co\Http2\Client("www.google.com", 443, true);
    $req = new co\Http2\Request;
    $req->path = "/index.html";
    $req->headers = [
        'host' => "www.google.com",
        "user-agent" => 'Chrome/49.0.2587.3',
        'accept' => 'text/html,application/xhtml+xml,application/xml',
        'accept-encoding' => 'gzip',
    ];
    $req->cookies = ['name' => 'rango', 'email' => 'rango@swoole.com'];
    $ret = $http->send($req);
    var_dump($http->recv());
});

其他 API

co::sleep(100);
co::fread($fp);
co::fwrite($fp, "hello world");
co::gethostbyname('www.google.com');

服务器端

$server = new Co\Http\Server('127.0.0.1', 9501);

$server->on('Request', function($request, $response) {

    $http = new Co\Http\Client("www.google.com", 443, true);
    $http->setHeaders(function () {
        "X-Power-By" => "Swoole/2.1.0",
    });
    $ret = $http->get('/');
 
    if ($ret) {
        $response->end($http->body);
    }
    else{
        $response->end("recv failed error : {$http->errCode}");
    }
});

$server->start();

Swoole提供了很多Co\ServerCo\WebSocket\ServerCo\Http\ServerCo\Redis\Server4个支持协程的Server类,可以在这些服务器程序中使用协程API

查看原文

赞 88 收藏 71 评论 28