diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 00000000..42aba533
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,2 @@
+github: [PegasusWang]
+custom: ["https://www.paypal.me/pegasuswang"]
diff --git a/.gitignore b/.gitignore
index 547c04b8..60dab294 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,8 @@
 *.swp
 build/*
 _build/*
+.ropeproject
+.DS_Store
+.todo_cache
+database/Mysql_bast_practice.htm
+.python-version
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
new file mode 100644
index 00000000..eeb27195
--- /dev/null
+++ b/.readthedocs.yaml
@@ -0,0 +1,15 @@
+version: 2
+
+build:
+  os: "ubuntu-22.04"
+  tools:
+    python: "3.10"
+
+# Build from the docs/ directory with Sphinx
+sphinx:
+  configuration: conf.py
+
+# Explicitly set the version of Python and its requirements
+python:
+  install:
+    - requirements: requirements.txt
diff --git a/Makefile b/Makefile
index cdac7ead..4f05907b 100755
--- a/Makefile
+++ b/Makefile
@@ -8,6 +8,12 @@ SPHINXOPTS=-n -W -d $(BUILD_DIR)/doctrees .
 html:
 	sphinx-build -b html $(SPHINXOPTS) $(BUILD_DIR)/html
 
+.PHONY: epub
+epub:
+	sphinx-build -b epub $(SPHINXOPTS) $(BUILD_DIR)/epub
+	@echo
+	@echo "Build finished. The epub file is in $(BUILD_DIR)/epub."
+
 .PHONY: coverage
 coverage:
 	sphinx-build -b coverage ${SPHINXOPTS} $(BUILD_DIR)/coverage
@@ -25,4 +31,12 @@ pdf: latex
 	cd $(BUILD_DIR)/latex && pdflatex -interaction=nonstopmode tornado.tex
 
 clean:
-	rm -rf build
+	rm -rf $(BUILD_DIR)
+
+.PHONY: livehtml
+livehtml:
+	sphinx-autobuild -b html $(SPHINXOPTS) $(BUILD_DIR)/html
+
+.PHONY: serve
+serve:
+	sphinx-autobuild -b html $(SPHINXOPTS) $(BUILD_DIR)/html
diff --git a/README.rst b/README.rst
index 9772bfe4..7ea02dc7 100644
--- a/README.rst
+++ b/README.rst
@@ -1,7 +1,65 @@
-==============
-天使汇开发指南
-==============
+==================================
+Python/Golang Web 入坑指南
+==================================
+
+.. code-block:: text
+
+     ____        _   _                    ______        __        __   _        ____       _     _
+    |  _ \ _   _| |_| |__   ___  _ __    / / ___| ___   \ \      / /__| |__    / ___|_   _(_) __| | ___
+    | |_) | | | | __| '_ \ / _ \| '_ \  / / |  _ / _ \   \ \ /\ / / _ \ '_ \  | |  _| | | | |/ _` |/ _ \
+    |  __/| |_| | |_| | | | (_) | | | |/ /| |_| | (_) |   \ V  V /  __/ |_) | | |_| | |_| | | (_| |  __/
+    |_|    \__, |\__|_| |_|\___/|_| |_/_/  \____|\___/     \_/\_/ \___|_.__/   \____|\__,_|_|\__,_|\___|
+           |___/
+
+`本电子书线上访问地址 https://python-web-guide.readthedocs.io/zh/latest/ <https://python-web-guide.readthedocs.io/zh/latest/>`_
+
+本指南根据作者的自学和工作经历提供(吐槽)一下python/golang
+web的学习路线，主要包括概念介绍，参考书籍，开发工具和开发流程等，希望可以帮助非科班人士通过自学入门python/golang
+网站开发，弥补学校教育和公司需求之间的鸿沟(也作为自己的学习笔记和面试参考手册)，同时也希望可以作为公司菜鸟实习生的培训手册，帮助公司快速培训新人上手开发，减轻招聘压力。
+笔者目前能力有限，希望有经验的python圈人士可以一起协作。
+本小书灵感来自于 requests 库作者的 `python-guide <https://github.com/kennethreitz/python-guide>`_ 。
+你可以使用强大的电子书阅读软件 `calibre <https://calibre-ebook.com/>`_ 下载epub格式阅读。
+
+如果您感兴趣，也可以参考慕课网教程 `《Python工程师面试宝典》 <https://coding.imooc.com/class/318.html>`_ 。
+本课程提供了详细的Python后端知识大纲和常考面试题，帮助自学的同学就业。如果本文档有误，您可以在 github 直接提 issue.
+
+注意：Python 不适合工程管理不完善的团队构建大型项目。如果贵团队没有编码规范、单元测试、静态检测、持续集成、文档注释中的一个或者几个，请慎用动态语言。
+Python 结合 Go 基本可以解决大部分业务场景，Python 用来快速实现业务和想法，Go 来解决性能瓶颈，这俩也是笔者目前使用最多的语言。
+如果因为某些网络原因打不开 readthedoc 网站，您可以参考下方快速上手使用 sphinx 本地构建电子书访问。
 
-作者: 王然 kxxoling@gmail.com
 
 .. image:: https://readthedocs.org/projects/z42/badge/?version=latest
+
+.. code-block:: python
+
+    # 快速上手构建本地电子书
+    # 使用方式 1
+    # 本项目页面托管在 readthedoc，如果国内因为网络原因打不开，可以使用如下方式在本地构建
+    git clone https://github.com/PegasusWang/python-web-guide.git    # 协作请fork一份你自己的地址
+    pip install -r requirements.txt # 安装 Sphinx==1.3.4
+    make html   # 构建 html 电子书，之后会在本地生成一个 _build/html 文件夹
+    cd _build/html   # 切换到构建好的 html 静态文件夹里
+    python3 -m http.server  # 启动一个本地文件服务器，或者 python2 用 python -m SimpleHTTPServer
+    # 之后打开 http://127.0.0.1:8000/ 即可本地访问电子书
+
+    # 方式2(推荐)：加入 sphinx-autobuild 自动编辑刷新
+    git clone https://github.com/PegasusWang/python-web-guide.git    # 协作请fork一份你自己的地址
+    pip install -r requirements.txt  # 安装 Sphinx==1.3.4, sphinx-autobuild
+    make serve  # 之后打开 http://127.0.0.1:8000/ 即可本地访问电子书，编辑保存直接自动刷新浏览器
+
+文档采用rst格式书写，用 `readthedocs <https://readthedocs.org/>`_ 托管。一个快速的rst语法demo `教程 <http://azuwis.github.io/sphinx_demo/demo.html>`_。 如果使用vim编写可以使用rst插件 `riv.vim <https://github.com/Rykka/riv.vim>`_ 配合 `InstantRst <https://github.com/Rykka/InstantRst>`_ 本地预览，定期pull一下拉取更新。
+欢迎你fork一份然后添加自己的章节，本书主要面对经验尚浅的同学作为自学的指导手册，并非速成指南，内容来自笔者日常学习和工作经验的持续总结。
+本电子版书集合了同事的智慧结晶，非常感谢你们带我入坑。
+本指南同时会有一些不负责任的吐槽。学到东西的请狂点 star，让笔者有动力更新更多业界实战干货，更多技术分享请关注作者知乎帐号 `pegasuswang <https://www.zhihu.com/people/pegasus-wang/activities>`_ ，知乎专栏 `Python 学习之路 <https://zhuanlan.zhihu.com/python-web>`_ ，`个人博客 <http://ningning.today/>`_ 。
+笔者还维护了一个 vim 视频教程专栏，感兴趣可以访问 `玩转vim <https://zhuanlan.zhihu.com/vim-video>`_
+
+TODO:
+=================================================================
+如果您觉得有用，可以打赏支持作者继续创作！
+
+
+.. raw:: html
+
+   <center>
+    <img src="https://user-images.githubusercontent.com/4470478/47126924-74800900-d2bd-11e8-8f24-6d03ddafbc07.png" alt="微信打赏" width=260 height=300>
+   </center>
diff --git "a/_image/goweb/concurrency/\346\211\207\345\207\272\346\211\207\345\205\245.png" "b/_image/goweb/concurrency/\346\211\207\345\207\272\346\211\207\345\205\245.png"
new file mode 100644
index 00000000..058b8d91
Binary files /dev/null and "b/_image/goweb/concurrency/\346\211\207\345\207\272\346\211\207\345\205\245.png" differ
diff --git "a/_image/goweb/gocode\351\230\205\350\257\273.png" "b/_image/goweb/gocode\351\230\205\350\257\273.png"
new file mode 100644
index 00000000..298603fd
Binary files /dev/null and "b/_image/goweb/gocode\351\230\205\350\257\273.png" differ
diff --git a/_image/microservice_distribute/slowflake.png b/_image/microservice_distribute/slowflake.png
new file mode 100644
index 00000000..b99e1b9d
Binary files /dev/null and b/_image/microservice_distribute/slowflake.png differ
diff --git "a/_image/microservice_distribute/\346\234\215\345\212\241\345\217\221\347\216\260\345\257\271\346\257\224.png" "b/_image/microservice_distribute/\346\234\215\345\212\241\345\217\221\347\216\260\345\257\271\346\257\224.png"
new file mode 100644
index 00000000..88bd6ea1
Binary files /dev/null and "b/_image/microservice_distribute/\346\234\215\345\212\241\345\217\221\347\216\260\345\257\271\346\257\224.png" differ
diff --git "a/_image/microservice_distribute/\347\206\224\346\226\255\345\231\250\345\216\237\347\220\206.png" "b/_image/microservice_distribute/\347\206\224\346\226\255\345\231\250\345\216\237\347\220\206.png"
new file mode 100644
index 00000000..03aed148
Binary files /dev/null and "b/_image/microservice_distribute/\347\206\224\346\226\255\345\231\250\345\216\237\347\220\206.png" differ
diff --git a/_static/favicon.ico b/_static/favicon.ico
index 55b6fda5..72a53a9b 100644
Binary files a/_static/favicon.ico and b/_static/favicon.ico differ
diff --git a/algorithms/algorithms.rst b/algorithms/algorithms.rst
new file mode 100644
index 00000000..b5deafc0
--- /dev/null
+++ b/algorithms/algorithms.rst
@@ -0,0 +1,2107 @@
+.. _algorithms:
+
+
+用python实现基本数据结构和算法
+=====================================================================
+
+1章：ADT抽象数据类型，定义数据和其操作
+--------------------------------------
+
+什么是ADT: 抽象数据类型，学过数据结构的应该都知道。
+
+How to select datastructures for ADT
+
+1. Dose the data structure provide for the storage requirements as specified by the domain of the ADT?
+2. Does the data structure provide the data access and manipulation functionality to fully implement the ADT?
+3. Effcient implemention?  based on complexity analysis.
+
+下边代码是个简单的示例，比如实现一个简单的Bag类，先定义其具有的操作，然后我们再用类的magic
+method来实现这些方法：
+
+::
+
+    class Bag:
+        """
+        constructor: 构造函数
+        size
+        contains
+        append
+        remove
+        iter
+        """
+        def __init__(self):
+            self._items = list()
+
+        def __len__(self):
+            return len(self._items)
+
+        def __contains__(self, item):
+            return item in self._items
+
+        def add(self, item):
+            self._items.append(item)
+
+        def remove(self, item):
+            assert item in self._items, 'item must in the bag'
+            return self._items.remove(item)
+
+        def __iter__(self):
+            return _BagIterator(self._items)
+
+
+    class _BagIterator:
+        """ 注意这里实现了迭代器类 """
+        def __init__(self, seq):
+            self._bag_items = seq
+            self._cur_item = 0
+
+        def __iter__(self):
+            return self
+
+        def __next__(self):
+            if self._cur_item < len(self._bag_items):
+                item = self._bag_items[self._cur_item]
+                self._cur_item += 1
+                return item
+            else:
+                raise StopIteration
+
+
+    b = Bag()
+    b.add(1)
+    b.add(2)
+    for i in b:     # for使用__iter__构建，用__next__迭代
+        print(i)
+
+
+    """
+    # for 语句等价于
+    i = b.__iter__()
+    while True:
+        try:
+            item = i.__next__()
+            print(item)
+        except StopIteration:
+            break
+    """
+
+--------------
+
+2章：array vs list
+------------------
+
+array: 定长，操作有限，但是节省内存；貌似我的生涯中还没用过，不过python3.5中我试了确实有array类，可以用import array直接导入
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+list: 会预先分配内存，操作丰富，但是耗费内存。我用sys.getsizeof做了实验。我个人理解很类似C++ STL里的vector，是使用最频繁的数据结构。
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+-  list.append:
+   如果之前没有分配够内存，会重新开辟新区域，然后复制之前的数据，复杂度退化
+-  list.insert: 会移动被插入区域后所有元素,O(n)
+-  list.pop:
+   pop不同位置需要的复杂度不同pop(0)是O(1)复杂度,pop()首位O(n)复杂度
+-  list[]: slice操作copy数据（预留空间）到另一个list
+
+来实现一个array的ADT:
+
+::
+
+    import ctypes
+
+    class Array:
+        def __init__(self, size):
+            assert size > 0, 'array size must be > 0'
+            self._size = size
+            PyArrayType = ctypes.py_object * size
+            self._elements = PyArrayType()
+            self.clear(None)
+
+        def __len__(self):
+            return self._size
+
+        def __getitem__(self, index):
+            assert index >= 0 and index < len(self), 'out of range'
+            return self._elements[index]
+
+        def __setitem__(self, index, value):
+            assert index >= 0 and index < len(self), 'out of range'
+            self._elements[index] = value
+
+        def clear(self, value):
+            """ 设置每个元素为value """
+            for i in range(len(self)):
+                self._elements[i] = value
+
+        def __iter__(self):
+            return _ArrayIterator(self._elements)
+
+
+    class _ArrayIterator:
+        def __init__(self, items):
+            self._items = items
+            self._idx = 0
+
+        def __iter__(self):
+            return self
+
+        def __next__(self):
+            if self._idex < len(self._items):
+                val = self._items[self._idx]
+                self._idex += 1
+                return val
+            else:
+                raise StopIteration
+
+Two-Demensional Arrays
+~~~~~~~~~~~~~~~~~~~~~~
+
+::
+
+    class Array2D:
+        """ 要实现的方法
+        Array2D(nrows, ncols):    constructor
+        numRows()
+        numCols()
+        clear(value)
+        getitem(i, j)
+        setitem(i, j, val)
+        """
+        def __init__(self, numrows, numcols):
+            self._the_rows = Array(numrows)     # 数组的数组
+            for i in range(numrows):
+                self._the_rows[i] = Array(numcols)
+
+        @property
+        def numRows(self):
+            return len(self._the_rows)
+
+        @property
+        def NumCols(self):
+            return len(self._the_rows[0])
+
+        def clear(self, value):
+            for row in self._the_rows:
+                row.clear(value)
+
+        def __getitem__(self, ndx_tuple):    # ndx_tuple: (x, y)
+            assert len(ndx_tuple) == 2
+            row, col = ndx_tuple[0], ndx_tuple[1]
+            assert (row >= 0 and row < self.numRows and
+                    col >= 0 and col < self.NumCols)
+
+            the_1d_array = self._the_rows[row]
+            return the_1d_array[col]
+
+        def __setitem__(self, ndx_tuple, value):
+            assert len(ndx_tuple) == 2
+            row, col = ndx_tuple[0], ndx_tuple[1]
+            assert (row >= 0 and row < self.numRows and
+                    col >= 0 and col < self.NumCols)
+            the_1d_array = self._the_rows[row]
+            the_1d_array[col] = value
+
+The Matrix ADT, m行，n列。这个最好用还是用pandas处理矩阵，自己实现比较\*疼
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+::
+
+    class Matrix:
+        """ 最好用pandas的DataFrame
+        Matrix(rows, ncols): constructor
+        numCols()
+        getitem(row, col)
+        setitem(row, col, val)
+        scaleBy(scalar): 每个元素乘scalar
+        transpose(): 返回transpose转置
+        add(rhsMatrix):    size must be the same
+        subtract(rhsMatrix)
+        multiply(rhsMatrix)
+        """
+        def __init__(self, numRows, numCols):
+            self._theGrid = Array2D(numRows, numCols)
+            self._theGrid.clear(0)
+
+        @property
+        def numRows(self):
+            return self._theGrid.numRows
+
+        @property
+        def NumCols(self):
+            return self._theGrid.numCols
+
+        def __getitem__(self, ndxTuple):
+            return self._theGrid[ndxTuple[0], ndxTuple[1]]
+
+        def __setitem__(self, ndxTuple, scalar):
+            self._theGrid[ndxTuple[0], ndxTuple[1]] = scalar
+
+        def scaleBy(self, scalar):
+            for r in range(self.numRows):
+                for c in range(self.numCols):
+                    self[r, c] *= scalar
+
+        def __add__(self, rhsMatrix):
+            assert (rhsMatrix.numRows == self.numRows and
+                    rhsMatrix.numCols == self.numCols)
+            newMartrix = Matrix(self.numRows, self.numCols)
+            for r in range(self.numRows):
+                for c in range(self.numCols):
+                    newMartrix[r, c] = self[r, c] + rhsMatrix[r, c]
+
+--------------
+
+3章：Sets and Maps
+------------------
+
+除了list之外，最常用的应该就是python内置的set和dict了。
+
+sets ADT
+~~~~~~~~
+
+A set is a container that stores a collection of unique values over a
+given comparable domain in which the stored values have no particular
+ordering.
+
+::
+
+    class Set:
+        """ 使用list实现set ADT
+        Set()
+        length()
+        contains(element)
+        add(element)
+        remove(element)
+        equals(element)
+        isSubsetOf(setB)
+        union(setB)
+        intersect(setB)
+        difference(setB)
+        iterator()
+        """
+        def __init__(self):
+            self._theElements = list()
+
+        def __len__(self):
+            return len(self._theElements)
+
+        def __contains__(self, element):
+            return element in self._theElements
+
+        def add(self, element):
+            if element not in self:
+                self._theElements.append(element)
+
+        def remove(self, element):
+            assert element in self, 'The element must be set'
+            self._theElements.remove(element)
+
+        def __eq__(self, setB):
+            if len(self) != len(setB):
+                return False
+            else:
+                return self.isSubsetOf(setB)
+
+        def isSubsetOf(self, setB):
+            for element in self:
+                if element not in setB:
+                    return False
+            return True
+
+        def union(self, setB):
+            newSet = Set()
+            newSet._theElements.extend(self._theElements)
+            for element in setB:
+                if element not in self:
+                    newSet._theElements.append(element)
+            return newSet
+
+Maps or Dict: 键值对,python内部采用hash实现。
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+::
+
+    class Map:
+        """ Map ADT list implemention
+        Map()
+        length()
+        contains(key)
+        add(key, value)
+        remove(key)
+        valudOf(key)
+        iterator()
+        """
+        def __init__(self):
+            self._entryList = list()
+
+        def __len__(self):
+            return len(self._entryList)
+
+        def __contains__(self, key):
+            ndx = self._findPosition(key)
+            return ndx is not None
+
+        def add(self, key, value):
+            ndx = self._findPosition(key)
+            if ndx is not None:
+                self._entryList[ndx].value = value
+                return False
+            else:
+                entry = _MapEntry(key, value)
+                self._entryList.append(entry)
+                return True
+
+        def valueOf(self, key):
+            ndx = self._findPosition(key)
+            assert ndx is not None, 'Invalid map key'
+            return self._entryList[ndx].value
+
+        def remove(self, key):
+            ndx = self._findPosition(key)
+            assert ndx is not None, 'Invalid map key'
+            self._entryList.pop(ndx)
+
+        def __iter__(self):
+            return _MapIterator(self._entryList)
+
+        def _findPosition(self, key):
+            for i in range(len(self)):
+                if self._entryList[i].key == key:
+                    return i
+            return None
+
+
+    class _MapEntry:    # or use collections.namedtuple('_MapEntry', 'key,value')
+        def __init__(self, key, value):
+            self.key = key
+            self.value = value
+
+The multiArray ADT, 多维数组，一般是使用一个一维数组模拟，然后通过计算下标获取元素
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+::
+
+    class MultiArray:
+        """ row-major or column-marjor ordering, this is row-major ordering
+        MultiArray(d1, d2, ...dn)
+        dims():   the number of dimensions
+        length(dim): the length of given array dimension
+        clear(value)
+        getitem(i1, i2, ... in), index(i1,i2,i3) = i1*(d2*d3) + i2*d3 + i3
+        setitem(i1, i2, ... in)
+        计算下标：index(i1,i2,...in) = i1*f1 + i2*f2 + ... + i(n-1)*f(n-1) + in*1
+        """
+        def __init__(self, *dimensions):
+            # Implementation of MultiArray ADT using a 1-D # array,数组的数组的数组。。。
+            assert len(dimensions) > 1, 'The array must have 2 or more dimensions'
+            self._dims = dimensions
+            # Compute to total number of elements in the array
+            size = 1
+            for d in dimensions:
+                assert d > 0, 'Dimensions must be > 0'
+                size *= d
+            # Create the 1-D array to store the elements
+            self._elements = Array(size)
+            # Create a 1-D array to store the equation factors
+            self._factors = Array(len(dimensions))
+            self._computeFactors()
+
+        @property
+        def numDims(self):
+            return len(self._dims)
+
+        def length(self, dim):
+            assert dim > 0 and dim < len(self._dims), 'Dimension component out of range'
+            return self._dims[dim-1]
+
+        def clear(self, value):
+            self._elements.clear(value)
+
+        def __getitem__(self, ndxTuple):
+            assert len(ndxTuple) == self.numDims, 'Invalid # of array subscripts'
+            index = self._computeIndex(ndxTuple)
+            assert index is not None, 'Array subscript out of range'
+            return self._elements[index]
+
+        def __setitem__(self, ndxTuple, value):
+            assert len(ndxTuple) == self.numDims, 'Invalid # of array subscripts'
+            index = self._computeIndex(ndxTuple)
+            assert index is not None, 'Array subscript out of range'
+            self._elements[index] = value
+
+        def _computeIndex(self, ndxTuple):
+            # using the equation: i1*f1 + i2*f2 + ... + in*fn
+            offset = 0
+            for j in range(len(ndxTuple)):
+                if ndxTuple[j] < 0 or ndxTuple[j] >= self._dims[j]:
+                    return None
+                else:
+                    offset += ndexTuple[j] * self._factors[j]
+            return offset
+
+--------------
+
+4章：Algorithm Analysis
+------------------------------------
+
+一般使用大O标记法来衡量算法的平均时间复杂度, 1 < log(n) < n < nlog(n) <
+n^2 < n^3 < a^n。
+了解常用数据结构操作的平均时间复杂度有利于使用更高效的数据结构，当然有时候需要在时间和空间上进行衡量，有些操作甚至还会退化，比如list的append操作，如果list空间不够，会去开辟新的空间，操作复杂度退化到O(n)，有时候还需要使用均摊分析(amortized)
+
+--------------
+
+5章：Searching and Sorting
+------------------------------------
+
+排序和查找是最基础和频繁的操作，python内置了in操作符和bisect二分操作模块实现查找，内置了sorted方法来实现排序操作。二分和快排也是面试中经常考到的，本章讲的是基本的排序和查找。
+
+::
+
+    def binary_search(sorted_seq, val):
+        """ 实现标准库中的bisect.bisect_left """
+        low = 0
+        high = len(sorted_seq) - 1
+        while low <= high:
+            mid = (high + low) // 2
+            if sorted_seq[mid] == val:
+                return mid
+            elif val < sorted_seq[mid]:
+                high = mid - 1
+            else:
+                low = mid + 1
+        return low
+
+      def bubble_sort(seq):  # O(n^2), n(n-1)/2 = 1/2(n^2 + n)
+          n = len(seq)
+          for i in range(n-1):
+              for j in range(n-1-i):    # 这里之所以 n-1 还需要 减去 i 是因为每一轮冒泡最大的元素都会冒泡到最后，无需再比较
+                  if seq[j] > seq[j+1]:
+                      seq[j], seq[j+1] = seq[j+1], seq[j]
+
+
+    def select_sort(seq):
+        """可以看作是冒泡的改进，每次找一个最小的元素交换，每一轮只需要交换一次"""
+        n = len(seq)
+        for i in range(n-1):
+            min_idx = i    # assume the ith element is the smallest
+            for j in range(i+1, n):
+                if seq[j] < seq[min_idx]:   # find the minist element index
+                    min_idx = j
+            if min_idx != i:    # swap
+                seq[i], seq[min_idx] = seq[min_idx], seq[i]
+
+
+    def insertion_sort(seq):
+        """ 每次挑选下一个元素插入已经排序的数组中,初始时已排序数组只有一个元素"""
+        n = len(seq)
+        for i in range(1, n):
+            value = seq[i]    # save the value to be positioned
+            # find the position where value fits in the ordered part of the list
+            pos = i
+            while pos > 0 and value < seq[pos-1]:
+                # Shift the items to the right during the search
+                seq[pos] = seq[pos-1]
+                pos -= 1
+            seq[pos] = value
+
+
+    def merge_sorted_list(listA, listB):
+        """ 归并两个有序数组 """
+        new_list = list()
+        a = b = 0
+        while a < len(listA) and b < len(listB):
+            if listA[a] < listB[b]:
+                new_list.append(listA[a])
+                a += 1
+            else:
+                new_list.append(listB[b])
+                b += 1
+
+        while a < len(listA):
+            new_list.append(listA[a])
+            a += 1
+
+        while b < len(listB):
+            new_list.append(listB[b])
+            b += 1
+
+        return new_list
+
+6章: Linked Structure
+------------------------
+
+list是最常用的数据结构，但是list在中间增减元素的时候效率会很低，这时候linked
+list会更适合，缺点就是获取元素的平均时间复杂度变成了O(n)
+
+::
+
+    # 单链表实现
+    class ListNode:
+        def __init__(self, data):
+            self.data = data
+            self.next = None
+
+
+    def travsersal(head, callback):
+        curNode = head
+        while curNode is not None:
+            callback(curNode.data)
+            curNode = curNode.next
+
+
+    def unorderdSearch(head, target):
+        curNode = head
+        while curNode is not None and curNode.data != target:
+            curNode = curNode.next
+        return curNode is not None
+
+
+    # Given the head pointer, prepend an item to an unsorted linked list.
+    def prepend(head, item):
+        newNode = ListNode(item)
+        newNode.next = head
+        head = newNode
+
+
+    # Given the head reference, remove a target from a linked list
+    def remove(head, target):
+        predNode = None
+        curNode = head
+        while curNode is not None and curNode.data != target:
+            # 寻找目标
+            predNode = curNode
+            curNode = curNode.data
+        if curNode is not None:
+            if curNode is head:
+                head = curNode.next
+            else:
+                predNode.next = curNode.next
+
+--------------
+
+7章：Stacks
+-----------
+
+栈也是计算机里用得比较多的数据结构，栈是一种后进先出的数据结构，可以理解为往一个桶里放盘子，先放进去的会被压在地下，拿盘子的时候，后放的会被先拿出来。
+
+::
+
+    class Stack:
+        """ Stack ADT, using a python list
+        Stack()
+        isEmpty()
+        length()
+        pop(): assert not empty
+        peek(): assert not empty, return top of non-empty stack without removing it
+        push(item)
+        """
+        def __init__(self):
+            self._items = list()
+
+        def isEmpty(self):
+            return len(self) == 0
+
+        def __len__(self):
+            return len(self._items)
+
+        def peek(self):
+            assert not self.isEmpty()
+            return self._items[-1]
+
+        def pop(self):
+            assert not self.isEmpty()
+            return self._items.pop()
+
+        def push(self, item):
+            self._items.append(item)
+
+
+    class Stack:
+        """ Stack ADT, use linked list
+        使用list实现很简单，但是如果涉及大量push操作，list的空间不够时复杂度退化到O(n)
+        而linked list可以保证最坏情况下仍是O(1)
+        """
+        def __init__(self):
+            self._top = None    # top节点, _StackNode or None
+            self._size = 0    # int
+
+        def isEmpty(self):
+            return self._top is None
+
+        def __len__(self):
+            return self._size
+
+        def peek(self):
+            assert not self.isEmpty()
+            return self._top.item
+
+        def pop(self):
+            assert not self.isEmpty()
+            node = self._top
+            self.top = self._top.next
+            self._size -= 1
+            return node.item
+
+        def _push(self, item):
+            self._top = _StackNode(item, self._top)
+            self._size += 1
+
+
+    class _StackNode:
+        def __init__(self, item, link):
+            self.item = item
+            self.next = link
+
+--------------
+
+8章：Queues
+-----------
+
+队列也是经常使用的数据结构，比如发送消息等，celery可以使用redis提供的list实现消息队列。
+本章我们用list和linked list来实现队列和优先级队列。
+
+::
+
+    class Queue:
+        """ Queue ADT, use list。list实现，简单但是push和pop效率最差是O(n)
+        Queue()
+        isEmpty()
+        length()
+        enqueue(item)
+        dequeue()
+        """
+        def __init__(self):
+            self._qList = list()
+
+        def isEmpty(self):
+            return len(self) == 0
+
+        def __len__(self):
+            return len(self._qList)
+
+        def enquue(self, item):
+            self._qList.append(item)
+
+        def dequeue(self):
+            assert not self.isEmpty()
+            return self._qList.pop(0)
+
+
+    from array import Array    # Array那一章实现的Array ADT
+    class Queue:
+        """
+        circular Array ，通过头尾指针实现。list内置append和pop复杂度会退化，使用
+        环数组实现可以使得入队出队操作时间复杂度为O(1)，缺点是数组长度需要固定。
+        """
+        def __init__(self, maxSize):
+            self._count = 0
+            self._front = 0
+            self._back = maxSize - 1
+            self._qArray = Array(maxSize)
+
+        def isEmpty(self):
+            return self._count == 0
+
+        def isFull(self):
+            return self._count == len(self._qArray)
+
+        def __len__(self):
+            return len(self._count)
+
+        def enqueue(self, item):
+            assert not self.isFull()
+            maxSize = len(self._qArray)
+            self._back = (self._back + 1) % maxSize     # 移动尾指针
+            self._qArray[self._back] = item
+            self._count += 1
+
+        def dequeue(self):
+            assert not self.isFull()
+            item = self._qArray[self._front]
+            maxSize = len(self._qArray)
+            self._front = (self._front + 1) % maxSize
+            self._count -= 1
+            return item
+
+    class _QueueNode:
+        def __init__(self, item):
+            self.item = item
+
+
+    class Queue:
+        """ Queue ADT, linked list 实现。为了改进环型数组有最大数量的限制，改用
+        带有头尾节点的linked list实现。
+        """
+        def __init__(self):
+            self._qhead = None
+            self._qtail = None
+            self._qsize = 0
+
+        def isEmpty(self):
+            return self._qhead is None
+
+        def __len__(self):
+            return self._count
+
+        def enqueue(self, item):
+            node = _QueueNode(item)    # 创建新的节点并用尾节点指向他
+            if self.isEmpty():
+                self._qhead = node
+            else:
+                self._qtail.next = node
+            self._qtail = node
+            self._qcount += 1
+
+        def dequeue(self):
+            assert not self.isEmpty(), 'Can not dequeue from an empty queue'
+            node = self._qhead
+            if self._qhead is self._qtail:
+                self._qtail = None
+            self._qhead = self._qhead.next    # 前移头节点
+            self._count -= 1
+            return node.item
+
+
+    class UnboundedPriorityQueue:
+        """ PriorityQueue ADT: 给每个item加上优先级p，高优先级先dequeue
+        分为两种：
+        - bounded PriorityQueue: 限制优先级在一个区间[0...p)
+        - unbounded PriorityQueue: 不限制优先级
+
+        PriorityQueue()
+        BPriorityQueue(numLevels): create a bounded PriorityQueue with priority in range
+            [0, numLevels-1]
+        isEmpty()
+        length()
+        enqueue(item, priority): 如果是bounded PriorityQueue, priority必须在区间内
+        dequeue(): 最高优先级的出队，同优先级的按照FIFO顺序
+
+        - 两种实现方式：
+        1.入队的时候都是到队尾，出队操作找到最高优先级的出队，出队操作O(n)
+        2.始终维持队列有序，每次入队都找到该插入的位置，出队操作是O(1)
+        (注意如果用list实现list.append和pop操作复杂度会因内存分配退化)
+        """
+        from collections import namedtuple
+        _PriorityQEntry = namedtuple('_PriorityQEntry', 'item, priority')
+
+        # 采用方式1，用内置list实现unbounded PriorityQueue
+        def __init__(self):
+            self._qlist = list()
+
+        def isEmpty(self):
+            return len(self) == 0
+
+        def __len__(self):
+            return len(self._qlist)
+
+        def enqueue(self, item, priority):
+            entry = UnboundedPriorityQueue._PriorityQEntry(item, priority)
+            self._qlist.append(entry)
+
+        def deque(self):
+            assert not self.isEmpty(), 'can not deque from an empty queue'
+            highest = self._qlist[0].priority
+            for i in range(len(self)):    # 出队操作O(n)，遍历找到最高优先级
+                if self._qlist[i].priority < highest:
+                    highest = self._qlist[i].priority
+            entry = self._qlist.pop(highest)
+            return entry.item
+
+
+    class BoundedPriorityQueue:
+        """ BoundedPriorityQueue ADT，用linked list实现。上一个地方提到了 BoundedPriorityQueue
+        但是为什么需要 BoundedPriorityQueue呢？ BoundedPriorityQueue 的优先级限制在[0, maxPriority-1]
+        对于 UnboundedPriorityQueue,出队操作由于要遍历寻找优先级最高的item，所以平均
+        是O(n)的操作，但是对于 BoundedPriorityQueue，用队列数组实现可以达到常量时间，
+        用空间换时间。比如要弹出一个元素，直接找到第一个非空队列弹出 元素就可以了。
+        (小数字代表高优先级，先出队)
+
+        qlist
+        [0] -> ["white"]
+        [1]
+        [2] -> ["black", "green"]
+        [3] -> ["purple", "yellow"]
+        """
+        # Implementation of the bounded Priority Queue ADT using an array of #
+        # queues in which the queues are implemented using a linked list.
+        from array import Array    #  第二章定义的ADT
+
+        def __init__(self, numLevels):
+            self._qSize = 0
+            self._qLevels = Array(numLevels)
+            for i in range(numLevels):
+                self._qLevels[i] = Queue()    # 上一节讲到用linked list实现的Queue
+
+        def isEmpty(self):
+            return len(self) == 0
+
+        def __len__(self):
+            return len(self._qSize)
+
+        def enqueue(self, item, priority):
+            assert priority >= 0 and priority < len(self._qLevels), 'invalid priority'
+            self._qLevel[priority].enquue(item)    # 直接找到 priority 对应的槽入队
+
+        def deque(self):
+            assert not self.isEmpty(), 'can not deque from an empty queue'
+            i = 0
+            p = len(self._qLevels)
+            while i < p and not self._qLevels[i].isEmpty():    # 找到第一个非空队列
+                i += 1
+            return self._qLevels[i].dequeue()
+
+--------------
+
+9章：Advanced Linked Lists
+--------------------------
+
+之前曾经介绍过单链表，一个链表节点只有data和next字段，本章介绍高级的链表。
+
+Doubly Linked
+List，双链表，每个节点多了个prev指向前一个节点。双链表可以用来编写文本编辑器的buffer。
+
+::
+
+    class DListNode:
+        def __init__(self, data):
+            self.data = data
+            self.prev = None
+            self.next = None
+
+
+    def revTraversa(tail):
+        curNode = tail
+        while cruNode is not None:
+            print(curNode.data)
+            curNode = curNode.prev
+
+
+    def search_sorted_doubly_linked_list(head, tail, probe, target):
+        """ probing technique探查法，改进直接遍历，不过最坏时间复杂度仍是O(n)
+        searching a sorted doubly linked list using the probing technique
+        Args:
+            head (DListNode obj)
+            tail (DListNode obj)
+            probe (DListNode or None)
+            target (DListNode.data): data to search
+        """
+        if head is None:    # make sure list is not empty
+            return False
+        if probe is None:    # if probe is null, initialize it to first node
+            probe = head
+
+        # if the target comes before the probe node, we traverse backward, otherwise
+        # traverse forward
+        if target < probe.data:
+            while probe is not None and target <= probe.data:
+                if target == probe.dta:
+                    return True
+                else:
+                    probe = probe.prev
+        else:
+            while probe is not None and target >= probe.data:
+                if target == probe.data:
+                    return True
+                else:
+                    probe = probe.next
+        return False
+
+
+    def insert_node_into_ordered_doubly_linekd_list(value):
+        """ 最好画个图看，链表操作很容易绕晕，注意赋值顺序"""
+        newnode = DListNode(value)
+
+        if head is None:    # empty list
+            head = newnode
+            tail = head
+
+        elif value < head.data:    # insert before head
+            newnode.next = head
+            head.prev = newnode
+            head = newnode
+
+        elif value > tail.data:    # insert after tail
+            newnode.prev = tail
+            tail.next = newnode
+            tail = newnode
+
+        else:    # insert into middle
+            node = head
+            while node is not None and node.data < value:
+                node = node.next
+            newnode.next = node
+            newnode.prev = node.prev
+            node.prev.next = newnode
+            node.prev = newnode
+
+循环链表
+
+::
+
+    def travrseCircularList(listRef):
+        curNode = listRef
+        done = listRef is None
+        while not None:
+            curNode = curNode.next
+            print(curNode.data)
+            done = curNode is listRef   # 回到遍历起始点
+
+
+    def searchCircularList(listRef, target):
+        curNode = listRef
+        done = listRef is None
+        while not done:
+            curNode = curNode.next
+            if curNode.data == target:
+                return True
+            else:
+                done = curNode is listRef or curNode.data > target
+        return False
+
+
+    def add_newnode_into_ordered_circular_linked_list(listRef, value):
+        """ 插入并维持顺序
+        1.插入空链表；2.插入头部；3.插入尾部；4.按顺序插入中间
+        """
+        newnode = ListNode(value)
+        if listRef is None:    # empty list
+            listRef = newnode
+            newnode.next = newnode
+
+        elif value < listRef.next.data:    # insert in front
+            newnode.next = listRef.next
+            listRef.next = newnode
+
+        elif value > listRef.data:    # insert in back
+            newnode.next = listRef.next
+            listRef.next = newnode
+            listRef = newnode
+
+        else:    # insert in the middle
+            preNode = None
+            curNode = listRef
+            done = listRef is None
+            while not done:
+                preNode = curNode
+                preNode = curNode.next
+                done = curNode is listRef or curNode.data > value
+
+            newnode.next = curNode
+            preNode.next = newnode
+
+利用循环双端链表我们可以实现一个经典的缓存失效算法，lru：
+
+::
+
+   # -*- coding: utf-8 -*-
+
+   class Node(object):
+       def __init__(self, prev=None, next=None, key=None, value=None):
+           self.prev, self.next, self.key, self.value = prev, next, key, value
+
+
+   class CircularDoubleLinkedList(object):
+       def __init__(self):
+           node = Node()
+           node.prev, node.next = node, node
+           self.rootnode = node
+
+       def headnode(self):
+           return self.rootnode.next
+
+       def tailnode(self):
+           return self.rootnode.prev
+
+       def remove(self, node):
+           if node is self.rootnode:
+               return
+           else:
+               node.prev.next = node.next
+               node.next.prev = node.prev
+
+       def append(self, node):
+           tailnode = self.tailnode()
+           tailnode.next = node
+           node.next = self.rootnode
+           self.rootnode.prev = node
+
+
+   class LRUCache(object):
+       def __init__(self, maxsize=16):
+           self.maxsize = maxsize
+           self.cache = {}
+           self.access = CircularDoubleLinkedList()
+           self.isfull = len(self.cache) >= self.maxsize
+
+       def __call__(self, func):
+           def wrapper(n):
+               cachenode = self.cache.get(n)
+               if cachenode is not None:  # hit
+                   self.access.remove(cachenode)
+                   self.access.append(cachenode)
+                   return cachenode.value
+               else:  # miss
+                   value = func(n)
+                   if not self.isfull:
+                       tailnode = self.access.tailnode()
+                       newnode = Node(tailnode, self.access.rootnode, n, value)
+                       self.access.append(newnode)
+                       self.cache[n] = newnode
+
+                       self.isfull = len(self.cache) >= self.maxsize
+                       return value
+                   else:  # full
+                       lru_node = self.access.headnode()
+                       del self.cache[lru_node.key]
+                       self.access.remove(lru_node)
+                       tailnode = self.access.tailnode()
+                       newnode = Node(tailnode, self.access.rootnode, n, value)
+                       self.access.append(newnode)
+                       self.cache[n] = newnode
+                   return value
+           return wrapper
+
+
+   @LRUCache()
+   def fib(n):
+       if n <= 2:
+           return 1
+       else:
+           return fib(n - 1) + fib(n - 2)
+
+
+   for i in range(1, 35):
+       print(fib(i))
+
+--------------
+
+10章：Recursion
+--------------------------------------
+
+    Recursion is a process for solving problems by subdividing a larger
+    problem into smaller cases of the problem itself and then solving
+    the smaller, more trivial parts.
+
+递归函数：调用自己的函数
+
+::
+
+    # 递归函数：调用自己的函数，看一个最简单的递归函数，倒序打印一个数
+    def printRev(n):
+        if n > 0:
+            print(n)
+            printRev(n-1)
+
+
+    printRev(3)    # 从10输出到1
+
+
+    # 稍微改一下，print放在最后就得到了正序打印的函数
+    def printInOrder(n):
+        if n > 0:
+            printInOrder(n-1)
+            print(n)    # 之所以最小的先打印是因为函数一直递归到n==1时候的最深栈，此时不再
+                        # 递归，开始执行print语句，这时候n==1，之后每跳出一层栈，打印更大的值
+
+    printInOrder(3)    # 正序输出
+
+Properties of Recursion: 使用stack解决的问题都能用递归解决
+
+- A recursive solution must contain a base case; 递归出口，代表最小子问题(n == 0退出打印)
+- A recursive solution must contain a recursive case; 可以分解的子问题
+- A recursive solution must make progress toward the base case. 递减n使得n像递归出口靠近
+
+Tail Recursion: occurs when a function includes a single recursive call
+as the last statement of the function. In this case, a stack is not
+needed to store values to te used upon the return of the recursive call
+and thus a solution can be implemented using a iterative loop instead.
+
+::
+
+    # Recursive Binary Search
+
+    def recBinarySearch(target, theSeq, first, last):
+        # 你可以写写单元测试来验证这个函数的正确性
+        if first > last:    # 递归出口1
+            return False
+        else:
+            mid = (first + last) // 2
+            if theSeq[mid] == target:
+                return True    # 递归出口2
+            elif theSeq[mid] > target:
+                return recBinarySearch(target, theSeq, first, mid - 1)
+            else:
+                return recBinarySearch(target, theSeq, mid + 1, last)
+
+--------------
+
+11章：Hash Tables
+--------------------
+
+基于比较的搜索（线性搜索，有序数组的二分搜索）最好的时间复杂度只能达到O(logn)，利用hash可以实现O(1)查找，python内置dict的实现方式就是hash，你会发现dict的key必须要是实现了 ``__hash__`` 和 ``__eq__`` 方法的。
+
+Hashing: hashing is the process of mapping a search a key to a limited
+range of array indeices with the goal of providing direct access to the
+keys.
+
+hash方法有个hash函数用来给key计算一个hash值，作为数组下标，放到该下标对应的槽中。当不同key根据hash函数计算得到的下标相同时，就出现了冲突。解决冲突有很多方式，比如让每个槽成为链表，每次冲突以后放到该槽链表的尾部，但是查询时间就会退化，不再是O(1)。还有一种探查方式，当key的槽冲突时候，就会根据一种计算方式去寻找下一个空的槽存放，探查方式有线性探查，二次方探查法等，cpython解释器使用的是二次方探查法。还有一个问题就是当python使用的槽数量大于预分配的2/3时候，会重新分配内存并拷贝以前的数据，所以有时候dict的add操作代价还是比较高的，牺牲空间但是可以始终保证O(1)的查询效率。如果有大量的数据，建议还是使用bloomfilter或者redis提供的HyperLogLog。
+
+如果你感兴趣，可以看看这篇文章，介绍c解释器如何实现的python dict对象： `Python dictionary implementation <http://www.laurentluce.com/posts/python-dictionary-implementation/>`__ 。我们使用Python来实现一个类似的hash结构。
+
+::
+
+    import ctypes
+
+    class Array:  # 第二章曾经定义过的ADT，这里当做HashMap的槽数组使用
+        def __init__(self, size):
+            assert size > 0, 'array size must be > 0'
+            self._size = size
+            PyArrayType = ctypes.py_object * size
+            self._elements = PyArrayType()
+            self.clear(None)
+
+        def __len__(self):
+            return self._size
+
+        def __getitem__(self, index):
+            assert index >= 0 and index < len(self), 'out of range'
+            return self._elements[index]
+
+        def __setitem__(self, index, value):
+            assert index >= 0 and index < len(self), 'out of range'
+            self._elements[index] = value
+
+        def clear(self, value):
+            """ 设置每个元素为value """
+            for i in range(len(self)):
+                self._elements[i] = value
+
+        def __iter__(self):
+            return _ArrayIterator(self._elements)
+
+
+    class _ArrayIterator:
+        def __init__(self, items):
+            self._items = items
+            self._idx = 0
+
+        def __iter__(self):
+            return self
+
+        def __next__(self):
+            if self._idx < len(self._items):
+                val = self._items[self._idx]
+                self._idx += 1
+                return val
+            else:
+                raise StopIteration
+
+
+    class HashMap:
+        """ HashMap ADT实现，类似于python内置的dict
+        一个槽有三种状态：
+        1.从未使用 HashMap.UNUSED。此槽没有被使用和冲突过，查找时只要找到UNUSEd就不用再继续探查了
+        2.使用过但是remove了，此时是 HashMap.EMPTY，该探查点后边的元素扔可能是有key
+        3.槽正在使用 _MapEntry节点
+        """
+
+        class _MapEntry:    # 槽里存储的数据
+            def __init__(self, key, value):
+                self.key = key
+                self.value = value
+
+        UNUSED = None    # 没被使用过的槽，作为该类变量的一个单例，下边都是is 判断
+        EMPTY = _MapEntry(None, None)     # 使用过但是被删除的槽
+
+        def __init__(self):
+            self._table = Array(7)    # 初始化7个槽
+            self._count = 0
+            # 超过2/3空间被使用就重新分配，load factor = 2/3
+            self._maxCount = len(self._table) - len(self._table) // 3
+
+        def __len__(self):
+            return self._count
+
+        def __contains__(self, key):
+            slot = self._findSlot(key, False)
+            return slot is not None
+
+        def add(self, key, value):
+            if key in self:    # 覆盖原有value
+                slot = self._findSlot(key, False)
+                self._table[slot].value = value
+                return False
+            else:
+                slot = self._findSlot(key, True)
+                self._table[slot] = HashMap._MapEntry(key, value)
+                self._count += 1
+                if self._count == self._maxCount:    # 超过2/3使用就rehash
+                    self._rehash()
+                return True
+
+        def valueOf(self, key):
+            slot = self._findSlot(key, False)
+            assert slot is not None, 'Invalid map key'
+            return self._table[slot].value
+
+        def remove(self, key):
+            """ remove操作把槽置为EMPTY"""
+            assert key in self, 'Key error %s' % key
+            slot = self._findSlot(key, forInsert=False)
+            value = self._table[slot].value
+            self._count -= 1
+            self._table[slot] = HashMap.EMPTY
+            return value
+
+        def __iter__(self):
+            return _HashMapIteraotr(self._table)
+
+        def _slot_can_insert(self, slot):
+            return (self._table[slot] is HashMap.EMPTY or
+                    self._table[slot] is HashMap.UNUSED)
+
+        def _findSlot(self, key, forInsert=False):
+            """ 注意原书有错误，代码根本不能运行，这里我自己改写的
+            Args:
+                forInsert (bool): if the search is for an insertion
+            Returns:
+                slot or None
+            """
+            slot = self._hash1(key)
+            step = self._hash2(key)
+            _len = len(self._table)
+
+            if not forInsert:    # 查找是否存在key
+                while self._table[slot] is not HashMap.UNUSED:
+                    # 如果一个槽是UNUSED，直接跳出
+                    if self._table[slot] is HashMap.EMPTY:
+                        slot = (slot + step) % _len
+                        continue
+                    elif self._table[slot].key == key:
+                        return slot
+                    slot = (slot + step) % _len
+                return None
+
+            else:    # 为了插入key
+                while not self._slot_can_insert(slot):    # 循环直到找到一个可以插入的槽
+                    slot = (slot + step) % _len
+                return slot
+
+        def _rehash(self):    # 当前使用槽数量大于2/3时候重新创建新的table
+            origTable = self._table
+            newSize = len(self._table) * 2 + 1    # 原来的2*n+1倍
+            self._table = Array(newSize)
+
+            self._count = 0
+            self._maxCount = newSize - newSize // 3
+
+            # 将原来的key value添加到新的table
+            for entry in origTable:
+                if entry is not HashMap.UNUSED and entry is not HashMap.EMPTY:
+                    slot = self._findSlot(entry.key, True)
+                    self._table[slot] = entry
+                    self._count += 1
+
+        def _hash1(self, key):
+            """ 计算key的hash值"""
+            return abs(hash(key)) % len(self._table)
+
+        def _hash2(self, key):
+            """ key冲突时候用来计算新槽的位置"""
+            return 1 + abs(hash(key)) % (len(self._table)-2)
+
+
+    class _HashMapIteraotr:
+        def __init__(self, array):
+            self._array = array
+            self._idx = 0
+
+        def __iter__(self):
+            return self
+
+        def __next__(self):
+            if self._idx < len(self._array):
+                if self._array[self._idx] is not None and self._array[self._idx].key is not None:
+                    key = self._array[self._idx].key
+                    self._idx += 1
+                    return key
+                else:
+                    self._idx += 1
+            else:
+                raise StopIteration
+
+
+    def print_h(h):
+        for idx, i in enumerate(h):
+            print(idx, i)
+        print('\n')
+
+
+    def test_HashMap():
+        """ 一些简单的单元测试，不过测试用例覆盖不是很全面 """
+        h = HashMap()
+        assert len(h) == 0
+        h.add('a', 'a')
+        assert h.valueOf('a') == 'a'
+        assert len(h) == 1
+
+        a_v = h.remove('a')
+        assert a_v == 'a'
+        assert len(h) == 0
+
+        h.add('a', 'a')
+        h.add('b', 'b')
+        assert len(h) == 2
+        assert h.valueOf('b') == 'b'
+        b_v = h.remove('b')
+        assert b_v == 'b'
+        assert len(h) == 1
+        h.remove('a')
+        assert len(h) == 0
+
+        n = 10
+        for i in range(n):
+            h.add(str(i), i)
+        assert len(h) == n
+        print_h(h)
+        for i in range(n):
+            assert str(i) in h
+        for i in range(n):
+            h.remove(str(i))
+        assert len(h) == 0
+
+--------------
+
+12章: Advanced Sorting
+-------------------------------
+
+第5章介绍了基本的排序算法，本章介绍高级排序算法。
+
+归并排序(mergesort): 分治法
+
+::
+
+    def merge_sorted_list(listA, listB):
+        """ 归并两个有序数组，O(max(m, n)) ,m和n是数组长度"""
+        print('merge left right list', listA, listB, end='')
+        new_list = list()
+        a = b = 0
+        while a < len(listA) and b < len(listB):
+            if listA[a] < listB[b]:
+                new_list.append(listA[a])
+                a += 1
+            else:
+                new_list.append(listB[b])
+                b += 1
+
+        while a < len(listA):
+            new_list.append(listA[a])
+            a += 1
+
+        while b < len(listB):
+            new_list.append(listB[b])
+            b += 1
+
+        print(' ->', new_list)
+        return new_list
+
+
+    def mergesort(theList):
+        """ O(nlogn)，log层调用，每层n次操作
+        mergesort: divided and conquer 分治
+        1. 把原数组分解成越来越小的子数组
+        2. 合并子数组来创建一个有序数组
+        """
+        print(theList)    # 我把关键步骤打出来了，你可以运行下看看整个过程
+        if len(theList) <= 1:    # 递归出口
+            return theList
+        else:
+            mid = len(theList) // 2
+
+            # 递归分解左右两边数组
+            left_half = mergesort(theList[:mid])
+            right_half = mergesort(theList[mid:])
+
+            # 合并两边的有序子数组
+            newList = merge_sorted_list(left_half, right_half)
+            return newList
+
+    """ 这是我调用一次打出来的排序过程
+    [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
+    [10, 9, 8, 7, 6]
+    [10, 9]
+    [10]
+    [9]
+    merge left right list [10] [9] -> [9, 10]
+    [8, 7, 6]
+    [8]
+    [7, 6]
+    [7]
+    [6]
+    merge left right list [7] [6] -> [6, 7]
+    merge left right list [8] [6, 7] -> [6, 7, 8]
+    merge left right list [9, 10] [6, 7, 8] -> [6, 7, 8, 9, 10]
+    [5, 4, 3, 2, 1]
+    [5, 4]
+    [5]
+    [4]
+    merge left right list [5] [4] -> [4, 5]
+    [3, 2, 1]
+    [3]
+    [2, 1]
+    [2]
+    [1]
+    merge left right list [2] [1] -> [1, 2]
+    merge left right list [3] [1, 2] -> [1, 2, 3]
+    merge left right list [4, 5] [1, 2, 3] -> [1, 2, 3, 4, 5]
+    """
+
+快速排序
+
+::
+
+    def quicksort(theSeq, first, last):    # average: O(nlog(n))
+        """
+        quicksort :也是分而治之，但是和归并排序不同的是，采用选定主元（pivot）而不是从中间
+        进行数组划分
+        1. 第一步选定pivot用来划分数组，pivot左边元素都比它小，右边元素都大于等于它
+        2. 对划分的左右两边数组递归，直到递归出口（数组元素数目小于2）
+        3. 对pivot和左右划分的数组合并成一个有序数组
+        """
+        if first < last:
+            pos = partitionSeq(theSeq, first, last)
+            # 对划分的子数组递归操作
+            quicksort(theSeq, first, pos - 1)
+            quicksort(theSeq, pos + 1, last)
+
+
+    def partitionSeq(theSeq, first, last):
+        """ 快排中的划分操作，把比pivot小的挪到左边，比pivot大的挪到右边"""
+        pivot = theSeq[first]
+        print('before partitionSeq', theSeq)
+
+        left = first + 1
+        right = last
+
+        while True:
+            # 找到第一个比pivot大的
+            while left <= right and theSeq[left] < pivot:
+                left += 1
+
+            # 从右边开始找到比pivot小的
+            while right >= left and theSeq[right] >= pivot:
+                right -= 1
+
+            if right < left:
+                break
+            else:
+                theSeq[left], theSeq[right] = theSeq[right], theSeq[left]
+
+        # 把pivot放到合适的位置
+        theSeq[first], theSeq[right] = theSeq[right], theSeq[first]
+
+        print('after partitionSeq {}: {}\t'.format(theSeq, pivot))
+        return right    # 返回pivot的位置
+
+
+    def test_partitionSeq():
+        l = [0,1,2,3,4]
+        assert partitionSeq(l, 0, len(l)-1) == 0
+        l = [4,3,2,1,0]
+        assert partitionSeq(l, 0, len(l)-1) == 4
+        l = [2,3,0,1,4]
+        assert partitionSeq(l, 0, len(l)-1) == 2
+
+    test_partitionSeq()
+
+
+    def test_quicksort():
+        def _is_sorted(seq):
+            for i in range(len(seq)-1):
+                if seq[i] > seq[i+1]:
+                    return False
+            return True
+
+        from random import randint
+        for i in range(100):
+            _len = randint(1, 100)
+            to_sort = []
+            for i in range(_len):
+                to_sort.append(randint(0, 100))
+            quicksort(to_sort, 0, len(to_sort)-1)    # 注意这里用了原地排序，直接更改了数组
+            print(to_sort)
+            assert _is_sorted(to_sort)
+
+    test_quicksort()
+
+利用快排中的partitionSeq操作，我们还能实现另一个算法，nth_element，快速查找一个无序数组中的第k大元素
+
+::
+
+    def nth_element(seq, beg, end, k):
+        if beg == end:
+            return seq[beg]
+        pivot_index = partitionSeq(seq, beg, end)
+        if pivot_index == k:
+            return seq[k]
+        elif pivot_index > k:
+            return nth_element(seq, beg, pivot_index-1, k)
+        else:
+            return nth_element(seq, pivot_index+1, end, k)
+
+    def test_nth_element():
+        from random import shuffle
+        n = 10
+        l = list(range(n))
+        shuffle(l)
+        print(l)
+        for i in range(len(l)):
+            assert nth_element(l, 0, len(l)-1, i) == i
+
+    test_nth_element()
+
+--------------
+
+13章: Binary Tree
+---------------------
+
+The binary Tree: 二叉树，每个节点做多只有两个子节点
+
+::
+
+    class _BinTreeNode:
+        def __init__(self, data):
+            self.data = data
+            self.left = None
+            self.right = None
+
+
+    # 三种depth-first遍历
+    def preorderTrav(subtree):
+        """ 先（根）序遍历"""
+        if subtree is not None:
+            print(subtree.data)
+            preorderTrav(subtree.left)
+            preorderTrav(subtree.right)
+
+
+    def inorderTrav(subtree):
+        """ 中(根)序遍历"""
+        if subtree is not None:
+            preorderTrav(subtree.left)
+            print(subtree.data)
+            preorderTrav(subtree.right)
+
+
+    def postorderTrav(subtree):
+        """ 后（根）序遍历"""
+        if subtree is not None:
+            preorderTrav(subtree.left)
+            preorderTrav(subtree.right)
+            print(subtree.data)
+
+
+    # 宽度优先遍历(bradth-First Traversal): 一层一层遍历, 使用queue
+    def breadthFirstTrav(bintree):
+        from queue import Queue    # py3
+        q = Queue()
+        q.put(bintree)
+        while not q.empty():
+            node = q.get()
+            print(node.data)
+            if node.left is not None:
+                q.put(node.left)
+            if node.right is not None:
+                q.put(node.right)
+
+
+    class _ExpTreeNode:
+        __slots__ = ('element', 'left', 'right')
+
+        def __init__(self, data):
+            self.element = data
+            self.left = None
+            self.right = None
+
+        def __repr__(self):
+            return '<_ExpTreeNode: {} {} {}>'.format(
+                self.element, self.left, self.right)
+
+    from queue import Queue
+    class ExpressionTree:
+        """
+        表达式树: 操作符存储在内节点操作数存储在叶子节点的二叉树。(符号树真难打出来)
+            *
+           / \
+          +   -
+         / \  / \
+         9  3 8   4
+        (9+3) * (8-4)
+
+        Expression Tree Abstract Data Type，可以实现二元操作符
+        ExpressionTree(expStr): user string as constructor param
+        evaluate(varDict): evaluates the expression and returns the numeric result
+        toString(): constructs and retutns a string represention of the expression
+
+        Usage:
+            vars = {'a': 5, 'b': 12}
+            expTree = ExpressionTree("(a/(b-3))")
+            print('The result = ', expTree.evaluate(vars))
+        """
+
+        def __init__(self, expStr):
+            self._expTree = None
+            self._buildTree(expStr)
+
+        def evaluate(self, varDict):
+            return self._evalTree(self._expTree, varDict)
+
+        def __str__(self):
+            return self._buildString(self._expTree)
+
+        def _buildString(self, treeNode):
+            """ 在一个子树被遍历之前添加做括号，在子树被遍历之后添加右括号 """
+            # print(treeNode)
+            if treeNode.left is None and treeNode.right is None:
+                return str(treeNode.element)    # 叶子节点是操作数直接返回
+            else:
+                expStr = '('
+                expStr += self._buildString(treeNode.left)
+                expStr += str(treeNode.element)
+                expStr += self._buildString(treeNode.right)
+                expStr += ')'
+                return expStr
+
+        def _evalTree(self, subtree, varDict):
+            # 是不是叶子节点, 是的话说明是操作数，直接返回
+            if subtree.left is None and subtree.right is None:
+                # 操作数是合法数字吗
+                if subtree.element >= '0' and subtree.element <= '9':
+                    return int(subtree.element)
+                else:    # 操作数是个变量
+                    assert subtree.element in varDict, 'invalid variable.'
+                    return varDict[subtree.element]
+            else:    # 操作符则计算其子表达式
+                lvalue = self._evalTree(subtree.left, varDict)
+                rvalue = self._evalTree(subtree.right, varDict)
+                print(subtree.element)
+                return self._computeOp(lvalue, subtree.element, rvalue)
+
+        def _computeOp(self, left, op, right):
+            assert op
+            op_func = {
+                '+': lambda left, right: left + right,    # or import operator, operator.add
+                '-': lambda left, right: left - right,
+                '*': lambda left, right: left * right,
+                '/': lambda left, right: left / right,
+                '%': lambda left, right: left % right,
+            }
+            return op_func[op](left, right)
+
+        def _buildTree(self, expStr):
+            expQ = Queue()
+            for token in expStr:    # 遍历表达式字符串的每个字符
+                expQ.put(token)
+            self._expTree = _ExpTreeNode(None)    # 创建root节点
+            self._recBuildTree(self._expTree, expQ)
+
+        def _recBuildTree(self, curNode, expQ):
+            token = expQ.get()
+            if token == '(':
+                curNode.left = _ExpTreeNode(None)
+                self._recBuildTree(curNode.left, expQ)
+
+                # next token will be an operator: + = * / %
+                curNode.element = expQ.get()
+                curNode.right = _ExpTreeNode(None)
+                self._recBuildTree(curNode.right, expQ)
+
+                # the next token will be ')', remmove it
+                expQ.get()
+
+            else:  # the token is a digit that has to be converted to an int.
+                curNode.element = token
+
+
+    vars = {'a': 5, 'b': 12}
+    expTree = ExpressionTree("((2*7)+8)")
+    print(expTree)
+    print('The result = ', expTree.evaluate(vars))
+
+Heap（堆）：二叉树最直接的一个应用就是实现堆。堆就是一颗完全二叉树，最大堆的非叶子节点的值都比孩子大，最小堆的非叶子结点的值都比孩子小。
+python内置了heapq模块帮助我们实现堆操作，比如用内置的heapq模块实现个堆排序:
+
+::
+
+    # 使用python内置的heapq实现heap sort
+    def heapsort(iterable):
+        from heapq import heappush, heappop
+        h = []
+        for value in iterable:
+            heappush(h, value)
+        return [heappop(h) for i in range(len(h))]
+
+但是一般实现堆的时候实际上并不是用数节点来实现的，而是使用数组实现，效率比较高。为什么可以用数组实现呢?因为完全二叉树的性质，
+可以用下标之间的关系表示节点之间的关系，MaxHeap的docstring中已经说明了
+
+::
+
+    class MaxHeap:
+        """
+        Heaps:
+        完全二叉树，最大堆的非叶子节点的值都比孩子大，最小堆的非叶子结点的值都比孩子小
+        Heap包含两个属性，order property 和 shape property(a complete binary tree)，在插入
+        一个新节点的时候，始终要保持这两个属性
+        插入操作：保持堆属性和完全二叉树属性, sift-up 操作维持堆属性
+        extract操作：只获取根节点数据，并把树最底层最右节点copy到根节点后，sift-down操作维持堆属性
+
+        用数组实现heap，从根节点开始，从上往下从左到右给每个节点编号，则根据完全二叉树的
+        性质，给定一个节点i， 其父亲和孩子节点的编号分别是:
+            parent = (i-1) // 2
+            left = 2 * i + 1
+            rgiht = 2 * i + 2
+        使用数组实现堆一方面效率更高，节省树节点的内存占用，一方面还可以避免复杂的指针操作，减少
+        调试难度。
+
+        """
+
+        def __init__(self, maxSize):
+            self._elements = Array(maxSize)    # 第二章实现的Array ADT
+            self._count = 0
+
+        def __len__(self):
+            return self._count
+
+        def capacity(self):
+            return len(self._elements)
+
+        def add(self, value):
+            assert self._count < self.capacity(), 'can not add to full heap'
+            self._elements[self._count] = value
+            self._count += 1
+            self._siftUp(self._count - 1)
+            self.assert_keep_heap()    # 确定每一步add操作都保持堆属性
+
+        def extract(self):
+            assert self._count > 0, 'can not extract from an empty heap'
+            value = self._elements[0]    # save root value
+            self._count -= 1
+            self._elements[0] = self._elements[self._count]    # 最右下的节点放到root后siftDown
+            self._siftDown(0)
+            self.assert_keep_heap()
+            return value
+
+        def _siftUp(self, ndx):
+            if ndx > 0:
+                parent = (ndx - 1) // 2
+                # print(ndx, parent)
+                if self._elements[ndx] > self._elements[parent]:    # swap
+                    self._elements[ndx], self._elements[parent] = self._elements[parent], self._elements[ndx]
+                    self._siftUp(parent)    # 递归
+
+        def _siftDown(self, ndx):
+            left = 2 * ndx + 1
+            right = 2 * ndx + 2
+            # determine which node contains the larger value
+            largest = ndx
+            if (left < self._count and
+                self._elements[left] >= self._elements[largest] and
+                self._elements[left] >= self._elements[right]):  # 原书这个地方没写实际上找的未必是largest
+                largest = left
+            elif right < self._count and self._elements[right] >= self._elements[largest]:
+                largest = right
+            if largest != ndx:
+                self._elements[ndx], self._elements[largest] = self._elements[largest], self._elements[ndx]
+                self._siftDown(largest)
+
+        def __repr__(self):
+            return ' '.join(map(str, self._elements))
+
+        def assert_keep_heap(self):
+            """ 我加了这个函数是用来验证每次add或者extract之后，仍保持最大堆的性质"""
+            _len = len(self)
+            for i in range(0, int((_len-1)/2)):    # 内部节点（非叶子结点）
+                l = 2 * i + 1
+                r = 2 * i + 2
+                if l < _len and r < _len:
+                    assert self._elements[i] >= self._elements[l] and self._elements[i] >= self._elements[r]
+
+    def test_MaxHeap():
+        """ 最大堆实现的单元测试用例 """
+        _len = 10
+        h = MaxHeap(_len)
+        for i in range(_len):
+            h.add(i)
+            h.assert_keep_heap()
+        for i in range(_len):
+            # 确定每次出来的都是最大的数字，添加的时候是从小到大添加的
+            assert h.extract() == _len-i-1
+
+    test_MaxHeap()
+
+    def simpleHeapSort(theSeq):
+        """ 用自己实现的MaxHeap实现堆排序，直接修改原数组实现inplace排序"""
+        if not theSeq:
+            return theSeq
+        _len = len(theSeq)
+        heap = MaxHeap(_len)
+        for i in theSeq:
+            heap.add(i)
+        for i in reversed(range(_len)):
+            theSeq[i] = heap.extract()
+        return theSeq
+
+
+    def test_simpleHeapSort():
+        """ 用一些测试用例证明实现的堆排序是可以工作的 """
+        def _is_sorted(seq):
+            for i in range(len(seq)-1):
+                if seq[i] > seq[i+1]:
+                    return False
+            return True
+
+        from random import randint
+        assert simpleHeapSort([]) == []
+        for i in range(1000):
+            _len = randint(1, 100)
+            to_sort = []
+            for i in range(_len):
+                to_sort.append(randint(0, 100))
+            simpleHeapSort(to_sort)    # 注意这里用了原地排序，直接更改了数组
+            assert _is_sorted(to_sort)
+
+
+    test_simpleHeapSort()
+
+--------------
+
+14章: Search Trees
+---------------------
+
+二叉差找树性质：对每个内部节点V， 1. 所有key小于V.key的存储在V的左子树。
+2. 所有key大于V.key的存储在V的右子树
+对BST进行中序遍历会得到升序的key序列
+
+::
+
+    class _BSTMapNode:
+        __slots__ = ('key', 'value', 'left', 'right')
+
+        def __init__(self, key, value):
+            self.key = key
+            self.value = value
+            self.left = None
+            self.right = None
+
+        def __repr__(self):
+            return '<{}:{}> left:{}, right:{}'.format(
+                self.key, self.value, self.left, self.right)
+
+        __str__ = __repr__
+
+
+    class BSTMap:
+        """ BST，树节点包含key可payload。用BST来实现之前用hash实现过的Map ADT.
+        性质：对每个内部节点V，
+        1.对于节点V，所有key小于V.key的存储在V的左子树。
+        2.所有key大于V.key的存储在V的右子树
+        对BST进行中序遍历会得到升序的key序列
+        """
+        def __init__(self):
+            self._root = None
+            self._size = 0
+            self._rval = None     # 作为remove的返回值
+
+        def __len__(self):
+            return self._size
+
+        def __iter__(self):
+            return _BSTMapIterator(self._root, self._size)
+
+        def __contains__(self, key):
+            return self._bstSearch(self._root, key) is not None
+
+        def valueOf(self, key):
+            node = self._bstSearch(self._root, key)
+            assert node is not None, 'Invalid map key.'
+            return node.value
+
+        def _bstSearch(self, subtree, target):
+            if subtree is None:    # 递归出口，遍历到树底没有找到key或是空树
+                return None
+            elif target < subtree.key:
+                return self._bstSearch(subtree.left, target)
+            elif target > subtree.key:
+                return self._bstSearch(subtree.right, target)
+            return subtree    # 返回引用
+
+        def _bstMinumum(self, subtree):
+            """ 顺着树一直往左下角递归找就是最小的,向右下角递归就是最大的 """
+            if subtree is None:
+                return None
+            elif subtree.left is None:
+                return subtree
+            else:
+                return subtree._bstMinumum(self, subtree.left)
+
+        def add(self, key, value):
+            """ 添加或者替代一个key的value, O(N) """
+            node = self._bstSearch(self._root, key)
+            if node is not None:    # if key already exists, update value
+                node.value = value
+                return False
+            else:   # insert a new entry
+                self._root = self._bstInsert(self._root, key, value)
+                self._size += 1
+                return True
+
+        def _bstInsert(self, subtree, key, value):
+            """ 新的节点总是插入在树的叶子结点上 """
+            if subtree is None:
+                subtree = _BSTMapNode(key, value)
+            elif key < subtree.key:
+                subtree.left = self._bstInsert(subtree.left, key, value)
+            elif key > subtree.key:
+                subtree.right = self._bstInsert(subtree.right, key, value)
+            # 注意这里没有else语句了，应为在被调用处add函数里先判断了是否有重复key
+            return subtree
+
+        def remove(self, key):
+            """ O(N)
+            被删除的节点分为三种:
+            1.叶子结点:直接把其父亲指向该节点的指针置None
+            2.该节点有一个孩子: 删除该节点后，父亲指向一个合适的该节点的孩子
+            3.该节点有俩孩子:
+                (1)找到要删除节点N和其后继S（中序遍历后该节点下一个）
+                (2)复制S的key到N
+                (3)从N的右子树中删除后继S（即在N的右子树中最小的）
+            """
+            assert key in self, 'invalid map key'
+            self._root = self._bstRemove(self._root, key)
+            self._size -= 1
+            return self._rval
+
+        def _bstRemove(self, subtree, target):
+            # search for the item in the tree
+            if subtree is None:
+                return subtree
+            elif target < subtree.key:
+                subtree.left = self._bstRemove(subtree.left, target)
+                return subtree
+            elif target > subtree.key:
+                subtree.right = self._bstRemove(subtree.right, target)
+                return subtree
+
+            else:    # found the node containing the item
+                self._rval = subtree.value
+                if subtree.left is None and subtree.right is None:
+                    # 叶子node
+                    return None
+                elif subtree.left is None or subtree.right is None:
+                    # 有一个孩子节点
+                    if subtree.left is not None:
+                        return subtree.left
+                    else:
+                        return subtree.right
+                else:   # 有俩孩子节点
+                    successor = self._bstMinumum(subtree.right)
+                    subtree.key = successor.key
+                    subtree.value = successor.value
+                    subtree.right = self._bstRemove(subtree.right, successor.key)
+                    return subtree
+
+        def __repr__(self):
+            return '->'.join([str(i) for i in self])
+
+        def assert_keep_bst_property(self, subtree):
+            """ 写这个函数为了验证add和delete操作始终维持了bst的性质 """
+            if subtree is None:
+                return
+            if subtree.left is not None and subtree.right is not None:
+                assert subtree.left.value <= subtree.value
+                assert subtree.right.value >= subtree.value
+                self.assert_keep_bst_property(subtree.left)
+                self.assert_keep_bst_property(subtree.right)
+
+            elif subtree.left is None and subtree.right is not None:
+                assert subtree.right.value >= subtree.value
+                self.assert_keep_bst_property(subtree.right)
+
+            elif subtree.left is not None and subtree.right is None:
+                assert subtree.left.value <= subtree.value
+                self.assert_keep_bst_property(subtree.left)
+
+
+    class _BSTMapIterator:
+        def __init__(self, root, size):
+            self._theKeys = Array(size)
+            self._curItem = 0
+            self._bstTraversal(root)
+            self._curItem = 0
+
+        def __iter__(self):
+            return self
+
+        def __next__(self):
+            if self._curItem < len(self._theKeys):
+                key = self._theKeys[self._curItem]
+                self._curItem += 1
+                return key
+            else:
+                raise StopIteration
+
+        def _bstTraversal(self, subtree):
+            if subtree is not None:
+                self._bstTraversal(subtree.left)
+                self._theKeys[self._curItem] = subtree.key
+                self._curItem += 1
+                self._bstTraversal(subtree.right)
+
+
+    def test_BSTMap():
+        l = [60, 25, 100, 35, 17, 80]
+        bst = BSTMap()
+        for i in l:
+            bst.add(i)
+
+    def test_HashMap():
+        """ 之前用来测试用hash实现的map，改为用BST实现的Map测试 """
+        # h = HashMap()
+        h = BSTMap()
+        assert len(h) == 0
+        h.add('a', 'a')
+        assert h.valueOf('a') == 'a'
+        assert len(h) == 1
+
+        a_v = h.remove('a')
+        assert a_v == 'a'
+        assert len(h) == 0
+
+        h.add('a', 'a')
+        h.add('b', 'b')
+        assert len(h) == 2
+        assert h.valueOf('b') == 'b'
+        b_v = h.remove('b')
+        assert b_v == 'b'
+        assert len(h) == 1
+        h.remove('a')
+
+        assert len(h) == 0
+
+        _len = 10
+        for i in range(_len):
+            h.add(str(i), i)
+        assert len(h) == _len
+        for i in range(_len):
+            assert str(i) in h
+        for i in range(_len):
+            print(len(h))
+            print('bef', h)
+            _ = h.remove(str(i))
+            assert _ == i
+            print('aft', h)
+            print(len(h))
+        assert len(h) == 0
+
+    test_HashMap()
+
diff --git a/api/index.rst b/api/index.rst
deleted file mode 100644
index 1ace3775..00000000
--- a/api/index.rst
+++ /dev/null
@@ -1,9 +0,0 @@
-天使汇 API
-===================================
-
-
-.. toctree::
-    :glob:
-    :maxdepth: 2
-
-    mobile
diff --git a/api/mobile.rst b/api/mobile.rst
deleted file mode 100644
index 11f2500a..00000000
--- a/api/mobile.rst
+++ /dev/null
@@ -1,1362 +0,0 @@
-====================
-手机 API (version 2)
-====================
-
-作者:  tonghs tonghuashuai@gmail.com
-
-简介
-====================
-
-改版以后的新接口。
-
-相关说明:
-
-* 无特殊说明，请求方式默认为 POST。
-* 带 *  的参数可以不传，不传时取默认值。
-
-
-注册登录
-======================
-
-获取图片验证码
-----------------------
-
-`http://mobile.tonghs.me/v2/home/img_verify_code <http://mobile.tonghs.me/?url=http://mobile.tonghs.me/v2/home/img_verify_code&method=get&_=获取图片验证码>`_
-
-method::
-    
-    GET
-
-params:: 
-
-    无
-
-return:: 
-
-    {
-        key: 验证码 key
-        img: 验证码图片 base64
-    }
-
-
-发送手机验证码
-----------------------
-
-
-`http://mobile.tonghs.me/v2/home/verify_code <http://mobile.tonghs.me/?url=http://mobile.tonghs.me/v2/home/verify_code&method=post&phone=18601980445&key=sEGLiNb0SciViQmQm0te6g&token=2E4WE&_=发送手机验证码>`_
-
-params:: 
-
-    phone: 手机号码
-    key: 图片验证码 key
-    token: 图片验证码
-
-return:: 
-
-    {
-        message: 返回信息
-        success: 是否成功 true/false
-        time: 剩余时间
-    }
-
-
-注册
-----------------------
-
-
-`http://mobile.tonghs.me/v2/home/register <http://mobile.tonghs.me/?url=http://mobile.tonghs.me/v2/home/register&method=post&phone=18601980445&key=sEGLiNb0SciViQmQm0te6g&code=289352&name=tonghs&password=tonghs&password_=tonghs&_=注册>`_
-
-
-params::
-
-    phone: 手机号码
-    code: 验证码
-    name: 姓名
-    password: 密码
-    password_: 确认密码
-    key: 图片验证码 key
-
-return::
-
-    {
-        message: 返回信息 
-        success: true/false
-    }
-
-
-登录
-----------------------
-
-
-`http://mobile.tonghs.me/v2/home/login <http://mobile.tonghs.me/?url=http://mobile.tonghs.me/v2/home/login&account=tonghuashuai@126.com&password=angel1234&_=登录>`_
-
-
-params::
-
-    account: 邮箱或手机号码
-    password: 密码
-
-return::
-
-    登录失败时
-
-    {
-        message: 返回信息
-        success: true/false
-    }
-
-
-    登录成功时
-
-    {
-        user: {
-            id: 用户ID
-            name: 姓名
-            avatar_big: 头像
-            avatar_small: 小头像
-            access_token: 
-            summary: 简介
-            firmname: 公司名
-            title: 职位
-            phone: 电话
-            qq: 
-            weixin: 
-            email: 
-            defaultpart: 参与身份1: 投资人 2: 领投人
-            regionid: 省份ID
-            regionname: 省份名
-            cityid: 城市ID
-            cityname: 城市名
-        }
-    }
-
-用户
-=====================
-
-投资人列表
---------------------
-
-`http://mobile.tonghs.me/v2/user <http://mobile.tonghs.me/?url=http://mobile.tonghs.me/v2/user&pagesize=10&pageindex=1&industry=&region=&_=投资人列表>`_
-
-params::
-
-    *pagesize: 每页条数, 默认为 10
-    *pageindex: 当前页数, 默认为 1
-    uid: 用户 ID
-    access_token:
-    *industry: 行业（筛选用）
-    *region: 区域（筛选用）
-
-return::
-
-    {
-        industry: 行业
-        regionid: 区域
-        total: 总条数
-        list: [{
-            id: 
-            name: 姓名
-            firmname: 公司 
-            title: 职位
-            followercount: 粉丝数
-            industry: 行业
-            region_name: 地区
-            isfollow: 是否关注
-            summary: 简介
-            part: 身份 0: 创业者 1: 投资人 2: 领投人
-            avatar: 头像
-            meetingtimes: 约谈数
-            view_count: 0 查看次数，主站无该数据
-        }]
-    }
-
-
-投资人列表搜索
---------------------
-
-`http://mobile.tonghs.me/v2/user_search <http://mobile.tonghs.me/?url=http://mobile.tonghs.me/v2/user_search&pagesize=10&pageindex=1&keyword=兰宁羽&_=投资人列表>`_
-
-params::
-
-    *pagesize: 每页条数, 默认为 10
-    *pageindex: 当前页数, 默认为 1
-    uid: 用户 ID
-    access_token:
-    keyword: 关键字
-
-return::
-
-    {
-        pagesize: 每页条数
-        pageindex: 当前页数
-        total: 总条数
-        list: [{
-            id: 
-            name: 姓名
-            firmname: 公司 
-            title: 职位
-            followercount: 粉丝数
-            industry: 行业
-            region_name: 地区
-            isfollow: 是否关注
-            summary: 简介
-            part: 身份 0: 创业者 1: 投资人 2: 领投人
-            avatar: 头像
-            meetingtimes: 约谈数
-            view_count: 0 查看次数，主站无该数据
-        }]
-    }
-
-
-投资人详情
---------------------
-
-`http://mobile.tonghs.me/v2/user/detail <http://mobile.tonghs.me/?url=http://mobile.tonghs.me/v2/user/detail&user_id=10839876&_=投资人详情>`__
-
-params::
-
-    uid: 用户 ID
-    access_token:
-    user_id: 要查看的用户的ID
-
-return::
-
-    {
-        user: {
-            id: ID
-            name: 姓名
-            avatar: 头像
-            part: 0: 创业者 1: 投资人
-            summary: 简介
-            style: 投资理念
-            business: 擅长领域
-            service: 提供服务
-            isfollow: 是否关注
-            following_count: 关注人数
-            following_list:
-            [{
-                id:
-                name: 姓名
-                title: 职位
-                avatar: 头像
-                industry: 行业
-                isfollow: 是否关注
-                summary: 简介
-                part: 身份 0: 创业者 1: 投资人 2: 领投人
-            }]
-            follower_count: 粉丝人数
-            follower_list: 
-            [{
-                id:
-                name: 姓名
-                title: 职位
-                avatar: 头像
-                industry: 行业
-                isfollow: 是否关注
-                summary: 简介
-                part: 身份 0: 创业者 1: 投资人 2: 领投人
-            }]
-            invested_com: 参与过的项目
-            [{
-                id: 公司 ID
-                name: 项目名
-                logo: 公司 logo
-                concept: 一句话介绍
-                viewercount: 查看次数
-                isfans: 是否是粉丝
-                creatorid: 创建人id
-                industry: 行业
-                region: 区域
-                shareurl: 微信分享链接
-                meetingcount: 约谈次数
-                day: 剩余天数
-                amount: 融资金额
-                fanscount: 粉丝数
-                finishamount: 完成融资数
-                part: 参与身份1: 投资人 2: 领投人
-
-            }]
-            comment_list: 评论列表
-            [{
-                user_id: 用户ID
-                name: 
-                avatar: 头像
-                title: 职位
-                content: 评论内容
-                time: 时间（时间戳，目前未取到）
-            }]
-            bbs_list: bbs 列表
-            [{
-                title: 标题
-                url: 连接
-                time: 时间（时间戳）
-            }]
-        }
-    }
-
-
-提交项目
-----------
-
-`http://mobile.tonghs.me/v2/user/submit_com <http://mobile.tonghs.me/?url=http://mobile.tonghs.me/v2/user/submit_com&com_id=10937375&target_id=10878516&_=提交项目>`_
-
-params::
-
-    uid: 用户ID
-    access_token: 
-    com_id: 项目ID
-    target_id: 要提交给的投资人的ID
-
-return::
-
-   {
-       message: 错误信息
-       success: true/false
-   }
-
-
-创业者详情
---------------------
-
-`http://mobile.tonghs.me/v2/user/detail <http://mobile.tonghs.me/?url=http://mobile.tonghs.me/v2/user/detail&user_id=10879982&_=创业者详情>`__
-
-params::
-
-    uid: 用户 ID
-    access_token:
-    user_id: 要查看的用户的ID
-
-return::
-
-    {
-        user: {
-            id: ID
-            name: 姓名
-            avatar: 头像
-            summary: 简介
-            skill: 技能
-            part: 0: 创业者 1: 投资人
-            isfollow: 是否关注
-            my_com: 参与过的项目
-            [{
-                id: 项目ID
-                name: 
-                logo: 
-                state: 项目状态
-                concept: 项目简介
-                viewercount: 查看次数
-                isfans: 是否是粉丝
-                industry: 行业
-                region: 区域
-                shareurl: 微信分享链接
-                meetingcount: 约谈次数
-                day: 剩余天数
-                amount: 融资金额
-                fanscount: 粉丝数
-                finishamount: 完成融资数
-
-            }]
-            following_count: 关注人数
-            following_list:
-            [{
-                id:
-                name: 姓名
-                title: 职位
-                avatar: 头像
-                industry: 行业
-                isfollow: 是否关注
-                summary: 简介
-                part: 身份 0: 创业者 1: 投资人 2: 领投人
-            }]
-            follower_count: 粉丝人数
-            follower_list: 
-            [{
-                id:
-                name: 姓名
-                title: 职位
-                avatar: 头像
-                industry: 行业
-                isfollow: 是否关注
-                summary: 简介
-                part: 身份 0: 创业者 1: 投资人 2: 领投人
-            }]
-            career: 工作经历
-            [{
-                start: 开始时间
-                end: 结束时间
-                company: 公司名称
-                title: 职位
-                txt: 介绍
-            }]
-            edu: 
-            [{
-                start: 0, 
-                major: 专业 
-                school: 学校 
-                degress: 学位
-            }]
-        }
-    }
-
-
-项目相关
-======================
-
-项目列表
-----------------------
-
-`http://mobile.tonghs.me/v2/startup <http://mobile.tonghs.me/?url=http://mobile.tonghs.me/v2/startup&pagesize=10&pageindex=1&state=0&industryid=&regionid=&type=0&_=项目列表>`_
-
-params::
-
-    client_id: 
-    *pagesize: 每页条数, 默认为 10
-    *pageindex: 当前页数, 默认为 1
-    uid: 用户 ID
-    access_token:
-    *state: 项目所处状态 全部 0 /上线 1 /预热 2（筛选用）
-    *industryid: 行业（筛选用）
-    *regionid: 区域（筛选用）
-    type: 请求类型
-        0: 项目列表
-        1: 我收到的项目
-        2: 我关注的项目
-        3: 我创建的项目
-
-return::
-
-    {
-        pageindex: 当前页数, 默认为 1
-        industryid: 行业
-        total: 总条数
-        regionid: 区域
-        list: [{
-            viewercount: 查看次数
-            concept: 一句话介绍
-            viewapply: 是否申请查看 1/0
-            name: 项目名
-            coinveststatus: 合投状态
-                10: '申请未通过',
-                20: '创建中',
-                21: '未申请合投',
-                30: '申请合投中',
-                40: '合投预热',
-                50: '合投待上线',
-                60: '合投上线',
-                70: '确认投资人名单',
-                80: '融资完成',
-                90: '融资失败',
-            isfans: 是否是粉丝 
-            industry: 行业
-            region: 区域
-            member_count: 成员数
-            shareurl: 微信分享链接
-            meetingcount: 约谈次数
-            day: 剩余天数
-            creatorname: 创建者姓名
-            amount: 融资金额
-            canview: 是否可以查看
-            creatorid: 创建者 ID
-            financingid: 融资 ID （目前没用）
-            creatorphone: 创建者手机
-            logo: 公司 logo
-            id: 公司 ID
-            fanscount: 粉丝数
-            finishamount: 完成融资数 
-        }]
-    }
-
-项目列表搜索
-----------------------
-
-`http://mobile.tonghs.me/v2/startup_search <http://mobile.tonghs.me/?url=http://mobile.tonghs.me/v2/startup_search&pagesize=10&pageindex=1&keyword=云视野&_=项目列表>`_
-
-params::
-
-    client_id: 
-    *pagesize: 每页条数, 默认为 10
-    *pageindex: 当前页数, 默认为 1
-    uid: 用户 ID
-    access_token:
-    *keyword: 关键字
-
-return::
-
-    {
-        pagesize: 每页条数
-        pageindex: 当前页数
-        total: 总条数
-        list: [{
-            viewercount: 查看次数
-            concept: 一句话介绍
-            viewapply: 是否申请查看 1/0
-            name: 项目名
-            coinveststatus: 合投状态
-                10: '申请未通过',
-                20: '创建中',
-                21: '未申请合投',
-                30: '申请合投中',
-                40: '合投预热',
-                50: '合投待上线',
-                60: '合投上线',
-                70: '确认投资人名单',
-                80: '融资完成',
-                90: '融资失败',
-            isfans: 是否是粉丝 
-            industry: 行业
-            region: 区域
-            member_count: 成员数
-            shareurl: 微信分享链接
-            meetingcount: 约谈次数
-            day: 剩余天数
-            creatorname: 创建者姓名
-            amount: 融资金额
-            canview: 是否可以查看
-            creatorid: 创建者 ID
-            financingid: 融资 ID （目前没用）
-            creatorphone: 创建者手机
-            logo: 公司 logo
-            id: 公司 ID
-            fanscount: 粉丝数
-            finishamount: 完成融资数 
-        }]
-    }
-
-项目详情页
-----------------------
-
-`http://mobile.tonghs.me/v2/startup/detail <http://mobile.tonghs.me/?url=http://mobile.tonghs.me/v2/startup/detail&com_id=12813761&_=项目详情页>`_
-
-params::
-
-    uid: 用户 ID
-    access_token:
-    com_id: 要查看的项目ID
-
-return::
-
-    {
-        id: 项目ID
-        name: "运策网"
-        url: 官方网站
-        creatorid: 创始人ID
-        logo: 
-        summary: 简介
-
-        demourl: 试用地址
-        android: android 下载地址
-        demouser: 试用用户
-        demopwd: 试用密码
-        weibo: 微博
-        weixin: 微信 
-        ios: iOS 下载地址
-
-        growthdata: 成长数据
-        [{
-            occurtime: 时间
-            name: 数据名称
-            quantity: 数量
-        }]
-
-        tutor: 导师
-        [{
-            id:
-            name: 姓名
-            title: 职位
-            avatar: 头像
-            industry: 行业
-            isfollow: 是否关注
-            summary: 简介
-            part: 身份 0: 创业者 1: 投资人 2: 领投人
-        }]
-
-        milestone: 大事记
-        [{
-            content: 内容
-            occurdate: 时间
-        }]
-
-        news: 新闻
-        [{
-            title: 标题
-            content: 内容
-            url: 连接
-            from: 来源
-            occurdate: 时间
-        }]
-
-
-        pics: 图片信息列表
-        [{
-            small: 小图片(320px)
-            big: 大图片(640px)
-        }]
-
-        incubator: 孵化器
-        [{
-            id: 
-            name: 
-            avatar: 头像
-            summary: 简介
-            region: 区域
-            start_date: 开始时间
-            end_date: 结束时间
-        }]
-
-        team: 团队
-        [{
-            id:
-            name: 姓名
-            title: 职位
-            avatar: 头像
-            industry: 行业
-            isfollow: 是否关注
-            summary: 简介
-            part: 身份 0: 创业者 1: 投资人 2: 领投人
-        }]
-    }
-
-项目融资信息
----------------
-
-`http://mobile.tonghs.me/v2/startup/finance <http://mobile.tonghs.me/?url=http://mobile.tonghs.me/v2/startup/finance&com_id=12813761&_=项目融资信息>`_
-
-params::
-
-    uid: 用户 ID
-    access_token:
-    com_id: 项目ID
-
-return::
-
-    {
-        stock_sale: 出让股权比例
-        min_quota: 最小融资额度
-        hope_amounta: 预计融资
-        price: 融资前估值
-        finace_history: 过往融资
-        txt: 除资金以外的其他需求
-        [{
-            time: 时间（时间戳）
-            content: 内容？（带确定，目前该字段内容为融资金额）
-        }]
-        vc_list: 本轮投资意向列表
-        [{
-            id:
-            name: 姓名
-            title: 职位
-            avatar: 头像
-            industry: 行业
-            isfollow: 是否关注
-            summary: 简介
-            part: 身份 0: 创业者 1: 投资人 2: 领投人
-        }]
-        pre_vc_list: 上一轮投资意向列表
-        [{
-            id:
-            name: 姓名
-            title: 职位
-            avatar: 头像
-            industry: 行业
-            isfollow: 是否关注
-            summary: 简介
-            part: 身份 0: 创业者 1: 投资人 2: 领投人
-        }]
-        purpose: 资金用途
-        qa_list: 提问列表
-        [{
-            q: 提问内容
-            q_time: 提问时间（时间戳）
-            a: 回答
-            a_time: 回答时间（时间戳）
-            user_id: 提问用户
-            name: 提问用户名
-            title: 提问用户职位
-        }]
-    }
-
-我收到的项目/我关注的项目/我创建的项目
-----------------------------------------------
-
-`http://mobile.tonghs.me/v2/startup/my <http://mobile.tonghs.me/?url=http://mobile.tonghs.me/v2/startup/my&pagesize=0&pageindex=1&state=0&type=0&_=我收到的项目/我关注的项目/我创建的项目>`_
-
-params::
-    
-    *pagesize: 每页条数, 默认为 10
-    *pageindex: 当前页数, 默认为 1
-    uid: 用户 ID
-    access_token:
-    *state: 项目所处状态 全部 0 /上线 1 /预热 2（筛选用）
-    type: 请求类型
-        0: 我收到的项目
-        1: 我关注的项目
-        2: 我创建的项目
-
-return::
-
-    {
-        pageindex: 当前页
-        total: 总条数
-        list:
-        [{
-            id: 公司ID
-            name: 项目名
-            logo: 公司logo
-            concept: 项目简介
-            member_count: 成员数
-            industry: 行业
-            region: 地区
-            stage: 所处阶段
-            fanscount: 粉丝数
-            viewcount: 查看数
-            meetingcount: 约谈数
-        }]
-    } 
-
-
-申请查看项目
-----------------------
-
-`http://mobile.tonghs.me/v2/startup/view_apply <http://mobile.tonghs.me/?url=http://mobile.tonghs.me/v2/startup/view_apply&com_id=12813761&_=申请查看公司>`_
-
-params::
-
-    uid: 用户ID
-    access_token:
-    com_id: 要查看的公司ID
-
-return::
-
-    {
-        message: 提示信息
-        success: true/false
-    }
-
-
-发送投资意向
-------------
-
-`http://mobile.tonghs.me/v2/startup/vc <http://mobile.tonghs.me/?url=http://mobile.tonghs.me/v2/startup/vc&com_id=12813761&amount=40&phone=18601980445&weixin=test&service=办公场所&angel_name=tonghs&_=发送投资意向>`_
-
-params::
-
-    uid: 用户ID 
-    access_token: 
-    com_id: 项目
-    amount: 投资金额
-    phone: 电话
-    weixin: 微信
-    service: 提供的服务
-    *angel_name: 投资人姓名 
-
-retrun::
-
-    {
-        message: 返回信息
-        success: true/false
-    }
-
-
-项目新闻列表
--------------
-
-`http://mobile.tonghs.me/v2/startup/news <http://mobile.tonghs.me/?url=http://mobile.tonghs.me/v2/startup/news&method=get&com_id=10925092&pageindex=1&pagesize=10&_=项目新闻列表>`_
-
-params::
-
-    uid: 用户ID
-    access_token: 
-    com_id: 项目ID
-    *pageindex: 当前页
-    *pagesize: 每页条数
-
-return::
-
-    {
-        list:
-        {
-            title: 标题
-            content: 内容
-            url: 
-            from: 来源
-            occurdate: 日期（时间戳）
-        }
-    }
-
-
-约谈
------
-
-`http://mobile.tonghs.me/v2/startup/meeting <http://mobile.tonghs.me/?url=http://mobile.tonghs.me/v2/startup/meeting&com_id=10925092&content=约谈约吗&_=约谈>`_
-
-params::
-    
-    uid: 用户ID
-    access_token: 
-    com_id: 项目ID
-    content: 内容
-
-return::
-
-    {
-        message: 提示信息
-        success: true/false
-    }
-
-分享项目
----------
-
-`http://mobile.tonghs.me/v2/startup/share <http://mobile.tonghs.me/?url=http://mobile.tonghs.me/v2/startup/share&com_id=12813761&user_list=10878516,10878516&content=test&_=%E5%88%86%E4%BA%AB%E9%A1%B9%E7%9B%AE>`_
-
-params::
-    
-    uid: 用户ID
-    access_token: 
-    com_id: 项目ID
-    user_list: 用户ID 列表，字符串，ID以逗号分隔，如：'10878516,10878515'
-    content: 内容
-
-return::
-
-    {
-        message: 提示信息
-        success: true/false
-    }
-
-
-关注和粉丝
-=====================
-
-关注人或项目
---------------------
-
-`http://mobile.tonghs.me/v2/follow <http://mobile.tonghs.me/?url=http://mobile.tonghs.me/v2/follow&id=10925092&_=关注人或项目>`_
-
-params::
-
-    id: 要关注的人或项目的ID
-    uid: 登录人ID
-    access_token:
-
-return::
-
-    {
-        message: 提示信息
-        success: true/false
-    }
-
-
-取消关注人或项目
---------------------
-
-`http://mobile.tonghs.me/v2/unfollow <http://mobile.tonghs.me/?url=http://mobile.tonghs.me/v2/unfollow&id=10925092&_=取消关注人或项目>`_
-
-params::
-
-    id: 要关注的人或项目的ID
-    uid: 登录人ID
-    access_token:
-
-return::
-
-    {
-        message: 提示信息
-        success: true/false
-    }
-
-关注的人
---------------------
-
-`http://mobile.tonghs.me/v2/user/focused <http://mobile.tonghs.me/?url=http://mobile.tonghs.me/v2/user/focused&method=post&type=0&id=10878516&pageindex=1&pagesize=10&_=关注的人>`_
-
-
-params::
-
-    uid: 用户ID
-    *id: 要查询的用户或公司的ID，如果不传，则默认用为当前登录人ID
-    access_token:
-    *pageindex: 页数
-    *pagesize: 每页显示条数
-    *type: 请求列表类型 0: 全部 1: 创业者 2: 投资人
-
-return::
-
-    {
-        list:
-        [{
-            id: 用户ID 
-            name: 姓名
-            title: 职位
-            company: 公司
-            industry: 行业
-            summary: 简介
-            part: 身份 0: 创业者 1: 投资人 2: 领投人
-            avatar: 头像
-            active_time: 活跃时间（时间戳） 
-        }]
-    }
-
-
-粉丝
---------------------
-
-`http://mobile.tonghs.me/v2/user/follower <http://mobile.tonghs.me/?url=http://mobile.tonghs.me/v2/user/follower&method=post&type=0&id=10878516&pageindex=1&pagesize=10&_=粉丝>`_
-
-
-params::
-
-    uid: 用户ID
-    *id: 要查询的用户或公司的ID，如果不传，则默认用为当前登录人ID
-    access_token:
-    *pageindex: 页数
-    *pagesize: 每页显示条数
-    *type: 请求列表类型 0: 全部 1: 创业者 2: 投资人
-
-return::
-
-    {
-        list:
-        [{
-            id: 用户ID 
-            name: 姓名
-            title: 职位
-            company: 公司
-            industry: 行业
-            summary: 简介
-            part: 身份 0: 创业者 1: 投资人 2: 领投人
-            avatar: 头像
-            active_time: 活跃时间（时间戳） 
-        }]
-    }
-
-
-个人设置
-==========
-
-修改姓名
---------
-
-`http://mobile.tonghs.me/v2/settings/profile/save_name <http://mobile.tonghs.me/?url=http://mobile.tonghs.me/v2/settings/profile/save_name&name=tonghs&_=修改姓名>`_
-
-
-params::
-    
-    uid: 用户ID
-    access_token:
-    name: 用户姓名
-
-return::
-    
-    {
-        message: 返回信息 
-        success: true/false
-    }
-
-
-修改公司
---------
-
-`http://mobile.tonghs.me/v2/settings/profile/save_com <http://mobile.tonghs.me/?url=http://mobile.tonghs.me/v2/settings/profile/save_com&com=angelcrunch&_=修改公司>`_
-
-params::
-    
-    uid: 用户ID
-    access_token:
-    com: 公司名称
-
-return::
-    
-    {
-        message: 返回信息 
-        success: true/false
-    }
-
-
-修改职位
---------
-
-`http://mobile.tonghs.me/v2/settings/profile/save_title <http://mobile.tonghs.me/?url=http://mobile.tonghs.me/v2/settings/profile/save_title&title=web developer&_=修改职位>`_
-
-params::
-    
-    uid: 用户ID
-    access_token:
-    title: 职位
-
-return::
-    
-    {
-        message: 返回信息 
-        success: true/false
-    }
-
-
-修改电话
---------
-
-`http://mobile.tonghs.me/v2/settings/profile/save_phone <http://mobile.tonghs.me/?url=http://mobile.tonghs.me/v2/settings/profile/save_phone&phone=18601980445&_=修改电话>`_
-
-params::
-    
-    uid: 用户ID
-    access_token:
-    phone: 电话
-
-return::
-    
-    {
-        message: 返回信息 
-        success: true/false
-    }
-
-
-修改QQ
---------
-
-`http://mobile.tonghs.me/v2/settings/profile/save_qq <http://mobile.tonghs.me/?url=http://mobile.tonghs.me/v2/settings/profile/save_qq&qq=522183104&_=修改QQ>`_
-
-params::
-    
-    uid: 用户ID
-    access_token:
-    qq: QQ
-
-return::
-    
-    {
-        message: 返回信息 
-        success: true/false
-    }
-
-
-修改微信
---------
-
-`http://mobile.tonghs.me/v2/settings/profile/save_weixin <http://mobile.tonghs.me/?url=http://mobile.tonghs.me/v2/settings/profile/save_weixin&weixin=weixin&_=修改微信>`_
-
-params::
-    
-    uid: 用户ID
-    access_token:
-    weixin: 微信
-
-return::
-    
-    {
-        message: 返回信息 
-        success: true/false
-    }
-
-
-修改简介
---------
-
-`http://mobile.tonghs.me/v2/settings/profile/save_summary <http://mobile.tonghs.me/?url=http://mobile.tonghs.me/v2/settings/profile/save_summary&summary=summary&_=修改简介>`_
-
-params::
-    
-    uid: 用户ID
-    access_token:
-    summary: 简介
-
-return::
-    
-    {
-        message: 返回信息 
-        success: true/false
-    }
-
-
-修改地区
---------
-
-`http://mobile.tonghs.me/v2/settings/profile/save_region <http://mobile.tonghs.me/?url=http://mobile.tonghs.me/v2/settings/profile/save_region&region=4295032832&_=修改地区>`_
-
-params::
-    
-    uid: 用户ID
-    access_token:
-    region: 区域ID
-
-return::
-    
-    {
-        message: 返回信息 
-        success: true/false
-    }
-
-
-修改头像
---------
-
-http://mobile.tonghs.me/v2/settings/profile/save_avatar
-
-params::
-    
-    uid: 用户ID
-    access_token:
-    avatar: 头像文件
-
-return::
-    
-    {
-        message: 返回信息 
-        success: true/false
-        avatar: 返回头像 url 值
-    }
-
-
-其他
-=====================
-
-获取区域列表
---------------------
-
-`http://mobile.tonghs.me/v2/settings/profile/getregionlist <http://mobile.tonghs.me/?url=http://mobile.tonghs.me/v2/settings/profile/getregionlist&method=get&_=获取区域列表>`_
-
-method::
-    
-    GET
-
-params::
-    
-    无
-
-return::
-    
-    {
-        list:
-        [{
-            parentid: 父级ID
-            ancestornames: 父级名称
-            id: 区域ID
-            name: 区域名称
-        }]
-    }
-
-示例::
-
-    {
-        list:
-        [{
-            parentid: "1"
-            ancestornames: ""
-            id: "1"
-            name: "中国"
-        }
-        {
-            parentid: "1"
-            ancestornames: "中国"
-            id: "4295032832"
-            name: "北京"
-        }
-        {
-            parentid: "4295032832"
-            ancestornames: "北京"
-            id: "4295033088"
-            name: "东城区"
-        }]
-    }
-
-说明:
-
-可通过以下方法判断国家、省（直辖市）和城市（区）:
-
-* 当id 的长度小于 10 时是国家
-* 当parentid 长度小于 10 时是省（直辖市）
-* 当parentid 长度等于 10 时是市（地区）
-
-提问
------------------
-
-`http://mobile.tonghs.me/v2/startup/q <http://mobile.tonghs.me/?url=http://mobile.tonghs.me/v2/startup/q&com_id=12813761&content=提问内容&_=提问>`_
-
-param::
-    
-    uid: 当前登录人ID
-    access_token:
-    com_id: 公司ID
-    content: 提问内容
-
-return::
-
-    {
-        success: true/false
-        message: 
-    }
-
-
-回答
-------------
-
-`http://mobile.tonghs.me/v2/startup/a <http://mobile.tonghs.me/?url=http://mobile.tonghs.me/v2/startup/a&q_id=546eb3ccb0d8b616cbbb8127&content=回答内容&_=回答>`_
-
-param::
-    
-    uid: 当前登录人ID
-    access_token:
-    q_id: 问题ID
-    content: 回答内容
-
-return::
-
-    {
-        success: true/false
-        message: 
-    }
-
-
-是否收到新项目
----------------
-
-`http://mobile.tonghs.me/v2/user/receive_com_count <http://mobile.tonghs.me/?url=http://mobile.tonghs.me/v2/user/receive_com_count&method=get&count=17&id=10878516&pageindex=1&pagesize=10&_=是否收到新项目>`_
-
-param::
-
-    uid: 当前登录人ID
-    access_token:
-    count: 本地保存的接受项目个数
-
-return::
-
-    {
-        success: true/false 是否接收到新项目
-        message: 
-        count: 新更新的项目数
-    }
-
-
-是否有新粉丝
-------------
-
-`http://mobile.tonghs.me/v2/user/follow_count <http://mobile.tonghs.me/?url=http://mobile.tonghs.me/v2/user/follow_count&method=get&count=17&id=10878516&pageindex=1&pagesize=10&_=是否有新粉丝>`_
-
-param::
-
-    uid: 当前登录人ID
-    access_token:
-    count: 本地保存的粉丝数
-
-return::
-
-    {
-        success: true/false 是否有新粉丝
-        message: 
-        count: 新更新的粉丝数
-    }
-
-
-系统通知
---------
-
-`http://mobile.tonghs.me/v2/settings/notice <http://mobile.tonghs.me/?url=http://mobile.tonghs.me/v2/settings/notice&method=get&_=系统通知>`_
-
-旧版: `http://mobile.tonghs.me/settings/notice <http://mobile.tonghs.me/?url=http://mobile.tonghs.me/settings/notice&method=get&_=系统通知>`_
-
-param::
-
-    uid: 当前登录人ID
-    access_token:
-
-return::
-    
-    {
-        list: [{
-            id: 通知ID
-            cid: 通知类型 详细类型见说明
-            create_time: 通知时间
-            txt: 详细数据类型见说明 
-        }]
-    }
-
-
-说明：
-
-通知类型::
-
-    提交项目: 3
-    分享项目: 4
-    项目进入预热: 5 
-    项目审核失败 : 6
-    发送投资意向: 7
-    合投上线: 8
-    融资成功: 9
-    融资失败: 10
-    投资人审核通过: 11
-    投资人审核失败: 12
-    管理员分享项目: 13（新版未上，目前没有）
-
-不同类型的返回值如下：
-
-提交项目::
-
-    user_id: 提交人ID
-    user_name: 提交人姓名
-    user_ico: 提交人ico
-
-    com_id: 项目ID
-    com_name: 项目名
-    com_ico: 项目logo
-
-分享项目::
-
-    user_id: 提交人ID
-    user_name: 提交人姓名
-    user_ico: 提交人ico
-
-    com_id: 项目ID
-    com_name: 项目名
-    com_ico: 项目logo
-
-    reson: 分享理由
-
-项目进入预热/合投上线/融资成功/融资失败::
-
-    com_id: 项目ID
-    com_name: 项目名
-    com_ico: 项目logo
-
-项目审核失败::
-
-    com_id: 项目ID
-    com_name: 项目名
-    com_ico: 项目logo
-
-    reason: 失败原因
-
-发送投资意向::
-
-    user_id: 提交人ID
-    user_name: 提交人姓名
-    user_ico: 提交人ico
-
-    com_id: 项目ID
-    com_name: 项目名
-    com_ico: 项目logo
-
-    amount: 发送金额
-
-
-投资人申请通过::
-
-    {}
-
-投资人申请未通过::
-
-    reason: 未通过原因
-
-    
-管理员分享项目::
-
-    com_id: 项目ID
-    com_name: 项目名
-    com_ico: 项目logo
-
-    reason: 分享理由
-    team: 团队成员介绍 
-    summary: 公司简介
-
-另外::
-
-   成功图片：http://dn-ac88.qbox.me/default_cheer.png 
-   成功图片：http://dn-ac88.qbox.me/default_sorry.png 
diff --git a/backend/dev.rst b/backend/dev.rst
deleted file mode 100644
index 5e6a7336..00000000
--- a/backend/dev.rst
+++ /dev/null
@@ -1,177 +0,0 @@
-.. _dev:
-
-======================
-天使汇开发流程简介
-======================
-
-
-python
-------
-
--  闭包
--  正则表达式
--  collections
--  defaultdict
--  itertools
--  enum
--  IntEnum
--  enumerate
--  time.mktime(time.strptime("2007-03-04 21:08:12", "%Y-%m-%d
-   %H:%M:%S"))
--  dateparser
--  python 的 新式类与旧式类 ， 以及super的意义
-
-
-supervisor
-----------
-
--  线上服务器如何看异常
-
-
-开发流程
----------------
-
-新建一个页面通常需要创建几个文件：
-
-* 模板 zapp/SITE/model/模块名.py
-* 视图 zapp/SITE/view/模块名.py
-* Coffee Script 脚本 coffee/SITE/auth/模块名/视图名.coffee
-* Make 模板 html/SITE/auth/模块名/视图名.html
-* CSS 样式文件 css/SITE/auth/模块名.css
-
-并在 `_url.py` 中注册视图。
-
-
--  新建url页面
--  render
--  css，js的引用
--  merge.conf
--  新建css，js，修改merge.conf需要重启开发服务器
--  View的类型
--  分页
--  在页面取得当前用户
--  搜索
--  自动补全
--  gearman 异步调用
--  JsOb
--  rendermail 发送邮件
--  redis key的定义 ， R.
--  model 中 使用绝对路径import以防止redis提示key重复定义
--  import \_env
--  配置文件 的 定义 与 自适应
--  make.py 生成配置文件
-
-
-Mako 模板
----------------
-
-Mako 模板的存放路径通常是 42web/html/SITE/模块名/文件名.html。
-Mako 模板的使用见 `教程 <http://docs.makotemplates.org/en/latest/>`_ ，通用部分通常需要剥离成一个 _base.html 文件中，
-不被直接 render 的模板命名以下划线 `_` 开头。
-
-一个基本的模板文件大概是这样的::
-
-    ## 这里继承父模版
-    <%inherit file="/SITE/_base/default.html" />
-    ## 添加头文件、CSS 文件
-    <%block name="head">
-    <meta name="viewport" content="initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=0, width=device-width">
-    ## 这里引入 CSS 文件，将目录名中的斜线换成下划线作为变量传入
-    <link rel="stylesheet" href="${css.SITE_auth_m}" />
-    </%block>
-
-    <%block name="script">
-    ## 在文件尾引入 JS 文件，变量名同为替换后的目录名，JS 文件来自于变异 Coffee Script
-    <script src="${js.SITE_auth_m_m}"></script>
-    </%block>
-
-
-Coffee Script
-----------------
-
-网页脚本用 Coffee Script，写完后需编译为 JavaScript
-（添加新的 CS 脚本需要手动重启 dev.sh 脚本，修改已存在的脚本会自动变异成 JS）。
-
-
-Python Model
------------------
-
-数据存储使用的是 MongoDB，通过封装过的 MongoKit 将数据 model 对应上 MongoDB 文档，
-需要 `from z42.web.mongo import mongo` 。下面是一个简单的 model 文件示例::
-
-    from z42.web.mongo import Doc
-    from z42.web.mongo import mongo
-
-    class UserIM(Doc):
-        structure = {
-            'user_id': int,
-            'phone': basestring,
-            'qq': basestring,
-            'weixin':basestring
-        }
-
-创建一个 UserIM 对象使用 upsert() 方法，如 `UserIM({'phone': PHONE}).upsert({user_id: 1001})` ，
-这行代码的作用是：如果存在 user_id 为 1001 的 UserIM，则将其 'phone' 设置为 PHONE，
-否则在 MongoDB 中插入这样一个 JSON 文档： {'user_id': 1001, 'phone': PHONE}。
-
-
-Python View
------------------
-
-   -  View
-   -  HostView
-   -  LoginView
-   -  AdminView
-   -  GodView
-   -  JsonErrView
-   -  RpcView
-   -  组合 View
-
-web 开发使用的框架是修改过的 Tornado，需要通过装饰器注册 URL。
-新建一个简单的 view 如下::
-
-    from _route import route
-    from z42.web.view.j import JsonErrView
-    from jsob import JsOb
-    from zapp.SITE.view._base import HostView
-
-    @route('/m/register/')
-    class register(HostView):
-        def get(self, sign):
-            self.render()
-
-    @route('/j/m/')
-    class _(JsonErrView, HostView):
-        def post(self, sign):
-            err = JsOb()            # 使用 JsOb 对象保存错误信息
-            o = self.json           # 获取 JSON 化的 POST 信息
-            if not o.title:
-                err.title = "链接已失效！"
-            if not err:
-                do_something()
-            self.render(err)
-
-这个 module 注册了两个 view，一个注册页面和一个 Ajax 接口。route 方法是一个用于注册 URL 的装饰器，
-装饰在 Handler class（通常继承自XxxView）上即可。
-
-register class 就是视图 Handler 的最简写法。对于 Ajax 视图，Handler 名意义不大，可以使用下划线命名。
-需要返回错误提示的视图，可以继承 JsonErrView。View 的继承用法详见 :ref:`views`
-
-
-上线流程
------------------
-
--  修改函数接口后， 用ag查找并修改些调用过的地方
--  函数命名规则 ：名词在前动词在后 ， 常用命名如下
-
--  user\_new 新建
--  user\_rm 删除
--  user\_dumps 返回一个包含各种相关数据的json对象
--  user\_id\_list\_by\_com\_id(limit, offset) 查询
--  user\_new 新建
--  user\_rm 删除
--  user\_dumps 返回一个包含各种相关数据的json对象
--  user\_id\_list\_by\_com\_id(limit, offset) 查询
--  user\_id\_count\_by\_com\_id
-
-我们通常把user\_id作为第一个参数
diff --git a/backend/index.rst b/backend/index.rst
deleted file mode 100644
index 2b9dadf9..00000000
--- a/backend/index.rst
+++ /dev/null
@@ -1,11 +0,0 @@
-===================================
-后端技术
-===================================
-
-
-.. toctree::
-    :glob:
-    :maxdepth: 2
-
-    *
-
diff --git a/backend/views.rst b/backend/views.rst
deleted file mode 100644
index 8c9b4c6e..00000000
--- a/backend/views.rst
+++ /dev/null
@@ -1,69 +0,0 @@
-
-
-.. _views: 
-
-==================================================
-视图介绍
-==================================================
-
-:作者: 王然 kxxoling@gmail.com
-
-
-
-View
--------------------------------------
-
-HostView
--------------------------------------
-
-页面视图通常继承自该类，示例代码::
-
-    from zapp.SITE.misc.web.host_view import HostView
-    @route('/')
-    class index(HostView):
-        def get(self):
-            self.render()
-
-
-JsonErrView
--------------------------------------
-
-该 class 提供一个返回 JSON 文件的 render() 方法，通常用于 Ajax 后台验证。示例代码::
-
-    from z42.web.view.j import JsonErrView
-    from zapp.SITE.misc.web.host_view import HostView
-
-    @route('/j/m')
-    class _(JsonErrView, HostView):
-        def post(self):
-            err = JsOb()
-            # 为 err 添加内容
-            self.render(err)
-
-
-LoginView
--------------------------------------
-
-该 class 提供登录验证，需要登录的视图继承该视图。示例代码::
-    
-    from zapp.SITE.view._base import LoginView
-    from zapp.SITE.misc.web.host_view import HostView
-    @route('/user/manage')
-    class _(LoginView, HostView):
-        def get(self):
-            self.render()
-
-
-LoginHostView
---------------------------------------
-
-JsonLoginView
---------------------------------------
-
-HostAdminView
---------------------------------------
-
-JsonHostAdminView
---------------------------------------
-
-
diff --git a/base/basics.rst b/base/basics.rst
new file mode 100644
index 00000000..cc5152c6
--- /dev/null
+++ b/base/basics.rst
@@ -0,0 +1,278 @@
+.. _basics:
+
+入门基础
+=====================================================================
+
+..
+
+  Python is a language for consenting adults. —Alan Runyan
+
+编程语言: Python(Golang请参考 go 章节)
+----------------------------------------------------------------------
+Python入门相对容易又可以干很多事(网站,运维,数据分析,爬虫,AI,量化投资等），是一门方便的工具语言。2016年TIOBE排名显示Python已经名列第四，成为脚本语言之首。
+国外的Youtube，Instagram，Pinterest，Reddit，Dropbox，Disqus，
+Quora等知名应用一开始都是基于Python构建，国内的豆瓣，知乎，今日头条，果壳，饿了么，搜狐等也是Python应用的典型。这也给了国内Python开发者一阵强心剂，Python的生态环境可以支撑起重量级的
+产品。这里不想挑起语言之争，php，nodejs，java，ruby等都有丰富的生态环境。不过目前来看，技术选型用Python在招聘、学习、培训、敏捷开发等方面还是一个比较折中的选择（主要在于人，而不是语言）。
+python，ruby之类的动态语言优势在于其生产力，你能在极短时间内就搭建出原型从而赢得产品竞争。
+推荐一下几本个人认为比较好的Python书籍:
+
+* `《python-guide》 <http://docs.python-guide.org/>`_ requests作者写的guide，偏向工程方面
+
+* `《use python》 <http://use-python.readthedocs.io/zh_CN/latest/>`_ use python
+
+* `《A Byte of Python》 <http://python.swaroopch.com/>`_ 一百多页的小书，可以快速熟悉Python语言。
+
+* `《Python核心编程》 <https://book.douban.com/subject/26801374/>`_ 比较全面的Python书籍，介绍了Python语言的方方面面。
+
+* `《Dive Into Python》 <http://www.diveintopython.net/>`_ 一本免费的开源书
+
+* `《Fluent Python》 <https://book.douban.com/subject/26278021/>`_ Python进阶的好书，没有之一，涉及了很多Python高级主题和实现特性。
+
+* `《Python3 Cookbook》 <http://python3-cookbook.readthedocs.io/>`_ Python进阶读物，汇集了很多技巧。
+
+* `《Dabeaz》 <http://www.dabeaz.com/>`_ Python 培训领域 number one，Pycon 常客，有很多高质量的教程
+
+* `《Python Cheatsheet》 <https://github.com/gto76/python-cheatsheet>`_  Python 语法快速参考
+
+当然还有Python的官方文档作为参考，不过有些文档比较晦涩，还是推荐书籍入门。网上目前也可以搜到很多免费的电子书。
+如果有时间可以看看国内廖雪峰写的Python教程，简单易懂，就是跳跃有点大（有些章节对新人来说不是很友好）。
+
+
+算法与数据结构
+----------------------------
+编写良好的代码需要了解常用的算法和数据结构，虽然你可能很少会自己实现，但是对于Python语言中一些常用数据结构如list, tuple, set, frozenset, dict和collections模块中的OrderedDict, defaultdict, deque, namedtuple, Counter等应该知道什么时候用。最主要的还是了解算法中递归，二分等常用思想，写出高效易用的代码。如果你想在线练习，可以做一些Acm基础题或者去leetcode等网站刷题。
+推荐书籍:
+
+* `《Python 算法与数据结构中文教程》 <https://github.com/PegasusWang/python_data_structures_and_algorithms>`_ 笔者自己撸的一个教程，包含免费的讲义和代码以及付费视频。
+* `《网易云课堂-Python 算法与数据结构教程》 <http://study.163.com/course/introduction.htm?courseId=1005526003>`_ Python 算法和数据结构视频教程
+* `《算法图解》 <https://book.douban.com/subject/26979890/>`_
+* `《算法导论》 <https://book.douban.com/subject/20432061/>`_  你可以挑选感兴趣的章节啃一啃，也可以去网易公开课看下视频教程。如果不是计算机专业的可以看下《计算机科学导论》这门公开课，正好也是以Python语言讲解的。
+* `《awesome-algorithm》 <https://github.com/PegasusWang/awesome-algorithm>`_
+
+
+计算机网络
+----------------------------
+对于应用开发者来说大部分时间可能不太会接触特别底层的问题，但是了解网络的运行原理还是必要的。网上有个面试题  `从输入URL 到页面加载完成的过程中都发生了什么事情？ <http://fex.baidu.com/blog/2014/05/what-happen/>`_ 如果对其中大部分的概念都了解就算是入门了。网络相关书籍可以随便找一本看看。Http协议对于web开发者来说比较重要，需要深入了解。推荐书籍:
+
+* `《图解Http》 <https://book.douban.com/subject/25863515/>`_
+  一本小白入门Http协议的好书，有大量图片示例。
+* `《Http权威指南》 <https://book.douban.com/subject/10746113/>`_
+  Http协议最权威的讲解，大部头著作，可以看看最基础的部分。
+* `《网络爬虫教程》 <https://piaosanlang.gitbooks.io/spiders/01day/README1.html>`_
+  非常不错的爬虫教程。感谢原作者，其实感觉这种把学习的内容总结成小书的方式很好。
+* `《Python3 网络爬虫实战》 <https://germey.gitbooks.io/python3webspider/>`_
+* `《使用 Flask-RESTful 设计 RESTful API》 <http://www.pythondoc.com/flask-restful/second.html>`_
+* `《restful-api-guidelines》 <https://opensource.zalando.com/restful-api-guidelines/index.html#table-of-contents>`_
+
+网络/并发编程(进阶)
+-----------------------------------
+如果想要深入了解一些网络框架的底层原理就需要学习底层socket网络编程了，比如《unix 网络编程》，《Linux 高性能服务器编程》等。
+推荐一些入门资料：
+
+* `《Beej's Guide to Network Programming》 <http://beej-zhcn.netdpi.net/>`_
+* `《Python并行编程》 <https://python-parallel-programmning-cookbook.readthedocs.io/zh_CN/latest/>`_
+
+
+Linux系统
+----------
+互联网时代软件慢慢从传统的桌面软件到移动端、浏览器演化(这其实也是脚本语言能在 IO 密集的 web 领域使用广泛的原因)，虽然 Linux 在桌面领域上完败，但是其开源和低成本的特性在网站服务器端、学术和工程领域使用广泛。
+大部分Python应用都是跑在Linux服务器上的，大部分开源服务器软件使用的也是linux系统，即使日常工作不使用linux，一些基本的linux命令也要了解。
+比如常用的文件操作，目录操作，进程操作等。你可以使用类unix系统mac或者linux版本ubuntu作为学习环境。
+推荐：
+
+* `《Linux工具快速教程》 <https://linuxtools-rst.readthedocs.io/zh_CN/latest/>`_
+* `《CONQUERING THE COMMAND LINE》 <http://conqueringthecommandline.com/book/>`_ 掌握这上面的命令基本就可以满足日常需求了。
+* `《13 Linux Network Configuration and Troubleshooting Commands》 <https://www.tecmint.com/linux-network-configuration-and-troubleshooting-commands/>`_
+* `《鸟哥的Linux私房菜.基础学习篇》 <https://book.douban.com/subject/4889838/>`_ 浅显易懂，入门Linux命令的好书。
+
+
+数据库
+----------
+了解常用的关系型和非关系型(NoSQL, Not Only SQL)的数据库的使用。
+现在网站业务后端用得比较多的数据库:
+
+- 传统关系型数据库（mysql等），使用广泛，不够灵活
+- 内存型(k-v型)数据库（redis等）
+- 文档型数据库（mongodb等）
+- 列式存储(HBase)，大数据场景下的 IO 问题
+- 全文搜索型(Elasticsearch)
+
+每种数据库各有优势和其使用场景，后端程序员需要了解下不同类型数据库的使用方法和应用场景，灵活应用到后端存储设计之中。
+关于各种数据库网上已经有不少资料，读者可以自行搜索学习，mysql 和 redis（包括使用、设计、原理、优化）是重中之重。可以看看《Mysql 必知必会》和《Redis 实战》
+对大数据感兴趣的可以学习下 Hadoop 生态系统。
+
+
+* `《Designing Data-Intensive Applications》 <https://book.douban.com/subject/26197294/>`_
+  了解各种数据存储模型，本书覆盖面很广，适合有一定基础的人阅读
+
+
+python 相关库的使用
+-------------------
+python一大优势在于数量丰富的库，灵活使用各种python库能帮助我们快速做出产品。作为web开发者，你需要了解常用python库和框架的使用，比如django/flask/tornado/sqlalchemy/requests/pandas等。
+
+web 框架
+-------------------
+大部分后端业务逻辑开发中都会使用 web 框架，提升开发效率。常用的 python web 框架有 Django、Flask、Tornado 等，一般 web
+框架都包含 http 处理、表单验证、模板引擎、ORM 、Restful 接口等，最好能熟练掌握一个 web 框架，帮助快速做出产品。
+
+版本控制
+----------
+目前最流行的应该就是git了，很多公司使用 gitlab 管理代码仓库。版本控制工具是多人协作必不可少的工具，入门的程序员需要掌握基本的git命令，可以把github作为个人练习的工具。
+遵守你们公司的 git 工作流程，git 是一个很强大但是很容易被初学者错误使用的工具。
+
+* `《语义化版本控制》 <http://semver.org/lang/zh-CN/>`_
+* `《Pro Git》 <https://git-scm.com/book/en/v2>`_
+
+Web 服务器
+----------
+Nginx 目前很流行，使用比较广泛，推荐学习和使用。熟悉基础的 LNMP 架构(Linux + Nginx + Mysql + Python)，目前很多公司采用了都是多语言+微服务架构(基于 docker)。
+你可能需要了解常见的 web 应用部署方式以及如何使用 nginx 等负载均衡
+
+
+微服务架构
+-------------------
+目前很多流行的网站采用了微服务架构，每个团队负责维护自己的服务(逃离单体地狱)。以下是学习微服务的一些比较好的书籍。
+
+* `《微服务设计》 <https://book.douban.com/subject/26772677/>`_ 入门微服务概念的一本书
+* `《微服务架构设计模式》 <https://book.douban.com/subject/33425123/>`_ 评价颇高的一本微服务实践书籍(java语言)
+
+
+前端知识
+----------
+基本的 html，css，javascript 需要有所了解。很多后端工程师需要做一些工具或者管理后台之类的，了解前端知识会有帮助。如果有兴趣深入前端可以了解下 Vue/React/Angular 等流行的框架。
+
+学习和搜索能力
+--------------
+初学者碰到的大部分技术问题都是可以通过 google 解决的，用好 google/stackoverflow/github 和各种技术论坛、牛人博客等能帮助你了解最新的技术。
+
+* `《Instagram Engineering》 <https://engineering.instagram.com/>`_ Instagram 技术博客，有不少 python 相关的技术文章
+
+
+业务领域知识
+------------
+不同公司业务不同，经营（挣钱）领域不同（游戏、广告、媒体、社交、金融等），可能需要了解相关领域知识，方便业务建模。建议找工作之前研调下相应公司、经营领域、使用的技术栈等，不要太盲目，找到自己感兴趣的方向(后端知识很广)，有时候方向和平台很重要，直接决定了你的工资和发展。
+比如基金公司可能需要了解投资相关知识，社交公司可能要懂一些 feed 设计知识，媒体公司可能需要懂多媒体相关知识。(当然重点还是用 python 实现业务逻辑)
+
+专业素养
+----------
+公司做项目不是自己过家家，需要你具备写文档，注释，单元测试，沟通表达、与人协作、处理业务的能力。如果你现在还不了解一个正规python项目都有哪些组建构成，请去github克隆一份知名的代码仓库，花点时间仔细分析下它的项目结构和源代码。
+比如著名网站reddit代码已经开源，大部分python实现，可以参考下。另外很多著名的python库，比如requests/flask等也可以作为参考。从笔者短暂的从业经历来看，大部分自学python的人不怎么遵守代码规范（pep8），
+不知道或者不重视单元测试（写个函数print下就觉得OK了），不知道怎么写注释和文档（docstring听过吗？）。所以希望学习python的你能遵守工程实践，具备良好的职业素养和编码习惯，推荐阅读《代码大全》《编程匠艺》之类的工程相关的书。
+
+* `《程序员的职业素养》 <https://book.douban.com/subject/11614538/>`_
+
+
+软件工程
+------------
+如果有时间我建议了解下软件工程相关的东西，在你没工作之前看书本的东西不会有太多体会，但是工作以后就会感受到做项目远远不是只有写代码这么简单。包括整个开发流程、进度管理、质量管理等还是有很多学问的。
+这里推荐一本邹欣(现任微软Windows中国工程团队首席研发总监)的书，读起来比较接地气。
+
+* `《构建之法-现代软件工程》 <https://book.douban.com/subject/27069503/>`_
+
+
+后端技术栈
+----------
+web 后端工程师的主要工作职责是实现网站、app 业务后端逻辑(产品业务逻辑)，涉及到的技术相关知识点基本就是上边列举的这些。
+对于技能需求可以在拉勾上搜一下Python的职位，看看各个公司对Python的要求。或者你可以写个拉勾网的爬虫，对数据做一个简单的统计，笔者当初找工作就是这么干的。找工作之前最好研究下期望公司的业务和使用的技术栈，有针对性学习。
+另外，真正做项目还需要你熟悉python的各种库和框架，比如django/flask/tornado/requests/sqlalchemy/unittest/celery等等，掌握了合适的工具才能快速上手做东西，公司恨不得你第一天入职第二天就能写项目。
+所以，在你入了门以后请尽快熟悉python web的技术栈。公司不管你会什么算法，只在乎你的生产力(有时候技术本身不重要，它的价值在于对业务、用户、顾客的贡献)。
+推荐一些文章供参考:
+
+
+* `《Python Web 学习路线图[视频]》 <https://zhuanlan.zhihu.com/p/36267942>`_
+* `《全栈增长工程师指南》 <https://github.com/phodal/growth-ebook>`_
+* `《web开发路线图》 <http://skill.phodal.com/>`_
+* `《后端都要学习什么？》 <https://www.zhihu.com/question/24952874>`_
+* `《PYTHON招聘需求与技能体系》 <http://www.wklken.me/posts/2013/12/21/python-jd.html>`_
+* `《PYTHON后端相关技术/工具栈》 <http://www.wklken.me/posts/2014/07/26/python-tech-stack.html>`_
+
+
+学习路线
+----------
+看了这么多是不是还有点懵，笔者当时自学的时候也没人带，没什么方向，走了很多弯路，找工作也不是一帆风顺。如果不是科班出身受过系统的计算机科学理论的训练，是比较吃亏的，只能通过大量针对性学习和练习来弥补。
+大概整理下自己学习 python web 的路线，方便大家做个参考(一个合格的工程师不是短时间能练成的)。其实这基本上也是后端工程师的学习路线，换一个编程语言或者框架都差不多。技术更新迭代非常快，后端技术还算比较稳定的，但是知识点很多很杂，有针对性学习比较好。
+如果你觉得这个教程列出的东西太多，建议就找最重要的知识点，每个知识点挑一本最合适的书学习，我列举的很多资料对于初学者来说可能短时间内难以消化，会有畏难心理和学习焦虑，建议多加练习通过正反馈提升自己学习的乐趣。（如果你还是个学生那很好，有大把的时间准备）
+
+- 学习并熟练掌握一门编程语言(学好英语)。这里笔者选择的是最近特别火的 python，它能干很多事。挑一本好的入门教材，通读并实践书中所有代码示例和练习题（练手感，坚持敲，大量敲）。了解该语言如何操作文本、进程、网络编程等，最后达到能熟练运用编程语言表达逻辑的能力。
+- 搭建好开发环境。初学者个人比较推荐 Ubuntu 系统 + Pycharm 社区版，都是可以免费获取的，我经常安利用 linux 或者 mac，和桌面端不同，企业大部分用的都是 linux server 部署 web 应用的(包括 docker 容器技术等都是基于 linux)，熟悉 linux 命令行、文件、进程操作等会给你找工作和日常工作带来便利。
+- 熟悉算法和数据结构。对于编程语言内置的数据结构、算法等要数量掌握和使用，常用数据结构和算法了解其原理，会计算时间空间复杂度，会自己实现(常见算法面试笔试常考)。
+- 熟悉网络协议 TCP/IP，HTTP，了解互联网是怎么运作的。既然是做网站，需要对网络运行原理比较了解。
+- 学习 web 框架和 python 库。做东西我们需要大量现成的轮子帮助我们，看下 django、flask 等流行的 tutorial，然后做个简单的网站出来（比如博客网站，一般按照教程撸一遍就入门了，python web 框架的文档非常完善）。最好能深入一个框架了解原理，比如看看 flask 和 Werkzeug 源码。
+- 了解前端知识。如果能独立做一个博客出来，大概对 html、css、js 就有所了解了。虽然是做后端，但是基本的前端知识也是必不可少的。
+- 学习业务常用数据库 mysql 和 redis，业界用得比较多的数据库。了解关系型数据库 mysql 基础概念、语句、索引优化等，了解内存型数据库 redis 的常用数据结构，使用场景、结构设计等。
+- 学习 git 版本控制。公司项目协作的时候都是有版本控制的，方便我们协作、记录、回滚代码等。学习编码规范，培养良好的编程习惯。我建议一开始就遵守 pep8，用好 autopep8，pylint 等工具，写出格式规范的代码，不要走野路子。（学习下文档和规范很棒的 python 开源代码）
+- 在 linux server 部署你的 python web 服务。你需要学习 linux 常用命令，web 服务器 nginx 等。最好能独立部署一个网站出来。(笔者经常安利 linux 或者 mac，即使不用来作为开发环境，也要熟悉 linux 命令，能帮助你在服务器上快速修改和调试代码)
+- 对照招聘网站中意的公司的招聘需求查漏补缺。初期就是要多学多练多 google，不是做项目就是在刷题。可以做一些博客、论坛、管理后台等小网站练手。
+- 老实说相比 java 和 php，python 后端岗位是比较少的，如果你学完了还没找到工作然后来臭骂我一顿我会感觉委屈的。我个人倾向于 python 是因为真爱，并且学习python 性价比很高，可以做很多事。如果你觉得不好找工作或者只是把 python 当玩具玩(比如用 pandas 分析自己的投资收益，回测等)，换个语言和技术栈后端路线图基本上还是这些，不会白学的。
+- 建议坚持写技术博客，学习笔记等，总结输出(比如所谓的费曼学习法就是强调你要把学到的讲给别人听才是真正理解了)。你可以使用 hexo 之类的静态博客，或者知乎专栏等现成的服务，或者 readthedoc、gitbook 之类的文档工具。好的技术博客是找工作的一大加分项，笔者工作以后依然坚持写博客记录日常所学，可以是读书笔记、学习心得、对某个技术的理解和实践、甚至是备忘录等。
+- 进阶建议：看《Fluent Python》 之类的进阶书籍；看优秀的源码，比如 python 一些内置库，flask 等优秀的框架源码(可以用 gitx 之类的工具从代码的最初提交开始看起)，能学到很多惯用法和稍微底层一些的东西。尝试仿写，比如实现个简单的 web 小框架，大概就了解框架的运行原理了。
+
+* `《How to be a Programmer 中文版》 <https://braydie.gitbooks.io/how-to-be-a-programmer/content/zh/>`_
+* `《Roadmap to becoming a web developer in 2019》 <https://github.com/kamranahmedse/developer-roadmap>`_
+
+
+求职与面试
+------------
+之前求职的时候每次面试都会充分准备（自己挂过很多次），提前一个月左右开始回顾重点理论知识(看面试相关的书)，刷常用算法，练习手写代码，看相对岗位的招聘需求等。最近面试就发现很多面试者无论是否是有经验都准备不足，忽略了基础知识。
+如果没有知名公司或者项目相关背景，很多招聘要求比较高的公司都会比较看重理论基础和学习能力。公司最好能有一份针对初级、中级、高级岗位的题目，尽量覆盖面广泛、难度适中，防止因为面试官的个人喜好影响面试结果。
+
+- 我建议你闲着没事的时候可以多看看招聘信息，熟悉各个公司对当前技术栈的要求，看看自己和意向公司差距在哪，查漏补缺
+- 电子简历尽量用 pdf 格式，方便跨平台打开。doc 等格式在不同的电脑上打开会有排版问题，很多后端技术面试官可能使用的是 mac 或者 linux。
+- 提前复习回顾重点知识，防止卡在基础上。比如 mac 下著名的 brew 工具作者面试 google 就因为没写出来反转二叉树被拒，后来去了苹果😂.(这就只能看人品和运气和眼缘了，如果没见到二面面试官或者 hr，大概率是挂了)。（树、链表、哈希表、二分、快排、TCP/UDP、HTTP、数据库ACID、索引优化等常考点）。
+- 白板编程，练习在纸上手写代码。虽然很多求职者都很抵触手写代码，但是白板编程确实是一种比较好的区分方式。你的思考过程、编码习惯、编码规范等都能看出来。
+- 如果被问到工程里不会使用但是比较刁钻的算法题，建议你和面试官沟通的时候问问这个算法或者题目在开发中有哪些实际使用场景，看看对方怎么说😎。
+- 面试的时候准备充分，简历要与招聘方需求对等。笔者每次面试都会带上白纸、笔、简历、电脑等，即使面试没过，至少也让面试官感觉我是有诚意的，给对方留下好印象。
+- 加分项：github、个人技术博客、开源项目、技术论坛帐号等，让面试官有更多渠道了解你，有时候仅仅根据几十分钟的面试来评判面试者是有失偏颇的。（比如面试者临场发挥不好；面试官个人偏好；会的都不问，问的都不会等）
+- 面试之前可以适当刷题，这几年招聘难度越来越大， 很多互联网公司都会问一些 leetcode 上的题目，如果不准备很有可能当场写不出来
+
+* `《interview_python》 <https://github.com/taizilongxu/interview_python>`_ python 面试题
+* `《程序员面试金典》 <https://github.com/taizilongxu/interview_python>`_ 程序员面试，很多公司会比较重视基础知识
+* `《Python后端工程师必备技能》 <http://skycrab.github.io/PythonEngineer>`_
+
+
+系统/架构设计进阶
+-----------------------
+对于有经验的工程师来说，系统设计也是一项重要的能力（也是除了存储系统、程序设计、网络通讯、操作系统之外经常被面试考到的）。比如设计一个短网址服务、简单的 feed 流系统、推荐系统、发号器服务等。笔者也处于学习中，推荐个资料供参考：
+(其实中高级后端涉及的其他东西还挺多的，系统设计、大数据存储、消息队列、分布式、缓存、并发优化、软件工程等)
+
+* `《backend-architectures》 <https://gist.github.com/PegasusWang/91294caa0ab26a5c67b9b52d56178905>`_
+* `《http://highscalability.com/》 <http://highscalability.com/>`_
+* `《https://github.com/PegasusWang/system-design-interview》 <https://github.com/PegasusWang/system-design-interview>`_
+* `《System Design》 <https://legacy.gitbook.com/book/soulmachine/system-design/details>`_ 常见系统设计题目
+* `《https://github.com/PegasusWang/system-design-primer》 <https://github.com/PegasusWang/system-design-primer>`_ 关于系统设计和架构设计相关的资料
+* `《system-design-and-architecture》 <https://github.com/puncsky/system-design-and-architecture>`_ 系统和架构设计
+* `《高并发设计知乎回答》 <https://www.zhihu.com/question/421237964/answer/1795200233>`_ 
+
+
+Web 开发常用 Python 库(Golang 常用库请参考 golang 章节)
+------------------------------------------------------------------------------
+列举平常开发常用的一些库和框架(你可以很容易 google 到它们的用法)，你不必一开始就掌握它们，但需要的时候了解它们的用法会大大提升你的开发效率，
+在开发工具章节我还会列举到更多能够提升开发效率的工具。
+
+- web/restful 框架：Django/Flask/Tornado
+- 异步http web框架：FastApi/aiohttp/Sanic
+- ORM: sqlalchemy, Peewee
+- 表单验证：WTForms, marshmallow
+- 数据处理和分析：Numpy, Pandas, Matplotlib
+- 异步：celery, asyncio, tornado
+- 并发：gevent, threading, concurrent.futures
+- 部署：uwsgi, gunicorn(推荐)
+- html 处理: lxml, beautifulsoup
+- 爬虫：requests, Scrapy
+- 单元测试：unittest, nose, pytest(推荐)
+- 图片处理：pillow
+- python2/3 兼容：six, 2to3
+- 代码检测：autopep8, pylint, flake8, mypy(python3)
+- 调试：Ipython, Ipdb, pdbpp
+- 终端：rich(美化颜色输出)
+- 命令行：click
+- 打包：Nuitka
+- 任务队列：apscheduler, huey
+
+裁员、劳动法与法律援助
+------------------------------------------------------------------------------
+最近几年裁员事件逐渐增多，互联网 ToC 端增长见顶，很多收益不好的公司或者创业公司效益不行。
+作为一个码农，要适当了解法律常识，学会维护自己的合法权益。
+
+- `如何看待网传网易裁员，让保安把身患绝症的 5 年老员工赶出公司一事？ <https://www.zhihu.com/question/357459810/answer/913255932?utm_source=wechat_session&utm_medium=social&utm_oi=31478419292160>`_
+- `网易裁员事件引发的思考：5点建议，越早懂，越能保护自己  <https://mp.weixin.qq.com/s/t3Ob6AHlYeO-rZf5c6cknw>`_
diff --git a/codingguide/42web.rst b/codingguide/42web.rst
deleted file mode 100644
index bc146446..00000000
--- a/codingguide/42web.rst
+++ /dev/null
@@ -1,198 +0,0 @@
-42WEB 框架使用不完全手册
-=====================================================================
-
-
-如何写文档
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-开发服务器上，文档的默认路径是 ::
-
-    ~/42web/static/doc/source
-
-写完之后，在 ::
-
-    ~/42web/static/doc/
-
-运行 ::
-
-    make html
-
-可以生成html， 访问 http://doc.你的域名, 可以浏览你新生成的文档
-
-
-编码规范
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-我们python编码风格参见 `Python 编码风格指南 <http://code.google.com/p/zhong-wiki/wiki/PEP8>`_
-
-一般而言，我们尽量使用英文单词的完整写法，而不使用缩写，以免陷入每个人缩写不统一的混乱局面。
-
-html / css / coffee / model 命名（函数名，文件名）保持一致，这样找到了其中一个，就能方便地定位其他的所有。
-
-html 的编码规范 我们参考 `Google HTML/CSS代码风格指南 <http://chajn.org/htmlcssguide/htmlcssguide.html>`_
-
-简单的说， 单元素标记（比如input，br，img，link，meta，hr等等）不要闭合。
-
-
-不要view中直接调用redis，mongo去操作数据库，通过model层将其封装为接口再调用
-
-
-redis
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-
-首先请阅读
-
-* `REDIS 中文文档 <http://www.redisdoc.com/en/latest/>`_
-
-
-使用演示 zapp/SITE/model/password_reset.py ::
-
-    import _env
-    from b64uuid import b64uuid
-    from z42.config import HOST
-    from zapp.SITE.model._db import redis, R
-
-    EXPIRE = 7*3600*24
-
-    R_PASSWORD_RESET = R.PASSWORD_RESET('%s')
-
-    def password_reset_link(user_id):
-        key = R_PASSWORD_RESET%user_id
-        token = redis.get(key)
-        if not token:
-            token = b64uuid()
-            redis.setex(key, EXPIRE, token)
-
-        return 'http://auth.%s/password_reset_verify/%s_%s'%(HOST, user_id, token)
-
-    def password_reset_verify(user_id, token):
-        return token and (token == redis.get(R_PASSWORD_RESET%user_id))
-
-用法说明 
-
-因为redis的内存数据库，所以我们通过 R 来生成 redis 的 key 以节省内存占用 , R的用法如 ::
-        
-        R_AAA = R.AAA()
-        R_BBB = R.BBB('%s')
-
-扩展阅读 `节约内存：Instagram的Redis实践 <http://blog.nosqlfan.com/html/3379.html>`_
-
-
-
-开发流程
----------------
-
--  代码结构
--  内部函数
--  代码结构
--  代码规范
-
-新建一个页面通常需要创建几个文件：
-
-* 模板 zapp/SITE/model/模块名.py
-* 视图 zapp/SITE/view/模块名.py
-* Coffee Script 脚本 coffee/SITE/auth/模块名/视图名.coffee 
-* Make 模板 html/SITE/auth/模块名/视图名.html
-* CSS 样式文件 css/SITE/auth/模块名.css
-
-并在 `_url.py` 中注册视图。
-
-
-Mako 模板
----------------
-
-Mako 模板的存放路径通常是 42web/html/SITE/模块名/文件名.html。 
-Mako 模板的使用见 `教程 <http://docs.makotemplates.org/en/latest/>`_ ，通用部分通常需要剥离成一个 _base.html 文件中，
-不被直接 render 的模板命名以下划线 `_` 开头。
-
-一个基本的模板文件大概是这样的::
-
-    ## 这里继承父模版
-    <%inherit file="/SITE/_base/default.html" />
-    ## 添加头文件、CSS 文件
-    <%block name="head">
-    ## 这里引入 CSS 文件，将目录名中的斜线换成下划线作为变量传入
-    <link rel="stylesheet" href="${css.SITE_auth_m}">
-    </%block>
-    
-    <%block name="script">
-    ## 在文件尾引入 JS 文件，变量名同为替换后的目录名，JS 文件来自于编译 Coffee Script
-    <script src="${js.SITE_auth_m_m}"></script>
-    </%block>
-
-
-几个注意点：
-
-1. 每个html模板都应该继承对应的基模板，不要直接写html
-
-#. js如果不写在block中会造成无法加载的问题
-
-
-Coffee Script
-----------------
-
-网页脚本用 Coffee Script，写完后需编译为 JavaScript
-（添加新的 CS 脚本需要手动重启 dev.sh 脚本，修改已存在的脚本会自动编译成 JS）。
-
-
-Python Model
------------------
-
-数据存储使用的是 MongoDB，通过封装过的 MongoKit 将数据 model 对应上 MongoDB 文档，
-需要 `from z42.web.mongo import mongo` 。下面是一个简单的 model 文件示例::
-
-    from z42.web.mongo import Doc 
-    from z42.web.mongo import mongo 
-
-    class UserIM(Doc):
-        structure = {
-            'user_id': int,
-            'phone': basestring,
-            'qq': basestring,
-            'weixin':basestring
-        }
-
-创建一个 UserIM 对象使用 upsert() 方法，如 `UserIM({'phone': PHONE}).upsert({user_id: 1001})` ，
-这行代码的作用是：如果存在 user_id 为 1001 的 UserIM，则将其 'phone' 设置为 PHONE，
-否则在 MongoDB 中插入这样一个 JSON 文档： {'user_id': 1001, 'phone': PHONE}。
-
-
-Python View
------------------
-
-web 开发使用的框架是修改过的 Tornado，需要通过装饰器注册 URL。
-新建一个简单的 view 如下::
-
-    from _route import route
-    from z42.web.view.j import JsonErrView
-    from jsob import JsOb
-    from zapp.SITE.view._base import HostView
-    
-    @route('/m/register/')
-    class register(HostView):
-        def get(self, sign):
-            self.render()
-
-    @route('/j/m/')
-    class _(JsonErrView, HostView):
-        def post(self, sign):
-            err = JsOb()            # 使用 JsOb 对象保存错误信息
-            o = self.json           # 获取 JSON 化的 POST 信息
-            if not o.title:
-                err.title = "链接已失效！"
-            if not err:
-                do_something()
-            self.render(err)
-
-这个 module 注册了两个 view，一个注册页面和一个 Ajax 接口。route 方法是一个用于注册 URL 的装饰器，
-装饰在 Handler class（通常继承自XxxView）上即可。
-
-register class 就是视图 Handler 的最简写法。对于 Ajax 视图，Handler 名意义不大，可以使用下划线命名。
-需要返回错误提示的视图，可以继承 JsonErrView。View 的继承用法详见 :ref:`views` 
-
-扩展阅读
------------------
-
-`MongoDB权威指南 <https://code.google.com/p/mycloub/downloads/detail?name=%5B%E4%B8%AD%E6%96%87%E7%89%88%5D%20MongoDB%E6%9D%83%E5%A8%81%E6%8C%87%E5%8D%97.pdf>`_
- 
diff --git a/codingguide/stream.rst b/codingguide/stream.rst
deleted file mode 100644
index 6571570b..00000000
--- a/codingguide/stream.rst
+++ /dev/null
@@ -1,11 +0,0 @@
-.. _stream:
-
-============
-开发流程
-============
-
--  开发流程
--  Fork 代码
--  代码提交
--  代码审查
--  Pull Request
diff --git a/codingstyle/codingstyle.rst b/codingstyle/codingstyle.rst
new file mode 100644
index 00000000..8fa6a674
--- /dev/null
+++ b/codingstyle/codingstyle.rst
@@ -0,0 +1,951 @@
+.. _codingstyle:
+
+编码之前碎碎念(工程实践)
+=====================================================================
+
+..
+
+  Controlling complexity is the essence of computer programming.  — Brian Kernighan
+
+有些人喜欢动态语言的表达能力和灵活性，有些人却讨厌动态语言，认为动态语言工程不友好，性能低、易出错、难重构。在项目中应该结合不同语言的生产力、性能、生态圈、招聘需求、产品周期等，灵活选取，扬长避短。
+动态语言比较适合构建 mvp（最小可用产品），所以很多创业公司后端、内部项目、微服务等在用。以下是笔者从业过程中总结的一些工程实践，因为动态语言本身的特性，需要良好的工程控制保证代码质量，否则将来项目代码仓库可能会失控。
+目前网上关于 python 项目工程的资料比较少，以下是笔者的一些实践经验(不局限于Python)，有一定局限性，仅供参考。
+
+技术雷达
+--------------------------------------
+中小公司尽量统一技术栈，减少运维，维护和学习成本，总结最佳实践。可以在数据库，框架，编程语言，基础组件等建立技术雷达，以供业务选型。
+
+- 统一编程语言和框架。比如使用 cookiecutter 之类的工具生成统一的项目代码框架，有利于统一维护。
+- 统一代码组织结构。有利于维护，代码迁移和重构，如果每个项目代码组织都不一样，维护和迁移起来会很累，一致的代码组织你甚至可以轻松把一个项目的文件夹搬到另一个
+- 统一数据库选型。统一建表规范，防止给以后埋坑
+- 统一基础组件(中小公司可能没有，尽量使用成熟的开源组件)
+
+不同团队之间可以使用不同技术栈，不过小组内部统一技术栈有利于快速业务迭代，总结 best practice。小组定期技术分享，打造学习型团队。
+
+
+代码风格
+--------------------------------------
+不一致的开发风格会给协作开发带来困难，同时也妨碍代码阅读，读代码的时间是远多于写代码的，所以有必要统一编码规范。推荐使用pep8或者其子集作为代码规范，使用vim插件python-mode开启pep8和pylint对代码静态检测。如果使用其他编辑器或者IDE工具最好也使用相关插件使代码符合规范。工程上的代码应该尽可能保持清晰易懂，推荐看看requests等优秀的开源库学习下。强烈建议新手看看以下参考写出格式规范的代码，强烈建议打开pep8和pylint，pylint可以帮助你干掉很多低级错误。建议使用py的公司都指定好自己的代码规范并且严格遵守，同时做好code review，防止造成以后的维护噩梦。
+不要搞N无代码(无文档，无注释，无单测，无规范，无 review，无静态检测......)，笔者铲💩多了对这种代码深恶痛绝。
+
+* `《PEP8.org》 <http://pep8.org/>`_
+* `《PEP 8 -- Style Guide for Python Code》 <https://www.python.org/dev/peps/pep-0008/>`_
+* `《Google开源项目风格指南-Python风格指南》 <http://zh-google-styleguide.readthedocs.io/en/latest/google-python-styleguide/contents/>`_ google风格的docstring比较赞
+* `《API_coding_style》 <http://deeplearning.net/software/pylearn/v2_planning/API_coding_style.html>`_
+* `《code-example》 <https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html>`_
+* `《编写优雅代码》 <http://www.kancloud.cn/kancloud/sina-boot-camp/64003>`_  新浪微博的培训课程，可以学习一下
+* `《烂代码的那些事》 <http://blog.2baxb.me/archives/1343>`_  Axb的自我修养，大神的文章
+* `《三种docstring示例》 <http://bwanamarko.alwaysdata.net/napoleon/format_exception.html>`_
+* `《Simple python style guide》 <http://liyangliang.me/posts/2015/08/simple-python-style-guide/>`_
+* `《python编程规范》 <http://blog.ganyutao.com/downloading/python%E7%BC%96%E7%A8%8B%E8%A7%84%E8%8C%83.pdf>`_
+* `《practice-go》 <https://dave.cheney.net/practical-go/presentations/qcon-china.html>`_
+
+
+一个简洁的代码规范(想偷懒的话直接用pylint 和 autopep8 过一遍，强烈建议项目开始的时候就使用 pylint 检测代码，保持 clean code):
+
+- 格式请遵守pep8,务必开启编辑器的pylint和pep8检测。vim请试试python-mode插件，现在不过一下 pylint 都不太敢提交代码了，动态语言太容易出错。
+- 业务逻辑应该限制一些过于灵活的特性，防止代码难以维护。比如元编程，随意的设置属性等，尽量保持业务代码易维护、易修改、易测试。
+- 模块、类和函数请使用docstring格式注释，除显而易见的代码，每个函数应该简洁地说明函数作用，函数参数说明和类型，返回值和类型。对于复杂的传入参数和返回值最好把示例附上。如有引用，可以把jira，github，stackoverflow，需求文档地址附上。 良好的文档和注释很考验人的判断（何时注释）和表达能力（注释什么）。
+- 动态语言的变量命名尽量可以从名称就知道其类型，比如url_list, info_dict_list，降低阅读和理解的难度。(我的感觉就是动态语言易编写，写不好后期更难维护)
+- 风格上衡量不了请参考知名开源项目的做法。以可读性和维护性作为标准。(比如知名网站reddit的python代码已经开源了，可以作为参考，强烈建议大家克隆一份，经常拉取更新，看看人家怎么写的python代码)
+- 阿里最近开源了一个规范《阿里巴巴Java开发手册》（其实这个教程也打算搞成 Python web 开发手册》，网上可以很容易搜到，写得比较细，建议新手下载来看看，有不少实战干货，很多思想是通用的，其实python的unittest等模块很多都是直接借鉴了java。还有新浪微博的《新兵训练营系列课程》
+- 给一些小团队的建议就是所有人统一用 pylint 和 autopep8 工具， pylint 检测代码有没有明显缺陷，autopep8 用来整理格式(类似于 golang 的 gofmt)，至少在风格上就不用在费心统一格式了，代码洁癖必备。
+- pylintrc 参考：https://github.com/PegasusWang/linux_config/blob/master/pylintrc 这里我忽略了很多无关紧要的提示(ignore配置)，你可以按需增减，默认的 pylint 配置对代码检查实在是太严格了，很多老鸟也过不了。建议项目一开始就加上静态检查，让代码更加 clean。
+
+* `《Python 项目工程实践》 <https://zhuanlan.zhihu.com/p/32902344>`_  如何通过工具构建良好的工程代码
+* `《Python 工匠：善用变量来改善代码质量》 <http://www.zlovezl.cn/articles/python-using-variables-well/>`_ 动态语言命名尽量可以表达出类型，否则不好维护
+* `《Python最佳实践》 <http://www.dongwm.com/archives/Python%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5/>`_  董伟明的文章
+* `《PYTHON 代码规范小结》 <http://www.wklken.me/posts/2016/11/03/python-code-style.html>`_
+
+编程范式
+--------------------------------------
+Python支持多重编程范式，过程式(Procedural)，面向对象(OOP)，简单函数式(Functional)编程。不同人，不同语言转过来的人，Python老鸟和菜鸟等写出来的代码风格迥异。对个人风格喜好不予评判，但是个人感觉还是需要深挖一些Python的特性，虽然Python容易入门，但是有些语言特性还是需要一段时间才能了解深入的。使用各种风格的时候要酌情判断，比如多个过程需要共享中间状态时，单纯的使用函数会写得很冗长，这时候就应当使用类。通常能用函数完成功能的就使用函数。当你无法判断哪种方式比较好的时候，请在解释器里边 ``import this`` 看看。当可以实现一样的功能时，往往简单易懂的方式就是最好的。一些参考:
+
+* `《requests》 <https://github.com/kennethreitz/requests>`_ requests库是接口设计的典范，可以参考参考。
+* `《Python3 面向对象编程》 <https://book.douban.com/subject/26468916/>`_ 关于Python面向对象和一些设计模式。
+* `《OOP vs Functional Programming vs Procedural》 <http://stackoverflow.com/questions/552336/oop-vs-functional-programming-vs-procedural>`_
+
+
+何谓Pythonic?
+--------------------------------------
+Python的世界里你会听到这个词"Pythonic"，大概就是指代码符合Python的惯用法，使用的都是Python的语法糖(我觉得可以翻译为『地道』)。比如从其他语言转到Python
+的写出来的代码很可能受到以前思维方式的影响(别像 java 一样写一堆 getter/setter)，写出来的代码不够Pythonic:
+比如:
+
+
+.. code-block:: python
+
+    # 不够Pythonic
+    if a < b and a > c:
+        pass
+
+    # python里却可以这么写
+    if c < a < b:
+        pass
+
+    # bad
+    i = 0
+    while i < mylist_length:
+        do_something(mylist[i])
+        i += 1
+
+    # good
+    for element in mylist:
+       do_something(element)
+
+    # bad, 不要使用默认可变对象作为默认参数
+    def f(a, b=[])
+        pass
+
+    # good, 可变类型使用 None 作为占位符，因为可变类型可能会被函数修改(副作用)，导致调用代码后边使用它的地方出问题
+    def f(a, b=None):
+        if b is None:
+            b = []
+
+
+Python有一些语法上的坑，比如默认参数只计算一次，不要使用可变类型作为默认参数等，看多了写多了就知道了。尤其是可变类型作为函数参数传入后被改变的情况（函数尽量不要有副作用,这里副作用指的就是修改了传入的可变参数的值），尤其要注意。
+一些参考帮助写出Pythonic的代码（注意pythonic 不是要你炫耀奇淫技巧，很多小白不会注意可读性和可维护性，控制复杂度，用动态语言各种瞎搞，维护起来心累）:
+
+
+* `《Transforming Code into Beautiful, Idiomatic Python》 <https://gist.github.com/JeffPaine/6213790>`_
+* `《Pythonic到底是什么玩意儿？》 <http://blog.csdn.net/gzlaiyonghao/article/details/2762251>`_ 赖勇浩的博客
+* `《python-guide Code Style》 <http://docs.python-guide.org/en/latest/writing/style/>`_ python-guide关于代码风格的介绍
+* `《Learning the Pythonic Way》 <https://www.cs.cmu.edu/~srini/15-441/F11/lectures/r04-python.pdf>`_ 一个cmu的课件
+* `《Writing Idiomatic Python3》 <http://share.sm3.su/writing_idiomatic_python_3.pdf>`_ 一本免费小书
+* `《编写高质量代码：改善Python程序的91个建议》 <https://book.douban.com/subject/25910544/>`_ 给国人的书捧捧场^_^
+* `《Code Like a Pythonista: Idiomatic Python》 <http://python.net/~goodger/projects/pycon/2007/idiomatic/handout.html>`_  我强烈推荐新手看看这个教程
+
+
+敏捷与TDD(中华田园敏捷开发：快糙猛，产出一堆 shit)
+------------------------------------------------------
+笔者非计算机科班出身，对于软件工程的东西也不是很懂，最近扫了一本《敏捷软件开发-原则、模式与实践》，感觉有些东西还是挺有启发的。在这里稍微提一下敏捷中的TDD(Test-driven development)吧。因为Python是动态类型语言，不像静态语言可以编译期检查，很多问题运行时暴露出来，而且动态语言语法灵活也容易刨坑。用TDD是可以提升代码质量的，虽然有时候完全用TDD可能有些死板，但是TDD的一些思想还是很值得借鉴：
+
+* 测试最重要的是对架构和设计的影响，不是为了测试而测试。一般难以测试的代码往往是设计不好，耦合严重的代码。没有测试的代码同时也给重构带来压力和隐患。
+
+编码的时候想着如何测试它，甚至都可以改善设计。对于动态语言，一直有『动态语言一时爽，代码重构火葬场』这种说法，说明动态语言如果没有良好的设计和测试，以后是会埋下不少隐患的。
+当你发现debug的时间甚至比写代码长很多的时候，当你发现总是返工对代码修修补补的时候，或者可尝试下TDD。
+你可以学习使用下python的unittest或者pytest等进行单元测试，以保证代码质量。个人工作经验也表明，难以测试的代码往往是设计不太好的代码。
+update: 经验表明，TDD未必是必要的，但是单元测试是很必要的。如果是新项目建议为所有的复杂函数写单元测试，为项目质量保证。大项目如果没有单元测试修改bug和重构会有很大风险。
+另外一般写测试之前先写个失败的例子(比如我会在测试函数开头加上 assert 0 失败一下确保我这个测试函数真正跑了的，我见过不止一次由于命名没有加test开头压根就没跑测试函数的，还以为测试通过了)，确定测试是真正运行了的，因为之前出现过乌龙，单测函数命名没有用 test 开头结果导致根本就没有运行这个测试用例，后来修正了以后跑失败了，如果先失败一次就会避免这个问题，说白了就是保证你的测试用例确实是跑了的(red-green testing)。
+感兴趣可以试试极限编程中的测试驱动开发和结对编程。
+下边是一些参考:
+
+* `《COMPREHENSIVE GUIDE TO CODE QUALITY: BEST PRACTICES AND TOOLS》 <http://codingsans.com/blog/code-quality>`_
+* `《敏捷开发的艺术》 <https://book.douban.com/subject/4037534/>`_
+* `《敏捷技能修炼》 <https://book.douban.com/subject/11614307/>`_  实践出真知
+* `《Tips for agile developers》 <http://web2.0coder.com/archives/92>`_
+* `《pytest: helps you write better programs》 <http://pytest.org/latest/>`_
+* `《Effective Python Testing With Pytest》 <https://realpython.com/pytest-python-testing/>`_
+* `《代码整洁之道》 <https://book.douban.com/subject/5442024/>`_
+* `《编写可读代码的艺术》 <https://book.douban.com/subject/10797189/>`_ 代码首先是写给人看的
+* `《重构-改善既有代码设计》 <https://book.douban.com/subject/4262627/>`_
+* `《软件调试修炼之道》 <https://book.douban.com/subject/6398127/>`_ 了解下调试和跟踪技术。
+* `《测试的道理》 <http://www.yinwang.org/blog-cn/2016/09/14/tests>`_ 垠神的博客
+
+
+业务代码的一些常见原则
+----------------------------
+对于什么是好代码，什么是坏代码我现在还没有太多经验，但是最近工作接手别人的代码感觉困难重重，还是too naive啊。每个人实力不同，风格不同，一起协作的时候确实会遇到很多问题和分歧。感觉code review啥的还是很有必要的，可以让菜鸟学习下老鸟的经验，也可以让老鸟指导下菜鸟的失误，同时避免过于个人化的糟糕风格（比如让人想立马离职的高达成百上千行的复杂函数，比如上来一堆不知道干啥的幻数，比如上来就 ``from shit import *`` 导致俺的编辑工具找不到定义，比如整个项目没有一行测试代码，比如不知道用logger，全用print+眼珠子瞅，一个bug找半天，比如没有pep8检测导致你的环境打开别人的代码彪了一堆警告......)。
+
+说好的规范呢，说好的设计模式呢，说好的高内聚低耦合呢？说好的KISS原则呢？说好的DYR原则呢？其实俺只是想多活几年，至少不要到三十岁头发掉光。啥设计模式的可以不用，能干活的代码就行，牢记几个原则，没事的时候对复杂的东西重构下，代码不能自解释的搞搞文档，不被队友坑同时不坑队友，俺就心满意足了 ，遇到坑队友就等着加班和折寿吧:(。最后还是列举一下常用原则、思想和注意事项吧(下边原则是笔者阅读很多工程相关的书后总结的，比较宽泛，最好import this看看python之禅，很多思想是通用的):
+老手区别于新手的一个重要特点就是，他能用掌握的代码、模式、工程知识来把复杂度控制在合理的范围之内，让代码具有可维护性，很多新手只会直来直去，需求多复杂就能把代码写得多复杂。
+
+
+* 可读性第一定理：代码的写法应当使别人理解它的时间最小化。如果有非常直白的表现方式，就不要用语法糖复杂化，导致理解困难。不要牺牲可读性过度追求短代码，合适的语法糖用在合适的地方，很多新手会炫技使用一些技巧导致代码难以阅读和维护。
+* KISS原则，Keep It Simple, Stupid。能简单的绝对不要复杂，不要炫耀代码技巧，简单可读最重要，后人会感谢你的，软件构建的核心就是控制复杂度。开发可以工作的、最简单的解决方案。除非有不可辩驳的原因，否则不要使用模式、原则和高难度技术之类的东西。很多新手没有控制复杂度的意识，很快弄出一堆难以维护的代码。
+* DRY原则，Don't Repeat Yourself。代码复杂重复了就及时抽取出来，至少不会碰到大问题。当然不要矫枉过正，过度追求设计和通用可能导致难以维护和理解。重复代码一旦接口变动的时候就是灾难，要修改很多地方，一定要十分警惕代码重复(警惕复制粘贴，往往代码重复是设计、抽象不合理、意图不明确的表现，而且复制代码经常会出现忘记修改一些细节产生 bug)。事不过三原则。Prefer duplication over the wrong abstraction. - Sandi Metz
+* YAGNI(You Aren't Gonna Need It)，不要猜测性编码，不用的及时删除，估计以后也不太可能会用到(经验表名你觉得将来可能会用到的基本都用不到，最后成了死代码)，冗余的无用代码会给维护者带来很多混淆和麻烦。Build the simplest thing that we need right now。『少即是多』
+* SLAP(Single Level of Abstraction Principle): 保持一个方法中的代码在同一个抽象层。
+* Clean Coder Rule: Always leave the code cleaner than you found it.  不用的代码及时清除，留着只会造成冗余和误解(如果你认为某段代码将来可能会用到，我明确告诉你基本上它是用不到的)。笔者经验是用动态语言写代码很难写出 clean code，必须上各种静态检测工具和规范来约束，防止代码腐化。
+* Design for failure. 微服务中一切都有可能失败。
+* 最少惊讶原则。让代码的副作用尽量最小或没有，函数式编程相比之下 bug 会更少。(有统计数据支撑的结论)
+* 快速失败，灵活使用断言护保代码。契约式编程(先验条件和后置条件)，越早失败，越容易排查错误。
+* 增量式编程。及时清理技术债务，代码坏味道，防止『破窗』。及时重构不合理代码，及时进行测试，『慢即是快』，越早发现错误修复成本越低。很多统计数据的结果都显示，一名程序员在公司每天能产出的工业级别的代码不会超过百行。
+* 隐藏复杂性。如果复杂性避免不了，应该尽让内部复杂，接口要保持简单易用，而不要因为业务逻辑复杂就堆砌一堆shit。合理抽象，隐藏细节。
+* 一次只做一件事(Do one thing, and do it well)。尽量避免复杂度过高的逻辑，尽量做到代码简单，意图明确。
+* 高内聚，低耦合。模块化。层次化。意义相近的东西应该放到同一个地方。写代码的时候想着怎么测试它就能避免过度复杂，耦合严重的代码。
+* 代码应当易于理解。 《代码大全》、《编写可读代码的艺术》、《代码整洁之道》啥的都是告诉你代码最好自解释，好理解。记住代码首先是给人看的，其次才是让机器执行的，不要过度设计。同时警惕你觉得过于『精巧』的实现，很有可能成为以后代码维护的大坑。可读性基本定律：代码的写法应该使别人理解它所需的时间最小化。聪明的程序员可能写出复杂、精巧的代码(但是对于整个团队的维护来说未必是好事)，专业的程序员会写出可读性高的代码。
+* 不要过早优化，最小可用原则。先测量(profiler)，后优化。根据二八定律，大部分性能瓶颈只在20%的部分，这些才是真正需要优化的地方。不要一开始写代码就极力想压榨所有性能，往往引入优化的同时也在引入风险、复杂度和难以调试的 bug。
+* 不要炫技，可读性最重要。合适的地方使用合适的技巧，不要过度炫耀语法糖导致维护和理解困难。大部分人不是造轮子的，你用不着太多奇淫技巧。
+* 不要重复发明轮子(除非你是在练习编程)。遇到问题首选稳定可靠的解决方案。比如处理excel报表等直接用pandas提供的函数非常方便，我经常看见还是有人自己写一堆恶心的处理函数而不用pandas。如果自己造轮子确保测试和文档，否则后续维护和上手会有很大成本。
+* 自动化。重复执行的任务应该使之自动化(代码构建，自动化运维等），手动执行的东西最容易出错。python是写自动化脚本最合适的语言。
+* Think about future, design with flexibility, but only implement for production. 尽量设计良好，避免繁杂和冗余。好的架构和设计都是不断演进的。
+* 文档化。哪些东西该文档化，哪些该注释需要做好，以便新手可以尽快上手。尽量做到代码即文档，tornado的文档和代码就是典范。
+* 服务化。项目做大了以后及时拆分业务，保持单个代码仓库大小在一定规模。超大规模的代码仓库在部署和维护上会遇到很多问题。
+* 不要直接吞掉任何非预知错误和异常，一定要做好日志记录。使用Sentry或其他工具记录好异常发生的信息，为定位bug提供便利，web端的bug一般不好复现。
+* 墨菲定律：只要有错误发生的可能性，这种错误就一定会发生。所以对代码质量要严格要求，不要心存侥幸。
+* 单元测试:F.I.R.S.T原则(Fast，Independent，Repeatable，Self-Validating，Timely)
+* ......还有的大家可以自己补充。我强烈建议新手或者自学的同学看《代码大全》或者《编程匠艺》之中的任何一本，带你快速入门。当然有些东西只是建议，编程中往往没有绝对正确(不要过度迷信某些所谓的实践和原则)，只有相对更优，No Silver Bullet，大家在实践中摸索吧。
+
+- `《编程到底难在哪里？》 <https://www.zhihu.com/question/22508677>`_ 感觉对于业务后端来说，难就难在『变化』，需求总是在变，如何控制复杂度并且快速响应需求是一个很大的挑战
+- `《Unix 编程艺术》 <https://book.douban.com/subject/1467587/>`_   如果你有时间可以当成小说看看，感觉有点宗教主义
+- `《The Elements of Programming Style》 <https://en.wikipedia.org/wiki/The_Elements_of_Programming_Style>`_
+
+
+还有OOP那一套(封装、继承、多态)，当你设计一个类的时候需要有所注意(SOLID原则):
+
+* 单一职责原则(Single-Responsibility Principle): It should have a single purpose in the system, and there should be only one reason to change it.
+* 开闭原则(Open-Closed Principle): 对修改关闭，对扩展开放。Code should open to extension but closed to modification.
+* 里氏代换原则(Liskov Substitution Principle): 所有使用基类的地方都可以使用子类替换。Anywhere you use a base class, you should be able to use a subclass and not know it.要遵守Liskov替换原则，相对基类的对应方法，派生类服务（方法）应该不要求更多，不承诺更少。
+* 接口隔离原则(Interface Segregation Principle): 不要强制客户端使用他们不需要的接口。Don't force clients to use interfaces they don't need.
+* 依赖倒置原则(Dependence Inversion Principle): 高层模块不应该依赖于底层模块，他们都应该依赖于抽象。 High-level modules shouldn't rely on low-level modules, both should rely on abstractions.
+* 迪米特原则(Law of Demeter):
+* 合成复用原则(Composite/Aggregate Reuse Principle):
+
+`《如何在Python里应用SOLID原则》 <http://aju.space/2016/06/17/use-S-O-L-I-D-in-python.html>`_
+
+Unix 哲学(来自《Unix 编程艺术》)，如果你对 unix/linux 的设计哲学和发展历史感兴趣可以看看这本书（我经常安利后端开发者使用 mac/linux 系统，它们在学术界和工程界更受欢迎）：
+
+* 模块原则：使用简单的接口拼合简单的部件
+* 清晰原则：清晰胜于机巧
+* 组合原则：设计时考虑拼接组合。组合优先于继承
+* 分离原则：策略同机制分离，接口同引擎分离
+* 简洁原则：控制复杂度
+* 吝啬原则：除非却无它法，不要编写庞大的程序
+* 透明性原则：设计要可见，以便审查和调试
+* 健壮原则：健壮源于透明与简洁
+* 表示原则：把知识叠入数据以求逻辑质朴而健壮
+* 通俗原则：接口设计避免标新立异
+* 缄默原则：如果一个程序没什么好说的，就缄默
+* 补救原则：出现异常时，马上退出并给出足够的错误信息
+* 经济原则：宁花机器一分钟，不花程序员一秒
+* 生成原则：避免手工hack，尽量编写程序去生成程序
+* 优化原则：雕琢前先要有原型，跑之前先学会走
+* 多样原则：绝不相信所谓『不二法门』的断言
+* 扩展原则：设计着眼未来，未来总比预想来得快
+
+python代码坏味道(新手经常犯的错误)
+--------------------------------------
+下边是笔者学习和维护代码的过程中总结的一些经验和发现的一些问题，可能有些地方会有分歧，python在工程实践方面的资料不如其他语言那么成熟，如果有分歧欢迎提 issue 讨论, 仅供参考（通常可能需要数月甚至数年的工程训练才能写出良好风格的代码）：
+
+风格相关:
+
+- 不pythonic，写得很业余(随意)，真就信了半天学会python。笔者写代码强制用pep8和pylint检测代码(集成到编辑器里)，除了一些无伤大雅的提示（比如行长度超过80），其他错误和提示全部消除。一开始比较痛苦，习惯了能大幅提升代码规范性。
+- 不要滥用动态特性，**不要** 在业务代码里使用元类，setattr 等随意设置属性，维护起来是个灾难。
+- 不要硬编码，不要用幻数！上来就整一个不知道啥意思的magic number or string，大学老师没教你不要滥用幻数(if status=1，来告诉我1是啥意思)？使用Enum/dict/对象/const等替代掉无意义的幻数。总有人偷懒使用幻数，别人看懵逼的。
+- 上来就 ``from shit import *`` 为了偷懒有可能会导致同名覆盖问题，还会让开发工具找不到定义，工程上不要这么用。
+- 包导入顺序混乱，没有按照pep8要求，实际上rope等工具能自动帮你整理顺序，我现在就是偷懒随意写，直接让rope给我整理。(标准库，三方库，本地库，同级按照字典序，vim的话可以用rope插件自动整理顺序)
+- 导入最好按照模块导入，使用的时候用module.func使用，防止from module import func的时候可能遇到的循环引用问题(模块设计不够合理)。
+- 变量名乱起，表意不明，推断不出类型，加重理解负担。我在想是不是动态语言用匈牙利命名法要好一些，命名尽量要可以看出类型，比如复数表示容器类型，nums，cnts等后缀表示数值(通过后缀和词性来使名称更容易被推断出来含义，比如是属性还是方法)。动态语言一大诟病就是容易类型出错，复杂类型推荐多写点类型注解(python2 用注释标识类型)。
+- 不遵守pep8，没有pylint检测，打开代码一堆语法警告，老子的编辑器满眼都是warnning，编辑器用不好就老老实实用pycharm，用编辑器就老老实实装好语法检测(pep8)和pylint检测插件，没有插件请考虑换一个editor。我个人的感觉就是python代码很容易写得难以维护，请务必加上pylint检测，帮助提高代码质量。还是推荐不想折腾编辑器的直接用好pycharm。
+- 没有逻辑分块，一点都不重视排版，没有美感（这个就算了），就算不限制一行超过80列，也不能写一行写几百列吧，左右转头脑瓜子疼(请不要用tab，全用空格，不要有多余空白，vim有类似插件去除无用空白的)。使用良好的分行，空格使代码更美观，逻辑更清晰。
+- 不要一行写太多逻辑，比如嵌套的列表推导。(Raymond's rule: One logical line of code equals one sentence in English)。好的代码读起来应该和读英文差不多，从上到下知道每一步都干了什么。不要轻易为了代码技巧缩短行数，易读性更重要。业务代码能不用奇淫技巧就千万别用，维护起来心累。
+- 统一编辑环境（editorconfig）、导入顺序（isort）、编码规范（autopep8）、静态检测（pylint），甚至统一命名规范和名词术语（不要相信各种中式英语，换一个人就看不懂了）。
+
+* `《https://docs.python.org/3/faq/programming.html#what-are-the-best-practices-for-using-import-in-a-module》 <https://docs.python.org/3/faq/programming.html#what-are-the-best-practices-for-using-import-in-a-module>`_
+* `《https://docs.python.org/3/faq/programming.html#how-can-i-have-modules-that-mutually-import-each-other》 <https://docs.python.org/3/faq/programming.html#how-can-i-have-modules-that-mutually-import-each-other>`_
+* `《unmaintainable-code》 <https://github.com/Droogans/unmaintainable-code>`_ 从反面教材学习如何编写 maintainable code
+
+异常相关：
+
+- 到处print，debug的时候加上，上线再删除（累不累亲？），logging模块很受冷落
+- 上来就try/except了，把异常都捕获了，吞掉异常导致排错困难。就在我写这段的时候又因为使用了他人未经测试的代码排错许久，就是因为吞了异常没打出来异常信息。
+- 捕获的异常应该尽量类型精确，范围清晰。不要上来就try一整个代码块，可以继承内置异常类定义自己的更为精确的异常类。
+- 使用sentry等工具记录异常，有利于排查问题(能保存堆栈和现场信息)。切记不要轻易吞掉非预知异常，一旦出现问题不好排查，笔者之前维护的项目曾踩过坑，后来笔者引入了sentry排查问题方便很多。
+- 捕获异常是为了处理它，确定要怎么处理异常，记录待修复？流程控制？交给上一层重新抛出(raise)？预知异常直接pass？
+- 了解你所使用的类库函数会抛出哪些异常，需不需要捕获异常？自定义函数抛出的异常最好在docstring里写出来。
+- 编写异常安全的代码: 即使发生了异常，也不会发生异常情况。比如，不会在数据库插入垃圾数据，不会异常终止等。
+- 不应当处理超出必要范围的异常，完全预测发生的异常是很困难的，应该抛出给上层程序处理。
+
+python2 编码问题：
+
+- 包含中文的字符串常量注意使用 u 前缀
+- 代码中尽量使用 unicode，需要网络 IO 和写入磁盘的时候使用 bytes
+
+
+模块相关：
+
+- 统一代码的分层结构（MVC），当拆分或者重构项目的时候，统一的代码分层结构会带来很多便利
+- 导入模块而不是具体的函数或类，防止代码结构层次设计不合理导致循环引用。碰到循环引用可以通过把导入语句写到函数里的形式延迟导入
+- 注意模块命名尽量不要和标准库或者第三方库冲突
+- 注意子模块名称不要和上层模块冲突,否则会 "Import Error: Cannot import Name XXX"。也可以用 ``from __future__ import absolute_import`` 解决，默认会从顶层包查找。
+- 推荐使用绝对导入
+
+
+函数相关:
+
+- 复杂函数没有docstring，接口易用性极差，传入了一个嵌套字典都不注释，娘来。python没有类型声明真是维护代码的一个大坑。
+- 保持函数参数和返回值尽量使用简单数据类型，，更加容易读懂和构造单元测试。你传入dict或者对象不写docstring我知道字典有哪些字段(最坑爹的是动态语言你还没法跳转过去看参数 object 定义)？如果传入了复杂的参数或者返回类型，最好加上 docstring 说明。看别人代码最头疼的就是看不出参数传的啥结构，返回啥结构，尤其是动态语言，十分隐晦。所以除非必要，保持参数类型尽量简单。
+- 函数要么修改传入的可变参数，要么返回一个值。请不要两者同时做。注意python默认参数只计算一次，如果默认参数不是immutable对象，最好使用None作为占位符。每次修改传入的可变参数之前要三思，出bug了不容易排查。注意 None 和 空值的差别，None 是单例的，用 is 来判断一个对象是否是 None。我们能写纯函数就用纯函数（返回结果只依赖于参数并且没有副作用的函数），不容易出错，并且易于测试和调试。
+- 函数尽量不要有副作用，如果没有很强的性能要求，尽量不要直接修改传入的可变参数而是返回一个新的结果。重构的时候有修改了可变参数的函数重构起来会非常麻烦，保证函数有明确的输入和输出做单元测试的时候会很方便。很多小 bug 都是因为非预期修改了传入的可变参数导致的。一个有明确输入和输出的函数更加容易理解和编写单元测试，想象一下你传进去一个字典在函数里瞎改，非常难以理解。
+- 避免在遍历一个序列的同时修改它，比如边遍历边移除列表里的元素，可能会导致非预期行为。
+- 超长函数，没有复用和拆分，抱歉我智商低，不能理解好几屏都翻不完的，见谅。这么长居然还tm能工作，牛逼(我发现越是新手写的代码越难理解,我实习那会总被说代码写得像面条)。控制复杂度，程序的复杂性决定了一个人要花多大努力才能理解程序。Dijkstra说过『一个聪明的程序员总是清楚地知道自己的脑力容量有限，因此他得十分小心谨慎地完成编程任务』。这不意味着为了处理复杂问题你得增大你的脑力，而是说你得想尽办法尽可能降低复杂性(彻底理解你要解决的问题)。要认识到人的脑力负荷是有限的，凡是你现在绞尽脑汁写的shit 一样的代码，将来维护起来都要花数倍的精力。如果遇到过长的代码，不如把逻辑分为几块，然后每一块抽出来作为函数并且合理命名，这样就容易理解了，别堆砌一长坨。
+- 函数『圈复杂度』太高，一堆嵌套逻辑判断，导致测试难以覆盖到所有分之，单元测试几乎就没法写，恩，你压根不写单元测试就当我没说。比如你可以用德摩根律、表驱动法替代过多if/else判断，每当你写下一个if的时候，确定是否需要对应的else。感兴趣的可以搜搜软件工程里关于圈复杂度的概念，降低复杂性是编写高质量代码的关键。也可以尝试用结构化编程、单出口等方式降低代码出错率。
+- 穿插着让人摸不着头脑的代码片段。（对于变态的产品需求或者非常triky的代码必须加上注释）。个人非常推崇『意图导向』编程，就是每写下一个块模、函数、类、代码片段的时候，除非显而易见或者约定俗成，否则都注释上你为什么需要它、它在哪里会用到。如果所有代码都得通读一边才能知道它是干啥的，是非常耗时的。(笔者挺痛恨阅读动态语言写的代码)
+- 没注意可变类型和非可变类型，传入可变类型并在函数里修改了参数(无意的修改)，坑。。。还有一种坑 ``a = b = c = [] or a, b, c = [], [], []`` ，注意可变类型会引用同一个对象，注意 python 中的深浅拷贝，可变与非可变对象。
+- 滥用 ``(*args, **kwargs)`` 导致函数接口模糊，有类似接口应该明确用docstring写明需要传入什么参数，"Explicity is better than implicity"，不要为了偷懒把代码写得隐晦。请尽量使用简单参数类型并保持接口清晰。
+- 返回多个值可以使用namedtuple封装，比用下标更直观。对于可能经常需要变动的返回值，返回字典或者对象要比返回tuple容易修改。但是这种复杂的返回类型最好在docstring里注释下返回结构。适当使用抽象数据类型（ADT）增加代码可读性。
+- 减少重复代码，否则将来接口变动一旦修改就要改动很多处，尽量保持函数简短并且尽量复用。『三则重构』原则。
+- 注意函数在每个返回点的结构保持一致，尤其是在多个分之有返回点的时候(我经常看到一些函数有多个 return，但是每个 return 的返回结构却是不一致的)。函数尽量返回相同的类型（比如返回一个空 list 而不是 None，这个是参考 sqlalchemy 设计，比如批量查询查不到返回空 list 而不是 None）
+- rpc 调用等有没有降级？对方服务跪了会不会影响我们的接口？
+- 不要多个函数嵌套在一起使用，比如 f(a(b()))，一旦出现问题很难定位是哪个函数的问题，即使是用 sentry 也不容易看出来。尽量每行代码明确表达一个清晰的逻辑，不要超过三层嵌套。
+- 接口注意几个点，是否代码易读，易用（docstring），正确工作（单元测试）。尽量接口写出来基本就能通过名称和docstring快速让别人知道怎么用的，传入哪些值，返回什么东西，会抛出什么异常。笔者维护代码最最痛苦的就是你得一行一行读代码甚至还得打断点才能搞清楚接口是做什么的(中间充斥者复杂的嵌套数据结构，只有打断点才能看出来)，十分痛苦，十分浪费时间，用python开发省的那点时间全TM用在维护和还技术债了。偷懒只能节省一个人的成本(甚至节省不了)，对项目来说是很不利的。
+- 参数过多的时候推荐调用的地方显示写出参数名 f(a=1,b=2)，当修改参数签名个数的时候调用点不容易出错，看代码的时候也比较容易知道每个参数的意义。建议一个函数传参 5 个以上以后参数就指定参数名进行传递，防止参数不匹配导致的 bug。
+- 修改函数定义的时候，为了保证之前所有的调用点兼容，应该只在函数定义所有参数之后添加新的参数，并且最好给上默认值(否则你需要确保所有调用函数的地方都要改动)，绝对不要随便修改旧的参数顺序。（防止没有显示指定参数名传递的函数传入顺序错乱，如果参数过多建议指定参数名传递关键字参数）
+- 如果没有特殊的性能需求，函数返回值尽量使用marshmallow之类序列化，之前的很多项目直接搞一个 dict 各种往里边塞字段然后返回，很难看清楚返回的啥结构，维护起来很累。
+- 注意深浅拷贝的赋值问题，浅拷贝会导致不同变量共享结构，其中一个修改了对其他指向同一个结构的其他变量都可见，有时候会产生难以排查的 bug。比如笔者曾经碰到过一个函数返回的数据共享内存(误以为每次返回的是新的值)，在函数外又修改了它，导致不同请求『神奇』地数据相互干扰了。
+
+类和面向对象相关:
+
+- 你真的需要一个类吗？不要到处OOP，也不要只会写function。你了解OOP的几大原则吗？
+- 业务逻辑代码中禁止使用元类，尽量避免使用 getattr/setattr 等动态特性，可能会给代码维护造成问题。除非是写框架，绝对不推荐在业务逻辑中使用任何黑魔法，以后维护起来简直就是噩梦。
+- 保持类的继承层级简单，适当使用mixin。
+- 注意尽量不要在非 ``__init__`` 方法中给类赋值属性，笔者在维护别人代码的过程中，发现经常在一些非 init 方法种赋值新的属性，导致后期难以维护，根本不知道这个对象包含哪些属性，删除一个属性的时候坑也多。
+- 尝试使用CRC(clas-responsibility-collaboration)：类-职责-交互卡片设计类。
+- 注意多继承时候的 MRO 顺序。
+- 保持类的单一职责，不要编写体积过大的类。
+- 除非开发框架， 业务里不要使用元类
+
+测试/单测相关:
+
+- 没有单元测试，不知道怎么写测试（print大法好？）。没有一点专业精神，或许和python大部分都是自学的业余选手有关，哈哈当然我也是。没有单元测试对于大项目和动态语言项目来说就是灾难，不敢重构，改bug后无法确认是否引入新bug。对于关键代码一定要保证必要的单元测试。对于喜欢造轮子的，也要保证单元测试。有点违反直觉的是，单元测试长期来看并不会降低工作效率，因为编写代码往往只是工作中一个小环节，很多时间是在调bug，而且没有单元测试几乎不敢重构不好的代码，为代码腐化埋下祸根。但试图编写大量测试会因为工作量大而望而却步，所以可以针对关键和易出错的地方编写必要的单元测试，否则以后修复bug没有测试就是灾难。好的测试代码甚至还能当成文档，解释调用参数和返回结果。
+- 不专业，写了几句代码print下结果就觉得正确了，单元测试呢？docstring呢？代码易用性和可维护性极差，未经测试的代码是不值得信任的。不要太相信自己，人人都会犯错，但不能反复犯一样的错。
+- 对于外部调用、网络请求、rpc调用等使用 mock 或者 stub。https://chase-seibert.github.io/blog/2015/06/25/python-mocking-cookbook.html
+- 基于代码行为测试，不要片面追求测试覆盖率。编写单测可以影响代码设计，不是为了测试而测试。一般难以测试的代码可能是复杂度太高，耦合比较大，有副作用（比如修改了传入的可变参数等）的，容易测试的代码往往是设计良好的代码。
+- 什么时候使用stub，什么时候使用 mock？网络请求和数据库查询在构造上一层测试(比如controller)的时候可以 mock 掉。
+- 单测中避免使用未定义或者随机行为。比如代码里依赖 random 或者时间戳，测试的时候就可能因为时间不满足失败（mock 掉time 库）
+
+* `《Writing Testable Code》 <http://misko.hevery.com/attachments/Guide-Writing%20Testable%20Code.pdf>`_
+
+
+日志相关:
+
+- 哪些地方需要打印日志？debug参数？记录用户行为？排查问题？记录哪些信息？使用什么日志级别？
+- 注意日志等级，使用debug/info/warnning/error要斟酌好。之前出现过生产环境使用了 debug 日志打印太多信息导致机器负载过高服务不可用的情况，注意不同环
+  境日志级别设置好，一般线上可以设置 ERROR 级别。
+- 管理后台之类的需求凡是针对数据表的危险修改操作都应该记录日志，方便追查问题
+
+ORM和Mysql数据库相关：
+
+- 遵守互联网公司数据库设计规范。网上很多开源的规范可以参考，结合自己公司制定规范(很多中小公司一开始没DBA，建表很乱，没有规范/注释，不统一业务名称，各种外键/NULL值，没有创建时间等固定字段很多坑，后期修改成本比较高)。如果一开始数据库搞得比较糙，后来无论优化还是拆分都会带来不少麻烦。
+- 数据库这一层的接口考虑下参数过滤，防止不恰当参数可能导致的慢查询。动态语言要注意变量类型和数据库字段类型不一致导致的查询索引失效。
+- 优先使用ORM，相比sql语句更加容易维护，同时避免了sql注入。Sqlalchemy只有你想不到，没有它做不到，除非你比较在意性能。
+- 获取对象的时候尽量传入需要的字段(数据表列)，减少数据传输同时还能避免拼对象的时间消耗，python构建对象比较耗时。
+- 注意不要在循环里使用查询语句，合并查询语句。比如不要在for循环中使用一个对象的relation查询(懒加载的时候，每次调用都会查询数据库)
+- 注意隐式类型转换导致的全表扫描。大家可以搜一下《数据库30条军规》，有一些坑应该避免。
+- Mysql需要存储表情：``CREATE DATABASE mydb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;``
+- 不同微服务之间尽量不要共享数据库，而是通过接口进行通信。避免一个服务迁移数据之后受到影响。
+
+* `《MySQL互联网业务数据库设计规范》 <https://www.verynull.com/2017/02/18/MySQL%E4%BA%92%E8%81%94%E7%BD%91%E4%B8%9A%E5%8A%A1%E6%95%B0%E6%8D%AE%E5%BA%93%E8%AE%BE%E8%AE%A1%E8%A7%84%E8%8C%83/>`_
+* `《Mysql高性能优化规范建议》 <https://www.cnblogs.com/huchong/p/10219318.html>`_
+
+HTTP相关：
+
+- 注意处理请求头 content-type 里 "application/json" 和  "application/x-www-form-urlencoded" 区别，一般框架会封装好
+- 注意跨域 cors 处理，一般也是封装到框架。
+
+* `《四种常见的 POST 提交数据方式》 <https://imququ.com/post/four-ways-to-post-data-in-http.html>`_
+* `《HTTP访问控制(CORS)》 <https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS>`_
+
+Redis相关:
+
+- Redis key 命名尽量能够体现出含义，但是也要注意过长的 key 影响内存占用，尽量保持 key 长度精简，在代码里注释说明
+- 使用 redis 之前最好先根据 qps 和内存占用做一个简单预估，防止线上出问题
+- 注意 redis key 超时时间的设置是否合理，否则可能会占用非常多内存
+- 使用集群 redis 应该注意防止大 key 产生，可能会严重影响 redis 性能
+- 注意一些分布式数据库限制(阅读官方文档)：比如腾讯云 redis 集群版 lua 脚本需要所有 key 都在一个节点；不支持非 watch 事务等。
+- 禁止线上 redis 服务器使用一些危险命令，比如 keys ，使用 scan 替代
+- 注意连接池是否会打满，一般是用全局单例的连接池，防止频繁建立 tcp 连接开销。注意不要一直创建连接池把连接数打满
+
+* `《REDIS内存容量的预估和优化》 <https://searchdatabase.techtarget.com.cn/7-20218/>`_
+* `《阿里云 Redis 开发规范》 <https://yq.aliyun.com/articles/531067>`_
+* `《Redis Best Practice》 <https://redislabs.com/redis-best-practices/>`_
+* `《Redis 容量预估工具》 <http://www.redis.cn/redis_memory/>`_
+
+
+Web 框架相关：
+
+- 推荐使用 Django/Tornado 统一管理路由配置的方式，而不是使用 Flask 装饰器路由的方式，方便统一查询和管理。
+
+
+内存泄露问题和排查：
+
+- 谨慎使用生存周期过长的全局对象。之前出现过不断append 一个全局 list 导致的内存泄露情况，很难判断销毁时机。
+- 如果涉及到循环引用，使用弱引用 weakref
+
+* `《使用gc、objgraph干掉python内存泄露与循环引用》 <https://www.cnblogs.com/xybaby/p/7491656.html>`_
+
+
+接口序列化和版本格式兼容问题：
+
+- 跨语言数据类型兼容：之前出现过后端修改了返回的 bool(true/false) 为 int(1/0) 导致客户端解析失败(甚至崩溃)的问题，注意不同后端动态语言和客户端序列化数据类型字段兼容问题（比如后端python客户端java)
+- 数字类型溢出问题：跨语言 rpc 或者序列化成 json 返回给前端数据，注意 int64 类型(比如发号器发的in64 id)转成 string(grpc会自动处理)。长整型会被js截断，很多新手可能会忘记这种跨语言序列化的坑，发现int64给前端以后被截断了，建议 int64 对于 js 直接给 string
+- 版本字段兼容：如果用的 json，定义好格式以后一旦上线不能删除和修改原有字段(可以新增字段)，防止使用老版本数据的客户端崩溃。如果是内部 rpc 框架(thrift/grpc)等，一般定义后的字段是不能修改其顺序序号的。
+- 新旧版本逻辑兼容：客户端(iOS/Android)一般会有新老版本共存的情况(有些用户不想升级)，可以通过获取客户端的平台和版本号区分返回不同的业务逻辑。最好封装一个版本判断框架，而不是一堆恶心的 if/else 判断穿插在代码里
+
+https://imweb.io/topic/581a8eb02373d1aa606c8be7
+
+
+文档注释相关:
+
+- 类型注解。动态类型语言容易出错，没有类型检查。建议 python3 使用好类型注解功能，python2 里尽量多用注释给复杂类型加上类型注释。如果你有过维护和修改别人 python 代码的经验，就会发现最头疼的就是搞清楚变量的类型结构问题。其实还有个小细节，比如 python 代码里用到的 redis key 的命名我一般都会加上类型或者注释，比如 some_zset_key，方便知道能做什么操作。
+- 如果是小团队(python大团队感觉会死人的)并且人都比较懒就那就『代码即文档』（有程序员说你让程序员写文档不是天方夜谭吗？你丫的哪个牛逼开源项目的文档是产品经理写的吗？？？excuse me, 代码写不好文档能好看点也行啊，你得让我不看shit一样的代码也能用你的接口啊）。python的特色docstring实际上就是最好的文档。
+- 不写注释就得确保你的代码高度可读，不然shit一样的代码又没注释和文档，你让接盘侠怎么活？
+- 注释有时候甚至可以帮助你思考设计，比如如果一个类、函数等如果难以用一句话描述它的职责，很有可能就违背了SRP（单一职责原则）。
+- 如果系统调用过程比较复杂， 最好用流程图标识一下。
+- 对于复杂的数据结构(比如嵌套类型)，可以适当注释出类型，比如最新的 tornado 源码里出现了这种注释 ` __impl_kwargs = None  # type: Dict[str, Any]`  。python3 实际上可以加上类型注解了，鉴于目前 python3 的普及程度，估计暂时也没啥用武之地了。
+- 代码应该像是短文一样从上往下能够容易读懂，如果是怼不了特殊需求必须 hack 代码才能实现，必须加上注释说明。否则又出现了『黑洞代码』让别人看着一脸懵逼。善于利用 TODO，HACK 当成注释前缀，方便维护代码的人理解。 HACK: ###,  TODO: ####
+- 特殊注释前缀。TODO(未完成代码), FIXME(修复我), HACK(比较 trick 才能实现的逻辑说明), NOTE(代码注意事项)，编写注释的时候可以遵守这些常见前缀。
+
+版本控制(git)相关：
+
+- 善用 git，多用分之，合理利用分之可以有效多个功能并行开发。git 分之是一个非常轻量的操作
+- 注意一定不要提交敏感信息到代码仓库；不要提交大的二进制文件等到 git，需要忽略的文件应该放到 gitignore 文件里。
+- 拆分功能，分次提交，尽量每个 commit 都是独立的小功能，可以单独回滚。不要一整次提交非常多的代码，不利于 review 和纠错
+- commit 信息要尽量描述清楚，不要瞎写比如『fix』这种毫无意义的 commit 信息。可以使用 commit message 模板或者 cz-cli 之类的提交工具
+
+线程安全相关：
+
+- CPython 实现中，如果内置类型的操作是单个字节码(bytecode)操作，我们可以认为是原子的，操作能保证线程安全。比如 ``L[0]=0`` 线程安全但是 ``L[0]+=1`` 不是线程安全的。你可以用 dis 模块来查看操作的字节码。可以认为 GIL 以字节码为粒度。
+- 虽然有些操作是原子的，比如字典赋值，但是如果用户自己实现了 ``__hash__`` 和 ``__eq__`` python 方法，就变成了非原子的。如果调研后无法确定是否是线程安全，最好使用锁。
+
+* `《Which Python Operations Are Atomic?》 <http://blog.qqrs.us/blog/2016/05/01/which-python-operations-are-atomic/>`_
+* `《Google Python Style Guide: Threading》 <https://google.github.io/styleguide/pyguide.html#Threading>`_
+
+python 代码性能优化相关：
+
+- 不要过早优化，虽然 python 性能一直被诟病。优化之前先使用 profile，火焰图 等工具查看性能瓶颈。基本上代码的耗时是遵守2/8定律的，集中优化最耗时的代码，衡量成本和收益。其实很多 python 内置库都是 c 写的，优化空间并不大。而且大部分 web 应用瓶颈在 IO 这块。
+- 在优化和可读性之间寻找平衡。
+- 优先从数据结构、算法、数据库、网络IO等层面优化，大部分 web 应用语言性能不会成为瓶颈，不过有些项目语言本身性能确实会成为瓶颈。
+- 对于 cpu 密集的代码可以使用 cython(不是 CPython) 编写扩展来优化速度，性能提升很明显，在 reddit 和 知乎都有使用；或者使用一些知名库的比如 numpy，pandas处理矩阵等。http://cython.org/
+- 更换语言（比如切到 golang），框架（使用异步框架），数据库（Nosql）甚至架构（微服务架构等），成本较高，动作较大，应该是最后的备选方案。
+- 常见的 web 后端性能优化措施：
+
+  - 批量：批量接口(比如数据库一次获取多条数据/redis pipeline等)，目的是避免多次网络I/O；消除数据库慢查询，索引优化等。
+  - 缓存：使用 redis 等内存型数据库缓存热数据，需要注意缓存失效问题(Cache-aside, Write-through, Write-back)，内存型数据库相比传统关系型数据库速度优势明显， 不过难以支持复杂查询。
+  - 异步：使用 celery 结合消息队列等把任务交给离线 worker 执行，防止阻塞当前请求。或者使用异步框架，tornado, python3 asyncio(至今仍不成熟) 等。
+  - 并发：使用 gevent(greenlet)、多线程 等并发请求数据，配合 gunicorn(master-slave模型) 部署。不过需要注意使用 gevent mysql driver 需要纯 python 编写的 driver 才能被 monkey patch
+  - 多线程/多进程：python 虽然有 GIL，但是 I/O 期间会释放 GIL，多线程仍可以大幅提升 I/O 密集应用的性能；多进程适用于 cpu 密集型应用。(threading/multiprocessing/concurrent.futures)
+
+目前来看基于 gevent+gunicorn 的并发方案是目前比较成熟的方案(知乎就是这么用)，业务代码无需修改，也是很多公司首选的方案，在很多公司都有使用，asyncio 生态圈等待成熟。
+
+* `《常见性能优化策略的总结-美团点评技术博客》 <https://zhuanlan.zhihu.com/p/24401056>`_
+* `《High Performance Python》 <http://ningning.today/2017/02/05/python/high-performance-python/>`_
+* `《gevent程序员指南》 <http://ningning.today/gevent-tutorial-cn/>`_
+* `《gevent调度流程解析》 <http://www.cnblogs.com/xybaby/p/6370799.html#undefined>`_
+* `《Pinterest How we use gevent to go fast》 <https://medium.com/@Pinterest_Engineering/how-we-use-gevent-to-go-fast-e30fa9f81334>`_
+* `《深入理解 Python 异步编程》 <https://github.com/denglj/aiotutorial>`_
+* `《gevent-asynchronous-io-made-easy》 <http://mauveweb.co.uk/posts/2014/07/gevent-asynchronous-io-made-easy.html>`_
+* `《python性能优化》 <http://www.cnblogs.com/xybaby/p/6510941.html>`_
+* `《性能优化指南：性能优化的一般性原则与方法》 <http://www.cnblogs.com/xybaby/p/9055734.html>`_
+* `《程序员必知的Python陷阱与缺陷列表》 <http://www.cnblogs.com/xybaby/p/7183854.html>`_
+* `《知乎是怎么运行 tornado web 服务的》 <https://zhuanlan.zhihu.com/p/31635068>`_ 知乎使用 gunicorn gevent 部署
+
+
+嗯，一开始就开启pep8和pylint检测能显著提升代码质量（各种错误警告逼着你写出规范的代码）。咱写不了诗一样的代码，也不能写shǐ 一样的代码，维护一个ugly的代码仓库能有效减少你的寿命。可能很多东西对老鸟来说都是显而易见的，不过菜鸟和高级菜鸟们还是需要多多练习积累经验。慢慢摸索吧骚年。。。。。。如果能主动读一读《代码大全》《编程匠艺》《clean code》《重构》之类的书更好(或者flask等优秀的开源项目代码)，别人会更乐意和你一起合作编程，不然你总会心想『天呐，千万别让我改那个家伙的代码，我宁愿离职！！！』
+
+另外想说的就是，python入门容易，很多人浅尝辄止，但是相对容易出错，想写出高质量的代码反而对人的素养要求更高。另外如果是新手推荐多看看优秀的开源项目代码，能学到很多。像我等平凡之辈自己瞎捯饬也捯饬不出来啥，倒不如多学学人家高手是怎么写的，实际上对于大部分公司的业务代码，不需要什么奇淫技巧，反倒是把代码写得直白易懂易维护最重要。
+对于比较灵活的动态语言，一定要定义好规范和使用静态检查，防止某些人瞎搞导致代码仓库难以维护。
+
+
+难以维护的Python代码
+--------------------------------------
+
+::
+
+    # python 没有 docstring 维护基本就靠命名了，对于复杂参数的类型没有注释看起来心累
+    def isRankingBetter(self, customer,topranking):
+        testranking = getRanking(customer)
+        return testranking > topranking
+
+    // java
+    public boolean isRankingBetter(Customer customer, int topranking) {
+        int testranking = getRanking(customer);
+        return testranking > topranking;
+    }
+
+上面是一段java和python的对比，用来说明为什么python难以维护。java版本一眼就能看出来传入参数的类型和返回值，但是遗憾的是python看不出来，在python中基本只有通过docstring你才能知道传入参数的类型。当项目大了以后，维护一份没有文档和注释的python项目基本就是灾难。笔者曾很喜欢python语言，认为python是“伪代码”语，表达能力强，但是有了维护python旧代码的经验后，我开始怀疑python是不是适合构建大型项目(python写多了以后反而越来越不喜欢动态语言)。
+
+当然很多知名应用是python构建的，我觉得老外们软件工程做得还是不错的，把控好代码质量和单元测试（比如Quora创始人曾经解释过他们为什么选择了python,他们不喜欢java的冗长繁琐，C#被微软束缚，facebook因为历史遗留问题使用php并不意味着php是个好选择,Quora最后选择python并通过严格的单元测试控制质量）。但是我经历的一些使用python的项目工程方面却比较糟糕，代码维护起来非常吃力，开始让我对python产生严重怀疑。
+
+java虽然写起来繁琐，但是不容易出错，动态语言写起来爽，但是维护和重构起来吃力，并且容易出错(写稍微大型的项目时要充分认识到这个问题)。我个人感觉就是使用动态语言要严格把控代码质量和文档，强制用pylint对代码静态检测，否则项目大了难以维护，python或许更适合有代码洁癖的人写，比较严肃的大型工程还是推荐java。踩过这些坑之后，希望你以后写python工程的时候注重代码的docstring，易读性，接口易用性，正确性等，不然写着爽后来也是要付出很大的维护代价的，实现功能仅仅是代码项目中的一小环。
+
+重视细节
+--------------------------------------
+
+版式与布局
+--------------------------------------
+
+良好的代码排版可以让人理解代码更容易，格式化的基本原理是用直观的布局显示程序的逻辑结构。一点经验:
+
+- 尽量遵守pep8，除了行长度可以适当放宽，比如django使用120列，我个人比较推崇120列，80列的时候经常超限制，比较浪费心思分行。短行在 web 显示，分屏，diff，code revew或者打印出来的时候都非常容易查看，所以不要写特别长的行。
+- 合理使用"换行"使代码更易理解，同时更美观
+- 合理使用"空行"和"括号"对代码块逻辑进行分隔，使层次清晰。尤其是比较长的代码逻辑，应该使用空行合理分割，相关逻辑的代码放到一起。即使是重构有利于搞清楚代码块的逻辑。
+
+::
+
+    # 分行之前，我见过最长的得俩屏幕连起来才能看完
+    daily_report_data = db.session.query(Data.event_date, func.sum(Data.revenue).label('revenue'), func.sum(Data.payout).label('payout')).filter(Data.tag != Data.TagEnum.arbitrage).filter(Data.event_date < self._next_month_date).filter(Data.event_date >= self._this_month_date).filter(Data.finance_type == Data.TypeEnum.normal).group_by(Data.event_date).all()
+
+    # 分行之后
+    daily_report_data = db.session.query(
+        Data.event_date,
+        func.sum(Data.revenue).label('revenue'),
+        func.sum(Data.payout).label('payout')
+    ).filter(
+        Data.tag != Data.TagEnum.arbitrage
+    ).filter(
+        Data.event_date < self._next_month_date
+    ).filter(
+        Data.event_date >= self._this_month_date
+    ).filter(
+        Data.finance_type == Data.TypeEnum.normal
+    ).group_by(
+        Data.event_date
+    ).all()
+
+    # 不好的分行
+    employee_hours = (schedule.earliest_hour for employee in
+                      self.public_employees for schedule in
+                      employee.schedules)
+    return min(h for h in employee_hours if h is not None)
+
+    # 更具有可读性的分行，分行方式巧妙影响着代码可读性
+    employee_hours = (
+        schedule.earliest_hour
+        for employee in self.public_employees
+        for schedule in employee.schedules
+    )
+    return min(
+        hour
+        for hour in employee_hours
+        if hour is not None
+    )
+
+
+你看看大概各需要几秒才能分别理解上边的代码，分行之后能在三秒之内大致理解代码是干啥的，但是太长行你光移动编辑器指针就要花几秒。所以有时候排版还是很重要的(想象一下每天盯着写成一坨和排版优美的代码分别是什么感受)，为了快速理解代码你要用上各种手段，尽量让代码更直观。当然有时候你拿不定注意怎么样选择的时候，就以一种最容易理解的方式写，下边是笔者常用的一些分行方式，有利于写出遵守pep8的代码:
+
+::
+
+    long_list_list_defition = [
+        'a_long_variable_name',
+        'b_long_variable_name',
+        'c_long_variable_name',
+    ]   # 这样定义的好处就是你可以非常方便的增添元素而不用修改定义结构
+
+    from some_module import (
+        a_long_variable_name, b_long_variable_name, c_long_variable_name,
+        d_long_variable_name
+    )
+
+    if a_long_variable_name and b_long_variable_name and c_variable_name \
+            and d_variable:
+        # 我更倾向于用括号而不是反斜线来分行
+        pass
+
+
+    if (a_long_variable_name and b_long_variable_name
+            and c_long_variable_name and d_long_variable_name):
+
+        pass
+
+
+    a_long_list_comprehension = [person.name
+                                 for person in db.session.query(Person.name)]
+
+
+    a_long_dict_comprehension = {
+        person.id: person.name
+        for person in db.session.query(Person.name, Person.id)
+    }
+
+
+    employee_id_list = [
+        ins.id for ins in Employee.get_role_team_members(
+            role_int, team_int, ['id']
+        )
+    ]
+
+
+    def long_variable_function_name_and_function_params(a_long_variable_name,
+                                                        b_long_variable_name,
+                                                        c_long_variable_name,
+                                                        d_long_variable_name):
+        pass
+
+
+
+    def long_variable_function_name_and_function_params(
+        a_long_variable_name,
+        b_long_variable_name,
+        c_long_variable_name,
+        d_long_variable_name
+    ):
+        pass
+
+
+    return {
+        'code': ErrorCode.OPERATOR_FAILED_NEED_TOKEN,
+        'msg': ErrorCode.OPERATOR_FAILED_NEED_TOKEN_MSG,
+        'data': {}
+    }, status_codes.unauthorized
+
+
+    new_employee = Employee.get_by_id(new_employee_id)
+    (
+        changed_advertiser_ids,
+        changed_account_ids
+    ) = assign_employee_advertiser_and_account(employee, new_employee)
+
+
+    result = a_very_very_very_very_very_very_very_very_long_function_name(
+        a_long_variable_name, b_long_variable_name,
+        c_long_variable_name, d_long_variable_name
+    )
+
+
+命名
+--------------------------------------
+
+首先你要遵守pep8的规定，使用惯用法来命名。或者根据你们公司的python编码规范（如果你们公司有的话）
+
+- joined_lower for functions, methods, attributes
+- ALL_CAPS for constants
+- StudlyCaps for classes
+
+另外注意动态语言因为没有类型声明，所以在阅读源代码的时候，如果名称起的不好，很难推测出代码中间变量的数据结构，给阅读代码带来障碍(用同事的话说就是，python维护基本就靠命名了，《代码大全》等书甚至用了数章来说明命名的艺术)。比如一个字典列表，或者嵌套字典等，笔者维护过python代码，深感其中坑太多。我个人的经验就是适度在命名中加入一些类型提示，比如使用nums, cnts等作为后缀很容易知道是数值类型，数据库类都会用Model作为后缀，复数单词或者some_list等很容易知道是序列，some_mapper或者some_dict, some_set等基本从命名就知道什么数据类型了。当然这只是我的经验，有些人会反对这种命名方式，老实说如果代码写得是自解释的，可以不用这么来，但是我个人感觉这种方式虽然冗余，但是确实给我维护和阅读代码带来了便利。
+
+python3中加入了type hint特性，所以我觉得类型声明对于维护代码来说还是非常便利的。但是注意，动态语言有鸭子类型的概念，所以有时候名称中的类型提示并不代表就是该类型，很可能造成歧义，这也是很多人反对在python中使用类似匈牙利命名法的原因。老实说我不怎么使用鸭子类型(虽然天然支持泛型)，我感觉鸭子类型是很多错误的来源(比如很多instanceof判断增加函数复杂度)，python3加上类型注解了，甚至mypy都加上类型检测了（python3中的注解只是为IDE工具提供便利，并没有真正的类型检查），说明类型提示对大型代码项目维护还是很重要的。我觉得对于软件工程重视不够的团队最好不要使用动态语言开发后台，写不好的话坑会很多，后期新人上手和维护成本很高，虽然python易上手，但想要写好工程代码，还是需要一定功底的。
+
+- 注意词性。比如过程用动宾结构，用返回值的描述命名函数，数据变量使用名词，布尔数据经常使用is等作为前缀，数字类型使用cnt等作为后缀。
+- 适当使用"匈牙利"命名法(能从命名推断类型)。比如一个变量明显是字典或者集合，加上后缀可能会更易理解，我个人是强烈建议通过前缀或者后缀增强名称的含义和类型（个人经验，有争议，不过我确实感觉这种代码更容易阅读理解，否则看一个变量看不出类型维护起来超级痛苦）
+- 含义精确，具体胜于抽象。不要频繁使用诸如data，info，result，handle，process等概念太广泛的词汇给变量命名，不要使用偏门的简写，为了代码可读性冗余一些都可以(实际上对于现代语言长命名有一定好处，能减少冲突，容易 grep)。模棱两可的命名往往代表着某种警告（比如内聚不合理，不是单一职责等）。命名要能凸显出右侧表达式结果的类型和含义。
+- 给函数命名的一个好办法：首先考虑应该给这个函数写上一句怎样的注释，然后想办法将注释变成函数名称。（来自《重构》）
+- 术语表和命名规范。其实项目如果能建立术语表比较好，要不每个项目都用不同的词语命名比较混乱。命名会直接影响对代码语义的理解，还是要非常重视的。（比如不同项目用同一个名字表示不同含义，不同的名字又表示同一个含义，协作的时候非常容易混淆）
+- 见其名，知其意。比如枚举类用 Enum 后缀，Handler 类用 Handler 后缀，类似的还有 Model 等，看到类的命名就知道继承了什么类。虽然有些冗余，但是很精确，看代码也方便理解
+- 不要自己随便造一些缩写词。除非是有术语表或者业内常用的缩写，不要自己造缩写词语。清晰胜过简短，必要的缩写请加上注释(这也是看别人代码发现一堆摸不着头脑的缩写总结出来的)
+- 变量的名称不要和循环里的临时变量名冲突。比如之前定义了 "name = 'hehe'", 同一个函数后边的循环语句尽量用 "for _name in names:" 如果循环后使用 name 就导致之前定义的 name 被循环里的最后一个值覆盖。（一般习惯用下划线前缀定义一个临时使用的变量，比如 for 循环或者列表推导里的变量，防止命名冲突)
+- 使用下划线开头区分是内部函数还是提供给外部调用(私有还是共有方法)。代码重构的时候区分哪些是内部函数，哪些是外部接口会比较方便一些。（类似 golang 根据首字母的大小写区分是否 export 给外部使用）
+
+(注意这几个词语：『函数function』指有返回值的函数，『过程procedure』指无返回值的函数(比如关闭一个文件)，『方法method』指的是类中的函数，称之为方法))
+
+
+注释与docstring
+--------------------------------------
+
+.. code-block:: python
+
+    def function_with_types_in_docstring(param1, param2):
+    """Example function with types documented in the docstring.
+
+    `PEP 484`_ type annotations are supported. If attribute, parameter, and
+    return types are annotated according to `PEP 484`_, they do not need to be
+    included in the docstring:
+
+    Args:
+        param1 (int): The first parameter.
+        param2 (str): The second parameter.
+
+    Returns:
+        bool: The return value. True for success, False otherwise.
+
+    .. _PEP 484:
+        https://www.python.org/dev/peps/pep-0484/
+
+    """
+
+这个是google的docstring示例,是我比较推崇的一种格式。还是那个问题，动态语言没有类型声明，所以复杂函数要在docstring里写清楚传入参数和返回值的描述和类型。良好的docstring能让维护代码的人一眼就看明白这个函数是怎么使用的，即使内部很复杂，也尽量保持接口简单，容易使用。经常有人传出个嵌套字典（dict的key是主键，每个key对应的value里还有字典），这种相对复杂的数据结构还不注释，每次看这种函数都要打断点看返回结构。这种就是典型的接口易用性差，只在意实现功能，完全不管别人使用，合作起来比较心累。
+
+- Docstrings = How to use code。代码约定
+- Comments = Why & how code works
+
+Docstring应该包括什么?接口易用性
+
+- 尽量让 api 或者函数的调用者看一眼 docstrig 就能知道它做了什么，传入和传出了什么（参数意义和格式），而不是非得深入代码的每个细节才能使用它，提升代码易用性。有些家伙提倡代码即文档，但其实很多代码实现比较狗屎，我不想看完一坨狗屎而是直接看 docstring 就知道怎么用。
+- docstring 分为文件(module)的、类的、函数的 docstring。文件的用来说明模块、脚本等用来做什么的；class 和 function 的用来描述其作用。
+- 意图(目的)。解释为什么需要它？有些对你来说很明显的东西对其他人来说不一定很明显。最好能用一句话描述意图和功能，简单明了。笔者在接手项目看代码的时候，很多时候知道代码做了啥，但是却不知道为啥需要以及在哪些地方会需要这些代码？
+- 描述参数，返回值和会抛出的异常。我举个简单的例子， ``def f(date): pass`` ，仅仅看date这个参数你不知道传入str还是datetime.date，如果传入字符串又有很多格式的字符串，需要哪种格式？所以这个时候一个简单的描述 ``date (str): 'YYYY-MM-DD'`` 就能让使用函数的人一下子明白了。当然如果有单元测试实际上测试代码也是很好的文档，我们通过单元测试就知道怎么传值。另外使用了 ``**kwargs`` 如果都不说明就太不厚道了。对于传入的复杂的数据类型，最好注释下，否则看代码会非常蒙逼
+- 使用注意事项。复杂的使用可以有demo示例说明。
+- 需求文档，使用的api或者github, stackoverflow等链接。比如有个很trick的实现是你查阅 stackoverflow解决的，可以附上地址帮助阅读代码的人找到出处。对如复杂的需求实现，附上需求文档也会帮助他人理解。使用了第三方或者自己造的api，附上地址可以让新人快速上手了解。这些都是一些小细节，但是却可以给自己和维护代码的人带来巨大的便利。
+- 大家都很懒，但是还是尽可能用极其简洁明了的话给所有的模块、类和函数来几句描述（为什么需要这个模块、类、函数？这个模块、类会在在哪里被使用？它完成了什么功能）？如果能很简单描述出来，说明代码功能明确，写得至少不算烂^_^。无法简单描述的话说明代码可能需要拆分。另外涉及到业务的代码一般还需要链接一下业务文档帮助后人理解和上手。
+
+注释分5类（来自《代码大全》），但是仅『总结性注释』和『意图注释』可以接受
+
+- 代码的重复:用不同的词语重申代码的内容
+- 代码的解释: 解释复杂的有效的和灵敏的代码，通常有用但是尽可能修改代码使得代码本身更清晰
+- 代码中标记： TODO 标记等，经验表明，往往写了 TODO 后来就一直成了 TODO，所以最好提交代码前把要做的 TODO 做完，TODO 仅仅作为一次代码合并之前的提示。TODO 注释记得加上姓名，日期，联系方式和提示，方便 grep。
+- 代码中的总结：简化代码为一句或两句话，这种注释比重复代码更有价值，能帮助人快速理解代码
+- 代码意图的描述：解释代码的目的。意图注释在问题一级上，而不是在答案一级，是一句利用答案的总结描述。『理解最初的编程意图是最难的问题』
+
+注释怎么写?
+
+- 注释的目的在于快速帮助阅读代码的人了解代码功能和意图，使用方式等，不是为了注释而注释，让你看一长坨无任何文档注释风格又不好的代码是一件相当痛苦的事情，尤其是动态语言这种还看不出类型的。（所以有人说动态语言不适合构建大型项目）
+- 当然，好代码 > 差代码+好注释，好的注释是很有价值的，坏注释不仅浪费时间还可能有害，自解释的代码最好。好的注释不是重复代码或解释它，而是使代码更清楚，注释在高于代码的抽象水平上解释代码要做什么事。
+- 适当注释，仔细衡量，不要隐晦也不要多余。
+- 及时更新。
+- 注释代码中一些tricky的技巧或者特殊的业务逻辑，否则会让读代码的人摸不着头脑。
+- 如果附上jira、bug、需求等的地址能够帮助理解代码，可以适当加上。
+- 如果代码命名良好，结构合理，一般来说是不需要什么注释的。但是用一句话解释下意图和功能也是极好的，因为很多时候仅仅是想知道代码怎么用，读一句注释要比分析几十行代码快得多。
+- 根据《代码大全》上注释的分类，仅『意图注释』和『总结注释』两类注释是可以接受的。
+
+很多东西都需要自己斟酌，不要矫枉过正，比如说需要注释你就写一堆没必要的冗余的注释，说遵守pep8尽量不超过80列你连url都要拆成两行，我。。。。。。如果有些规范相冲突，你就以代码的可读性为标准，所有标准都是为了良好的代码设计的。我最怕和随意的程序员一起干活，随意就是写个函数print下就觉得正确了，没有docstring和注释，写的接口让别人难以使用。
+
+公司项目毕竟不是自己过家家，我现在就是自己的小项目也会注重规范（自己维护起来也方便，不要相信你的记忆力）。很多用python的小公司就是很不规范，维护起来真心累。也希望所有看到这里的python学习者可以把规范重视起来(很多知名开源项目文档都相当不错)，这也是一个职业程序员应该具备的素养。毕竟大部分人不是造轮子的人，能把业务逻辑实现地简单优雅易维护也是一种能力。
+
+* `《The Art of Readable Code》 <http://ningning.today/2017/07/22/%E8%BD%AF%E4%BB%B6%E5%B7%A5%E7%A8%8B/the-art-of-readable-code/>`_
+
+异常处理
+--------------------------------------
+一般在我们的代码中会出现三种错误类型：
+
+- 语法错误(Syntax Error): 比如手残打错了关键字等，可以通过编译器或者lint工具检查出来。动态语言要用好静态检测工具，防止代码上线了才发现直接跪了，修改成本高。（动态语言一大劣势）
+- 逻辑错误(Logic Error): 逻辑错误一般是由于程序员的粗心或者需求理解不对导致的(比如该用+号用了-号)，也是一般bug产生的原因，可以通过单元测试等方式避免。
+- 运行时错误(Runtime Error): 比如权限问题，文件不存在，网络请求失败等IO操作经常会抛出异常，这种错误需要程序员有意识进行处理，而不能假设操作一定就是成功的，尤其是涉及网络 IO 的地方。
+
+之前没怎么写过工程代码的小盆友可能一开始会忽视对各种异常的处理，这里需要提醒的就是，工程代码如果想写得健壮就需要对程序中可能会出错或者抛出异常的地方进行异常捕获，捕获之后进行处理或者上抛给调用者(raise)。
+提倡一定的防御式编程，减少程序因为异常导致的崩溃，主要是通过文档或者源码了解使用的代码、第三方库等会抛出哪些异常，应该如何处理。
+
+
+* `《google docstring示例》 <http://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html>`_
+
+* `《注重细节:代码排版，命名与注释》 <http://ningning.today/2017/01/22/python/python-coding-details/>`_
+
+web安全
+--------------------------------------
+- 防范常见的xss，csrf，sql注入等漏洞，不要信任来自外部的任何输入。对于外部接收的参数都要过滤，比如表单，对外的 api 等。
+
+  - sql 注入：恶意 sql 参数被拼接到 sql 语句中。解决：不要直接拼接 sql；使用 orm；使用安全占位符；参数校验等
+  - 越权：未对当前用户身份严格校验，用户操作超出权限范围。解决：最小授权原则；校验登录态；身份校验从登录态中获取，禁止在
+    参数中传递；严格校验、限制用户操作范围和权限
+  - 敏感信息：对手机号、qq、身份证等应该脱敏展示(脱敏 sdk)；对合作接口进行白名单和鉴权校验
+  - 支付安全：校验商品金额、数量；禁止并发获取；对于运营配置类的数据（比如获奖概率、金额）等严格校验
+  - SSRF(Server-Side Request Forgery):服务端请求伪造漏洞，攻击者传入特殊构造的 URL 伪造服务端对内网发起恶意请求。校验外
+    部传入的地址或者url的合法性。
+  - 验证码：需要对验证码的请求次数、频率、过期时间进行验证
+  - CSRF: 盗用用户身份发送恶意请求。通过验证 HTTP Referer, 使用 csrf_token 随机数（推荐) 或开启浏览器 Cookie SameSite等
+  - XSS: 用户提交为没有转义导致浏览器执行恶意代码。特殊字符进行转义
+  - 开源组件：最要对开源代码进行安全扫描，并且更新有漏洞的第三方库
+  - 云服务存储：应该针对用户上传文件的格式、大小、频率等进行限制，防止被盗用 cdn、存储容量。
+  - 秘钥泄露：禁止在前端/客户端/后台程序中硬编码秘钥，应当配置到专门的配置服务中并且做好权限控制，上传文件等应该使用云服
+    务的临时秘钥由后台下发进行上传
+
+- 优先使用 orm 框架而不是拼接 sql 字符串可以避免很多 sql 注入之类的问题，利用框架自带的安全机制杜绝一些网络安全问题
+- 对内的函数无需每一层都加上参数过滤（基于约定或者规范编程，没有遵守约定抛出的异常由调用者负责处理）。
+- 有一个例外就是数据库查询的参数，最好经过一次参数校验，防止不合理参数造成慢查询等问题(比如参数传递一个非常大的查询分页导致慢查询）
+- 使用断言保护代码，直接拒绝不合理参数。注意隐私参数需要加密保存
+- 注意服务配置不要泄露（比如密码传到了 github），注意服务一定要认证，不要裸奔。
+- 接口防刷，做好频率控制。可以在网关层面/业务层等做频率控制。如果是需要根据用户状态来限制频率可以借助 redis 计数来做
+- 传输加密(https); 敏感数据加密保存，脱敏展示。
+
+小白的踩坑记录
+=====================================================================
+
+文档化
+--------------------------------------
+团队项目开发前的统一三要素：统一需求/开发文档，统一代码规范，统一环境（编译/测试/发布）。
+很多程序员是懒得写文档的，仿佛牛逼的程序员不需要写。但是看人家真正牛逼的开源项目比如flask和tornado等，无论是代码还是文档都做得相当棒。对于一些项目，有些东西如部署步骤；常用命令等还是可以记录下来的，可以使用wiki或者readthedoc，gitbooks等文档工具记录一下，方便新人上手。如果不知道记录啥，就把你发现不止一次会用到的东西文档化。个人认为需求文档也应该有历史记录，方便接手的人可以快速了解业务和需求变更。数据库字段的含义也应该及时记录和更新。
+
+Readme Driven Development:
+
+- Explain the system's pupose. (What is the business reason ? Why are we here?)
+- Describe the scope. (What defines what the system does and doesn't do?)
+- Summarise what it does. (What does it actually do? What is it for?)
+
+只有少数很复杂的系统需要详细的文档，架构图、UML、数据模型、处理流程、业务逻辑等需要整理成文档。Write the minimum viable system documentation.
+
+
+代码分支与代码管理
+--------------------------------------
+做好代码分之管理，分清楚开发、特性、bugfix等代码分枝，不要在同一个分之上一下修改太多功能，导致修复问题不好定位。比如经常和同事做一个需求，结果一个人把几个需求堆到一个分之改了，把不该上的功能也给上了，这种小细节还是需要注意的，否则就会给测试、上线等带来严重麻烦。命名分之的时候注意使用有意义的命名，比如附带上task的号码，jira号等等，把分之和你要解决的问题关联起来。
+
+
+代码提交
+--------------------------------------
+对于一个严谨的开发团队来说，即使是 git 提交信息的规范最好也是需要规定的。比如提交是 feature，bugfix，还是修改配置等。
+可以使用工具来规范统一的提交信息。
+
+* `《优雅的提交你的 Git Commit Message》 <https://zhuanlan.zhihu.com/p/34223150>`_
+
+
+注释
+--------------------------------------
+有经验的人都知道看别人的代码是一件很痛苦的事情，尤其是没有任何注释的代码。代码除了完成需求外，最重要的就是维护和协作，除非你觉得你做的项目活不过仨月(或你自己玩玩的项目随便你怎么艹)，否则就一定要重视代码质量，防止代码腐化(破窗)以至难以协作和维护。有时候比写注释更难的是知道何时写，写什么注释？python里有规范的docstring用来给类和函数进行注释，除了说明功能外，关于github,stackoverflow链接、复杂的传入传出参数（比如嵌套字典作为参数这种你都不注释就很不合适了)，类型说明、需求文档和bug的jira地址等都可以注释。凡是你回头看代码一眼看不出来干啥的，都应该有适当的注释，方便自己也方便别人。
+
+当然，最重要的是代码清晰易读，好的命名和编写风格的代码往往是自解释的，看代码大致就可以看出功能。建议就是给所有的模块、类和函数都加上注释，除非一眼能看出来这个东西干啥，否则都应该简洁注释下，让别人不用一行行看你的代码就大概知道你这个东西是干啥的。最后注意的就是一旦函数更改及时更新注释。qiniu的sdk写得就不错，可以去github看看。总之，"Explicit is better than implicit.", 代码里不要有隐晦的东西，一时偷懒将来可能会付出几倍的维护代价，请对将来的自己和他人负责。
+
+* `《python docstring》 <http://bwanamarko.alwaysdata.net/napoleon/format_exception.html>`_
+
+Code Review(代码审查)
+--------------------------------------
+笔者认为code review是一件非常重要的事情，可以有效防止代码腐化，同时方便同事了解业务(可以说编码规范、静态分析、代码审查和单元测试是保证代码质量的几个重要工具，没有使用这几个工具之一将来代码都可能难以维护)。可以在公司搭建Phabricator（facebook在用）gitlab 类似工具进行代码review。可惜小公司流程不严格，codereview总是坚持不下去，要不就是被同事吐槽总是给他挑刺。实际上如果是新手能够从code review当中快速学到很多东西，比如编程惯用法，摆脱不良编码习惯，不良设计和难以维护的代码等。review的时候对事不对人，代码如果有明显缺陷快速记录个TODO等待review后修正，以一种开放和学习的心态看待review，慢慢整个团队的实力和代码质量就会提高。review就是个互相学习进步的过程，正规的团队都应该严格遵守，而不只是走走流程。
+(没有 review 过的代码可能很快就会成为一坨shit)
+
+- 一次检查代码在 200-400 行比较适合，不要一次提交太多代码，应当合理分配每一次提交的代码量和功能点
+- 建立 review 检查表，防止不合理、过于复杂、明显缺陷、可读性差的代码。眼睛足够多，bug 无处藏。越早修复缺陷，成本越低。
+- 建立提交模板，每个提交是需求、bugfix还是啥一目了然，同时贴上需求、jira 等地址，方便追溯。
+- 对事不对人，review 和被 review 的人都要以一种开放和学习的良好心态看待 review，共同进步。新手或者新加入项目的人不要过度吹毛求疵(会有很大心理负担和反感情绪)，共事久了步调和代码风格慢慢趋同了。
+- 及时复查，防止一次太多的commit。使用 gitlab 等工具可以在代码 diff 的地方评论，这样方便对照别人的评论迅速修改代码里的问题
+- commit 信息关联。提交的代码解决了什么问题，如果是需求需要在 gitlab 附上需求文档地址，如果是 bug fix，附上对应的 sentry 或者 jira 链接，让每个commit 有意义并且可以追溯。在代码片段里加上文档、jira 地址等对于代码护维也很重要。
+- 检查内容：
+    - 逻辑是否正确，代码行为是否符合预期
+    - 代码规范（风格和命名等，动态语言没类型声明，很依赖良好的命名推断变量含义和类型）。同志们学好英语，命名真不是个简单的问题(尤其是各种中式英文和缩写)。
+    - 是否有单测
+    - 是否健壮（安全性、性能、异常捕获）
+    - 必要的文档和注释（意图，外部链接需要注上）
+    - 可读性和可维护性(是否有过于复杂的逻辑)
+    - commit 信息（commit信息是否准确，比如附上 jira 或者需求文档地址，bug 地址等，你的代码变动都应该有迹可循, 目前团队加上了提交模板，对于 bug fix、新特性、重构等都需要填写对应的模板信息 https://www.conventionalcommits.org/zh/v1.0.0-beta.2/）
+    - 代码洁癖要适度，如果代码遵守了规范并能正确解决问题，就不要吹毛求疵。review 过程中出现分歧是很常见的，每个人都有自己的编码习惯。如果出现难以解决的分歧，可以列出优劣表格，对各自的方式有一个量化的分析（比如从实现难度、可读性、可扩展性、可维护性等方面打分）。如果无伤大雅，不必吹毛求疵。
+
+* `《https://www.kevinlondon.com/2015/05/05/code-review-best-practices.html》 <https://www.kevinlondon.com/2015/05/05/code-review-best-practices.html>`_
+* `《如何用人类的方式进行 Code Review》 <https://zhuanlan.zhihu.com/p/31581735>`_
+* `《硅谷 Code Review 流程图》 <https://www.infoq.cn/article/tQrY-B15aRoBdzyr2aOh?utm_source=weibo&utm_medium=infoq&utm_campaign=newinfoq&utm_content=0515Airbnb>`_
+
+
+日志与异常记录
+--------------------------------------
+一定要有良好的日志记录习惯。良好的日志对于记录问题至关重要。python有方便的日志模块帮助我们记录，日志输出的代价是比较小的，python的日志模块尽量做到对函数功能没有性能影响，可以在线上和开发环境设置不同的log等级，方便开发调试。注意别再日志语句里引入了bug或异常。有时候需要判断什么时候需要日志，记录哪些东西方便我们排查问题，分析数据。
+对于异常，一定『不要吞掉任何异常』，常有新手上来就try/except，也不区分非退出异常，也没有日志记录(坑啊......)。请先阅读python文档的异常机制，可以使用Sentry等工具记录异常。同时发生异常时候的时间，调用点，栈调用信息，locals()变量等要注意记录，给排查错误带来便利。有些错误的复现是比较困难的，这时候日志和异常的作用就凸显出来了。
+
+* `《每个 Python 程序员都要知道的日志实践》 <http://mp.weixin.qq.com/s?__biz=MzA4MjEyNTA5Mw==&mid=2652564362&idx=1&sn=f33910af004f276bbef7ae52e0757bcb&chksm=8464c3c0b3134ad617bcffd865894344367fdd2995a0d5ff9c4da30e0c158b3d02b3d616f615&mpshare=1&scene=23&srcid=1124K7Ht1FP2A1Fnvi3HTBE5#rd>`_
+* `《日志的艺术（The art of logging）》 <http://www.cnblogs.com/xybaby/p/7954610.html>`_
+
+代码自查
+--------------------------------------
+- 该加的日志是否加上了？日志等级是否正确？代码里的各项配置是否正确
+- 代码规范是否符合标准？是否有用工具格式化
+- 加锁的地方有没有对应的释放锁？打开的资源是否关闭了？会不会导致资源泄露？
+
+服务上线
+--------------------------------------
+
+- 如果上线流程复杂，应该建立上线流程表和上线自查表，防止出问题(当然如果 devops 做得好很多问题可以避免，最容易出错的是人为因素)
+- 一般会先灰度上线(部署少量容器/机器)，如果发现上线的代码有问题，需要立即回滚到上一个可用的部署版本(不要立马写代码尝试修复)
+- 上线之后应当关注异常上报、错误上报、服务日志、机器资源性能等监控系统指标，防止出现问题，确认无误以后可以全量发布
+
+重构与维护
+--------------------------------------
+不知道你有没有这种感觉，看那些知名代码库flask等，人家写的代码水平是比较高的，但是自己的项目确实一团糟。我觉得代码要经常去重构，想着怎么写更优雅，更容易理解和维护。我个人感觉好的代码就是不断修改出来的，实现一个需求的时候，适当想想怎么设计更加优雅易维护，编写代码的时候注意想着可读性。完成需求了如果代码可以设计更优雅，可以尝试重构下，慢慢代码水平就上来了。如果总是直来直去堆砌需求代码，业务逻辑写再多依然不会有进步(我个人感觉写python有时候反而会降低编程能力)。牛人和计算机高手很多，能写出良好的工程代码的人却很少(试想一下让你维护一个『牛人』的『精巧』代码)。代码一次编写，却可能被无数次查看、修改和维护，在可读性和可维护性上的努力长远来看是值得的，编写代码只是整个软件项目中很小的一部分。写代码的时候最好也从维护者的角度思考一下。
+Code Quality: Simple, Well-tested, Bug free, Clear, Refactored, Documented, Extensible, Fast.
+
+- 面对代码屎山。不要妄想一上来就大规模重构，每次需求需要修改代码的时候，保持相关的代码块更加整洁就可以。破窗理论
+- 避免彻头彻尾重写，增量式重写代码。开发新代码比重写旧的代码让人更加舒服。天生乐观会让我们低估了复制旧功能要付出的精力和时间。
+- 重构：在不改变代码功能的情况下优化代码设计。修改功能和优化代码不要同时做。优化应该以可读性为标准。
+- 接手老项目的时候不要盲目大规模重构，但要保证代码仓库越来越『干净』，不要破罐子破摔。
+- 可以通过设计(需求)归档、代码规范、静态检测工具、单元测试、必要的注释和文档、code review(代码复审)、重构、服务化等手段增加项目的可维护性。
+- 动态语言的重构工具支持不够完善，重构的时候要注意别改坏了逻辑，要十分谨慎。单元测试可以保证原有逻辑没有被破坏。
+
+* `《重构 - 读书笔记(PYTHON示例)》 <http://www.wklken.me/posts/2017/06/17/refactoring-07.html>`_  来自 wklken's blog
+* `《详解圈复杂度》 <http://kaelzhang81.github.io/2017/06/18/%E8%AF%A6%E8%A7%A3%E5%9C%88%E5%A4%8D%E6%9D%82%E5%BA%A6/>`_
+
+Python 做业务后端的优缺点分析
+--------------------------------------
+笔者从实习开始做 Python 后端，经历过一些新项目、老项目，以及和很多 python 工程师(豆瓣、知乎)协作过，大概总结下 python 做 web 业务后端的优缺点吧，尽量客观，
+总得来说就是用动态语言写项目在追求高生产力的同时要严格把控工程质量。先说优点:
+
+- 多面手。python 可以写方便地写爬虫、网站、数据分析、运维脚本等，都有比较成熟的框架，目前比较火，TIOBE 里脚本语言排第一。
+- 轮子多。python 发布时间比 java 还早，大量现成的轮子可以用。我觉得即使是 web 之王 php 做网站开发体验和效率上来说并不比 python 强。这可能也是 Instagram 和  Quora 等选择 Python 的原因。
+- 表达能力强，语法糖多，生产力高，适合快速构建原型。笔者喜欢动态语言的一个原因就是表达能力强，用更少代码完成功能。(代码行数越少意味着高生产力和低出错率，当然不一定对可读性有帮助)
+
+缺陷:
+
+- 解释性语言执行效率低，大部分时间用在 IO 密集场景，比如 web 后端。不过大部分公司不用担心性能问题，除非真到了一定用户量级。
+- 开发工具支持不够完善，不如 java 有那么完善的 IDE，Pycharm 还不错，但是依然解决不了滥用动态特性导致的补全和跳转等问题
+- 易编写，但难以重构和维护，易出错，工程上不够友好，较难写出 clean code(笔者基本上会强制上 pylint and autopep8, 模仿 gofmt 吧)。基本上重构只能依据字符串匹配，老实说每次重构有稍微大一些的改动都会有点担心
+- 滥用动态特性导致代码不好维护。这是个双刃剑，但是对工程来说还是不要滥用。有时候会利用一些动态特性使用黑魔法来快速完成需求，但是工程上来说这是很不利于维护的。这是很多人抨击动态语言不适合大型项目的原因，一般需要在编码规范里明确说明哪些能用，哪些不能用。
+- 没有类型声明，看不出一些复杂类型的数据结构（Python、php 都在不遗余力地加上 type hint），代码写糙了维护起来很累（看不出复杂变量的类型和结构，阅读代码吃力，我都是给复杂类型加上类型注释），命名和编码习惯很重要
+- 缺少一些最佳实践（技术、小白文章偏多，工程实践文章比较少），以很多 python 的中小公司在软件工程上管理不够，无规范、无文档、无注释、无单测、无持续集成的尿性，还是慎用动态语言瞎胡搞，后期维护成本会很高。
+- python2，3 不兼容，迁移有成本。我个人觉得 python 敢于抛弃当初的设计是值得赞赏的，但是很多企业并没有足够的资源来去迁移
+- 招人不好招，这两年 python 雷声大，雨点小，而且基本都是在 AI 领域。有经验的 python 后端远没有 Java 好招(往往很多人学得还是半吊子写代码很随意，不重视工程质量)，更多是创业公司、中小公司使用
+
+技术选型都是在权衡吧，因为灵活性与工程性、开发效率和运行效率很多时候无法兼得。
+
+
+高质量服务自查表
+------------------------------------
+分布式系统中会遇到各种失败问题，开发健壮的后端服务需要注意很多细节问题，依赖各种后端组件。下边是一份总结不错的自查表，可以对照看一下。
+
+* `《开发更高质量的服务》 <http://vearne.cc/archives/39164>`_
+
+
+开发习惯
+------------------------------------
+
+- 认识和熟悉所在团队中的成员（笔者一直做得不够好，这一条远比想象中重要，内向性格有时候会比较吃亏），良好的沟通和协调能力能帮助你更快完成(或者委托)任务。
+- 确保正确理解需求，确保熟悉所做的业务，正确理解业务能减少很多无用功(想象一下你写了很多代码结果因为理解失误需要全删掉的心情)；需求分析；适当设计。流程图或者文档有时候可以帮助理清楚业务。比如知乎有 rfc 机制，每次做一个稍微大点的需求都需要写设计文档。可以通过复述给产品人员听的方式确认双方是否理解一致。
+- 番茄工作法，劳逸结合(working smart rather than working hard)，一次只做一件事(do one thing and do it well)。长时间专注写代码是非常消耗精力的。确保编码期间足够专注。快速迭代。
+- 边写边测，增量式编程。虽没有使用 TDD 开发的习惯，但是对于稍复杂的逻辑就要写单测，以便及时发现错误，越早发现越容易修复(修复成本随时间指数增加)。我习惯用文件变动监控工具(when-changed fswatch等)检测文件变动，每次保存文件自动跑相关测试(比如 nose pytest 等都可以执行单个文件或类的测试,你可以快速验证当前代码是否有问题，及时修改或者重构)。TDD 的好处之一就是改善设计，自顶向下考虑，笔者有时候也会尝试用 TDD。
+- 注释先行，意图导向，表达明确，牢记可读性可维护性，可追溯（附上需求文档地址，方便维护者查看）。写一个模块、类或者函数之前先想好它的功能，按照功能命名，之后写简单的注释描述其意图和功能，通常不超过三句话，虽然大部分时间只有一句话(只做一件事) ，但是能快速让后来的维护者了解你的意图。别看人代码最头疼的就是看不出代码究竟是要干啥。
+- 文档驱动编程(Document Driven Development):比如写一个脚本的时候，应该在文件头部注明需求地址 url(保证代码功能、意图等是可追溯的)，写下实现方式和目的等。有时候对于很复杂的业务逻辑笔者会用自然语言描述步骤，之后再用代码实现。对于需要经常维护的代码，必要的文档是值得的。
+- 边开发，边重构，及时清理技术债。如果有代码写糙了（圈复杂度太高、可读性差、代码重复等坏味道），应该及时重构不好的代码，这时的重构成本是最小的。代码写得复杂到自己都快看不懂了是个危险的信号。
+- 善用工具。比如笔者使用的 vim 插件 python-mode 集成了 pylint、pep8、pyflakes、autopep8、isort 等工具，方便快速检测代码是否有语法错误和规范问题。每次保存文件后我都会在 vim 里执行一遍 pylint 和 pep8 检测，确保代码在规范上没问题。
+  (即便如此动态语言依旧很容易犯错，比如使用了未定义的属性，参数个数不一致等开发工具都不会报错，但是一上线就报了异常，所以动态语言编码还是需要很谨慎，同时通过良好的编码习惯、测试和 code review 来消除缺陷，有些同事说用动态语言
+  写大型项目会睡不好觉，不无道理。目前笔者所在的小团队就在 CI 上加了 flake8，pylint 检测，代码写糙了过不了，同时所有同事提交之前用 autopep8 格式化代码，用 isort 整理导入包顺序，避免了风格不统一的问题)
+- 重视规范。代码量上去以后没有规范就是噩梦，也是很多小公司代码不忍直视的原因。(无文档、无注释、无单测、风格混乱、难以维护)
+- 追根溯源，需求归档。在代码、提交信息、文档中记录需求文档地址、引用地址等。方便维护者能够根据代码提交寻找代码意图，尤其是几乎没有任何文档注释的代码。让人上来就看一段不知所云的代码无比痛苦。写代码有时候和写文章、论文差不多，可以在 docstring 里附上相关链接。commit 信息都应该足够重视，不要瞎写，要能体现代码提交意图（修复 bug、新 feature、代码优化等）
+- 交叉引用。一般我们会使用 goodle doc 之类的工具协作需求和代码设计文档，之后在相关代码的 docstring 里注释上产品和代码设计链接，方便维护者了解需求和代码设计。
+- 结对编程。结对编程和TDD是极限编程中大力提倡的，国内似乎没有多少公司在实践，一般帮助新人了解项目或者带实习生的时候，结对能帮助新人快速上手。有时候两个人一起边讨论边写代码要比写完后在 gitlab 一条条评论快很多。
+- 总结复盘。不论是大小 bug，每次犯错都应该在你的小本本记录一下，不要重复犯错。
+
+平常可以留心下周围优秀的同事都有哪些好习惯，我们可以学习并改善下自己的开发流程。
+
+- 12 Schedule Time to Lower Technical Debt
+- 11 Favor Hign Cohesion(low cyclomatic complexity)
+- 10 Favor Losse Coupling
+- 9 Program with Intention(Simple Design: Passes the tests; Revieals intention; No duplication; Fewest elements)
+- 8 Avoid Primitive Obsession(Imperative code is packed with accidental complexity)
+- 7 Prefer Clear Code over Clever Code
+- 6 Apply Zinsser's Principle on Writing(Simplicity;Clarity;Brevity;Humanity)
+- 5 Comment Why, not What
+- 4 Avoid Long Methods--Apply SLAP (long is not about length of code, but levels of abstraction)
+- 3 Give Good Meaningful Names (if we can't name it appropriately, it may be a sign we've not yet understood its true purpose)
+- 2 To Tactical Code Reviews
+- 1 Reduce State & State Mutation
+
+
+技术氛围建设
+------------------------------------
+对我个人而言，一个良好的团队想要留住人才，有竞争力的薪资，友好的同事关系，学习成长型团队是我比较看重的三点。国内感觉不好
+的一点就是不太重视技术，导致大部分技术从业者吃青春饭，希望以后能有所改善吧。
+
+- code review。通过结对编程或者code review可以快速让新人适应新的开发团队，统一开发风格，学习良好的编码习惯。
+- 技术分享。每周团队成员技术分享，打造学习型团队。最好围绕团队目前正在使用到的技术栈
+- one on one : 每周一对一小组 leader 谈话，及时沟通反馈技术或者业务上的问题。(北美很多公司有这种习惯，我个人觉得挺好的)
+- 技术复盘。如果有成员导致了重大 bug，可以一起开复盘会进行分析总结，如何避免再犯。
+
+开发流程(需求->分析->设计->实现-测试)
+-----------------------------------------------------
+总了一下开发步骤，当然如果需求紧急一下一些步骤可以省略，但是对于一些对质量要求特别高的场景，最好严格遵守开发步骤，防止
+上线之后出问题造成重大损失。
+
+- 需求评审。一般需求评审环节也需要技术人员参与评估，以此衡量开发排期，防止不合理需求出现(比如技术上短期不可行等技术风险, 明显不合理的需求也要给出自己的意见)
+- 设计文档。包含需求背景，技术调研，技术选型，关键流程图，架构图，数据表设计，接口功能格式设计，项目排期等。可以据此构造一个模板，每次填写相关项。不要上来就写代码，不仔细设计可能后以后留下隐患
+- 技术评审。根据项目大小和重要性由同事共同 review 技术方案，最好组内有技术老手负责把关。之前使用 google doc 来编写技术方案文档，大家可以一起评论和编辑。
+- 需求分解。功能需求，性能需求，容错容灾需求，监控需求等，评估开发量。
+- 编码实现，使用公司规定的 git 工作流和编码规范开发。推荐合理使用 git 分支，防止多个功能被一起带上线，或者难以回滚。
+- 自查代码。git commit 之前先自己审查一下代码，看看有没有小bug，不规范甚至有错误的地方。可以使用自己的一套自查表。
+- 服务自查。参考这个表格自查一下服务，是否有没有漏洞 `《开发更高质量的服务》 <http://vearne.cc/archives/39164>`_
+- 代码审查。至少一个同事负责代码 review，方便同事了解业务，修正不合理代码。
+- 单测+自测。项目最好有单测，接口自测。一般来说会有自己的开发环境来自测。再简单的功能也要自测，不要太自信，否则 bug 会教你做人
+- 功能提测（提测模板：包含需求背景，测试流程，期望结果，测试注意事项等），一般由测试工程师来完成。
+- 监控上报。服务或者业务有没有需要监控的，比如视频上传成功率等，功能完成之后需要加上相关的监控上报，指标报警等。
+- 压力测试。如果预估有相当大的访问压力(评估最大 qps，缓存该加的加上)，必须在上线之前使用压力测试工具压测，防止线上出问题，比如使用 locust/JMeter 来压测并生成结果。压测的时候监控服务和底层组件的压力。
+- bug 跟进和修复，复盘总结问题等。可以根据 bug 影响级别定级，高级别 bug 需要一起复盘总结，防止再次出问题。
+- 复盘总结。出了哪些 bug，踩的坑，可以改进的地方等可以总结成文档。
+
+
+Think more, type less. Aim for minimalism, fewer states, less mutability, and just enough code for the known, relevant parts of the problem.
+
+
+《The Zen of Python》 - Tim Peters
+
+::
+
+    Beautiful is better than ugly.
+    Explicit is better than implicit.
+    Simple is better than complex.
+    Complex is better than complicated.
+    Flat is better than nested.
+    Sparse is better than dense.
+    Readability counts.
+    Special cases aren't special enough to break the rules.
+    Although practicality beats purity.
+    Errors should never pass silently.
+    Unless explicitly silenced.
+    In the face of ambiguity, refuse the temptation to guess.
+    There should be one-- and preferably only one --obvious way to do it.
+    Although that way may not be obvious at first unless you're Dutch.
+    Now is better than never.
+    Although never is often better than *right* now.
+    If the implementation is hard to explain, it's a bad idea.
+    If the implementation is easy to explain, it may be a good idea.
+    Namespaces are one honking great idea -- let's do more of those!
diff --git a/codingtools/codingtools.rst b/codingtools/codingtools.rst
new file mode 100644
index 00000000..c6c9ad74
--- /dev/null
+++ b/codingtools/codingtools.rst
@@ -0,0 +1,637 @@
+.. _codingtools:
+
+开发和编程工具
+=====================================================================
+
+..
+
+  Do one thing, and do it well. - A principle of Unix philosopy
+
+工欲善其事，必先利其器 开(装)发(逼)工具
+--------------------------------------------------
+
+- Pycharm/IDEA/Goland(JB全家桶)。专业的python IDE，功能很强大，特别喜欢它的代码merge工具，不想被编辑器折腾死的推荐直接使用，五星级推荐。(除了内存占用大点)。如果你不喜欢折腾编辑器，请直接用 IDE，经常看见一些用裸编辑器写代码的，代码规范检测都没有。
+- Neovim/vim/Emacs/Vscode/Sublime/Atom: 跨平台的编辑器。vim 目前是笔者的最爱，你可以在慕课网和 B站搜索到我的视频教程
+- meld/vimdiff/kdiff3: 跨平台的文件对比和冲突合并工具。当然还有付费的 BeyondCompare，对笔者来说大部分场景 vimdiff 就很好用
+- devdocs.io: 文档查询工具，可以在线查询
+- EditorConfig: http://editorconfig.org/ 用来统一编辑器配置。如果成员用不同的操作系统和编辑器，建议使用。尤其是对于 python 这种使用缩进的语言
+
+一定要有个趁手的开发工具(它甚至比你女朋友都重要)，不管是IDE还是编辑器，你程序员生涯的小半辈子都在和它打交道(提升编辑效率的秘诀在于多用键盘快捷键，少用鼠标，以及可以高度定制、和扩展功能的编辑器)。甚至编程字体你都要谨慎选取，比如字体可以很好区分'1', 'l', 'I', '0', 'O', 'S', '5'等易混淆字符，给浏览代码带来便利。如果使用的是mac可以google下 "Mac OS X development environment setup"，有惊喜呦。最后注意你用编辑器的话一定要用 pylint，pep8 检测插件，否则不遵守规范可能会导致用 IDE 打开项目后一堆警告(别人会想问候你祖宗的)。
+更多 mac 工具可以参考：https://github.com/jaywcjlove/awesome-mac 。搜 awesome-python 或者 awesome-flask 等有很多类似项目。
+
+一些提升效率的建议：
+
+- 指法用对，练习盲打，提升敲击数字键的成功率
+- 熟悉你的开发工具，各种 IDE 或者编辑器快捷键。vim 等高度定制的编辑器允许你按照自己的习惯修改快捷键
+- 学习一门脚本语言（编写自动化脚本），编写你的命令行工具（一个可执行的 python 文件放到 bin 下就是一个命令行工具)
+- 多用键盘快捷键少用鼠标。修改系统键盘按键，比如 capslock 使用频率比较低，一般我改成了 ctrl 键，因为用开发工具频繁使用到 ctrl，改成 ctrl 之后效率提高不少，小指头也不疼啦。
+- 自动化（比如监听文件变动刷新浏览器、重启http服务等）
+- 用好终端和命令行工具（我建议后端/运维等用 mac 或者 linux 系统，因为 server 大多跑的 linux，熟悉命令行会给你调试和面试带来便利）
+
+* `《使用vim+tmux+zsh+autojump高效工作》 <http://ningning.today/2016/11/09/tools/vim-tmux-zsh-autojump/>`_
+* `《玩转 vim 与 Terminal (视频)》 <https://zhuanlan.zhihu.com/vim-video>`_
+* `《Mac 无鼠标编程指南（视频)》 <https://zhuanlan.zhihu.com/p/91031876>`_
+
+
+MacOS 开发效率工具
+--------------------------------------------------
+- Item2: 替代原生的终端。https://medium.com/@RyanDavidson/make-your-terminal-more-colourful-and-productive-with-iterm2-and-zsh-11b91607b98c
+- Alfred: mac 下一款功能强大的工具，不过我一般只用它快速打开软件(比如直接输入app名前缀快速切换不同app)。可以用 python 编写一些自己的 workflow 提高效率(https://github.com/deanishe/alfred-workflow)，比如把时间戳转成日期等。 https://github.com/derimagia/awesome-alfred-workflows
+- Dash: 强悍的文档查询工具。支持非常多编程语言和框架。windows 和 linux 可以用 Zeal。或者 https://devdocs.io/
+- CheatSheet: https://www.mediaatelier.com/CheatSheet/ 显示 mac 快捷键
+- mac-setup: https://github.com/sb2nov/mac-setup mac 下各种编程语言开发环境配置指引
+- Hammerspoon: 开源的mac窗口管理工具 https://github.com/Hammerspoon/hammerspoon
+- Magnet/chunkwm/Amethyst/yabai 窗口管理辅助工具，拖拽窗口到屏幕边缘可以自动半屏， 全屏或者四分之一屏幕，还可以设定快捷键完成分屏，现在已经离不开magnet 分屏了。
+- Tiles: 类似 magnet 窗口管理软件，免费！ https://freemacsoft.net/tiles/
+- Karabiner-Elements(mac): 改键工具 https://github.com/tekezo/Karabiner-Elements 也可以用来禁用内置键盘(mac自带修改capslock为 control)
+- Be Focused: mac 下番茄工作法工具，可以用来给任务计时，或者提醒该休息下了
+- Qbserve: https://qotoqot.com/qbserve/ 一个 mac 下的时间追踪软件，可以查看自己在不同软件耗时，改善时间和工作管理
+- xbar: 一款可以在 macos 菜单栏显示任意信息的 App，可以通过脚本扩展功能。有插件商店
+- vimac: 用 vim 的方式操作 mac，彻底抛弃鼠标提升效率！ https://github.com/dexterleng/vimac
+
+网址 https://gist.github.com/lornajane/3892c39098cf70baa9c7a1874cddf233  列举了使用纯键盘操作 mac 的建议和所用软件
+
+Mac微信工具
+--------------------------------------
+- 微信小助手：https://github.com/MustangYM/WeChatExtension-ForMac  支持微信多开、消息防撤回、微信皮肤等多种功能
+
+命令行工具
+--------------------------------------
+- tmux/tmuxp。比screen好用，可以用来分屏，托管进程等，服务器端必备神器，ubuntu下基本就不用使用terminator之类的分屏工具了。最近看youtube视频还发现有人在服务器上使用tmux和vim结对编程，两个人同时attach到一个session里，基情四射。
+- vim。本人比较喜欢的编辑器，平常写代码、博客、文档等使用频繁，配上各种插件编辑效率很高。http://vimawesome.com/ 可以到这个上面安装排名靠前的那些插件，能够大大提高编辑效率，部分替代IDE(本人装了六七十个插件，满足各种变态的编辑需求)。其他优秀的编辑器sublime，atom，vscode，emacs等根据个人喜好来吧，不过vim等终端友好的编辑器方便在服务器上直接写代码，和本地体验一样，缺点就是补全和跳转支持不完善，对新人不友好，也可以 Pycharm  和 vim插件配合。(在google搜索python awesome等可以在github上搜索到一些awesome项目，总结了该语言很多技术工具)。网上还有很多牛人开源了自己的 dotfiles，我们可以参考下别人的 vimrc 配置。
+- neovim: 新时代的 vim，我在这个配置(https://github.com/PegasusWang/vim-config)上自定义了自己的配置，使用起来性能和反应速度上远超原生的老古董 vim，目前笔者已经全面迁移到 neovim，用着很爽。感兴趣可以关注笔者知乎专栏，我录了一些针对初学者的教学视频。插件搜索：https://neovimcraft.com/
+- vim-bootstrap: 一个快速建立不同编程语言 vimrc 的创建工具 https://github.com/editor-bootstrap/vim-bootstrap
+- ranger: 用 vim 的方式管理文件，替代 mac 下难用的 finder。
+- oni: https://github.com/onivim/oni/ 构建在 neovim 上的 IDE。还有 VimR 等项目。
+- wemux: tmux 共享，https://github.com/zolrath/wemux
+- sshfs: 本地挂在服务器文件夹
+- tmate: https://tmate.io 终端共享工具，结对编程。很多现代化编辑器 vscode, atom 提供结对编程的插件。
+- asciinema: 终端会话记录工具。https://asciinema.org/ 可以用来录制终端演示操作，而不用录屏。
+- oh-my-zsh。替代原生的bash shell，提供了好多方便的特性和漂亮主题，支持插件比如 zsh autocomplete。linux/mac下vim+tmux+zsh简直是绝配，甚至可以直接在服务器上方便地撸代码，跟本地开发体验没区别。
+- brew(mac)。类似ubuntu下的apt-get，可以方便安转各种软件和工具。
+- autojump。方便在命令行里来回跳转目录。
+- tldr: 列举出常见命令行工具用法。linux 命令行很多参数又不好记，man 手册比较难用，可以用这个 tldr 替代。https://github.com/tldr-pages/tldr
+- rmtrash/safe-rm: linux 自带的 rm 非常危险，可以使用安全删除工具先放到垃圾桶。`saferm  <https://zhuanlan.zhihu.com/p/91515325>`_
+- when-changed: 一个可以监控文件变动并且自动执行命令的工具，写脚本代码观察编写边输出结果的时候比较方便。笔者很多视频演示过
+- https://github.com/onceupon/Bash-Oneliner
+- epy-reader: https://github.com/wustho/epy 一个在终端下阅读 epub 电子书的工具，用快捷键 "o" 还可以打开图片
+- 现代 unix 工具：https://github.com/ibraheemdev/modern-unix
+- fanyi: 命令行版本的翻译工具 https://github.com/afc163/fanyi
+- cheat.sh: 程序员小抄工具，可以快速查询各种命令、语法等用法 https://github.com/chubin/cheat.sh
+- mackup: mac 备份软件配置  https://github.com/lra/mackup 
+- edex-ui: 一个炫酷的终端模拟工具 https://github.com/GitSquared/edex-ui
+
+Mac 终端快速配置
+--------------------------------------
+- 安装iterm2 替代难看的原生终端
+- 安装 home brew 方便装各种工具
+- 安装 oh-my-zsh or fish
+- 安装 oh-my-zsh 主题 powerlevel9k 或者 spaceship。推荐使用异步的更快的 https://github.com/romkatv/powerlevel10k。 常见其他主题有(pure, powerlevel9k, spaceship)
+- 安装 vim / spacevim /neovim
+- 最好安装 tmux 用来替代iterm2 分屏
+- 按需安装 htop/autojump/z/fzf/ag 等命令行工具(使用 brew 可以安装)
+
+可以参考我 github 上的快速配置教程：
+
+- https://github.com/PegasusWang/linux_config/tree/master/mac 笔者的 MacOS 终端快速配置
+- https://github.com/bhilburn/powerlevel9k/wiki/Show-Off-Your-Config
+
+打字速度练习
+--------------------------------------
+虽然程序员不是打字员，但是对笔者来说，需要记录大量文档和阅读笔记，高效的键盘和打字速度可以大幅增加(我使用 HHKB键盘+Vim编辑器无鼠标编程)码字效率。
+打字速度上去以后(一开始正确率比速度重要，如果不是刻意练习正确率打字再多效果也不明显)，你可能会更加喜欢记录文档和笔记了，脑子里的想法可以快速用键盘来实现出来。
+编程和平常打字不同的一大区别就是需要经常使用各种特殊的符号，如果没有经过刻意练习会经常打错符号。
+
+- https://www.ratatype.com/typing-test/  先来做一个 wpm 测试吧
+- https://typing.io/  包含代码片段的练习网站
+- https://www.keybr.com/ 可以根据英文『音节』来练习，提升因为打字速度和正确率
+- http://www.speedcoder.net/  可以展示出来正确的指法
+- https://www.typing-lessons.org/ 一套系列的打字和指法教程，没事的时候可以练习一下，摆脱二指禅打字。笔者目前就练习这个
+- https://www.keyhero.com/wpm-typing-tips/ 同样一个针对程序员的打字练习网站。笔者没事就会练习一下
+- gtypist (brew install gnu-typist; gtypist) 一个linux/MacOS 命令行打字练习工具
+- https://kaiyiwing.gitee.io/qwerty-learner/ 既可以练习打字，又能学习单词，提供了四六级和雅思托福等词库。还有编程语言 API 词库
+
+Mac 屏保软件
+--------------------------------------
+- https://github.com/JohnCoates/Aerial
+- https://github.com/packagesdev/savehollywood
+
+Git 相关
+--------------------------------------
+- gitx(mac):方便查看代码提交历史，便于了解整个代码仓库是怎样一步步构建的。http://gitx.frim.nl/user_manual.html
+- tig: text-mode interface for git. 喜欢命令行的可以尝试下，比如可以在终端下浏览提交记录和文件修改。 https://github.com/jonas/tig
+- git-extras: 提供了很多方便的 git 命令行工具，比如 git summary 可以输出代码的提交统计。https://github.com/tj/git-extras
+- diff-so-fancy: 让命令行下使用 gitdiff 更加可读 https://github.com/so-fancy/diff-so-fancy
+- git-cz: https://github.com/commitizen/cz-cli 用来统一 git commmit 提交信息，代替 gitmessage。参考文章：https://juejin.im/post/5afc5242f265da0b7f44bee4
+- pre-commit: git pre commit 工具，比如可以在 commit 之前增加代码静态检查或者运行单元测试。 https://pre-commit.com/
+- gitignore.io: 搜索ignore文件模板，有常见编程语言的示例模板。https://www.gitignore.io
+- lazygit/gitui: 终端 git UI 工具
+
+
+Chrome 开发者插件
+--------------------------------------
+- SurfingKeys/vimium/Cvim: chrome 插件，可以用 vim 的方式操作浏览器，很方便，不用鼠标也能完成大部分操作，笔者使用的SurfingKeys比较强大，可以用 ctrl+i 来用 vim 编辑输入
+- wasavi: 想在网页编辑框使用 vim 可以试试这个插件。安装完之后使用 ctrl+enter 就可以了 https://github.com/akahuku/wasavi
+- FE助手：前端插件，Json 格式化/代码美化等很多有用的工具
+- Octotree: Chrome github 浏览插件，可以把 github 项目的目录树结构展示出来，非常方便
+- Github Hovercard: 可以在浏览 github 的时候展示一些链接信息，比如可以查看作者的个人页
+- Momentum: 美化 Chrome 界面的插件，有很多漂亮的图片作为背景
+- Onetab: 用来整合 chrome tab到一个页面，如果打开了太多窗口特别占用资源的时候可以使用
+- LGTM: 有些开发过程中，需要一个同事review 代码后评价 LGTM(Looks Good To Me)才能 merge 代码，这里是图片版的 LGTM
+- SimpRead: 简悦，阅读模式 chrome 插件，支持导出和分享。试了一下导出知乎回答还不错
+- Awesome Screenshot: 截长图、浏览器录屏工具。有时候有一些 pdf 只能预览不能下载，其实可以用它的截长图功能导出来 pdf
+- 暴力猴：支持超多的插件，比如护眼插件、youtute 下载插件等 https://violentmonkey.github.io/ 还可以搜索插件市场 https://greasyfork.org/zh-CN
+- Copytables: 可以复制网页中的表格为 html、csv 等
+
+* `《在浏览器中嵌入 NeoVim，可使用本地配置》 <https://zhuanlan.zhihu.com/p/86027644>`_
+
+编程字体(适合代码显示)
+--------------------------------------
+- Monaco (Mac)
+- Consolas (Windows)
+- Hack
+- Source Code Pro (Adobe)
+- [FiraCode(连字符特性)](https://github.com/tonsky/FiraCode)
+- Jetbrinas Mono 字体
+- Nerdfont [https://www.nerdfonts.com/]
+
+
+代码工具/算法可视化
+--------------------------------------
+- codelf: https://github.com/unbug/codelf 不会命名？变量名查询神器
+- python 可视化：http://www.pythontutor.com/live.html#mode=edit
+- VisuAlgo: https://visualgo.net/en 算法可视化
+- https://www.cs.usfca.edu/~galles/visualization/RedBlack.html
+- https://github.com/krahets/hello-algo 算法可视化
+
+
+代码辅助和检测工具
+--------------------------------------
+- pylint: 代码静态检测工具，请务必集成在你的编辑器或者IDE里（推荐）。能帮你少犯很多错误，动态语言写项目要十分谨慎，非常容易犯错。或者在CI加上 hook 每次 push 代码的时候检测。pylintrc 参考：https://github.com/PegasusWang/linux_config/blob/master/pylintrc 这里我忽略了很多无关紧要的提示，默认的 pylint 配置对代码检查实在是太严格了，很多老鸟也过不了。我敢打赌大部分 python 项目用默认 pylint 检查都是不及格分。（pylint 会给代码算个分, 10分制）
+- mypy: 类型检查工具，结合 python3 的 type hint 或者 python2 中的类型注释可以做类型检查。https://mypy.readthedocs.io/en/latest/cheat_sheet_py3.html
+- pep8: python代码风格检测工具(推荐)。懒人可以试试 autopep8 工具，自动格式化。所有人的代码都过一遍 pylint 和 autopep8(放宽行长度) 看起来就比较一致了。甚至可以配置编辑器保存后自动执行 autopep8，类似 gofmt
+- autopep8/yapf: python 代码自动格式化工具，懒人必备。都可以集成到 vim 里，比如使用  Plugin 'Chiel92/vim-autoformat'  工具一键格式化。不过注意有时会无法正确处理多重缩进，这个比较危险，代码逻辑都变了，还是自己写代码的时候注意下格式。
+- prospector: 集成了众多python代码检测工具
+- mccabe: 圈复杂度检测工具。McCabe 是一种度量程序复杂度的方法，如果单个子程序复杂度过高，或许就需要拆分逻辑提高程序的易读性。
+- pyflakes
+- bandit: 用于Python代码的安全性分析，openstack 的项目 https://github.com/openstack/bandit
+- rope，可以用来重构等，功能强大。笔者经常用rope自动帮我重新整理导入的包顺序。
+- python-mode: 一个vim插件，有很多 python 补全，语法检测等支持。并且集成了很多 python 工具(pylint,pep8等)，笔者正在用。
+- jedi-vim: 一个 vim 插件，python 支持补全和重构。注意和 rope 的自动补全有冲突，不要同时启用。
+- Pyreverse: 代码 UML 生成工具, 帮助我们理解继承关系 (https://pythonhosted.org/theape/documentation/developer/explorations/explore_graphs/explore_pyreverse.html)
+- Epydoc: Automatic API Documentation Generation for Python
+- 2to3/python-modernize: python2 转 python3 工具。目前 Instagram 已经全面迁移到 python3
+- 编写2/3兼容代码：http://python-future.org/compatible_idioms.html
+- kite: 基于机器学习的自动补全，支持多种编辑器和 IDE https://www.kite.com/
+
+* `《[转] Instagram 在 PyCon 2017 的演讲摘要》 <https://zhuanlan.zhihu.com/p/27232791>`_
+
+我觉得对于动态语言使用好静态代码检测工具还是很有必要的，最好集成在你的开发工具里(比如使用vim的python-mode插件可以很容易整合这几个代码检测工具)，辅助你写出高质量代码，否则大型动态语言项目维护起来就是灾难。python会给你一种代码很好写的错觉，不严格要求经常会写出来难以维护的烂代码，甚至导致代码仓库失控。通过 pep8、pylint、mccae 检测过的代码如果警告和错误都消除以后，从代码风格来说基本是没有大问题的，笔者一开始用的时候也是各种警告，修正过很多代码警告以后，以后代码就越来越规范和整洁了。https://github.com/PyCQA 。对于懒人的话直接用 autopep8 ，再也不用纠结格式问题了。目前笔者在公司的一些后端项目中就加入了 flake8 和 pylint 检测（自定义了 pylintrc 文件忽略一些无伤大雅的警告），代码写糙了 CI 都过不了。
+我个人强烈建议，所有的人用 isort 整理包导入顺序，用 autopep8 格式化代码，用 pylint 静态检测，（笔者目前的小团队就是这么做的），这样提交的代码格式会非常一致，而且代码非常干净，大项目也不容易失控，动态语言写项目真的很容易出错。能用工具就尽量用工具帮我们解决格式等问题，多余的精力用来思考代码逻辑本身。
+
+代码质量检测平台
+--------------------------------------
+- Covrralls
+- Sonar: https://www.sonarqube.org
+
+项目工具
+--------------------------------------
+- pigar: 找出项目使用到的依赖库
+- buildout: 项目构建工具
+- pyenv/virtualenv/pipenv：多版本管理
+- cloc, boyter/scc: 命令行代码行数统计工具，scc 速度快很多
+
+
+代码仓库托管
+---------------------------------------
+- gitlab: 公司用得多
+- github: 著名的程序员同性交友网站
+- bitbucket: 类似 github，好处是支持免费的私有仓库。当你不想共享代码的时候可以用
+
+
+项目模板脚手架
+--------------------------------------
+微服务化的时代经常需要创建很多类似的项目代码模板，这个时候项目脚手架就分方便了。
+统一的项目模板对于运维和开发都比较重要，有利于降低维护成本，大家写的代码结构都比较一致非常有利于维护和迁移模块。
+
+- cookiecutter: 一系列项目模板生成工具，懒人必备。https://github.com/audreyr/cookiecutter。笔者之前内部项目就直接用 flask-cookiecutter 直接生成的。
+  可以到 http://cookiecutter-templates.sebastianruml.name/ 搜索感兴趣的编程语言或者框架的代码模板直接拿来修改使用
+- yeoman: http://yeoman.io/generators/ 前端项目模板生成工具
+- ant-design: 后端管理后台项目解决方案 https://ant.design/docs/react/practical-projects-cn
+
+
+持续集成
+--------------------------------------
+- gitlab
+- Travis CI
+- Jenkins
+- Sonar: https://www.sonarqube.org/ 代码质量管理
+
+配置中心
+--------------------------------------
+- Apollo: https://github.com/ctripcorp/apollo
+- Spring Cloud Config
+- Disconf
+
+Api 工具
+--------------------------------------
+- checklist: http://python.apichecklist.com/
+
+DSL
+--------------------------------------
+- PLY
+- PyParsing: 用来实现 DSL 比较方便。
+- Parsley
+
+
+测试工具
+--------------------------------------
+- py.test
+- nosetest
+- unittest
+- tox
+- mock: mocking makes unit testing easier
+
+文档/写书/笔记工具
+--------------------------------------
+- google doc/石墨: 支持多人协作编辑
+- gitbook/docsify/docusaurus + markdown: 可以写文档或电子书，托管到 github 上，可以生成 pdf。
+- doocer: http://doocer.com/ 写 kindle 电子书工具
+- sphinx + readthedoc(或者 mkdocs，支持 markdown) （代码即文档），python 项目很多在用这个生成文档。这本小书就是这么写出来的。`编写《Redis 设计与实现》时用到的工具 <http://blog.huangz.me/diary/2013/tools-for-writing-redisbook.html>`_
+- swagger/apidocjs: 适合写 restful 文档。如果使用 grpc 可以直接生成。
+- jupyter(ipython) notebook，可以做笔记或者代码演示或者ppt，支持rst，md等格式，搞数据科学的人用得比较多，配合 RISE (https://github.com/damianavila/RISE) 可以做代码交互式 slideshow，非常好的工具
+- Confluence: 适合作为团队的项目文档工具，团队大了以后文档还是很重要的
+- vimwiki/emacs org-mode: 依赖于vim/emacs 编辑器，可以做个人笔记，不过笔者还是比较倾向于独立于编辑器的工具
+- Graphviz: 通过编写代码来生成图片 http://graphviz.org/
+- pandoc: 用于各种格式文档之间的转换，比如 html->markdown, html->rst, markdown->rst
+- Onenote: 微软出品笔记工具，手写和绘图功能很不错，笔者在一些教程里使用它来绘图演示
+
+参考: `Self-publishing a book with reStructuredText, Sphinx, Calibre, and vim <https://digitalsuperpowers.com/blog/2019-02-16-publishing-ebook.html>`_
+
+Swagger 工具
+--------------------------------------
+- swagger编辑器: https://swagger.io/tools/swagger-editor/
+- swagger-edit: https://github.com/huan/swagger-edit 本地编写文件预览，可以用自己喜欢的编辑器了（依赖 docker)
+
+
+静态博客工具
+--------------------------------------
+静态建站工具允许我们用 github pages 建立静态博客，省去了服务器的费用。笔者的 https://pegasuswang.github.io 就是基于 hexo 搭建
+
+- hexo: 基于 nodejs 编写的静态博客工具 https://hexo.io/zh-cn/
+- hugo: https://gohugo.io/ go 编写的静态博客建站工具
+- gitalk: https://github.com/gitalk/gitalk#install  基于 github 的评论系统
+
+日志、异常收集工具
+--------------------------------------
+
+- Sentry: 用来记录异常非常好用，能看到完善的栈信息，方便排错。Python 社区用的比较多
+- Fluentd
+- ELK: Elasticsearch, Logstash, Kibana 日志聚合和搜索系统
+
+管理及运维、监控工具(devops很火)
+--------------------------------------
+- Supervisor.进程管理
+- Fabric(封装了paramiko).应用部署
+- docker/k8s.最近比较火的容器技术。很多采用微服务架构的公司使用 docker 作为容器部署服务，或者构建一致的开发环境
+- SaltStack和Ansible. 配置管理
+- StatsD\Graphite\Prometheus等web监控
+- Netdata: 强大的系统监控工具 https://github.com/netdata/netdata
+- glances: 支持终端监控 https://github.com/nicolargo/glances
+
+API gateway
+--------------------------------------
+- kong: open-source API gateway and a microservices management layer. https://github.com/Kong/kong
+
+
+调试工具
+--------------------------------------
+- IPython/Bpython: 代替原生的解释器，支持补全，语法高亮等
+- ipdb/pdb: ipdb 支持自动补全，比原生的 pdb 要好用一些。
+- pdbpp: https://pypi.org/project/pdbpp/
+- postman: 接口调试 gui 工具，也可以导出成各种编程语言的 HTTP 请求代码，或者粘贴请求地址并且修改参数
+- curl: 如果不想使用 postman 等 GUI 工具（比如在服务器上本地测试无法使用这种工具），可以用 curl 命令或者 python requests 库模拟请求
+- https://curl.trillworks.com/ 把 curl 命令参数转成 requests 代码。 https://github.com/NickCarneiro/curlconverter/。
+- https://curlconverter.com/ 类似的 curl 请求转成 代码。可以安装命令行工具(不过似乎当前需要 node v16.15.1 版本)
+- httpie : 类似 curl 但是参数更加友好的命令行请求工具
+- httpbin.org 一个使用 flask 编写的 http 调试网站，你可以通过 http 客户端发送请求到该网址验证 http 参数等
+- curl/requests 互相转化: https://github.com/oeegor/curlify https://github.com/spulec/uncurl
+- Violentmonkey: 油猴脚本。有比较多插件 https://greasyfork.org/zh-CN 可以搜索使用
+
+调试小技巧：使用 chrome 开发者工具右键请求点击copy as curl，然后可以用 uncurl 转成 requests请求调试代码。
+
+
+抓包/网络工具
+--------------------------------------
+- mitmproxy: 用 python 实现的终端命令行http抓包工具，可以将请求直接导出成 curl 请求，python 代码甚至 locust 测试脚本，非常方便，笔者经常用来抓包和调试。
+- charles: mac下的 http抓包软件(收费)。注意如果 https 乱码：菜单栏 Proxy–>SSL Proxying Setting–>选择SSL Proxying——>点击add在弹框中port里填写443(443代表通用https端口号)， host中的*代表抓取所有地址的包。
+- wireshark: 支持tcp抓包，对于一些使用自有协议的抓包，没法通过 http 请求抓包，可以使用 wireshark。wireshark是学习网络协议的好帮手
+- termshark: 类似 wireshark 的一个命令行版本的抓包工具 https://github.com/gcla/termshark
+- tcpdump: 服务器命令行抓包工具
+- netwox: 网络工具集，可以用来创造任意的 TCP、UDP 和 IP 数据报文
+
+参考:
+
+- `Wireshark抓包iOS入门教程  <http://mrpeak.cn/blog/wireshark/>`_
+
+
+Proxy
+--------------------------------------
+- proxychains-ng/privoxy: 把socks5 转成 http代理
+
+
+爬虫相关
+--------------------------------------
+- Scrapy: 知名的爬虫框架。生态比较丰富
+- pyspider: 国人写的一个不错的爬虫框架
+- requests: 一般小爬虫用 requests 绰绰有余。
+- lxml/BeautifulSoup/pyquery: 解析 html，xml 等。
+- tornado: 异步的 http client 可以写爬虫
+- redis/celery: 实现队列、异步爬虫。异步方案也比较多
+- phantomjs/puppeteer/playwright-python: 用来处理动态网站。puppeteer 基于 nodejs。可以用来写爬虫，控制浏览器，自动化测试等
+- pyppeteer/selenium: 基于 python 动态网站爬虫处理，或者用于自动化测试
+- portia: 类似造数、八爪鱼之类的可视化爬虫 https://github.com/scrapinghub/portia
+- Pholcus/Colly/crawlab: Go语言编写的爬虫框架
+
+自动化测试
+--------------------------------------
+- uiautomator2: https://github.com/openatx/uiautomator2
+- taobao-iphone-device: https://github.com/alibaba/taobao-iphone-device
+
+异步任务框架
+--------------------------------------
+- celery: python 社区一个流行的异步任务框架
+- machinery: golang 的异步任务框架 https://github.com/RichardKnop/machinery
+- Airflow: 任务调度 https://airflow.apache.org/docs/apache-airflow/1.10.1/index.html#
+- xxl-job: 分布式任务调度平台 https://github.com/xuxueli/xxl-job/
+
+参考：https://zhuanlan.zhihu.com/p/92152648 Python 定时任务最佳实践
+
+python定时任务框架
+--------------------------------------
+- schedule：Python job scheduling for humans. 轻量级，无需配置的作业调度库
+- python-crontab： 针对系统 Cron 操作 crontab 文件的作业调度库
+- Apscheduler：一个高级的 Python 任务调度库
+- Celery： 是一个简单，灵活，可靠的分布式系统，用于处理大量消息，同时为操作提供维护此类系统所需的工具, 也可用于任务调度
+
+端口扫描
+--------------------------------------
+ZMap: 是密歇根大学研究人员发布的软件，可以在千兆网络条件下 45 分钟完成全网单端口扫描。支持 TCP SYN 、ICMP、UDP 等多种模式。可以用来搜寻代理
+
+后台管理
+--------------------------------------
+- Ant Design Pro: 基于 react 的后台管理方案，可以用来快速搭建后台运营 or 管理
+- flask-admin/Django admin: 框架自带的后台管理。flask-admin 也有类似功能
+
+RPC
+--------------------------------------
+- thrift: facebook 开源的 rpc 框架，很多大公司在使用
+- grpc: grpc是一个高性能、开源和通用的 RPC 框架，面向移动和 HTTP/2 设计。目前提供 C、Java 和 Go 语言版本，分别是：grpc, grpc-java, grpc-go. 其中 C 版本支持 C, C++, Node.js, Python, Ruby, Objective-C, PHP 和 C# 支持. https://github.com/grpc/grpc
+
+Rest
+--------------------------------------
+- Django Rest Framework(DRF): https://www.django-rest-framework.org/
+- Flask-Restful: https://flask-restful.readthedocs.io/ ，可以用 cookiecutter-flask-restful 快速启动一个 restful 后端项目
+- fastapi: https://github.com/tiangolo/fastapi python3 异步框架
+- GRPC: https://github.com/grpc-ecosystem
+- Gin: go web 框架
+
+数据处理和可视化
+--------------------------------------
+- pandas: 处理报表经常用，非常适合处理矩阵、DataFrame、excel 等。配合一些前端可视化库可以弄报表啥的。碰到  Excel
+  处理的强烈建议使用。录了一个小视频讲了下简单的 pands 处理 excel https://zhuanlan.zhihu.com/p/37654682
+- matplotlib: python 绘图。数据可视化有很多其他 python 和前端解决方案
+
+
+压测(benchmark)工具
+--------------------------------------
+- locust: 基于 python gevent 实现的压测工具。http://locust.io/， 有 web 界面，支持编写 python 脚本模拟测试，高度定制化。
+  https://github.com/SvenskaSpel/locust-plugins 支持插件实现更多协议和压测功能。好处是压测脚本可以用版本库管理起来复用
+- ab/wrk/siege: 常见的命令行测试工具，用于一些简单的压测
+- JMeter: 基于 Java 的压测工具
+- pts: 阿里云提供的一个压测工具。https://cn.aliyun.com/product/pts
+
+一些 web 框架的压测结果：
+
+- https://github.com/the-benchmarker/web-frameworks
+- https://www.techempower.com/benchmarks/
+
+Profiler 性能分析工具
+-------------------------------------------------------------------------
+- pyflame: https://github.com/uber/pyflame 火焰图工具，分析 cpu 耗时
+- pyinstrument:https://github.com/joerick/pyinstrument  python 执行耗时分析工具
+
+APM (Application Performance Management)
+-------------------------------------------------------------------------
+- Zipkin: https://link.zhihu.com/?target=https%3A//github.com/openzipkin/zipkin
+
+
+参考：https://www.zhihu.com/question/27994350
+
+数据库命令行/可视化工具
+--------------------------------------
+- mycli: mysql 命令行补全等。https://github.com/dbcli/mycli
+- MysqlWorkbench/Sequel Pro: mysql 客户端工具。也可以用来生成生成 ER 图
+- Dbeaver 跨平台的mysql/sqllite等可视化工具。mac可以通过 ``brew install --cask dbeaver-community`` 安装
+- Navicat Premium: 强大的数据库管理工具，收费
+- pt-online-schema-change: mysql数据库变更工具
+- Medis: redis client 工具
+- MongoChef: Mongodb 客户端工具
+
+- gen: 根据 mysql 生成 golang gorm model。https://github.com/smallnest/gen
+- sqlacodegen: 从 mysql 生成python sqlalchemy model定义。https://github.com/agronholm/sqlacodegen
+
+参考：https://www.webucator.com/article/how-to-create-an-er-diagram-for-a-mysql-database-w/
+
+
+绘图/流程图/思维导图工具
+--------------------------------------
+- processon: http://processon.com/ 使用了下感觉还不错，支持流程图、时序图、思维导图等，可以 clone 别人看好的图作为模板
+- draw.io: https://www.draw.io/  UML 图的功能比较好用，笔者一般流程图用 processon，UML和时序图一般用 draw.io(可以导入飞书)
+- echart.js: 数据可视化 https://echarts.apache.org/examples/zh/index.html
+- 亿图：www.edrawmax.cn 类似 processon，有很多好看的模板
+- Gliffy Diagrams: https://chrome.google.com/webstore/detail/gliffy-diagrams/bhmicilclplefnflapjmnngmkkkkpfad/related
+- carbon/codeimg.io: https://carbon.now.sh/ 可以根据代码生成图片，在分享代码却没有高亮的时候比较方便。codeimg 类似
+- colorhunt: 配色方案（很多程序员的图配色不好看，可以学习一些好看的配色方案) https://colorhunt.co/
+- excalidraw: 手写风格绘图 https://excalidraw.com/
+- onemodel: 程序员绘图，有很多编程图标 https://www.onemodel.app/
+
+量化投资(偏向python量化)
+--------------------------------------
+- tushare: https://github.com/waditu/tushare 有本小白参考书: https://wizardforcel.gitbooks.io/python-quant-uqer/
+- efinance: https://github.com/Micro-sheep/efinance efinance 是一个可以快速获取基金、股票、债券、期货数据的 Python 库，回测以及量化交易的好帮手
+- akshare: https://github.com/akfamily/akshare  开源财经数据接口库
+
+效率，时间管理工具
+--------------------------------------
+不像计算机，人脑其实不善于多进程工作（基于脑科学研究），最好一次做好一件事情，如果中间有各种任务穿插，可以用 todolist 工
+具记录之后分配轻重缓急统一处理，减少大脑的负荷。
+
+- teambiation/trello: todo list 工具，管理任务。今天做了什么；计划做什么；哪些困难导致工作被阻塞(实在搞不定的记下来及时向同事求助)；发现了什么问题；今天学到了什么。(类似于开发日志之类的玩意，每天都是真正做了事情的，并且最好每天都是学到了新东西的)。有时候一些小灵感或者解决问题的思路在没有纸笔的情况下也能迅速记录到工具里，防止遗忘。
+- 番茄工作法：人长期专注的时间是有限的，找到适合自己的最佳番茄钟，并且每个时间段都专注于一件事，每件事分清轻重缓急，要事优先。在休息时间处理喝水、上厕所等杂事，做几个深呼吸给脑瓜子充点氧，或者活动下筋骨，眺望下远处。预防职业病（最近有看到工程师视网膜脱落的，要重视身体健康）。
+- Be Focused: mac 下番茄工作法工具，可以用来给任务计时，或者提醒该休息下了
+- 复盘。无论是写代码、做需求、改bug等，事后反思总结。分析并且记录耗时的地方和可以改进的地方(怎么让自己涨点记性，整理 checklist)，对于一些错误或者坑也可以记录成文档当做团队的知识财富。
+- zapier: https://zapier.com/ 一个连接 app 自动化工作流的工具，比如可以用来定期提醒发邮件等，非程序员也能实现定时任务啦
+
+程序员外设/健康工具
+--------------------------------------
+- 键盘/鼠标腕垫：长时间使用键盘手腕压力比较大，可以考虑买一个几十块的软垫放在键盘下边托住手腕，减轻手腕压力
+- 主动降噪耳机和纯音乐：选择类似于《阿尔法波高效记忆音乐》《巴洛克学习音乐》等，能帮助你隔绝噪音，或者你可以在youtube/网易云音乐等搜索到很多类似工作或学习音乐(搜优美钢琴曲)。反正笔者听歌的时候会想歌词反而会打扰思路，一般就是听这种不怎么让你瞎想的音乐。降噪耳机如果不差钱可以考虑主动降噪耳机(WI1000X/QC30)，效果好一些，网上也有一些对比视频。对于嘈杂的工作环境来说，绝对是一个非常值得的投资。
+- 人体工学座椅/鼠标/键盘/usb 屏幕挂灯，土豪必备
+- 办公室午休床，隔音耳塞
+- 海露人工泪液/湿房镜/防蓝光镜片/usb热敷眼罩: 缓解干眼症
+- 录音笔。最近裁员有点多，你懂的。笔者用的一款搜狗的录音笔，非常小巧，可以用来记录会议，语音转文字做字幕，实时翻译等。
+- 机械/静电容键盘（键盘可以说是程序员最在意的工具了，推荐几个我个人认为比较适合写代码的，个人推荐红轴，无段落感，打字行云流水)
+
+  - HHKB: 码农神器静电容键盘，不过不用 vim，非 linux/unix 用户慎用。没有方向键和F区
+  - 宁芝（niz）PLUM普拉姆静电容键盘: 同静电容键盘，键位更多，适合大众用户
+  - 阿米洛（Varmilo） 苹果MAC双系统机械键盘: 机械键盘，适合 mac
+  - Poker2: 键位类似 HHKB，同样是可编程迷你键盘，可以替代 HHKB，笔者之前长期使用
+  - Filco Minila Air: 同样是迷你机械键盘，适合大众程序员
+
+参考: `程序员双十一剁手指南  <https://zhuanlan.zhihu.com/p/89192238>`_
+
+视频课程录制(vlog工具)
+--------------------------------------
+在 mac 下录制了一些 vim 和 python 的视频教程(b 站或者知乎可以看到)，记录下使用到的一些硬件和软件工具，硬件工具均可网购，部分软件收费：
+
+- keycastr: mac 按键回显到屏幕，最近录制 vim 视频教程的时候有用到。https://github.com/keycastr/keycastr
+- youbute-dl/lux(golang): https://github.com/rg3/youtube-dl 命令行油管视频下载工具
+- aria2: https://aria2.github.io/ 轻量级的命令行下载工具
+- FFmpeg: 强大的视频处理工具，可以用来截图，截取视频片段等
+- ScreenFlow/Camtasia/Obs: 屏幕录制，剪辑工具，收费。笔者用来录制屏幕用
+- licecap: 一款小巧的免费 gif 录制工具
+- TunesKit Video Cutter(mac): 视频分割、合并工具
+- iZotope RX6: 音频降噪工具，去除杂音、呼吸声等等，收费
+- Audacity: 音频处理 https://www.audacityteam.org/download/
+- MediaInfo: 查看视频信息
+- HandBrake: 视频压缩工具，免费工具。直接从录屏工具导出的视频体积可能会非常大，推荐压缩后上传到网站
+- SketchBookPro/Deskscribble(收费): 白板/黑板工具，配合 wacom 手写板可以把屏幕当成黑板或者白板使用。模仿可汗学院的授课方式，笔者在讲述 Python 算法的课程里使用到。
+- Wacom/绘王 手绘板：用来实现屏幕手写，配合绘图或者白板软件当做黑板使用，方便手写做一些演示或者推导。
+- Blue yeti电容麦/Rode NT usb电容麦/铁三角Atr2100动圈麦：使用 mac 内置麦克风音频效果比较差，可以考虑专业的播客级麦克风，录制出来的视频声音要清楚很多。usb 麦克风即插即用，非常方便，但是灵敏底噪大。动圈麦在嘈杂环境表现更好，不会收录杂音。
+- VideoScribe: 制作手绘风格视频，提升视频趣味性。收费
+- 课件制作: PowerPoint, Keynote, AxeSlide, Focusky 等。笔者现在喜欢使用 OneNote 配合手写板在视频里进行图解演示。
+- Mousepose: 鼠标高亮增强工具。演示的时候可以高亮部分区域，其他部分置灰
+- 免费字体：思源字体(思源宋体、思源黑体)；站酷字体。视频中的一些字体可能要考虑版权问题，推荐使用无版权字体
+- canva: 一个好用的封面设计网站，可以用来设计 vlog/课程 视频封面图 https://www.canva.com
+- qmcdump: 命令行版本的 qq 音乐解码器
+
+MacOS 图片软件
+--------------------------------------
+- ImageOptim: https://imageoptim.com 一个开源免费的mac图片压缩工具
+- iShot: 一个强大的 macos 截图、截长图软件(截长图比较好用)
+
+HTML Presentation Tools
+--------------------------------------
+如果觉得用 ppt 做分享比较老套，可以尝试一些使用 HTML 来做 slide show 的工具。或者使用 markdown 生成 html 幻灯片。
+
+- slidev: nodejs 编写 https://github.com/slidevjs/slidev (推荐，有标注功能)
+- reveal-md: 使用 markdown 转成网页 slides。https://github.com/webpro/reveal-md 亲测使用起来很方便，依赖 nodejs
+- reveal.js: The HTML Presentation Framework
+- RISE: 在 jupyter 里做 slide show，甚至可以直接在网页里运行 Python 代码。 https://github.com/damianavila/RISE
+- remark: A simple, in-browser, markdown-driven slideshow tool. https://github.com/gnab/remark
+- md2googleslides: markdown 转成google slides  https://github.com/gsuitedevs/md2googleslides
+
+* `《HTML-presentation-tools.md》 <https://gist.github.com/PegasusWang/5d00c2e32943f1e3258e964eb64ce4aa>`_
+* `《markdown-for-slide-decks.md》 <https://gist.github.com/johnloy/27dd124ad40e210e91c70dd1c24ac8c8>`_
+
+Terminal slides
+--------------------------------------
+在终端下使用 markdown 演示
+
+- slides: go 编写的终端 ppt 工具 https://github.com/maaslalani/slides
+- present: python 语言的终端演示工具 https://github.com/vinayak-mehta/present
+
+思维导图工具
+--------------------------------------
+
+- coggle.it: 一款免费的在线思维导图工具 https://coggle.it/
+- GitMind: 在线思维导图工具，可以多人协作 https://gitmind.cn/
+
+电子阅读器/电子书软件
+--------------------------------------
+笔者使用的 Kindle 和 国产的大屏幕 Boox，目前小米生态的墨案也推出了大屏幕(10.3)寸水墨屏阅读器。
+当然你可以使用平板电脑，不过长时间盯着屏幕对眼睛不太好，笔者倾向于使用水墨屏阅读器。
+
+- Koreader: http://koreader.rocks/ 一款支持多种主流电子书格式的开源电子书阅读器，支持Kindle/Android等
+- calibre: https://calibre-ebook.com 跨平台的电子书管理和阅读桌面软件
+
+参考：
+
+* `《Kobo Aura One刷机折腾记录：激活、安装koreader、中英字体、字典》 <http://www.dealwithem.com/3457282/>`_
+
+Linux network debug Tools
+--------------------------------------
+注意：dig/nslookup 等直接请求 dns server，会忽略 etc/hosts
+
+- ping/tcping：特定域名的 ip 是否可达。ping send ICMP echo request
+
+  - ping google.com
+  - ping -c 3 google.com
+
+- dig/host: get DNS records。用来替代 nslookup
+
+  - dig google.com 默认返回 A 记录
+  - dig google.com MX
+  - dig -x 8.8.8.8 反向查询
+  - host -a google.com 类似dig
+  - host 8.8.8.8
+
+- route: shows and manipulate ip routing table
+- traceroute 诊断网络延迟。诊断到目标路径的设备延迟
+
+  - traceroute google.com  命令返回的星号指示丢失包
+  - traceroute -n google.com , to avoid reverse dns lookup use -n
+  - traceroute -I google.com, send ICMP packet (default UDP, -T TCP, some servers block UDP)
+
+- mtr, realtime tracing, 结合了ping,traceroute,nslookup的相关特性
+- ss(socket statistics), checking connection performance。socket 统计，比netstat快，利用了 tcp_diag
+
+   - https://www.cnblogs.com/peida/archive/2013/03/11/2953420.html
+
+- arp, view the arp table
+- tcpdump, packet analysis。https://juejin.cn/post/6844904084168769549
+
+  - tcpdump -i <network_device> tcp
+  - tcpdump -i <network_device> port 80
+  - tcpdump -c 20 -i <network_device> port 80 , -c number of events
+  - tcpdump -c 20 -i <network_device> src XXX.XXX.XXX.XXX
+  - ifconfig, you can obtain the device names likes this。查看和配置机器网卡
+  - tcpdump -w /path/ -i <network_device>, tcpdump to a file
+  - tcmpdump -r /path
+  - sudo tcpdump -i lo0 port 6379 -nnX -vvv -A  # 本地 redis 抓包
+
+- netstat, network statisic,  display connection info, routing table information etc
+- lsof(查看端口进程): lsof -i:8000
+- iftop: 查询流量异常的进程
+- curl: 发送 http 请求，类似的还有一些比如 httpie
+- nc: 作为tcp|udp服务器,或者作为工具,模拟发送tcp,udp包
+- trickle: 用户空间带宽控制管理的工具
+- Nmap: 端口扫描工具
+
+Linux debug Tools
+--------------------------------------
+
+- gdb
+- valgrind
+- ltrace: tracing system and library calls
+- strace —— Trace system calls and signals。 跟踪进程的系统调用或信号产生的情况。
+- lsof: tracking open files
+- pmap: viewing memory allocation
+- fuser file_name 或者 ``ls -l /proc/*/fd/* | grep filename`` 找到文件被哪些进程使用
+
+命令行工具
+--------------------------------------
+
+- https://github.com/agarrharr/awesome-cli-apps
+- https://github.com/chubin/awesome-console-services
+- https://github.com/herrbischoff/awesome-command-line-apps
+
+
+参考：
+--------------------------------------
+
+- https://likegeeks.com/linux-network-commands/
+- https://unix.stackexchange.com/questions/50098/linux-network-troubleshooting-and-debugging
+- https://www.tecmint.com/linux-network-configuration-and-troubleshooting-commands/
+- https://github.com/mrzool/unix-as-ide
diff --git a/conf.py b/conf.py
index 7650c906..891ea0d2 100755
--- a/conf.py
+++ b/conf.py
@@ -7,7 +7,7 @@
 from docutils.utils import get_source_line
 
 
-def _warn_node(self, msg, node):
+def _warn_node(self, msg, node, **kw):
     """ Monkey patch to ignore specific errors """
     if not msg.startswith(('py:obj reference target not found', 'nonlocal image URI found')):
         self._warnfunc(msg, '%s:%s' % get_source_line(node))
@@ -16,8 +16,8 @@ def _warn_node(self, msg, node):
 
 master_doc = "index"
 
-project = "z42-doc"
-copyright = "2015, ac"
+project = "python-golang-web-guide"
+copyright = "2025, 公众号|知乎|B站 PegasusWang"
 
 version = release = "0.1"
 
@@ -31,10 +31,13 @@ def _warn_node(self, msg, node):
 
 primary_domain = 'py'
 default_role = 'py:obj'
+highlight_language = "none"
 
 autodoc_member_order = "bysource"
 autoclass_content = "both"
 
+pygments_style = 'sphinx'
+
 # Without this line sphinx includes a copy of object.__init__'s docstring
 # on any class that doesn't define __init__.
 # https://bitbucket.org/birkenfeld/sphinx/issue/1337/autoclass_content-both-uses-object__init__
@@ -82,3 +85,7 @@ def _warn_node(self, msg, node):
     import sphinx_rtd_theme
     html_theme = 'sphinx_rtd_theme'
     html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
+
+# Disable epub mimetype warnings https://github.com/sphinx-doc/sphinx/issues/10350
+suppress_warnings = ["epub.unknown_project_files"]
+
diff --git a/database/mysql.rst b/database/mysql.rst
new file mode 100644
index 00000000..e7130111
--- /dev/null
+++ b/database/mysql.rst
@@ -0,0 +1,342 @@
+.. _mysql:
+
+=============
+Mysql
+=============
+
+
+数据库操作
+=====================================================================
+
+批量 kill 掉查询
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+有时候需要批量 kill 掉查询进程，有几种方式，比如生成 sql 文件执行：
+
+.. code-block:: sql
+
+    mysql> select concat('KILL ',id,';') from information_schema.processlist
+    where user='root' and time > 200 into outfile '/tmp/a.txt';
+
+    mysql> source /tmp/a.txt;
+
+
+有时候没法生成文件（权限原因），可以直接生成 sql 语句 copy 下来复制到命令行也可以，或者连接成一行方便复制：
+
+.. code-block:: bash
+
+    mysql > select concat('KILL ',id,';') from information_schema.processlist where db='dbname';`
+
+    mysql > select GROUP_CONCAT(stat SEPARATOR ' ') from (select concat('KILL ',id,';') as stat from information_schema.processlist where db='dbname') as stats;
+
+    # 按客户端 IP 分组，看哪个客户端的链接数最多
+    select client_ip,count(client_ip) as client_num from (select substring_index(host,':' ,1) as client_ip from processlist ) as connect_info group by client_ip order by client_num desc;
+
+    # 查看正在执行的线程，并按 Time 倒排序，看看有没有执行时间特别长的线程
+    select * from information_schema.processlist where Command != 'Sleep' order by Time desc;
+
+    # 找出所有执行时间超过 5 分钟的线程，拼凑出 kill 语句，方便后面查杀
+    select concat('kill ', id, ';') from information_schema.processlist where Command != 'Sleep' and Time > 300 order by Time desc;
+
+
+也可以通过 python 脚本来完成，原理也是查询进程 id 然后删除：
+
+.. code-block:: py
+
+    # https://stackoverflow.com/questions/1903838/how-do-i-kill-all-the-processes-in-mysql-show-processlist
+
+    import pymysql # pip install pymysql
+
+    connection = pymysql.connect(host='',
+                                 user='',
+                                 db='',
+                                 password='',
+                                 cursorclass=pymysql.cursors.DictCursor)
+
+    with connection.cursor() as cursor:
+        cursor.execute('SHOW PROCESSLIST')
+        for item in cursor.fetchall():
+            if item.get('db') == 'dbname': # 过滤条件
+                _id = item.get('Id')
+                print('kill %s' % item)
+                cursor.execute('kill %s', _id)
+        connection.close()
+
+删除大表(借助一个临时表)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. code-block:: bash
+
+    # https://stackoverflow.com/questions/879327/quickest-way-to-delete-enormous-mysql-table
+    CREATE TABLE new_foo LIKE foo;
+    RENAME TABLE foo TO old_foo, new_foo TO foo;
+    DROP TABLE old_foo;
+    # 发现好像直接用 truncate table tablename; 清理千万级别表也挺快的
+
+统计表的大小并排序
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. code-block:: sql
+
+    # https://stackoverflow.com/questions/9620198/how-to-get-the-sizes-of-the-tables-of-a-mysql-database
+    SELECT
+         table_schema as `Database`,
+         table_name AS `Table`,
+         round(((data_length + index_length) / 1024 / 1024), 2) `Size in MB`
+    FROM information_schema.TABLES
+    ORDER BY (data_length + index_length) DESC;
+
+统计数据库大小
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. code-block:: bash
+
+   # https://stackoverflow.com/questions/1733507/how-to-get-size-of-mysql-database
+   SELECT table_schema "DB Name",
+           ROUND(SUM(data_length + index_length) / 1024 / 1024, 1) "DB Size in MB"
+   FROM information_schema.tables
+   GROUP BY table_schema;
+
+   # 输出前十个大表
+   select table_schema as database_name,
+          table_name,
+          round( (data_length + index_length) / 1024 / 1024, 2)  as total_size,
+          round( (data_length) / 1024 / 1024, 2)  as data_size,
+          round( (index_length) / 1024 / 1024, 2)  as index_size
+   from information_schema.tables
+   where table_schema not in ('information_schema', 'mysql',
+                              'performance_schema' ,'sys')
+         and table_type = 'BASE TABLE'
+         -- and table_schema = 'your database name'
+   order by total_size desc
+   limit 10;
+
+
+查看表信息
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. code-block:: sql
+
+    mysql > show table status;
+    mysql > show table status where Rows>100000;
+
+纵向显示
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+有时候表字段比较多的时候，查询结果显示会很乱，可以使用竖屏显示的方式，结尾加上 ``\G``
+
+.. code-block:: bash
+
+    mysql > select * from user limit 10 \G
+
+导出和导入表的数据
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. code-block:: sql
+
+    shell > mysqldump -u user -h host -p pass db_name table_name > out.sql
+    mysql > source /path/to/out.sql
+
+重命名数据库
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. code-block:: sql
+
+   # https://stackoverflow.com/questions/67093/how-do-i-quickly-rename-a-mysql-database-change-schema-name
+   mysqldump -u username -p -v olddatabase > olddbdump.sql
+   mysqladmin -u username -p create newdatabase
+   mysql -u username -p newdatabase < olddbdump.sql
+
+
+Python Mysql 操作
+=====================================================================
+
+Sqlalchemy 示例
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. code-block:: py
+
+    # -*- coding: utf-8 -*-
+
+    """
+    sqlalchemy 快速读取 mysql 数据示例
+
+    # pip install SQLAlchemy -i https://pypi.doubanio.com/simple --user
+    pip install SQLAlchemy==1.3.20 -i https://pypi.doubanio.com/simple --user
+    pip install pymysql -i https://pypi.doubanio.com/simple --user
+    """
+
+    import sqlalchemy as db
+    from sqlalchemy import text
+
+    """
+    # 本机 mysql 创建一个测试表
+
+    CREATE TABLE `area_code` (
+      `id` int(11) NOT NULL AUTO_INCREMENT,
+      `code` bigint(12) NOT NULL DEFAULT '0' COMMENT '行政区划代码',
+      `name` varchar(32) NOT NULL DEFAULT '' COMMENT '名称',
+      PRIMARY KEY (`id`),
+      KEY `idx_code` (`code`)
+    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+    """
+
+    def sqlalchemy_demo():
+        # https://towardsdatascience.com/sqlalchemy-python-tutorial-79a577141a91
+        url = "mysql+pymysql://root:wnnwnn@127.0.0.1:3306/testdb"  # 本地用的测试地址
+        engine = db.create_engine(url)
+        connection = engine.connect()
+        metadata = db.MetaData()
+        table = db.Table('area_code', metadata, autoload=True, autoload_with=engine)
+
+        # 插入单个数据
+        query = db.insert(table).values(code=10010, name="北京")
+        connection.execute(query)
+
+        # 插入多个数据
+        query = db.insert(table)
+        values = [
+            {'code': 10020, 'name': '上海'},
+            {'code': 10030, 'name': '杭州'},
+        ]
+        connection.execute(query, values)
+
+        # 查询
+        query = db.select([table]).order_by(db.desc(table.columns.id)).limit(10)
+        rows = connection.execute(query).fetchall()
+        for row in rows:
+            print(row.id, row.code, row.name)
+
+        # 修改
+        query = db.update(table).values(name="帝都").where(table.columns.code == 10010)
+        connection.execute(query)
+
+        # 删除行
+        query = db.delete(table).where(table.columns.code == 10010)
+        connection.execute(query)
+
+      def sqlalchemy_text_demo():
+          """直接执行 sql 语句 """
+          url = "mysql+pymysql://root:wnnwnn@127.0.0.1:3306/testdb"  # 本地用的测试地址
+          engine = db.create_engine(url)
+          connection = engine.connect()
+
+          sql = text("show tables;")
+          res = connection.execute(sql)
+          for i in res:
+              print(i)
+
+    if __name__ == "__main__":
+        sqlalchemy_demo()
+
+
+Go Mysql 操作
+=====================================================================
+
+go 可以使用 gorm 或者 database/sql
+
+.. code-block:: go
+
+    package main
+
+    import (
+        "database/sql"
+        "fmt"
+        "log"
+        "time"
+
+        _ "github.com/go-sql-driver/mysql"
+    )
+
+    func main() {
+        db, err := sql.Open("mysql", "root:root@(127.0.0.1:3306)/root?parseTime=true")
+        if err != nil {
+            log.Fatal(err)
+        }
+        if err := db.Ping(); err != nil {
+            log.Fatal(err)
+        }
+
+        { // Create a new table
+            query := `
+                CREATE TABLE users (
+                    id INT AUTO_INCREMENT,
+                    username TEXT NOT NULL,
+                    password TEXT NOT NULL,
+                    created_at DATETIME,
+                    PRIMARY KEY (id)
+                );`
+
+            if _, err := db.Exec(query); err != nil {
+                log.Fatal(err)
+            }
+        }
+
+        { // Insert a new user
+            username := "johndoe"
+            password := "secret"
+            createdAt := time.Now()
+
+            result, err := db.Exec(`INSERT INTO users (username, password, created_at) VALUES (?, ?, ?)`, username, password, createdAt)
+            if err != nil {
+                log.Fatal(err)
+            }
+
+            id, err := result.LastInsertId()
+            fmt.Println(id)
+        }
+
+        { // Query a single user
+            var (
+                id        int
+                username  string
+                password  string
+                createdAt time.Time
+            )
+
+            query := "SELECT id, username, password, created_at FROM users WHERE id = ?"
+            if err := db.QueryRow(query, 1).Scan(&id, &username, &password, &createdAt); err != nil {
+                log.Fatal(err)
+            }
+
+            fmt.Println(id, username, password, createdAt)
+        }
+
+        { // Query all users
+            type user struct {
+                id        int
+                username  string
+                password  string
+                createdAt time.Time
+            }
+
+            rows, err := db.Query(`SELECT id, username, password, created_at FROM users`)
+            if err != nil {
+                log.Fatal(err)
+            }
+            defer rows.Close()
+
+            var users []user
+            for rows.Next() {
+                var u user
+
+                err := rows.Scan(&u.id, &u.username, &u.password, &u.createdAt)
+                if err != nil {
+                    log.Fatal(err)
+                }
+                users = append(users, u)
+            }
+            if err := rows.Err(); err != nil {
+                log.Fatal(err)
+            }
+
+            fmt.Printf("%#v", users)
+        }
+
+        {
+            _, err := db.Exec(`DELETE FROM users WHERE id = ?`, 1)
+            if err != nil {
+                log.Fatal(err)
+            }
+        }
+    }
diff --git a/database/redis.rst b/database/redis.rst
index accfeba2..a26a108c 100644
--- a/database/redis.rst
+++ b/database/redis.rst
@@ -4,3 +4,114 @@
 Redis
 =============
 
+
+Scan 命令代码示例
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+有一些需求需要我们扫描 redis 的所有键值，如果用 keys 会阻塞redis 非常危险，推荐用 scan 命令。如果需要扫描一个hash/zset
+等也有对应的 hscan, zscan 等命令可以使用。
+
+- 返回的结果可能会有重复，需要客户端去重复，这点非常重要;
+- 遍历的过程中如果有数据修改，改动后的数据能不能遍历到是不确定的;
+- 单次返回的结果是空的并不意味着遍历结束，而要看返回的游标值是否为零;
+
+参考：https://www.lixueduan.com/post/redis/redis-scan/ Redis Scan 原理解析与踩坑
+
+.. code-block:: python
+
+    import redis  # pip install redis
+
+
+    def init_redis():
+        HOST, PORT, PWD = "host", 6379, "pwd"  # 用你的 redis 配置替代，最好读取配置不要硬编码密码等保证安全
+        return redis.Redis(host=HOST, port=PORT, password=PWD)
+
+
+    def get_all_node_ids(r):
+        """ 获取所有的腾讯云 redis 集群 master node_id。扫描的需要覆盖所有 master 节点。如果scan 一个复合结构不需要扫所有节点
+        redis cluster 架构
+        u'ip:port@12028': {u'connected': True,                                                                                                                                                                                     [149/176]
+                                 u'epoch': u'17',
+                                 u'flags': u'master',
+                                 u'last_ping_sent': u'0',
+                                 u'last_pong_rcvd': u'1580544851000',
+                                 u'master_id': u'-',
+                                 u'node_id': u'XXXXXXXXXXXXX',
+                                 u'slots': [[u'8192', u'10239']]},
+        """
+        from collections import defaultdict
+        node_dict = r.execute_command("cluster nodes")
+        master_slaves = defaultdict(list)  # {master_id : [slave_ids]}
+        for _addr_id, info in node_dict.items():
+            if info.get("flags", "") == "slave":
+                master_id = info["master_id"]
+                master_slaves[master_id].append(info["node_id"])
+        master_ids = list(master_slaves.keys())
+        slave_ids = []  # 获取其中一个 slave id
+        for _, slave_ids in master_slaves.items():
+            slave_ids.append(slave_ids[0])
+        return master_ids, slave_ids
+
+
+    def scan(r, node_id='', cursor=0, match=None, count=None):
+        pieces = [cursor]
+        if match is not None:
+            pieces.extend([b'MATCH', match])
+        if count is not None:
+            pieces.extend([b'COUNT', count])
+        if node_id:
+            pieces.append(node_id)
+        return r.execute_command('SCAN', *pieces)
+
+
+    def scan_playcount(r):  # r redis client
+        _master_ids, slave_ids = get_all_node_ids(r)
+        num = 0
+        for node_id in slave_ids:  # scan 每一个 redis slave 节点
+            cursor = 0  # reset cursor if error
+
+            while True:
+                cursor, keys = scan(r, node_id, cursor, "UR_*", 10000)  # 这里 match 设置你需要的前缀
+                for key in set(keys):  # 注意如果不是幂等的，这里可能重复，需要去重
+                    print(key)  # 这里根据你的需求处理 key
+
+                if cursor == 0:  # 这里说明没有多余数据了，退出
+                    break
+        print("all nums:{}".format(num))
+
+
+    def main():
+        redis_client = init_redis()
+        scan_playcount(redis_client)
+
+
+    if __name__ == '__main__':
+        main()
+
+
+也可以 scan 单个复合结构, golang 代码示例如下(不用获取所有 master 节点了，只有一个 key):
+
+.. code-block:: go
+
+   // 遍历数据源  redis zset 获取点赞数
+   func getRedisZsetNum(key string) ([]string, error) {
+       rc := storage.LikeRedisClient // redigo/redis 的 client
+       iter := 0
+       var memberScores []string
+       for {
+           arr, err := redis.Values(rc.Do("ZSCAN", key, iter, "MATCH", "*"))
+           if err != nil {
+               return nil, fmt.Errorf("key:%s error retrieving '%s' keys", key, "*")
+           }
+
+           iter, err = redis.Int(arr[0], nil)
+           k, err := redis.Strings(arr[1], nil) // k  [m1 score1 m2 score2]
+
+           logger.Debugf("redis scan key:%s k:%+v, err:%v", key, k, err)
+           memberScores = append(memberScores, k...)
+
+           if iter == 0 {
+               break
+           }
+       }
+       return memberScores, nil
+   }
diff --git a/debug/index.rst b/debug/index.rst
new file mode 100644
index 00000000..c4219210
--- /dev/null
+++ b/debug/index.rst
@@ -0,0 +1,376 @@
+Debug 调试神技
+================
+..
+
+  凡是可能出错的事就一定会出错。 - 墨菲定律
+
+调试/bug 定位/debug
+--------------------------------------
+调试也是个很重要的问题，不可能保证代码没bug，要命的是有时候写代码完成功能的时间还没调试的时间多，编码不要过度求快，逻辑正确更重要。
+复现是排错的第一步，之后通过各种方式确定原因（访问日志、邮件报的异常记录）等，通过走查代码、断点调试（二分法等）确定错误位置，确定好错误原因了就好改了。修复后最好反思下问题的原因、类型等，哪些地方可以改进，争取下次不犯一样的错，慢慢减少错误才能越来越高效。
+比如像是知乎、字节等公司都有统一的事故通报平台，隐去涉密信息后发送到全公司让大家复盘学习，尽量避免同样的bug问题再次在其他团队重复出现给公司带来损失。
+
+如何定位和修复 bug：复现->定位->修复->验证->复盘。大胆假设，小心求证；思路不通，换个角度。定位需要找到 bug 出现时候的上下文信息，可以用 log，sentry，kibana 日志系统等查看。确认之后通过走查代码、断点调试等方式寻找代码逻辑错误。
+如果是比较复杂的系统，之间的交互比较多，可以边调试边记录，把关键的流程调用、系统交互逻辑、关键点的日志记录到文档里，整体梳理，对于排查比较难以解决的 bug 也有帮助。
+
+- 及时止损。出现问题第一步应该及时排查线上代码、配置、AB实验等有无变更，还有上下游的代码或者配置有无变更，如果有应该及时回滚止损。一般来说突发的线上问题大部分都是变更导致的，应该先排查变更并且及时回滚止损。
+- 复现，偶尔才复现的代码是很难排查错误的。如果不好复现但是有 sentry 之类的记录工具也是极好的，sentry 会记录当前栈信息和变量信息，非常有利于排错。
+- 走查代码。使用 pylint 等静态检测工具排除低级错误(你应该把它集成到开发工具里)。
+- 看提交记录。最近代码的修改记录，是否是别人的代码引入了 bug。是否可以回滚到上一个可用部署解决呢？(注意一旦一个新的上线出问题，应该先回滚部署而不是回滚代码)
+- 看日志，各种日志(logging, nginx)，看 sentry 异常信息。很多框架或者工具都有 debug 模式，打开 debug 模式可以获取到更多有用信息（但是要注意线上慎用 debug 级别日志）
+- 加日志。如果已有的日志没能排查出来关键信息，可以适当增加 debug 日志记录更充分的数据。比如关键函数的输入和输出，关键rpc调用/数据库查询/第三方库调用/重要数据结构的输入和输出等。
+- 问同事，问源码作者(脸皮要厚)，让同事帮忙 review 审查代码。有时候人有思维定势，你死磕不出来的问题别人可能一眼就看出来了。多学习一下高手解决问题的流程和思路
+- 借助搜索引擎。很多问题 google/stackoverflow/github 上都可以搜到，善用搜索引擎解决问题。
+- 小黄鸭调试法，桌子上放个小黄鸭（小黄鸡儿也行），然后尝试从头到尾给它讲解有问题的代码段，说不定就在你给它代码描述过程中发现了问题。
+- 断点调试。看变量值，控制变量法调试。二分法(分而治之)排查代码位置，快速试错定位。比如一个地方很有隐秘的错误，但是又不好快速确定位置，我们就可以用二分加断点的方式快速定位到具体哪一块出了问题。如果不好断点就多加一些临时日志，不断缩小问题代码范围。
+- 使用调试器(命令行or IDE 调试工具）。 ipdb/pdb 断点配合 python 一些内置方法比如 ``print/vars/locals/pprint`` 等断点调试，使用 curl/chrome 开发者工具/mitmproxy 等调试请求。代码异常可以通过 ``import traceback; traceback.print_exc()`` 打印出来。
+- 日志比对/输入输出对拍。在重构系统的时候，首先保持原有系统和重构之后代码的正确性。可以通过比对日志，比对输入和输出值的方式确保正确
+- 功能对拍。可以用不同的语言、框架等实现同一个功能，看看是否是因为某些框架的 bug 导致，比如一个框架没问题，另一个有问题就可以断定是该框架实现本身有问题。
+- 排除法。不断记录灵感/想法/可能的原因等，做排除法，缩小问题范围，说不定就可以发现 bug 的藏身点。
+- 依赖库bug。一般经过广泛使用的第三方库是可以信赖的，但是公司自己造的轮子（尤其是文档和单测都没有的），还是有可能出 bug 的。有可能是依赖而非自己代码逻辑 bug。
+- 升级后出问题。是否有完善的功能测试和单元测试保证回归没有问题？升级代码修改了哪些部分？降级之后能否复现？
+- 同样的代码/服务别人没问题你的有问题。你们运行的版本一致么？运行环境、环境变量一致吗？依赖第三方库的版本一致么？一般总会有一个不一致导致的问题。建议锁定编程语言、第三方库、运行环境版本并保持一致。
+- 服务一会有问题一会没有。是不是有的机器服务未更新或者灰度失败导致只有部分机器更新了？
+- 偶现问题。之前碰到过绑定测试环境出现问题，结果请求偶现会打到线上导致表现不一致的问题，比较坑而且不好排查(内部的平台问题)。通过链路查询得到的 ip 地址可以锁定那些偶现请求是否正确打到了你的联调环境
+- 只有个别的数据有问题。检查一些这些数据的字段配置是否和其他业务一致，对比一下大概率是配置字段不同导致的
+- 服务超载。长期稳定运行的服务突然出问题一般不太可能是业务逻辑 bug，重点关注指标 cpu/io/memory/磁盘/log/连接数 是否被打满，是否无法继续正常服务，如果是服务器负载问题也会导致服务失败，不一定是主逻辑代码有问题(当然也有可能是连接池使用不当导致)。
+- 是否是缓存的问题？缓存数据过期了么？缓存是否一致呢？能否清理缓存解决？测试环境禁用一下缓存看看表现如何(笔者之前改完代码一直 debug 没生效结果发现是缓存未失效导致一直是旧数据)
+- 是否是配置的问题？配置的时候参数填写的是否正确，有没有去掉多余的无用的空白符？别傻傻地把测试写到正式或者正式写到测试环境。比如笔者遇到过对比配置结果手动填写的配置多了空格导致比对失败
+- 是否是特殊输入问题？偶现的问题可能是由某些特殊的输入参数代码未处理导致的，能否通过日志或者构造某些特殊的输入复现？
+- 服务之间的依赖关系如何？有没有分布式链路追踪，哪一步调用关系出了问题？是否是没有降级，有没有碰到雪崩？服务间有没有循环调用?
+- 监控报警。各种服务指标监控是否有报警？报警是否正常？如果没有及时监控到是否可以增加相关指标的报警？
+- 硬件问题。硬件问题出现较少比如某个机器宕机，但是大规模部署的服务还挺常见的，一旦监控出现某个机器请求异常基本上可以断定是机器硬件问题，需要及时排查和剔除异常机器
+- 负载均衡问题。可能表现为服务时而可用时而不可用，或者因为不同机器服务代码不一样表现不一致（不是期望的灰度逻辑）
+
+发现并且修复问题之后，我们需要通过之前的 bug 来涨涨记性，如何避免类似的问题再犯，养成良好的编码和思维习惯。
+
+- 重视静态检查/编译器/IDE 开发工具的缺陷提示，尽量连 warning 提示都不要留，及时修复缺陷，保证高质量的代码可以有效减少 bug 产生。
+- 善用工具。比如进程/cpu/内存/io/fd/流量/proc等，可以通过日志/监控/pdb/gdb/strace/pstack/ps/pmap/top/iostat/netstat/tcpdump/prof 等多种工具定位和排查
+- 不要死磕，一个法子不行换一个。死磕可能会耗费太长时间并且容易进入死胡同(思维定势)，在一个大型复杂系统中定位 bug 原因是对技术、经验、毅力、灵感、心理素质的很大考验，休息一会甚至睡一觉醒来可能就解决了。
+- 极难排查和复现的 bug 可以无限期搁置，bug 永远修不完的
+- 找到 bug 修复以后增加相应单元测试用例，这样对回归测试非常有利，同时避免重复犯一样的错误。tricky 的地方要加上注释。
+- 修复原因而非现象。你要排查出来真正导致 bug 的原因，而不是仅仅通过魔改代码修复了不合理现象。又比如仅仅依赖重启解决内存泄露等问题，而不去排查真正泄露的原因（当然可能排查起来很艰难）
+- 真的是代码的问题么？还是非代码因素：比如代码是否正确部署上线等(比如之前脑残查一个 bug 无解最后发现是部署系统失败部署到线上压根没成功，还是老代码，根本没起作用）。如果实在没发现代码级别错误，单测也比较完善，可能就要考虑下非代码因素。
+- 配置/环境问题。是否是因为配置而非代码逻辑 bug 导致的，线上/测试/开发环境 的配置是否正确，是否脑子抽了写串了，比如测试环境的配置写到了正式环境(这种看似低级的错误笔者在工作中就遇到过)
+- 建立个人 bug 清单和上线核对清单，避免再次出现犯过的错误。你的每一个错误都应该自己用一个笔记软件或者小本本记录下来，避免再次犯错(小心被扣工资)。上线之前检查日志等级，进程数设置是否正确，建立核对清单，养成好的思维习惯
+- bug 总结：建立错误检查表(核对清单)，哪些可以避免的记录下来，防止以后再犯。(团队的知识财富)。比如笔者在关闭一个 bug 单的时候会注明 bug 产生的原因和修复方式，而不是修复完成之后就不长记性了
+- 流程自动化。凡是可以自动化的就自动化，依赖人的行为反而是最容易出错的。脚本一旦编写通过之后就可以无限次正确使用，远比人为操作可靠。
+
+bug主要来源于粗心(比如拼写错误)、认知偏差(比如错误理解和使用API)、系统复杂度的增加等。
+可以通过复杂度控制、设计复审、代码审查、代码静态分析、单测/功能测试等找出来，我们可以综合利用以上手段尽量减少代码缺陷，大幅减少给代码擦屁股的时间。
+
+* `《软件调试修炼之道》 <https://book.douban.com/subject/6398127/>`_ 笔者比较推荐的一本书，告诉你正确的思维方式
+* `《调试九法》 <http://www.wklken.me/posts/2015/11/29/debugging-9-rules.html>`_
+* `《Python ipdb 调试大法[视频]》 <https://zhuanlan.zhihu.com/p/36810978>`_ 笔者经常在服务器上进行命令行调试，一些技巧
+
+常见的 bug 类型
+--------------------
+
+打算记录一下自己犯过和见过同事犯过的一些常见 bug 类型，尽量避免重复犯错，笔者会长期不定期更新这个错误列表，不断吸取自己
+和别人的经验。笔者这里也强烈建议你自己整理一个文件，专门用来记录你曾经犯下的错误并引以为戒，争取不要重复之前的 bug。
+
+需求理解错误:
+~~~~~~~~~~~~~~~~~~~~~~
+- 需求理解不一致。业务开发中很常见的一个问题，产品/开发/测试理解不一致导致实现被当成 bug，不明确的地方一定要沟通好互相阐述确保需求理解一致再去开发，防止返工。
+
+代码错误:
+~~~~~~~~~~~~~~~~~~~~~~
+
+- 拼写错误。不要笑，这个错误其实很常见，推荐打开编辑器的拼写检查和相同词高亮，可以消除一些类似问题。还有就是直接 copy 类似代码然后忘记改一些小细节也容易出问题而且不好排查(少复制粘贴)
+- 类型错误。在动态语言和弱类型语言当中比较常见的一种错误(动态语言确实更容易出 bug)，可以借助类型强转，type hint 工具。
+- 资源没有关闭。打开的文件/IO流/连接等资源一定要关闭，防止资源泄露。go 的 defer 和 python 的 with 最好用上
+- 深浅拷贝问题。不同语言可能又不同的拷贝模型，确定你的参数是深拷贝还是浅拷贝，能否修改，修改了之后是否有副作用。
+- 数组越界错误。注意涉及到数组的时候使用的下标是否会越界。越界了 python 抛出异常，go 直接 panic 掉，并且 go 不支持负数下标
+- 参数校验。一般来自用户的输入都要假设参数可能是错误甚至是恶意参数，后台必须要进行类型、大小、范围、长度、边界、空值等进行检查，防止恶意参数导致服务出问题。应该将不合法参数尽可能拦截在上游
+- 参数单位是否匹配。比如 go 需要时间的参数 time.Duration 有没有乘以对应的 time.Second/MilliSecond 等。
+- 参数顺序不对。如果函数参数太多可能导致看走眼顺序写错了，所以强烈建议如果参数太多，封装成对象或者一个结构体传参。
+- 路径错误。编写一些脚本需要处理文件的时候，推荐使用绝对路径比较不容易出错。
+- 空值错误。比如直接赋值一个 go 里边声明的 map 会 panic，你需要先给 map make 一个值，很多 go 新手会重复犯这个错(go slice 却可以直接声明之后 append)
+- 零值和空值。有时候我们根据业务来区分零值（一个类型的初始化值）和空值 (None/nil等)，注意处理上的细微区别。
+- 闭包问题。循环里闭包引用的是最后一个循环变量的值，需要注意一下，很多语言都有类似问题，可以通过临时变量或者传参的方式避免
+- 遍历修改列表问题。一边遍历，一边修改可能会使得迭代器失效而出错，最好不要遍历的时候修改列表。
+- 遍历修改元素值问题。这一点 go 和 python 表现不同，go 比如你去循环一个 ``[]Struct`` 是无法修改每个元素的，go 会拷贝每一个元素值，需要通过下标或者指针修改
+- 影子变量(shadow)。很多语言同名的局部作用域变量会隐藏外部作用域变量，最好不要同名冲突，否则可能不是期望结果。建议使用go vet/go-nyet 之类的静态检查工具检查
+- 空数据和nil。注意在 go 里空数据指针比如 &SomeStruc{} 和 nil 序列化的结果是不同的，注意序列化之后的差别
+
+函数(复杂度)问题
+~~~~~~~~~~~~~~~~~~~~~~
+- 循环调用。在一些复杂场景中，a 函数调用了b 函数，b 函数里边又因为某些条件调用了 a 函数导致循环调用，可能导致 cpu 飙高，严重的可能打垮下游服务。
+- 调用放大。一次请求链路中可能会多次请求同一个函数导致请求放大(重复调用或者循环里调用等)。go 语言可以利用 context(WithValue) 来缓存结果，防止一次调用链路中的重复请求。不要在for循环里做网络请求或者特别重的逻辑，防止打垮下游
+  (适合只读或者读写场景，注意不能是读-写-读) (参考：https://github.com/ag9920/go-ctxcache)
+- 破坏协议约定。比如之前约定是参数错误不抛出异常，而修改之后抛出了异常，可能导致调用方业务逻辑有问题。
+- 强依赖与弱依赖。对于强依赖代码如果调用下游出错应该返回错误(比如再让用户重试)，而弱依赖可以根据业务场景设计合理的默认返回降级值(兜底值+打印日志)。
+- 导出和非导出。go 语言通过大小写决定函数/结构体字段是否是非导出的，对于内部使用的函数、字段应该严格使用小写不要导出，隐藏细节方便之后修改。否则一旦导出被外部使用之后重构起来就会更加困难
+- 超长函数。控制复杂度，尽量不要写太长的函数。超长的函数最好及时重构降低复杂度
+- 冗余代码。不用的代码应及时删除，很多人会感觉将来会用到，实际上大概率将来也不会用到，及使用到通过版本库可以很容易找回来。但是如果不删代码将来会非常难以修改
+
+参考：
+
+- `基于 context.Context 的 Golang loader 缓存 <https://juejin.cn/post/7154318373663277093>`_
+
+数值错误
+~~~~~~~~~~~~~~~~~~~~~~
+- 数值截断错误。注意强制类型转换是否会发生截断，损失精度，结果是否符合期望。如果需要精确数值，比如银行存款、电商交易可以
+  用定点数或者整数。
+- 数值范围越界：注意前端 javascript(设计缺陷) 无法表示完整的 int64，传给前端需要用 string 替换 int64 (被坑过好几次, 有些序列化协议会自动给你把int64转成string处理)。
+  golang里如果有一个 json 数据包含了 int64，反序列化到一个 ``map[string]interface{}`` 也会有类似问题，可以用 UseNumber() 属性解决。https://zhuanlan.zhihu.com/p/347985826
+- 浮点数比较：浮点数不能直接用等号比较，应该是比较两个数的差值小于指定范围
+
+内存问题
+~~~~~~~~~~~~~~~~~~~~~~
+- 解引用空指针。是否引用了空指针的值导致直接 panic？比如 go 里边直接对一个 nil map 赋值 panic。指针有没有 nil 检查(一些嵌套的结构体指针可能忘记检查是否是nil就访问导致panic，比如访问a.b.c 但是b是 nil)
+- 内存泄露。有没有循环引用？有没有全局变量值一直增长或者被引用没有释放？有没有多个对象底层引用的其实是同一块内存始终无法释放(比如直接赋值)？
+- OOM(out of memory): 比如使用了本地缓存需要严格控制本地缓存的使用量大小(过期和剔除策略)，防止内存持续增长进程被杀掉影响服务稳定性。
+
+网络问题
+~~~~~~~~~~~~~~~~~~~~~~
+- 网络请求合理超时。一切网络client(http/rpc/mysql/redis请求等) 都应该设置合理的超时参数，比如有些 go 的 client 需要显式自己传进去超时参数，否则可能导致 block 或者下游大量超时导致线程堆积。超时时间可以参考 P99等响应时间合理设置
+- 连接池打满。连接池应该是服务共享的(单例)，而不是每个请求都要去创建连接池导致打满连接池。请检查 client 的连接池和超时参数设置是否合理。
+- 长短连接使用不当。注意有些需要长连接的场景，可以避免频繁建立 tcp 握手的开销。(http keepalive)
+- 接口限制。接口请求参数有没有进行限制，一次请求的数据量是否太大，有没有加上分页参数，日志会不会一次打印太多导致 IO 压力大
+- 带宽打满。比如 redis 有比较大的 key 可能导致并发请求的时候打满带宽，可以扩容带宽同时限制 redis 的热 key 和大 key。
+- 幂等问题。调用下游服务成功了，但是因为网络问题没拿到结果调用端认为失败了又进行重试，可能会造成数据不一致。可以用带有过期时间的缓存/版本号等来做幂等。
+
+RPC/Web 框架
+~~~~~~~~~~~~~~~~~~~~~~
+- 请求参数限制。比如一般 rpc 请求会限制每次请求的最大的参数个数，如果一次性请求太多可能需要分批并发请求
+- debug 模式。注意线上一定要关闭掉 debug 方式防止泄露关键信息。很多框架在 debug 模式下会显示一些关键信息，可能会被黑客利用
+- 序列化协议版本问题。client/server 序列化的方式是否一致？版本是否一致？不同的版本之间有时候可能会有一些微妙的 bug
+- 调用重试。由于超时或者服务抖动可能需要重试，注意重试次数、间隔时间(线性、随机、指数退避)等问题，避免重试风暴
+
+参考:
+
+- `如何优雅地重试 <https://www.infoq.cn/article/5fboevkal0gvgvgeac4z>`_
+
+兼容性问题
+~~~~~~~~~~~~~~~~~~~~~~
+- 新特性版本号兼容。对于客户端新上线的需求，是否限制了特定的平台和版本号才能展示或者下发(ab实验是否过滤了老版本)，防止老版本无法处理导致崩溃
+- 协议文件兼容。一般线上会同时跑很多版本的 App，修改协议要慎重，错误修改协议严重可能导致老版本 App 不可用甚至崩溃(只加新字段，别改老字段)
+
+  - 对于 json 等格式应当只增加新字段，不要修改和删除老字段，防止老版本解析失败。同样也不应该修改老字段的含义或者功能！(笔者遇到过因为一个之前定义的list长度变化导致的代码问题)
+  - 对于 Ptotocol buffers,Thrift 等协议，之后新增的字段必须是可选的或者具有默认值。(旧代码不会写入require字段导致检查失败)
+  - 同样 PB, Thrift 协议也不建议删除老字段，如果必须删除只能删除可选字段，而且不能再次使用相同的标签号。不要修改之前已有
+    的required字段为optional的或者删除required字段，否则就会出现版本不兼容问题，老客户端访问新服务会出现参数错误。不确定的情况建议用 optional 字段。
+  - json 无法表示 64 位数字，如果后台需要传递 64 位 id 给客户端，必须使用 string 类型，否则会被截断！超过 9007199254740992 就会有精度丢失问题。
+    在序列化golang的 map[string]interface{} 也会有类似问题，拿到的float64再转成int64就会丢精度。你会发现pb一些序列化框架会自动把int64序列化成string类型防止丢精度
+  - 如果返回的结构体是一个 list，最好外层包一下不要直接返回一个裸的 list，否则后续要增加字段就没法在 list 上加了
+
+
+数据库问题
+~~~~~~~~~~~~~~~~~~~~~~
+- 查询参数非法。查询数据库的时候可能因为一些不合理参数导致数据库慢查询,比如一次查询太多导致慢查询，非法 id 透传到了数据库层。可以在入口处做一下限制和严格校验，比如限制limit 大小，过滤不合法 id
+- 查询参数类型不匹配。注意如果传入类型不对，可能导致数据库没法利用索引导致慢查询，比如字符串类型传了一个int, 注意查询的参数类型和数据库类型匹配(尤其是动态语言)
+- 慢查询：没有索引，索引设计不合理可能导致慢查询问题，有没有慢查询监控？ 对于分布式数据库，有没有使用分片键查询？一定要有数据库慢查询的监控告警，新需求上线放量期间注意观察数据库的负载
+- 连接池跳涨。除了不当使用连接池之外，如果是启动了大量的服务容器也可能有这个问题，注意限制单服务连接池的大小
+- 连接池过大。连接池数量设置太大效率反而可能降低，应该根据实际压测结果设置一个比较合理的值，并非越大越好
+- 连接未及时释放。如果协程里有事务处理代码 panic 了并且没有 recover，即使协程退出了，但是依旧占用DB连接且不会主动释放，可能导致连接泄露
+- 字符集问题。注意如果字符串需要存一些特殊的 emoji 表情符号，需要使用 utf8mb4 字符集。
+- 请求放大。不要在for循环等语句里边做网络请求比如访问数据库、redis、rpc 调用等（除非你明确知道你在干什么？有及时退出条件和请求间隔么），使用批量请求并限制每次请求个数，防止打挂数据库(批量优先于并发)
+- SQL注入。尽量不要使用直接拼接 sql 的方式，比较容易出现 sql 注入。使用 orm 或者一些第三方库可以有效减少注入问题
+- 数据加密。敏感数据一开始就要加密存储，不要明文直接存储用户的敏感信息，比如电话、用户密码等，一旦泄露数据十分危险
+- 数据误删。笔者还真遇到了因为别人渗透测试误删了线上数据库重要数据导致服务大量出错，一定要做好数据库备份
+- 主从延迟。写后即读场景中读取的时候没有读到写入的数据可能是主从复制延迟过高，可以通过读取主库(确保读取量级不会压垮主库或影响写入速度)，写缓存读缓存(注意缓存本身也有主从延迟问题)、消息队列冗余信息等方式处理。最好可以从业务角度设计尽量不依赖主从延迟
+- 字段类型问题：
+
+  - 自增类型作为主键应该选择 BIGINT，目前很多大业务int容易超过最大范围。每张表都应该设置一个主键(可以用snowflake等算法生成，会暴露出去的 id 不要直接用连续自增数字防止被遍历)
+  - 涉及到金钱比如余额等，推荐用整数类型(单位用分)而不是DECIMAL 类型，性能更好而且存储更紧凑，同时避免了计算精度问题
+  - 时间字段建议使用 DATETIME，时区问题可以在前端或者服务端转换。(int不容易看出来具体时间，TIMESTAMP最大只能到2038年)
+
+并发问题
+~~~~~~~~~~~~~~~~~~~~~~
+- 线程安全。如果不是线程安全的操作(原子操作)，应该通过加锁等方式做数据同步。比如 go 里边如果多个 goroutine 并发读写 map 程序会出错(lock/sync.Map)。利用好 race detector。
+  但是有些语言有 GIL 可以保证内部数据结构的一些原子操作，这个时候可以不用加锁，所以要区分不同编程语言决定。
+- goroutine泄露。确保你的 goroutine 可以完成退出(比如没有死循环，没有channel block住)，防止大量未执行结束的 goroutine 堆积。通过上报 go 的 runtime goroutine 数量指标可以发现
+- 死锁问题。锁的粒度对不对？锁有没有正确加锁和释放锁？加锁和释放锁的类型是否匹配(Lock/Unlock, Rlock/Runlock())，次数是否匹配？
+- 并发操作导致资损。可能由于客户端错误的并发请求导致一些涉及到发放资金的操作会并发执行，如果后台没有加分布式锁可能导致并发请求都会成功，从而有超发带来资金损失
+- 并发修改丢失。对于一些非原子性操作比如 CAS 操作修改缓存，并发场景可能导致数据不符合期望。使用 lua 脚本/redis原生结构/分布式锁/请求排队等方式保证原子性修改
+- 并发修改panic。go 语言对于同一个变量的并发修改会导致 panic 可以通过加锁解决。不同协程对于同一个结构体的不同成员变量并发操作不会造成 panic，但是由于cpu cache line的原因会有一定的性能问题
+
+资金损失问题(资损)
+~~~~~~~~~~~~~~~~~~~~~~
+- 并发操作资损。未拦截并发操作，导致多发或者超发。使用分布式锁等方式拦截，注意临界区的粒度
+- 重复操作资损。比如重试操作导致重复发放(失败重试、消息队列重新消费等)。使用 CAS 版本号、幂等id设计等避免重复操作资损。
+- 幂等设计。比如对于奖励发放即使重试应该使用之前同一个幂等 id 来避免重复发放。
+
+依赖库问题
+~~~~~~~~~~~~~~~~~~~~~~
+- 依赖版本是否一致。笔者曾经因为开发工具的自动 import 引入了错误的包版本导致一个挺难查的 bug（vendor 和 gopath 下不同的redigo 版本)，
+  要小心因为不同版本导致的一些极其隐蔽的 bug。最好通过包管理工具锁定依赖的第三方库版本; 还要注意 IDE 工具自动导入的包对不对
+- 能否升级解决。有些知名的库或者编程语言（go/python）等都是开源并且不断迭代的，在一些旧版本出现的隐蔽的bug直接可以升级解决(可以搜索提交记录和 issue等看修复的问题记录)
+- 升级服务出问题。升级有时候可以解决一些 bug，但是也可能引入新 bug？能否通过回退到上一个版本解决(比如git checkout 到一个历史提交)？是否详细看过升级日志(release notes)，修改了哪些东西？是兼容升级还是不兼容升级？
+- 清理无用依赖。对于不用的依赖也有可能引入问题，不用的依赖最好清理掉，比如 ``go mod tidy`` 或者清理掉 python ``requirements.txt``
+
+日志错误
+~~~~~~~~~~~~~~~~~~~~~~
+- 日志级别错误。线上使用了 debug 级别，可能会产生大量日志，如果没有滚动日志可能会导致服务器磁盘打满。一定要注意不同环境日志级别，推荐集中式日志收集系统。
+  线上应该只打印重要的 info 和 error 级别日志，或者不重要的日志也可以使用一定采样率打印。遇到过几次对方服务把日志打满服务不可用的情况
+- 日志参数错误。日志语句对应的占位符要和传参的个数一致，类型要匹配，比如本来是数字的使用了 ``"%s"`` 而不是 ``"%d"``
+- 缺少必要信息。如果是为了 debug 加上的日志一定要有足够的上下文信息、关键参数帮助排查问题，同时也要注意日志不要泄露敏感数据（比如密码等）
+- 日志过大：除了注意日志等级，还要注意是否输出了过大的日志导致磁盘 IO 飙升，适当精简日志量，不要打印过大的结构体(json等/请求体/透传参数等)，或者提升线上日志等级只打印异常和ERROR。线上一定要关闭 DEBUG 日志
+- 危险操作记录。对于一些修改和删除数据的危险操作，比如一些后台管理系统等，一定要加上日志记录，方便排查问题和找到误操作人
+
+错误/异常处理
+~~~~~~~~~~~~~~~~~~~~~~
+- 不要忽略任何一个错误/异常。除非你有 100% 的把握可以显示忽略，否则至少要在发生错误或者异常的地方加上日志，出问题之后错误被吞掉会极难排查。笔者这个地方吃过亏，吞掉了错误导致排查困难
+- 集中收集。一般搭建 sentry（异常、错误收集）；ELK（集中式日志收集）来进行集中收集，方便针对异常、日志进行聚合和搜索。否则散布在各个服务器上很难排查问题
+
+配置错误
+~~~~~~~~~~~~~~~~~~~~~~
+- 配置环境写串。看起来是一个很傻的错误，但是其实还挺常见，注意不同环境配置是否对的上，别把测试的写到正式环境了。启动服务时打印配置看看
+- 服务启动命令是否写错。有些服务依赖命令行启动的时候容易写错参数，建议通过配置文件的形式传进去。
+- 配置字符串是否有多余空白符。笔者也被这个小问题坑过，手动编辑的时候人工加上了空白符导致我比对出错，注意配置参数都要去掉空白符。应该在前端用户输入阶段就过滤掉不合法字符
+- 配置安全。不要硬编码到配置文件或者代码文件 git 仓库里，涉及到密码的配置应该使用统一的配置中心，防止代码仓库泄露秘钥等风险。
+- 框架/编程语言配置。很多web/rpc框架的线程数、golang 容器的 GOMAXPROCS(uber-go/automaxprocs) 配置是否合理可能影响程序性能
+- 配置校验。人为的业务数据配置经常出现数据范围、类型等写错的情况，或者多加了一些错误的分隔符等，关键数据需要配置系统或者业务代码做一下校验，防止资金损失
+- 配置变更。修改配置之后应该现在测试和预发布环境进行验证，确认无误后再修改线上环境配置。故障平台统计显示很大一部分问题都是线上变更导致
+
+字符串问题
+~~~~~~~~~~~~~~~~~~~~~~
+- 比对字符串。单元测试的时候注意比对的字符串可能因为多了空格的问题没法严格比对。注意可以去掉空格之后对比，笔者曾经因为不
+  同字符串就多了一个空白符比对失败查了好久，被坑过。比对字符串特征而不是直接对比字符串
+- split空字符串。py/go split(s, sep) 一个空字符串得到的是一个长度为 1 且第一个元素是空字符串的数组，而非空数组。
+
+分布式系统问题
+~~~~~~~~~~~~~~~~~~~~~~
+分布式系统中可能会碰到的问题：
+
+- 网络中的数据包可能会丢失、重新排序、重复递送或任意延迟(超时)
+- 时钟只是尽其所能地近似(时钟回拨等)
+- 节点可以暂停（例如，由于垃圾收集）或随时崩溃：检测和剔除故障节点(负载均衡)；失败转移(主从)
+
+常见的业务开发可能会碰到的坑：
+
+- 分布式锁。分布式服务对于需要数据同步的操作可以使用分布式锁，注意分布式锁的超时问题(本身是否高可用)。Redission 实现比较完善
+- 时钟倾斜(clock skew)。如果代码强依赖时间戳在不同的服务器上可能因为时钟差距导致问题，可以采用适当取整对齐时钟。有一些第
+  三方库允许一定的时间差容忍（比如乘以一个误差因子)。https://github.com/dgrijalva/jwt-go/issues/383
+- 分布式数据库。注意有些分布式数据库插入数据之后不会返回主键。可以用分布式 id 生成器(snowflake算法)指定主键作为 shard key
+- 时钟同步出错(ntp同步问题)。笔者最近碰到的问题，云服务机器时钟出问题了，导致我一些服务鉴权带上时间戳参数的失败了。依赖
+  时间的比如 snowflake算法 如果出现时钟回拨可能会产生重复 id。
+- 日历时钟与单调时钟。(参考《设计数据密集型应用》第八章-分布式系统的麻烦)
+
+  - Time-of-day clock(日历时钟) : 返回从 epoch(UTC 时间 1970 年 1 月 1 日午夜)开始的秒数(可能回拨)。需要从 NTP(网络时间协议) 服务器同步信息。linux的 clock_gettime(CLOCK_REALTIME) 或者 java System.currentTimeMillis()。日历时钟无法用来测量经过时间
+  - Monotoinc clock: 经常用来衡量时间间隔（time interval），例如超时或者服务器响应时间，保证不会回跳，但是单调钟的绝对值无意义。linux的 clock_gettime(CLOCK_MONOTONIC) 或者 java System.nanoTime()
+  - python3 的 time.monotonic() 方法和go 1.9 之后的 time 包使用了单调时钟
+  - bwmarrin/snowflake 包使用了 go 的 time 包解决时钟漂移的问题，参考 https://github.com/bwmarrin/snowflake/pull/18
+
+缓存(redis)问题
+~~~~~~~~~~~~~~~~~~~~~~
+缓存在大幅提升性能的同时也会带来很多问题，比如缓存一致性等。一致性问题在开发和测试中(尤其多级缓存)会带来很多困难，而且一
+旦数据结构变更，处理缓存失效逻辑也会变得复杂。所以如无必要，不要轻易引入缓存，笔者之前碰到过因为数据结构改变的缓存 bug 就很难处理。
+
+- 超高热点 key：对于微博/直播之类的应用，比如明星出轨或者热门直播等，可能有某些热点的 key 集中到单台 redis 上导致压力过大(看一下 redis 热点 key 统计方便排查问题)，可以考虑再加一层进程内缓存。比如使用 go-cache 等进程内缓存库。
+  编写代码的时候应该注意到可能发生这种热点 key 的问题(测试环境压测+观察热点 key)，应当谨慎使用 redis，充分利用进程缓存/key hash是有效的方案。或者写多个 key 然后每次获取随机取一个。
+- redis版本和集群模式。使用云 redis 的时候之前因为使用了 lua 脚本，但是测试环境和线上使用了不同的 redis 集群版本，发现测试
+  环境测试一直没问题，但是一到线上就不起作用。建议保持线上和测试环境的基础组件版本一致。
+- 系统调用结果缓存。比如一些日志库获取本机 ip 的时候没有缓存下来，导致大量系统调用，类似结果可以放到缓存或者全局变量
+- 并发修改缓存。比如存了一个kv值是json数据结构但是却有并发修改，可能会造成数据丢失。使用分布式锁、redis 原生结构、redis+lua 脚本等保证原子性修改
+- 多级缓存。如果上游缓存了调用结果，那么当前代码修改后可能不起作用。联调或者排查问题的时候记住多级缓存的场景可能带来的问题
+- 缓存一致性。无论是先更新缓存再更新数据库，或者先更新数据库再失效缓存，并发场景都不能保证完全一致。推荐先更新数据库，再
+  删除缓存出现缓存不一致概率最小，也是目前最常用的一种方案(Cache Aside 旁路模式)
+- 热 key 和大 key。热key 一般通过本地缓存或者哈希分片的方式解决，大 key 一般也应该尽量从业务上避免，可以拆分或者写数据库做冷热分离
+- 老 key 和长 key。如果是作为缓存的key一定要设置过期时间防止永久驻留(根据业务预估失效期)。缓存key的名字也应该尽量短，本身也占用空间
+- redigo: 注意go的一个常用 redis 库如果查询不到 key 会返回 redis.ErrNil，需要和其他的 err 做区分。一般来说go里查不到和返回错误是需要业务上做不同的处理
+- redis cluster 集群错误：有时候要实现 redis lua 原子操作，对于 redis cluster，操作的所有key必须在一个slot上(或者可以指定hash tag 落到同一个 slot)，否则返回错误信息。
+  同理 redis cluster 下 mset/mget/pipeline 等都需要操作同一个 slot，腾讯云 redis 在 proxy 层给你实现了，可以直接批量操作。
+- 请求放大。业务变更或者代码逻辑错误(比如 for 循环里请求 redis 等) 都有可能打垮缓存服务，缓存组件要有及时的利用率、请求量等告警。优先使用redis 提供的批量操作
+- 缓存失效(缓存污染)。如果新上线的代码修改了数据结构导致和已有缓存的数据结构不同，那么上线的代码必须设计好失效机制让老的缓存数据先失效(或者代码可以兼容)，否则有严重的业务问题(如果缓存失效期比较短问题倒是不大)：
+
+  - 上线期间灰度部署新老代码都在跑，老代码会读到新的缓存数据结构导致现有逻辑可能出问题(比如字段含义改变甚至不兼容的时候)
+  - 上线之后新代码读取还没失效的老的缓存数据，也可能会导致现有逻辑有问题(比如新加的字段读不到)
+  - 稳妥的开发和上线方式：
+
+    1. 只新增字段，不要修改数据结构老字段或者改变其含义(类型、长度等)。 这样保证灰度期间老的代码逻辑不会影响
+    2. 新上线代码判断获取的缓存有没有新字段，如果没有新字段则认为是过期缓存，删除对应的缓存数据并回源重建。这样保证新代码没有影响
+    3. 或者通过给key增加配置版本号的机制，来控制失效缓存。比如上线后缓存污染了，通过配置增加缓存key版本号自动失效旧缓存重建新缓存
+
+参考:
+
+- `热点key问题的发现与解决 <https://www.alibabacloud.com/help/zh/doc-detail/67252.htm>`_
+- `Lua脚本使用规范 <https://help.aliyun.com/document_detail/92942.html>`_
+- `redis容量预估工具 <http://www.redis.cn/redis_memory/>`_
+
+消息队列问题
+~~~~~~~~~~~~~~~~~~~~~~
+- Kafka 只能保证单个分区有序。如果要保证有序可以使用单个分区(丧失吞吐性，不推荐)；指定消息key为业务id，保证同一个业
+  务 id 的消息发到同一个分区保证有序，从而保持因果一致性(推荐)
+
+脚本编写问题
+~~~~~~~~~~~~~~~~~~~~~~
+- 先用日志替换写操作。需要跑一些脚本的时候，可能会修改数据库，如果脚本直接修改了数据并且脚本有 bug 可能就会导致数据异常并很难回滚。
+  建议所有的写操作写替换成日志打印出来，确认无误之后再去执行，更加保险。
+- 数据备份。用脚本操作重要数据之前建议先备份一份，防止操作出错无法恢复。或者操作之前导出数据，之后出问题再用于恢复
+
+服务构建问题
+~~~~~~~~~~~~~~~~~~~~~~
+- 版本检查。go/python 版本是否一致
+- 环境检查。环境变量，或者构建参数、 go env 等是否一致
+
+数据问题
+~~~~~~~~~~~~~~~~~~~~~~
+- 资源隔离。不同环境下比如测试和正式环境的资源一定要严格隔离开，防止数据污染。
+
+后台服务
+~~~~~~~~~~~~~~~~~~~~~~
+- 自动拉起。如果服务因为严重错误退出了(比如 go panic 了，python 未捕获异常进程退出了)，能否快速拉起服务？
+- 异地部署。是否已经做到了两地三机房？一个机房挂了之后，服务能否正常继续工作
+- 数据不一致。如果程序在关键流程中退出了，是否会导致数据不一致的问题？有方法修复么？是幂等操作么？比如交易系统定期对账
+- 自动扩容。如果突然请求量上去了，服务能否在短时间之内快速扩容应对压力？
+- 快速回滚部署。如果线上出了问题，能否快速回滚到上一个可用的稳定版本保证服务可以继续稳定执行？回滚是否会有不兼容情况，导致其他依赖你的服务不正常？
+- 拆分部署。对于一些特别核心的接口，可以分开部署。防止其他接口有问题了，造成核心服务不稳定。（一个项目的接口重要性不同)
+
+服务监控(监控三板斧：度量指标+告警、链路追踪、日志)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+- qps监控。有没有监控服务每个接口的 qps？有没有监控接口的成功失败率？返回码？
+- 响应时间。每个接口请求的响应时间有没有做监控？ TP90, TP99 分别是多少？
+- 链路追踪。微服务中各种系统互相调用，有没有用 open-tracing 之类的进行链路追踪？
+- 业务监控。使用 Grafana 之类的监控系统对关键业务数据进行打点监控，防止某些业务异常
+- 失败报警。关键接口、服务挂了，机器负载高了有没有及时发送报警提醒？
+- 异常上报。区分于日志，异常一般是发生了比较严重的错误，业界有比如 sentry 这种集中式异常收集平台来上报异常，一般除了无法
+  避免的网络问题之外，大部分异常都是需要开发者修复的。
+
+写完代码之后检查一下该加的日志有没有加，该上报的指标有没有上报，错误能否及时捕捉并且上报到平台上。
+
+熔断降级
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+- 容量预估。对于请求量级比较大的服务，上线之前各个基础组件、下游服务等应该有合理的容量预估，需要扩容的是否提前扩容充分。
+- 熔断保护。对于核心服务，如果流量短时间暴增，能否监控到并且正常处理。如果下游服务打挂了，能否熔断保护，应当确保调用其他 rpc 服务加上熔断器保护。
+- 柔性降级。柔性可用是在有损服务价值观支持下的方法，重点在于实际上会结合用户使用场景，根据资源消耗，调整产品策略，
+  设计几个级别不同的用户体验场景，保证尽可能成功返回关键数据，并正常接受请求，绝不轻易倒下。简言之就是保证关键接口兜底策略
+- 压力测试。上线之前有没有预估过最高 qps 然后做过压力测试并且监控各个基础组件和下游服务的压力和稳定性？对于有突发流量的场景也应该做好压测及时发现问题（比如依赖下游无法及时扩容导致大量请求失败）
+- 混沌测试。如果随机停掉一些依赖服务，你的服务会有问题么？有没有类似混沌测试保证接口没问题？
+- 接口限流。是匀速限流（leaky bucket 漏桶算法）还是可以允许突发流量（token bucket 令牌桶算法）？限流之后是丢弃还是降级(fallback)？如果是新增接口需要配置限流么？
+- 频率限制。对于一些用户相关接口有没有针对用户操作进行频率限制(比如借助 redis 限制操作频率)？如果接口被恶意刷量了如何处理？
+
+想一下，如果你的服务接口突然 qps 暴增了几十甚至上百倍(比如类似微博热点推送，直播间涌入，轮询接口等)，你的服务能扛得住么？
+各种基础组件 mysql/redis 等会挂掉么？如果扛不住能够限流降级保证服务依然可用么？)
+(很多场景不能保证一定可以及时扩容，基础设施不能保证一定能够扩容成功，这个时候需要从代码框架层面考虑熔断降级)
+笔者之前就因为疏忽，一个接口短时间 qps 翻了几百倍导致接口大量失败。
+
+运营事故(经验发现不少 bug 或者事故来源于错误的运营配置)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+- 用户填写数据校验。对于关键用户配置数据，加上数据格式、类型、范围检查，比如配置中奖概率、商品价格等，防止填错导致重大财务损失。
+- 配置多人审核。比如推送数据，应该建立多人审批流机制，多人审核都无误之后才允许给用户推送数据
+- 配置验证。修改配置后应该在测试环境验证逻辑没有问题之后再发布线上，禁止未经测试直接线上发布
+
+上线部署(超过一半的事故来自于线上代码、配置等的变更)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+- 依赖方检查。依赖的下游服务、实验策略等是否都已经上线。如果是 AB 实验应该上线后回归没问题再开实验放量
+- 配置检查。需要上线提前配置的服务是否都已经配置完成，比如网关配置、业务配置、框架配置、熔断限流配置等(预期高 qps 的接口要配置)等。仔细检查正式和测试环境的配置不要写串了！
+- 小流量观察，分阶段观察。无论是部署服务或者配置还是 AB 实验放量，都应该先小流量观察，每个放量阶段(小流量-10%-50%-100%)应该至少观察 10 分钟。包括后台服务的线上错误日志、请求监控、客户端的崩溃率、基础组件的稳定性、容器的利用率、业务监控告警等，确保没问题再慢慢全量。
+  笔者碰到过后台放量过快导致客户端偶现 bug 崩溃率上升的问题(上线只注意到了后台监控，没想到客户端也可能出问题)。
+- 规范上线。不要在业务流量高峰期、或者上游突然开始放量的时候部署服务，严重的时候可能有雪崩的风险(滚动重启期间扛不住流量雪崩)
+- 变更回滚。如果小流量上线观察期间发现有问题应第一时间回滚及时止损，之后再去排查问题并且复盘。
+- 粒度控制。每次应该只上线一个代码合并（需求或者问题修复），不要多个功能一起上(哪怕只是几行修改最好都分开提交单独上线)。否则一旦出问题掺杂多个提交将难以回滚处理
+- 上线顺序。先上代码还是先上配置。如果先上配置，会有一些新的配置先下发了会不会有问题？如果先上代码读不到新的配置是否有问题？建议在技术文档里规范好上线顺序严格执行，尤其涉及到多方上线依赖顺序的。
+  笔者遇到一个比较坑的问题是，先上了配置，结果代码里有修改数据配置默认值的操作，代码还没上导致这个默认配置字段没有被修改，下发给端上引起了问题。
+
+服务自查
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+- 上线之前请阅读以上内容，详细检查自己的服务是否有缺陷
+
+参考
+--------------------------------------
+- `50 Shades of Go: Traps, Gotchas, and Common Mistakes for New Golang Devs  <http://devs.cloudimmunity.com/gotchas-and-common-mistakes-in-go-golang/>`_
+- `《开发更高质量的服务》 <http://vearne.cc/archives/39164>`_
+- `《Go101 内存内泄漏场景》 <https://gfw.go101.org/article/memory-leaking.html>`_
+- `《SRE实战 实践》 <https://developer.aliyun.com/article/1277547>`_
diff --git a/design/design.rst b/design/design.rst
new file mode 100644
index 00000000..cb0ccf44
--- /dev/null
+++ b/design/design.rst
@@ -0,0 +1,1501 @@
+.. _designpatterns:
+
+用python实现设计模式
+=====================================================================
+
+    Patterns mean "I have run out of language." - Rich Hickey
+
+之前学习设计模式的时候总是没有什么感觉，因为记性不好一直没记住多少。python不像java中比较强调设计模式(编程套路)，动态语言也内置了像是装饰器、迭代器等模式，另外python中的『一切皆对象』、鸭子类型等也导致python中实现的设计模式和其他语言有些差别。根据YAGNI(you
+aren't gonna need it)和KISS(Keep it simple
+sutpid)原则，如果能用简单易懂的方式实现，最好不要滥用设计模式以免增加复杂度和维护难度。本博客是《Mastering
+Python Design
+Patterns》的读书笔记，涵盖大部分设计模式(力求pythonic实现)，有兴趣可以参考下，代码示例版本为python3.5.2。（3.6都发布了，已经被时代遗弃😢......)
+
+--------------
+
+1: The Fctory Pattern(工厂模式: 解决对象创建问题)
+-------------------------------------------------
+
+先来看三种创建模式中的第一种工厂模式。
+解释：处理对象创建，客户端可以申请一个对象而不用知道对象被哪个class创建。可以方便地解耦对象的使用和创建。有两种实现，工厂方法和抽象工厂.
+
+Method(工厂方法)：
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+执行单独的函数，通过传参提供需要的对象的信息。通过一个demo看看例子:
+
+::
+
+    import json
+    import xml.etree.ElementTree as etree
+
+    class JSONConnector:
+        def __init__(self, filepath):
+            self.data = dict()
+            with open(filepath, mode='r', encoding='utf8') as f:
+                self.data = json.load(f)
+
+        @property
+        def parsed_data(self):
+            return self.data
+
+
+    class XMLConnector:
+        def __init__(self, filepath):
+            self.tree = etree.parse(filepath)
+
+        @property
+        def parsed_data(self):
+            return self.tree
+
+
+    def connection_factory(filepath):
+        """ 工厂方法 """
+        if filepath.endswith('json'):
+            connector = JSONConnector
+        elif filepath.endswith('xml'):
+            connector = XMLConnector
+        else:
+            raise ValueError('Cannot connect to {}'.format(filepath))
+        return connector(filepath)
+
+Abstract Factory(抽象工厂: 解决复杂对象创建问题)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+工厂方法适合对象种类较少的情况，如果有多种不同类型对象需要创建，使用抽象工厂模式。以实现一个游戏的例子说明，在一个抽象工厂类里实现多个关联对象的创建：
+
+::
+
+    class Frog:
+        def __init__(self, name):
+            self.name = name
+
+        def __str__(self):
+            return self.name
+
+        def interact_with(self, obstacle):
+            """ 不同类型玩家遇到的障碍不同 """
+            print('{} the Frog encounters {} and {}!'.format(
+                self, obstacle, obstacle.action()))
+
+
+    class Bug:
+        def __str__(self):
+            return 'a bug'
+
+        def action(self):
+            return 'eats it'
+
+
+    class FrogWorld:
+        def __init__(self, name):
+            print(self)
+            self.player_name = name
+
+        def __str__(self):
+            return '\n\n\t----Frog World -----'
+
+        def make_character(self):
+            return Frog(self.player_name)
+
+        def make_obstacle(self):
+            return Bug()
+
+
+    class Wizard:
+        def __init__(self, name):
+            self.name = name
+
+        def __str__(self):
+            return self.name
+
+        def interact_with(self, obstacle):
+            print('{} the Wizard battles against {} and {}!'.format(
+                self, obstacle, obstacle.action()))
+
+
+    class Ork:
+        def __str__(self):
+            return 'an evil ork'
+
+        def action(self):
+            return 'kill it'
+
+
+    class WizardWorld:
+        def __init__(self, name):
+            print(self)
+            self.player_name = name
+
+        def __str__(self):
+            return '\n\n\t------ Wizard World -------'
+
+        def make_character(self):
+            return Wizard(self.player_name)
+
+        def make_obstacle(self):
+            return Ork()
+
+
+    class GameEnvironment:
+        """ 抽象工厂，根据不同的玩家类型创建不同的角色和障碍 (游戏环境)
+        这里可以根据年龄判断，成年人返回『巫师』游戏，小孩返回『青蛙过河』游戏"""
+        def __init__(self, factory):
+            self.hero = factory.make_character()
+            self.obstacle = factory.make_obstacle()
+
+        def play(self):
+            self.hero.interact_with(self.obstacle)
+
+--------------
+
+2: The Builder Pattern(构造模式: 控制复杂对象的构造)
+----------------------------------------------------
+
+当对象需要多个部分组合起来一步步创建，并且创建和表示分离的时候。可以这么理解，你要买电脑，工厂模式直接返回一个你需要型号的电脑，但是构造模式允许你自定义电脑各种配置类型，组装完成后给你。这个过程你可以传入builder从而自定义创建的方式。
+
+::
+
+    # factory pattern
+    MINI14 = '1.4GHz Mac mini'
+
+
+    class AppleFactory:
+
+        class MacMini14:
+            def __init__(self):
+                self.memory = 4  # in gigabytes
+                self.hdd = 500  # in gigabytes
+                self.gpu = 'Intel HD Graphics 5000'
+
+            def __str__(self):
+                info = ('Model: {}'.format(MINI14),
+                        'Memory: {}GB'.format(self.memory),
+                        'Hard Disk: {}GB'.format(self.hdd),
+                        'Graphics Card: {}'.format(self.gpu))
+                return '\n'.join(info)
+
+        def build_computer(self, model):
+            if model == MINI14:
+                return self.MacMini14()
+            else:
+                print("I don't know how to build {}".format(model))
+
+
+    # 使用工厂
+    afac = AppleFactory()
+    mac_mini = afac.build_computer(MINI14)
+    print(mac_mini)
+
+
+    # builder模式
+
+
+    class Computer:
+        def __init__(self, serial_number):
+            self.serial = serial_number
+            self.memory = None      # in gigabytes
+            self.hdd = None         # in gigabytes
+            self.gpu = None
+
+        def __str__(self):
+            info = ('Memory: {}GB'.format(self.memory),
+                    'Hard Disk: {}GB'.format(self.hdd),
+                    'Graphics Card: {}'.format(self.gpu))
+            return '\n'.join(info)
+
+
+    class ComputerBuilder:
+        def __init__(self):
+            self.computer = Computer('AG23385193')
+
+        def configure_memory(self, amount):
+            self.computer.memory = amount
+
+        def configure_hdd(self, amount):
+            self.computer.hdd = amount
+
+        def configure_gpu(self, gpu_model):
+            self.computer.gpu = gpu_model
+
+
+    class HardwareEngineer:
+        def __init__(self):
+            self.builder = None
+
+        def construct_computer(self, memory, hdd, gpu):
+            self.builder = ComputerBuilder()
+            [step for step in (self.builder.configure_memory(memory),
+                            self.builder.configure_hdd(hdd),
+                            self.builder.configure_gpu(gpu))]
+
+        @property
+        def computer(self):
+            return self.builder.computer
+
+    # 使用buidler，可以创建多个builder类实现不同的组装方式
+    engineer = HardwareEngineer()
+    engineer.construct_computer(hdd=500, memory=8, gpu='GeForce GTX 650 Ti')
+    computer = engineer.computer
+    print(computer)
+
+--------------
+
+3:The Prototype Pattern(原型模式:解决对象拷贝问题)
+--------------------------------------------------
+
+这是创建模式中的最后一个，用来克隆一个对象，有点像生物学中的有丝分裂。我们可以使用python内置的copy模块实现。拷贝分为深拷贝和浅拷贝，深拷贝会递归复制并创建新对象，而浅拷贝会利用引用指向同一个对象.深拷贝的优点是对象之间互不影响，但是会耗费资源，创建比较耗时；如果不会修改对象可以使用浅拷贝，更加节省资源和创建时间。
+
+-  "A shallow copy constructs a new compound object and then (to the
+   extent possible) inserts references into it to the objects found in
+   the original.
+-  A deep copy constructs a new compound object and then, recursively,
+   inserts copies into it of the objects found in the original."
+
+::
+
+    import copy
+    from collections import OrderedDict
+
+    class Book:
+        def __init__(self, name, authors, price, **rest):
+            '''Examples of rest: publisher, length, tags, publication
+            date'''
+            self.name = name
+            self.authors = authors
+            self.price = price      # in US dollars
+            self.__dict__.update(rest)
+
+        def __str__(self):
+            mylist = []
+            ordered = OrderedDict(sorted(self.__dict__.items()))
+            for i in ordered.keys():
+                mylist.append('{}: {}'.format(i, ordered[i]))
+                if i == 'price':
+                    mylist.append('$')
+                mylist.append('\n')
+            return ''.join(mylist)
+
+
+    class Prototype:
+        def __init__(self):
+            self.objects = {}
+
+        def register(self, identifier, obj):
+            self.objects[identifier] = obj
+
+        def unregister(self, identifier):
+            del self.objects[identifier]
+
+        def clone(self, identifier, **attr):
+            """ 实现对象拷贝 """
+            found = self.objects.get(identifier)
+            if not found:
+                raise ValueError('Incorrect object identifier: {}'.format(identifier))
+            obj = copy.deepcopy(found)
+            obj.__dict__.update(attr)    # 实现拷贝时自定义更新
+            return obj
+
+
+    def main():
+        b1 = Book('The C Programming Language', ('Brian W. Kernighan', 'Dennis M.Ritchie'),
+                price=118, publisher='Prentice Hall', length=228, publication_date='1978-02-22',
+                tags=('C', 'programming', 'algorithms', 'data structures'))
+
+        prototype = Prototype()
+        cid = 'k&r-first'
+        prototype.register(cid, b1)
+        b2 = prototype.clone(cid, name='The C Programming Language (ANSI)', price=48.99, length=274,
+                            publication_date='1988-04-01', edition=2)
+        for i in (b1, b2):
+            print(i)
+            print("ID b1 : {} != ID b2 : {}".format(id(b1), id(b2)))
+
+
+--------------
+
+4: The Adapter Pattern(适配器模式: 解决接口不兼容问题)
+------------------------------------------------------
+
+开始介绍结构型设计模式，结构型设计模式通过组合对象来实现新功能。适配器模式通过引入间接层来实现不兼容接口之间的适配。现实中最好的例子就是手机充电口，不同型号安卓手机都可以用同样的充电线充电。在python中可以通过继承实现适配，也可以通过使用class的\_\_dict\_\_属性。
+开闭原则：适配器模式和OOP中的开闭原则关系密切，开闭原则强调对扩展开放，对修改关闭。通过适配器模式我们可以通过创建适配器模式在不修改原有类代码的情况下实现新的功能。
+
+::
+
+    class Computer:
+        def __init__(self, name):
+            self.name = name
+
+        def __str__(self):
+            return 'the {} computer'.format(self.name)
+
+        def execute(self):
+            """ call by client code """
+            return 'execute a program'
+
+
+    class Synthesizer:
+        def __init__(self, name):
+            self.name = name
+
+        def __str__(self):
+            return 'the {} synthesizer'.format(self.name)
+
+        def play(self):
+            return 'is playing an electroinc song'
+
+
+    class Human:
+        def __init__(self, name):
+            self.name = name
+
+        def __str__(self):
+            return 'the {} human'.format(self.name)
+
+        def speak(self):
+            return 'says hello'
+
+
+    class Adapter:
+        def __init__(self, obj, adapted_methods):
+            """ 不使用继承，使用__dict__属性实现适配器模式 """
+            self.obj = obj
+            self.__dict__.update(adapted_methods)
+
+        def __str__(self):
+            return str(self.obj)
+
+
+    # 适配器使用示例
+    def main():
+        objs = [Computer('Asus')]
+        synth = Synthesizer('moog')
+        objs.append(Adapter(synth, dict(execute=synth.play)))
+        human = Human('Wnn')
+        objs.append(Adapter(human, dict(execute=human.speak)))
+
+        for o in objs:
+            # 用统一的execute适配不同对象的方法，这样在无需修改源对象的情况下就实现了不同对象方法的适配
+            print('{} {}'.format(str(o), o.execute()))
+
+
+    if __name__ == "__main__":
+        main()
+
+--------------
+
+5: The Decorator Pattern(装饰器模式： 无需子类化实现扩展对象功能问题)
+---------------------------------------------------------------------
+
+通常给一个对象添加新功能有三种方式： - 直接给对象所属的类添加方法。 -
+使用『组合』 - 使用『继承』，优先使用组合而非继承。
+装饰器模式提供了第四种选择，通过动态改变对象扩展对象功能。其他编程语言通常使用继承实现装饰器装饰器模式，而python内置了装饰器。装饰器有很多用途，比如数据校验，事务处理，缓存，日志等。比如用装饰器实现一个简单的缓存，python3.5自带了functools.lru\_cache
+
+::
+
+    from functools import wraps
+
+    def memoize(fn):
+        known = dict()
+
+        @wraps(fn)
+        def memoizer(*args):
+            if args not in known:
+                known[args] = fn(*args)
+            return known[args]
+        return memoizer
+
+
+    @memoize
+    def fibonacci(n):
+        assert(n >= 0), 'n must be >= 0'
+        return n if n in (0, 1) else fibonacci(n-1) + fibonacci(n-2)
+
+--------------
+
+6: The Facade Pattern(外观模式: 简化复杂对象的访问问题)
+-------------------------------------------------------
+
+外观模式用来简化复杂系统的访问。通过简化的接口只访问需要的部分，隐藏系统复杂性。想象一下公司接线员，虽然公司内部运行机制比较复杂，但是接线员可以迅速帮你解决特定问题。
+我们以实现个简单的操作系统示例说明外观模式：
+
+::
+
+    from abc import ABCMeta, abstractmethod
+    from enum import Enum
+
+    State = Enum('State', 'new running sleeping restart zombie')
+
+
+    class Server(metaclass=ABCMeta):
+        """ 抽象基类 """
+        @abstractmethod
+        def __init__(self):
+            pass
+
+        def __str__(self):
+            return self.name
+
+        @abstractmethod
+        def boot(self):
+            pass
+
+        @abstractmethod
+        def kill(self, restart=True):
+            pass
+
+
+    class FileServer(Server):
+        def __init__(self):
+            '''actions required for initializing the file server'''
+            self.name = 'FileServer'
+            self.state = State.new
+
+        def boot(self):
+            print('booting the {}'.format(self))
+            '''actions required for booting the file server'''
+            self.state = State.running
+
+        def kill(self, restart=True):
+            print('Killing {}'.format(self))
+            '''actions required for killing the file server'''
+            self.state = State.restart if restart else State.zombie
+
+        def create_file(self, user, name, permissions):
+            '''check validity of permissions, user rights, etc.'''
+            print("trying to create the file '{}' for user '{}' with permissions {}".format(name, user, permissions))
+
+    class ProcessServer(Server):
+        def __init__(self):
+            '''actions required for initializing the process server'''
+            self.name = 'ProcessServer'
+            self.state = State.new
+
+        def boot(self):
+            print('booting the {}'.format(self))
+            '''actions required for booting the process server'''
+            self.state = State.running
+
+        def kill(self, restart=True):
+            print('Killing {}'.format(self))
+            '''actions required for killing the process server'''
+            self.state = State.restart if restart else State.zombie
+
+        def create_process(self, user, name):
+            '''check user rights, generate PID, etc.'''
+            print("trying to create the process '{}' for user '{}'".format(name, user))
+
+
+    class OperatingSystem:
+        ''' 实现外观模式，外部使用的代码不必知道 FileServer 和 ProcessServer的
+        内部机制，只需要通过 OperatingSystem类调用'''
+        def __init__(self):
+            self.fs = FileServer()
+            self.ps = ProcessServer()
+
+        def start(self):
+            """ 被客户端代码使用 """
+            [i.boot() for i in (self.fs, self.ps)]
+
+        def create_file(self, user, name, permissions):
+            return self.fs.create_file(user, name, permissions)
+
+        def create_process(self, user, name):
+            return self.ps.create_process(user, name)
+
+    def main():
+        os = OperatingSystem()
+        os.start()
+        os.create_file('foo', 'hello', '-rw-r-r')
+        os.create_process('bar', 'ls /tmp')
+
+    main()
+
+--------------
+
+7: The Flyweight Pattern(享元模式: 实现对象复用从而改善资源使用)
+-------------------------------------------------------------------
+
+    Flyweight design pattern is a technique used to minimize memory usage and improve performance by introducing data sharing between similar objects.
+
+OOP编程中容易出现对象创建带来的性能和内存占用问题，需要满足以下条件：
+
+- 需要使用大量对象(python里我们可以用\_\_slots\_\_节省内存占用)
+- 对象太多难以存储或解析大量对象。
+- 对象识别不是特别重要，共享对象中对象比较会失败。
+
+经常使用对象池技术来实现共享对象，比如数据库中经常使用连接池来减少开销，预先建立一些连接池，每次取一个连接和数据库交互。
+
+::
+
+    # 使用对象池技术实现享元模式
+    import random
+    from enum import Enum
+    TreeType = Enum('TreeType', 'apple_tree cherry_tree peach_tree')
+
+
+    class Tree:
+        pool = dict()
+
+        def __new__(cls, tree_type):
+            obj = cls.pool.get(tree_type, None)
+            if obj is None:
+                obj = object.__new__(cls)
+                cls.pool[tree_type] = obj
+                obj.tree_type = tree_type
+            return obj
+
+        def render(self, age, x, y):
+            print('render a tree of type {} and age {} at ({}, {})'.format(self.tree_type, age, x, y))
+
+
+    def main():
+        rnd = random.Random()
+        age_min, age_max = 1, 30    # in years
+        min_point, max_point = 0, 100
+        tree_counter = 0
+
+        for _ in range(10):
+            t1 = Tree(TreeType.apple_tree)
+            t1.render(rnd.randint(age_min, age_max),
+                    rnd.randint(min_point, max_point),
+                    rnd.randint(min_point, max_point))
+            tree_counter += 1
+
+        for _ in range(3):
+            t2 = Tree(TreeType.cherry_tree)
+            t2.render(rnd.randint(age_min, age_max),
+                    rnd.randint(min_point, max_point),
+                    rnd.randint(min_point, max_point))
+            tree_counter += 1
+
+        for _ in range(5):
+            t3 = Tree(TreeType.peach_tree)
+            t3.render(rnd.randint(age_min, age_max),
+                    rnd.randint(min_point, max_point),
+                    rnd.randint(min_point, max_point))
+            tree_counter += 1
+        print('trees rendered: {}'.format(tree_counter))
+        print('trees actually created: {}'.format(len(Tree.pool)))
+        t4 = Tree(TreeType.cherry_tree)
+        t5 = Tree(TreeType.cherry_tree)
+        t6 = Tree(TreeType.apple_tree)
+        print('{} == {}? {}'.format(id(t4), id(t5), id(t4) == id(t5)))
+        print('{} == {}? {}'.format(id(t5), id(t6), id(t5) == id(t6)))
+
+
+    if __name__ == '__main__':
+        main()
+
+--------------
+
+8: The Model-View-Controller Pattern(mvc模式：解耦展示逻辑和业务逻辑)
+---------------------------------------------------------------------
+
+    When using MVC, make sure that you creating smart models (core
+    functionality), thin controllers (functionality required for the
+    communication between the view and the controller), and dumb views
+    (representation and minimal processing).
+
+MVC模式既是一种设计模式，也是软件架构模式。比如流行的django框架就是mvc(MTV)模式。Model层负责和数据库交互，View层负责展现逻辑，Controller层负责粘合Model和View层，将各个部分解耦，使代码更易扩展和维护。
+
+::
+
+    quotes = ('A man is not complete until he is married. Then he is finished.',
+            'As I said before, I never repeat myself.',
+            'Behind a successful man is an exhausted woman.',
+            'Black holes really suck...', 'Facts are stubborn things.')
+
+
+    class QuoteModel:
+        def get_quote(self, n):
+            try:
+                return quotes[n]
+            except IndexError:
+                return 'Not found'
+
+
+    class QuoteTerminalView:
+
+        def show(self, quote):
+            print('And the quote is: "{}"'.format(quote))
+
+        def error(self, msg):
+            print('Error: {}'.format(msg))
+
+        def select_quote(self):
+            return input('Which quote number would you like to see? ')
+
+
+
+    class QuoteTerminalController:
+        def __init__(self):
+            self.model = QuoteModel()
+            self.view = QuoteTerminalView()
+
+        def run(self):
+            valid_input = False
+            while not valid_input:
+                n = self.view.select_quote()
+                try:
+                    n = int(n)
+                except ValueError:
+                    self.view.error("Incorrect index '{}'".format(n))
+                else:
+                    valid_input = True
+                    quote = self.model.get_quote(n)
+                    self.view.show(quote)
+
+
+    def main():
+        controller = QuoteTerminalController()
+        while True:
+            controller.run()
+
+--------------
+
+9: The Proxy Pattern(代理模式：通过一层间接保护层实现更安全的接口访问）
+-----------------------------------------------------------------------
+
+在访问真正的对象之前做一些操作。有四种常用的代理类型:
+
+-  A remote
+   proxy.使得访问远程对象就像本地访问一样，例如网络服务器。隐藏复杂性，使得访问本地远程统一。比如ORM
+-  A virtual
+   proxy。用来实现延迟访问，比如一些需要复杂计算的对象，python里可以实现lazy\_property，性能改善
+-  A protection/protective proxy.
+   控制敏感对象的访问，加上一层保护层，实现安全控制
+-  A smart(reference) proxy.
+   访问对象时加上一层额外操作，例如引用计数和线程安全检查。weakref.proxy()
+
+代理模式的功能还是很强大的，先来看看使用描述符实现LazyProperty，在对象创建以后第一次访问才会真正生成
+
+::
+
+    class LazyProperty:
+        """ 用描述符实现延迟加载的属性 """
+        def __init__(self, method):
+            self.method = method
+            self.method_name = method.__name__
+
+        def __get__(self, obj, cls):
+            if not obj:
+                return None
+            value = self.method(obj)
+            print('value {}'.format(value))
+            setattr(obj, self.method_name, value)
+            return value
+
+
+    class Test:
+        def __init__(self):
+            self.x = 'foo'
+            self.y = 'bar'
+            self._resource = None
+
+        @LazyProperty
+        def resource(self):    # 构造函数里没有初始化，第一次访问才会被调用
+            print('initializing self._resource which is: {}'.format(self._resource))
+            self._resource = tuple(range(5))    # 模拟一个耗时计算
+            return self._resource
+
+
+    def main():
+        t = Test()
+        print(t.x)
+        print(t.y)
+        # 访问LazyProperty, resource里的print语句只执行一次，实现了延迟加载和一次执行
+        print(t.resource)
+        print(t.resource)
+
+
+    main()
+
+再看那个用代理实现安全控制的例子，我们给SensitiveInfo里的add操作加上密钥验证，例子也很简单
+
+::
+
+    class SensitiveInfo:
+        def __init__(self):
+            self.users = ['nick', 'tom', 'ben', 'mike']
+
+        def read(self):
+            print('There are {} users: {}'.format(len(self.users), ' '.join(self.users)))
+
+        def add(self, user):
+            self.users.append(user)
+            print('Added user {}'.format(user))
+
+
+    class Info:
+        '''protection proxy to SensitiveInfo'''
+        def __init__(self):
+            self.protected = SensitiveInfo()
+            # 为了方便示例这里直接写死在代码里，为了安全不应该这么做
+            self.secret = '0xdeadbeef'
+
+        def read(self):
+            self.protected.read()
+
+        def add(self, user):
+            """ 给add操作加上密钥验证，保护add操作"""
+            sec = input('what is the secret? ')
+            self.protected.add(user) if sec == self.secret else print("That's wrong!")
+
+
+    def main():
+        info = Info()
+        while True:
+            print('1. read list |==| 2. add user |==| 3. quit')
+            key = input('choose option: ')
+            if key == '1':
+                info.read()
+            elif key == '2':
+                name = input('choose username: ')
+                info.add(name)
+            elif key == '3':
+                exit()
+            else:
+                print('unknown option: {}'.format(key))
+    main()
+
+上面这个示例有几个缺点 1.
+SensitiveInfo可以被直接实例化使用，绕过Info类，可以考虑使用abc模块避免SensitiveInfo被直接实例化
+2. 密钥直接写死在代码里，应该用安全性较高密钥写到配置或者环境变量里
+
+我们使用抽象基类来修正第一个缺陷，只需要修正类代码而不用改main函数里的使用代码
+
+::
+
+    from abc import ABCMeta, abstractmethod
+
+
+    class SensitiveInfo(metaclass=ABCMeta):
+        def __init__(self):
+            self.users = ['nick', 'tom', 'ben', 'mike']
+
+        @abstractmethod
+        def read(self):
+            pass
+
+        @abstractmethod
+        def add(self, user):
+            pass
+
+
+    class Info(SensitiveInfo):
+        '''protection proxy to SensitiveInfo'''
+        def __init__(self):
+            # self.protected = SensitiveInfo()
+            super().__init__()
+            self.secret = '0xdeadbeef'    # 为了方便示例这里直接写死在代码里
+
+        def read(self):
+            print('There are {} users: {}'.format(len(self.users), ' '.join(self.users)))
+
+        def add(self, user):
+            """ 给add操作加上密钥验证，保护add操作"""
+            sec = input('what is the secret? ')
+            self.users.append(user) if sec == self.secret else print("That's wrong!")
+
+--------------
+
+10: The Chain of Responsibility Pattern (责任链模式:创建链式对象用来接收广播消息)
+---------------------------------------------------------------------------------
+
+    The Chain of Responsibility pattern is used when we want to give a
+    chance to multiple objects to satisfy a single request, or when we
+    don't know which object (from a chain of objects) should process a
+    specific request in advance.
+
+开始介绍行为型设计模式，行为型设计模式处理对象之间的交互和算法问题。在责任连模式中，我们把消息发送给一系列对象的首个节点，对象可以选择处理消息或者向下一个对象传递,只有对消息感兴趣的节点处理。用来解耦发送者和接收者。在python里通过dynamic
+dispatching来实现，以一个事件驱动系统来说明：
+
+::
+
+    class Event:
+        def __init__(self, name):
+            self.name = name
+
+        def __str__(self):
+            return self.name
+
+
+    class Widget:
+
+        """Docstring for Widget. """
+
+        def __init__(self, parent=None):
+            self.parent = parent
+
+        def handle(self, event):
+            handler = 'handle_{}'.format(event)
+            if hasattr(self, handler):
+                method = getattr(self, handler)
+                method(event)
+            elif self.parent:
+                self.parent.handle(event)
+            elif hasattr(self, 'handle_default'):
+                self.handle_default(event)
+
+
+    class MainWindow(Widget):
+        def handle_close(self, event):
+            print('MainWindow: {}'.format(event))
+
+        def handle_default(self, event):
+            print('MainWindow: Default {}'.format(event))
+
+
+    class SendDialog(Widget):
+        def handle_paint(self, event):
+            print('SendDialog: {}'.format(event))
+
+
+    class MsgText(Widget):
+        def handle_down(self, event):
+            print('MsgText: {}'.format(event))
+
+
+    def main():
+        mw = MainWindow()
+        sd = SendDialog(mw)    # parent是mw
+        msg = MsgText(sd)
+
+        for e in ('down', 'paint', 'unhandled', 'close'):
+            evt = Event(e)
+            print('\nSending event -{}- to MainWindow'.format(evt))
+            mw.handle(evt)
+            print('Sending event -{}- to SendDialog'.format(evt))
+            sd.handle(evt)
+            print('Sending event -{}- to MsgText'.format(evt))
+            msg.handle(evt)
+
+    if __name__ == "__main__":
+        main()
+
+--------------
+
+11: The Command Pattern(命令模式：用来给应用添加Undo操作)
+---------------------------------------------------------
+
+命令模式帮助我们把一个操作(undo,redo,copy,paste等)封装成一个对象，通常是创建一个包含Operation所有逻辑和方法的类。
+通过命令模式可以控制命令的执行时间和过程，还可以用来组织事务。
+用一些文件操作类来说明命令模式的使用
+
+::
+
+    import os
+
+    class RenameFile:
+
+        def __init__(self, path_src, path_dest):
+            """ 在init里保存一些必要信息，比如undo需要的老的和新的文件名 """
+            self.src, self.dest = path_src, path_dest
+
+        def execute(self, verbose=False):
+            if verbose:
+                print("[renaming '{}' to '{}']".format(self.src, self.dest))
+            os.rename(self.src, self.dest)
+
+        def undo(self, verbose=False):
+            if verbose:
+                print("[renaming '{}' back to '{}']".format(self.dest, self.src))
+            os.rename(self.dest, self.src)
+
+
+    def delete_file(path, verbose=False):
+        if verbose:
+            print("deleting file '{}".format(path))
+        os.remove(path)
+
+
+    class CreateFile:
+        def __init__(self, path, txt='hello world\n'):
+            self.path, self.txt = path, txt
+
+        def execute(self, verbose=False):
+            if verbose:
+                print("[creating file '{}']".format(self.path))
+            with open(self.path, mode='w', encoding='utf-8') as out_file:
+                out_file.write(self.txt)
+
+        def undo(self):
+            delete_file(self.path)
+
+
+    class ReadFile:
+        def __init__(self, path):
+            self.path = path
+
+        def execute(self, verbose=False):
+            if verbose:
+                print("[reading file '{}']".format(self.path))
+            with open(self.path, mode='r', encoding='utf-8') as in_file:
+                print(in_file.read(), end='')
+
+
+    def main():
+        orig_name, new_name = 'file1', 'file2'
+        commands = []
+        for cmd in CreateFile(orig_name), ReadFile(orig_name), RenameFile(orig_name, new_name):
+            commands.append(cmd)
+        [c.execute() for c in commands]
+
+        answer = input('reverse the executed commands? [y/n] ')
+        if answer not in 'yY':
+            print("the result is {}".format(new_name))
+            exit()
+        for c in reversed(commands):
+            try:
+                c.undo()   # 执行undo
+            except AttributeError:
+                pass
+
+    main()
+
+--------------
+
+12: The Interpreter Pattern(解释器模式：用来实现Domain Specific Language(DSL))
+------------------------------------------------------------------------------
+
+本章我们实现一个简单的控制大门Gate类的DSL。使用\ `pyparsing <http://infohost.nmt.edu/tcc/help/pubs/pyparsing/web/index.html>`__\ 来解析我们定义的控制大门的语法命令。
+pyparsing自带了很多有用的函数和类帮助我们从文本中抽取需要的信息，比如我们方便地处理c++源文件中的注释:(``pip install pyparsing``)
+
+::
+
+    >>> text = '// Look out of the yard? What will we see?'
+    >>> print pp.cppStyleComment.parseString(text)
+    ['// Look out of the yard? What will we see?']
+    >>> print pp.cppStyleComment.parseString('/* Author: R. J. Gumby */')
+    ['/* Author: R. J. Gumby */']
+
+再比如我们一句话就可以去除C++源码中的注释:
+
+::
+
+    from pyparsing import cppStyleComment
+    code = """
+    /* Hello World program */
+
+    #include<stdio.h>
+
+    main()
+    {
+        printf("Hello World");    // print hello
+
+    }
+    """
+    print(cppStyleComment.suppress().transformString(code))
+
+下面实现我们的控制Gate的DSL
+
+::
+
+    from pyparsing import Word, OneOrMore, Optional, Group, Suppress, alphanums
+
+    class Gate:
+        def __init__(self):
+            self.is_open = False
+
+        def __str__(self):
+            return 'open' if self.is_open else 'closed'
+
+        def open(self):
+            print('opening the gate')
+            self.is_open = True
+
+        def close(self):
+            print('closing the gate')
+            self.is_open = False
+
+
+    def main():
+        # 首先定义我们的DSL格式，我们这里最简单的控制语法就是   "open -> gate"
+        word = Word(alphanums)
+        command = Group(OneOrMore(word))
+        token = Suppress("->")
+        device = Group(OneOrMore(word))
+        argument = Group(OneOrMore(word))
+        event = command + token + device + Optional(token + argument)
+
+        gate = Gate()
+        cmds = ['open -> gate', 'close -> gate']    # 两个自定义的命令
+        open_actions = {'gate': gate.open}
+        close_actions = {'gate': gate.close}
+
+        for cmd in cmds:
+            print(event.parseString(cmd))    # [['open'], ['gate']]
+            cmd, dev = event.parseString(cmd)
+            cmd_str, dev_str = ' '.join(cmd), ' '.join(dev)
+            print(cmd_str, dev_str)
+            if 'open' in cmd_str:
+                open_actions[dev_str]()
+            elif 'close' in cmd_str:
+                close_actions[dev_str]()
+
+    if __name__ == "__main__":
+        main()
+
+这样就实现了一个简单的大门控制语言，不过功能很弱。
+
+--------------
+
+13: The Observer Pattern(发布订阅模式：用来处理多个对象之间的发布订阅问题)
+--------------------------------------------------------------------------
+
+如果用过blinker库或者redis的pub，sub，对发布订阅应该会比较熟悉。该模式用在当一个对象的状态变更需要通知其他很多对象的时候，比如rss订阅或者在社交网站上订阅某个频道的更新。事件驱动系统也是一种发布订阅模式，事件作为发布者，监听器作为订阅者，只不过这里多个事件监听器可以监听同一个事件。
+我们这里实现一个"Data
+Formatter"来解释发布订阅模式，一种数据可以有多个格式化Formatter，当数据更新的时候，会通知所有的Formatter格式化新的数据。使用继承来实现。
+
+::
+
+    class Publisher:
+        def __init__(self):
+            self.observers = []
+
+        def add(self, observer):
+            if observer not in self.observers:
+                self.observers.append(observer)
+            else:
+                print('Failed to add : {}').format(observer)
+
+        def remove(self, observer):
+            try:
+                self.observers.remove(observer)
+            except ValueError:
+                print('Failed to remove : {}').format(observer)
+
+        def notify(self):
+            [o.notify_by(self) for o in self.observers]
+
+
+    class DefaultFormatter(Publisher):
+        def __init__(self, name):
+            super().__init__()
+            self.name = name
+            self._data = 0
+
+        def __str__(self):
+            return "{}: '{}' has data = {}".format(
+                type(self).__name__, self.name, self._data)
+
+        @property
+        def data(self):
+            return self._data
+
+        @data.setter
+        def data(self, new_value):
+            try:
+                self._data = int(new_value)
+            except ValueError as e:
+                print('Error: {}'.format(e))
+            else:
+                self.notify()    # data 在被合法赋值以后会执行notify
+
+
+    class HexFormatter:
+        """ 订阅者 """
+        def notify_by(self, publisher):
+            print("{}: '{}' has now hex data = {}".format(
+                type(self).__name__, publisher.name, hex(publisher.data)))
+
+
+    class BinaryFormatter:
+        """ 订阅者 """
+        def notify_by(self, publisher):
+            print("{}: '{}' has now bin data = {}".format(
+                type(self).__name__, publisher.name, bin(publisher.data)))
+
+
+    if __name__ == "__main__":
+        df = DefaultFormatter('test1')
+        print(df)
+        print()
+        hf = HexFormatter()
+        df.add(hf)
+        df.data = 3
+        print(df)
+
+        print()
+        bf = BinaryFormatter()
+        df.add(bf)
+        df.data = 21
+        print(df)
+
+--------------
+
+14: The State Pattern(状态模式：实现有限状态机)
+-----------------------------------------------
+
+    A state machine is an abstract machine that has two key components:
+    states and transitions. A state is the current (active) status of a
+    system. A transition is the switch from one state to another. A
+    state meachine has only one active state at a specific point in the
+    time.
+
+我们通过实现操作系统中进程的生命周期来演示状态模式的使用：
+
+::
+
+    # 先装下pip3 install state_machine
+    from state_machine import (
+        acts_as_state_machine, State, Event, before, after, InvalidStateTransition
+    )
+
+
+    @acts_as_state_machine
+    class Process:
+        # 先来定义状态机的状态 states
+        created = State(initial=True)    # 初始状态
+        waiting = State()
+        running = State()
+        terminated = State()
+        blocked = State()
+        swapped_out_waiting = State()
+        swapped_out_blocked = State()
+
+        # 再定义状态机的转移 transitions
+        wait = Event(from_states=(created, running, blocked,
+                                swapped_out_waiting), to_state=waiting)
+        run = Event(from_states=waiting, to_state=running)
+        terminate = Event(from_states=running, to_state=terminated)
+        block = Event(from_states=(running, swapped_out_blocked),
+                    to_state=blocked)
+        swap_wait = Event(from_states=waiting, to_state=swapped_out_waiting)
+        swap_block = Event(from_states=blocked, to_state=swapped_out_blocked)
+
+        def __init__(self, name):
+            self.name = name
+
+        # The state_machine module provides us with the @before and @after
+        # decorators that can be used to execute actions before or after a
+        # transition occurs, respectfully.
+        @after('wait')
+        def wait_info(self):
+            print('{} entered waiting mode'.format(self.name))
+
+        @after('run')
+        def run_info(self):
+            print('{} is running'.format(self.name))
+
+        @before('terminate')
+        def terminate_info(self):
+            print('{} terminated'.format(self.name))
+
+        @after('block')
+        def block_info(self):
+            print('{} is blocked'.format(self.name))
+
+        @after('swap_wait')
+        def swap_wait_info(self):
+            print('{} is swapped out and waiting'.format(self.name))
+
+        @after('swap_block')
+        def swap_block_info(self):
+            print('{} is swapped out and blocked'.format(self.name))
+
+
+    def transition(process, event, event_name):
+        """
+        Args:
+            process (Process obj):
+            event (Event obj): wait, run, terminate...
+            event_name (str): name of event
+        """
+        try:
+            event()
+        except InvalidStateTransition:
+            print('Error: transition of {} from {} to {} failed'.format(
+                process.name, process.current_state, event_name))
+
+
+    def state_info(process):
+        """ 当前状态机的状态 """
+        print('state of {}: {}'.format(process.name, process.current_state))
+
+    if __name__ == "__main__":
+        RUNNING = 'running'
+        WAITING = 'waiting'
+        BLOCKED = 'blocked'
+        TERMINATED = 'terminated'
+        p1, p2 = Process('process1'), Process('process2')
+        [state_info(p) for p in (p1, p2)]
+
+        print()
+        transition(p1, p1.wait, WAITING)
+        transition(p2, p2.terminate, TERMINATED)
+        [state_info(p) for p in (p1, p2)]
+        print()
+        transition(p1, p1.run, RUNNING)
+        transition(p2, p2.wait, WAITING)
+        [state_info(p) for p in (p1, p2)]
+        print()
+        transition(p2, p2.run, RUNNING)
+        [state_info(p) for p in (p1, p2)]
+        print()
+        [transition(p, p.block, BLOCKED) for p in (p1, p2)]
+        [state_info(p) for p in (p1, p2)]
+        print()
+        [transition(p, p.terminate, TERMINATED) for p in (p1, p2)]
+        [state_info(p) for p in (p1, p2)]
+
+--------------
+
+15: The Strategy Pattern(策略模式：动态选择算法策略)
+----------------------------------------------------
+
+现实中往往解决问题的方式不止一种，我们可能需要根据问题的特征选择最优的实现策略，以排序算法为例子，挑选一个合适的排序算法的时候，需要考虑待排序数组的以下特征：
+
+
+- 元素个数。算法输入规模，大部分排序算法在输入规模很小的时候效率相差不大，只有一部分nlogn平均时间复杂度的适合排序大规模。
+- 最好/平均/最坏时间复杂度.这个往往是挑选排序算法时候优先考虑的。
+- 空间复杂度。是不是原地排序(inplace),需要额外的空间吗？在内存限制苛刻的时候就需要考虑
+- 稳定性。排序算法是稳定的吗？稳定是指相同大小的值排序后保持相对顺序。
+- 实现复杂度。算法是否容易实现，其他大致相同的情况下，优先考虑易维护的代码。
+
+策略模式允许我们根据待处理数据的特征灵活选用当前特征下最优的实现，比如常见库的排序算法一般都是混合了多种排序算法的实现，python使用的是Tim
+Peters在2002年设计的结合了合并排序和插入排序的\ `Timsort <https://en.wikipedia.org/wiki/Timsort>`__.
+函数在python里是一等公民，可以简化策略模式的实现。
+
+::
+
+    def f1(seq):
+        pass
+
+    def f2(seq):
+        pass
+
+    def f(seq):
+        if len(seq) >= threshold_value:    # 大于某个阈值
+            f1(seq)    # 在数量较多时候具有良好的效率
+        else:
+            f2(seq)
+
+--------------
+
+16: The Template Pattern(模板模式：抽象出算法公共部分从而实现代码复用)
+----------------------------------------------------------------------
+
+    Don't repeat yourself.
+
+模板模式中，我们可以把代码中重复的部分抽出来作为一个新的函数，把可变的部分作为函数参数，从而消除代码冗余。实际上这种模式在代码重构的时候是经常使用的 ，使用一个有意思的例子来说明下，请安装\ ``pip install cowpy``\ ，真有人闲的\*疼写这个玩意
+
+::
+
+    from cowpy import cow
+
+
+    def dots_style(msg):
+        msg = msg.capitalize()
+        msg = '.' * 10 + msg + '.' * 10
+        return msg
+
+
+    def admire_style(msg):
+        msg = msg.upper()
+        return '!'.join(msg)
+
+
+    def cow_style(msg):
+        msg = cow.milk_random_cow(msg)
+        return msg
+
+
+    def generate_banner(msg, style=dots_style):
+        print('-- start of banner --')
+        print(style(msg))
+        print('-- end of banner --\n\n')
+
+
+    def main():
+        msg = 'happy coding'
+        [generate_banner(msg, style) for style in (dots_style, admire_style,
+                                                cow_style)]
+
+    if __name__ == "__main__":
+        main()
+
+
+    """
+    -- start of banner --
+    ..........Happy coding..........
+    -- end of banner --
+
+
+    -- start of banner --
+    H!A!P!P!Y! !C!O!D!I!N!G
+    -- end of banner --
+
+
+    -- start of banner --
+    ______________
+    < happy coding >
+    --------------
+        o
+        o
+        ^__^         /
+        (**)\_______/  _________
+        (__)\       )=(  ____|_ \_____
+    U    ||----w |  \ \     \_____ |
+            ||     ||   ||           ||
+    -- end of banner --
+    """
+
+--------------
+
+单例模式: 使得一个类最多生成一个实例。
+--------------------------------------
+
+    Design ptterns are discoverd, not invented. - Alex Martelli
+
+很奇怪，本书讲完了都没有讲到单例模式。python的单例模式有各种实现，元类、装饰器等，但是还有一种说法：
+
+    I don't really see the need, as a module with functions (and not a
+    class) would serve well as a singleton. All its variables would be
+    bound to the module, which could not be instantiated repeatedly
+    anyway. If you do wish to use a class, there is no way of creating
+    private classes or private constructors in Python, so you can't
+    protect against multiple instantiations, other than just via
+    convention in use of your API. I would still just put methods in a
+    module, and consider the module as the singleton.
+
+也就是说，实际上，python中，如果我们只用一个实例，直接这么写就行
+
+::
+
+    # some module.py
+    class SingletonClass:
+        pass
+
+    # 在别处我们想用这个实例都直接使用 module.single_instance 这个实例就好。
+    # 这是最简单也是最直观的一种方式,嗯，直接导入这个实例用，而不是导入class，简单粗暴
+    single_instance = SingletonClass()
+
+`Is there a simple, elegant way to define singletons in Python?
+[closed] <http://stackoverflow.com/questions/31875/is-there-a-simple-elegant-way-to-define-singletons-in-python>`__
+
+其他实现：
+
+::
+
+    # http://stackoverflow.com/questions/6760685/creating-a-singleton-in-python
+    class BaseClass:
+        pass
+
+
+    # 装饰器实现
+    def singleton(class_):
+        instances = {}
+        def getinstance(*args, **kwargs):
+            if class_ not in instances:
+                instances[class_] = class_(*args, **kwargs)
+            return instances[class_]
+        return getinstance
+
+
+    @singleton
+    class MyClass(BaseClass):
+        pass
+
+
+    class Singleton(object):
+        _instance = None
+        def __new__(class_, *args, **kwargs):
+            if not isinstance(class_._instance, class_):
+                class_._instance = object.__new__(class_, *args, **kwargs)
+            return class_._instance
+
+
+    class MyClass(Singleton, BaseClass):
+    pass
+
+
+    # 元类实现
+    class Singleton(type):
+        _instances = {}
+        def __call__(cls, *args, **kwargs):
+            if cls not in cls._instances:
+                cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
+            return cls._instances[cls]
+
+
+    # Python2
+    class MyClass(BaseClass):
+        __metaclass__ = Singleton
+
+    # Python3
+    class MyClass(BaseClass, metaclass=Singleton):
+        pass
+
+
+--------------
+
+面向过程与面向对象
+--------------------------------------
+设计模式讲完了，来看看python中OOP的相关东西。
+
+- 过程式: 基本都是一个个函数(function)来实现功能，你给我一些参数，我对参数做出各种操作，返回需要的结果。
+- 面向对象：把资源抽象成一个类，数据(data)和方法(method)的集合。在构造函数中进行数据属性的初始化，在方法中进行对象数据的各种操作。实际在python里，一切皆对象。
+
+哪种方式更好这个我暂时没有定论，编程规范也不会说强制你使用哪种风格。编码中往往没有绝对正确的，只有相对更优的，如果不好定论，那就一致，易读，易用，易维护的风格优先。一般来说，能用函数实现的优先使用函数，相比类更简单易维护。如果多个过程共享一些状态（操作+数据），这时候使用类就比较适合。使用类的时候尽量保持继承层级简单，如果同样可以完成功能，优先使用组合而非继承。
+
+--------------
+
+python中的抽象基类
+--------------------------------------
+在python中我们可以使用内置的abc(abstract base class)模块来实现抽象基类。什么时候需要一个抽象基类呢？
+
+- 抽象基类是没法被实例化的。
+- 抽象基类中定义抽象方法强制子类去实现，没有实现抽象方法的子类也无法实例化。
+
+::
+
+    # 为了实现这两个特性，我们可以这么写
+    class Base:
+        def foo(self):
+            raise NotImplementedError()
+
+        def bar(self):
+            raise NotImplementedError()
+
+    class Concrete(Base):
+        def foo(self):
+            return 'foo() called'
+
+        # Oh no, we forgot to override bar()...
+        # def bar(self):
+        #     return "bar() called"
+
+但是这么写依然可以实例化Base，python2.6以后引入了abc模块帮助我们实现这个功能。
+
+::
+
+    from abc import ABCMeta, abstractmethod
+
+    class Base(metaclass=ABCMeta):
+        @abstractmethod
+        def foo(self):
+            pass
+
+        @abstractmethod
+        def bar(self):
+            pass
+
+    class Concrete(Base):
+        def foo(self):
+            pass
+        # We forget to declare bar() again...
+
+使用这种方式如果没有在子类里实现bar方法你是没有办法实例化子类的。合理使用抽象基类定义明确的接口。另外应该优先使用collections定义的抽象基类，比如要实现一个容器我们可以继承 collections.Container
+
+* `《Abstract Base Classes in Python》 <https://dbader.org/blog/abstract-base-classes-in-python>`_
+* `《http://stackoverflow.com/questions/3570796/why-use-abstract-base-classes-in-python》 <http://stackoverflow.com/questions/3570796/why-use-abstract-base-classes-in-python>`_
+* `《https://docs.python.org/2/library/abc.html》 <https://docs.python.org/2/library/abc.html>`_
+
+
+--------------
+
+python中的Mixin
+--------------------------------------
+Mixin是为了给一个类扩充功能用的，它也没法被实例化。我们可以在Mixin类里实现一些方法给类扩充功能，合理使用mixin也能避免复杂的继承关系。你可能会问了，那为啥不直接写在类里头，比如用@staticmethod方法(我就有这个疑问)？我的理解是这样的，为了『高内聚』。如果你用过pylint检测代码，你会发现你在写类的一个方法时，如果在写一个method时没有使用到任何self里的东西，pylint会给提示『R0201 Method could be a function [pylint]』，意思是这个方法可以可以单独写成一个函数，不必要写在类里。也就是说，只有一个类里实现的方法都是使用了self里的数据时才能成为高内聚的（我不知道我这样理解对不对）。例子：flask_login插件有个UserMixin给定义的用户类实现登录功能。关于多重继承和 mixin 在 Ruby 之父的书《松本行弘的程序世界》中有不错的科普。
diff --git a/devtools/hg.rst b/devtools/hg.rst
index cb3d7d6b..d4177006 100644
--- a/devtools/hg.rst
+++ b/devtools/hg.rst
@@ -77,25 +77,25 @@ hg 基本流程
 Tips
 ----------------------
 
-* 向版本库添加/删除文件 `hg add/rm <path>`
+* 向版本库添加/删除文件 ``hg add/rm <path>``
 
-* 移动版本库中的文件 `hg mv`
+* 移动版本库中的文件 ``hg mv``
 
-* 查找某段代码的责任人 `hg blame`
+* 查找某段代码的责任人 ``hg blame``
 
-* 从现有代码初始化版本仓库 `hg init`
+* 从现有代码初始化版本仓库 ``hg init``
 
-* 建立 hg 服务器 `hg serve`
+* 建立 hg 服务器 ``hg serve``
 
-* 更新代码库至最新提交 `hg update`
+* 更新代码库至最新提交 ``hg update``
 
-* 切换至分支 `hg update -r <branch>`
+* 切换至分支 ``hg update -r <branch>``
 
-* 放弃所有修改，返回至上一个提交 `hg update -C`
+* 放弃所有修改，返回至上一个提交 ``hg update -C``
 
-* 搜索 `hg grep`
+* 搜索 ``hg grep``
 
-* 放弃某个文件的修改 `hg revert`
+* 放弃某个文件的修改 ``hg revert``
 
 
 分支命名规则
@@ -134,7 +134,7 @@ Tips
 
     pip install mercurial_keyring
 
-在 `~/.hgrc` 文件后写入：
+在 ``~/.hgrc`` 文件后写入：
 
 .. code-block:: shell
 
diff --git a/devtools/ipython.rst b/devtools/ipython.rst
index 344ce54e..a6e1831c 100644
--- a/devtools/ipython.rst
+++ b/devtools/ipython.rst
@@ -11,19 +11,19 @@ IPython 用法
 
 启动IPython就是运行可执行文件ipython。你会看到一个提示符，如果你曾经玩过标准Python命令行提示符，你会发现这个有点儿不同::
 
-    [jjones@cerberus ~]$ /usr/local/python24/bin/ipython 
-    Python 2.4 (#2, Nov 30 2004, 09:22:54) 
-    Type "copyright", "credits" or "license" for more information. 
+    [jjones@cerberus ~]$ /usr/local/python24/bin/ipython
+    Python 2.4 (#2, Nov 30 2004, 09:22:54)
+    Type "copyright", "credits" or "license" for more information.
 
-    IPython 0.6.6 -- An enhanced Interactive Python. 
-    ? -> Introduction to IPython's features. 
-    %magic -> Information about IPython's 'magic' % functions. 
-    help -> Python's own help system. 
-    object? -> Details about 'object'. ?object also works, ?? prints more. 
+    IPython 0.6.6 -- An enhanced Interactive Python.
+    ? -> Introduction to IPython's features.
+    %magic -> Information about IPython's 'magic' % functions.
+    help -> Python's own help system.
+    object? -> Details about 'object'. ?object also works, ?? prints more.
 
-    In [1]: 
+    In [1]:
 
-要退出IPython（Linux系统上）就输入Ctrl-D（会要求你确认），也可以输入Exit或Quit（注意大小写）退出而不需要确认。 
+要退出IPython（Linux系统上）就输入Ctrl-D（会要求你确认），也可以输入Exit或Quit（注意大小写）退出而不需要确认。
 
 特性
 ============
@@ -33,195 +33,195 @@ Magic
 
 IPython有一些"magic"关键字::
 
-    %Exit, %Pprint, %Quit, %alias, %autocall, %autoindent, %automagic, 
-    %bookmark, %cd, %color_info, %colors, %config, %dhist, %dirs, %ed, 
-    %edit, %env, %hist, %logoff, %logon, %logstart, %logstate, %lsmagic, 
-    %macro, %magic, %p, %page, %pdb, %pdef, %pdoc, %pfile, %pinfo, %popd, 
-    %profile, %prun, %psource, %pushd, %pwd, %r, %rehash, %rehashx, %reset, 
-    %run, %runlog, %save, %sc, %sx, %system_verbose, %unalias, %who, 
-    %who_ls, %whos, %xmode 
+    %Exit, %Pprint, %Quit, %alias, %autocall, %autoindent, %automagic,
+    %bookmark, %cd, %color_info, %colors, %config, %dhist, %dirs, %ed,
+    %edit, %env, %hist, %logoff, %logon, %logstart, %logstate, %lsmagic,
+    %macro, %magic, %p, %page, %pdb, %pdef, %pdoc, %pfile, %pinfo, %popd,
+    %profile, %prun, %psource, %pushd, %pwd, %r, %rehash, %rehashx, %reset,
+    %run, %runlog, %save, %sc, %sx, %system_verbose, %unalias, %who,
+    %who_ls, %whos, %xmode
 
-IPython 会检查传给它的命令是否包含magic关键字。如果命令是一个magic关键字，IPython就自己来处理。如果不是magic关键字，就交给 Python（标准解释器）去处理。如果automagic打开（默认），你不需要在magic关键字前加%符号。相反，如果automagic是关闭的，则%是必须的。在命令提示符下输入命令magic就会显示所有magic关键字列表以及它们的简短的用法说明。良好的文档对于一个软件的任何一部分来说都是重要的，从在线IPython用户手册到内嵌文档（%magic），IPython当然不会在这方面有所缺失。 
+IPython 会检查传给它的命令是否包含magic关键字。如果命令是一个magic关键字，IPython就自己来处理。如果不是magic关键字，就交给 Python（标准解释器）去处理。如果automagic打开（默认），你不需要在magic关键字前加%符号。相反，如果automagic是关闭的，则%是必须的。在命令提示符下输入命令magic就会显示所有magic关键字列表以及它们的简短的用法说明。良好的文档对于一个软件的任何一部分来说都是重要的，从在线IPython用户手册到内嵌文档（%magic），IPython当然不会在这方面有所缺失。
 
 Tab自动补全
 ------------
 
-IPython一个非常强大的功能是tab自动补全。如果你对Python很了解，可能会想，标准Python交互式解释器也可以tab自动补全啊。你要做的只是:: 
+IPython一个非常强大的功能是tab自动补全。如果你对Python很了解，可能会想，标准Python交互式解释器也可以tab自动补全啊。你要做的只是::
 
-    [jjones@cerberus ~]$ /usr/local/python24/bin/python 
-    Python 2.4 (#2, Nov 30 2004, 09:22:54) 
-    [GCC 3.4.2 20041017 (Red Hat 3.4.2-6.fc3)] on linux2 
-    Type "help", "copyright", "credits" or "license" for more information. 
-    >>> import rlcompleter, readline 
-    >>> readline.parse_and_bind('tab: complete') 
-    >>> 
+    [jjones@cerberus ~]$ /usr/local/python24/bin/python
+    Python 2.4 (#2, Nov 30 2004, 09:22:54)
+    [GCC 3.4.2 20041017 (Red Hat 3.4.2-6.fc3)] on linux2
+    Type "help", "copyright", "credits" or "license" for more information.
+    >>> import rlcompleter, readline
+    >>> readline.parse_and_bind('tab: complete')
+    >>>
 
-是的，标准Python交互式解释器和IPython都支持“普通”自动补全和菜单补全。使用自动补全，你要先输入一个匹配模型，然后按Tab键。如果是“普通”自动补全模式（默认），Tab后会： 
+是的，标准Python交互式解释器和IPython都支持“普通”自动补全和菜单补全。使用自动补全，你要先输入一个匹配模型，然后按Tab键。如果是“普通”自动补全模式（默认），Tab后会：
 
-* 匹配模型按最大匹配展开。 
-* 列出所有匹配的结果。 
+* 匹配模型按最大匹配展开。
+* 列出所有匹配的结果。
 
-例如:: 
+例如::
 
-    In [1]: import os 
-    In [2]: os.po 
-    os.popen os.popen2 os.popen3 os.popen4 
-    In [2]: os.popen 
+    In [1]: import os
+    In [2]: os.po
+    os.popen os.popen2 os.popen3 os.popen4
+    In [2]: os.popen
 
-输入os.po然后按Tab键，os.po被展开成os.popen（就象在In [2]:提示符显示的那样），并显示os所有以po开头的模块，类和函数，它们是popen，popen2， popen3和popen4。 
+输入os.po然后按Tab键，os.po被展开成os.popen（就象在In [2]:提示符显示的那样），并显示os所有以po开头的模块，类和函数，它们是popen，popen2， popen3和popen4。
 
-菜单补全稍有不同。关闭默认Tab补全，使用菜单补全，你需要修改配置文件$HOME/.ipython/ipythonrc。注释掉： 
-    readline_parse_and_bind tab: complete 
+菜单补全稍有不同。关闭默认Tab补全，使用菜单补全，你需要修改配置文件$HOME/.ipython/ipythonrc。注释掉：
+    readline_parse_and_bind tab: complete
 
-取消注释: 
-    readline_parse_and_bind tab: menu-complete 
+取消注释:
+    readline_parse_and_bind tab: menu-complete
 
 不同于“普通”自动补全的显示当前命令所有匹配列表，菜单补全会随着你每按一次Tab键而循环显示匹配列表中的项目。例如::
 
-    In [1]: import os 
-    In [2]: os.po 
+    In [1]: import os
+    In [2]: os.po
 
-结果是： 
+结果是：
 
-    In [3]: os.popen 
+    In [3]: os.popen
 
 接下来每次按Tab键就会循环显示匹配列表中的其它项目：popen2，popen3，popen4，最后回到po。菜单补全模式下查看所有匹配列表的快捷键是Ctrl-L::
 
-    In [2]: os.po 
-    os.popen os.popen2 os.popen3 os.popen4 
-    In [2]: os.po 
+    In [2]: os.po
+    os.popen os.popen2 os.popen3 os.popen4
+    In [2]: os.po
 
 自省
 ------------
 
 Python有几个内置的函数用于自省。IPython不仅可以调用所有标准Python函数，对于那些Python shell内置函数同样适用。典型的使用标准Python shell进行自省是使用内置的dir()函数::
 
-    >>> import SimpleXMLRPCServer 
-    >>> dir(SimpleXMLRPCServer.SimpleXMLRPCServer) 
-    ['__doc__', '__init__', '__module__', '_dispatch', 
-    '_marshaled_dispatch', 'address_family', 'allow_reuse_address', 
-    'close_request', 'fileno', 'finish_request', 'get_request', 
-    'handle_error', 'handle_request', 'process_request', 
-    'register_function', 'register_instance', 
-    'register_introspection_functions', 'register_multicall_functions', 
-    'request_queue_size', 'serve_forever', 'server_activate', 'server_bind', 
-    'server_close', 'socket_type', 'system_listMethods', 
-    'system_methodHelp', 'system_methodSignature', 'system_multicall', 
-    'verify_request'] 
+    >>> import SimpleXMLRPCServer
+    >>> dir(SimpleXMLRPCServer.SimpleXMLRPCServer)
+    ['__doc__', '__init__', '__module__', '_dispatch',
+    '_marshaled_dispatch', 'address_family', 'allow_reuse_address',
+    'close_request', 'fileno', 'finish_request', 'get_request',
+    'handle_error', 'handle_request', 'process_request',
+    'register_function', 'register_instance',
+    'register_introspection_functions', 'register_multicall_functions',
+    'request_queue_size', 'serve_forever', 'server_activate', 'server_bind',
+    'server_close', 'socket_type', 'system_listMethods',
+    'system_methodHelp', 'system_methodSignature', 'system_multicall',
+    'verify_request']
 
 嗯，非常棒。事实上非常实用。几年来我一直这么做，对此非常满意。这是一个漂亮的列表，包含了 SimpleXMLRPCServer.SimpleXMLRPCServer的所有方法，类，模块等等。因为dir()是一个内置函数，在 IPython中也能很好的使用它们。但是IPython的操作符?和??功能还要强大::
 
-        In [1]: import SimpleXMLRPCServer 
+        In [1]: import SimpleXMLRPCServer
 
-        In [2]: ? SimpleXMLRPCServer.SimpleXMLRPCServer 
-        Type: classobj 
-        String Form: SimpleXMLRPCServer.SimpleXMLRPCServer 
-        Namespace: Interactive 
-        File: /usr/local/python24/lib/python2.4/SimpleXMLRPCServer.py 
-        Docstring: 
-        Simple XML-RPC server. 
+        In [2]: ? SimpleXMLRPCServer.SimpleXMLRPCServer
+        Type: classobj
+        String Form: SimpleXMLRPCServer.SimpleXMLRPCServer
+        Namespace: Interactive
+        File: /usr/local/python24/lib/python2.4/SimpleXMLRPCServer.py
+        Docstring:
+        Simple XML-RPC server.
 
-        Simple XML-RPC server that allows functions and a single instance 
-        to be installed to handle requests. The default implementation 
-        attempts to dispatch XML-RPC calls to the functions or instance 
-        installed in the server. Override the _dispatch method inherited 
-        from SimpleXMLRPCDispatcher to change this behavior. 
+        Simple XML-RPC server that allows functions and a single instance
+        to be installed to handle requests. The default implementation
+        attempts to dispatch XML-RPC calls to the functions or instance
+        installed in the server. Override the _dispatch method inherited
+        from SimpleXMLRPCDispatcher to change this behavior.
 
-        Constructor information: 
-        Definition: SimpleXMLRPCServer.SimpleXMLRPCServer(self, addr, 
-        requestHandler=, logRequests=1) 
+        Constructor information:
+        Definition: SimpleXMLRPCServer.SimpleXMLRPCServer(self, addr,
+        requestHandler=, logRequests=1)
 
-? 操作符会截断长的字符串。相反，?? 不会截断长字符串，如果有源代码的话还会以语法高亮形式显示它们。 
+? 操作符会截断长的字符串。相反，?? 不会截断长字符串，如果有源代码的话还会以语法高亮形式显示它们。
 
 历史
 -----------
 
 当你在IPython shell下交互的输入了大量命令，语句等等，就象这样::
 
-    In [1]: a = 1 
+    In [1]: a = 1
 
-    In [2]: b = 2 
+    In [2]: b = 2
 
-    In [3]: c = 3 
+    In [3]: c = 3
 
-    In [4]: d = {} 
+    In [4]: d = {}
 
-    In [5]: e = [] 
+    In [5]: e = []
 
-    In [6]: for i in range(20): 
-    ...: e.append(i) 
-    ...: d[i] = b 
-    ...: 
+    In [6]: for i in range(20):
+    ...: e.append(i)
+    ...: d[i] = b
+    ...:
 
 你可以快速查看那些输入的历史记录::
 
-    In [7]: hist 
-    1: a = 1 
-    2: b = 2 
-    3: c = 3 
-    4: d = {} 
-    5: e = [] 
-    6: 
-    for i in range(20): 
-    e.append(i) 
-    d[i] = b 
+    In [7]: hist
+    1: a = 1
+    2: b = 2
+    3: c = 3
+    4: d = {}
+    5: e = []
+    6:
+    for i in range(20):
+    e.append(i)
+    d[i] = b
 
 要去掉历史记录中的序号（这里是1至6），使用命令hist -n::
 
-    In [8]: hist -n 
-    a = 1 
-    b = 2 
-    c = 3 
-    d = {} 
-    e = [] 
-    for i in range(20): 
-    e.append(i) 
-    d[i] = b 
+    In [8]: hist -n
+    a = 1
+    b = 2
+    c = 3
+    d = {}
+    e = []
+    for i in range(20):
+    e.append(i)
+    d[i] = b
 
-这样你就可以方便的将代码复制到一个文本编辑器中。要在历史记录中搜索，可以先输入一个匹配模型，然后按Ctrl-P。找到一个匹配后，继续按Ctrl-P会向后搜索再上一个匹配，Ctrl-N则是向前搜索最近的匹配。 
+这样你就可以方便的将代码复制到一个文本编辑器中。要在历史记录中搜索，可以先输入一个匹配模型，然后按Ctrl-P。找到一个匹配后，继续按Ctrl-P会向后搜索再上一个匹配，Ctrl-N则是向前搜索最近的匹配。
 
 
-编辑 
+编辑
 ===========
 
 当在Python提示符下试验一个想法时，经常需要通过编辑器修改源代码（甚至是反复修改）。在IPython下输入edit就会根据环境变量$EDITOR调用相应的编辑器。如果$EDITOR为空，则会调用vi（Unix）或记事本（Windows）。要回到IPython提示符，直接退出编辑器即可。如果是保存并退出编辑器，输入编辑器的代码会在当前名字空间下被自动执行。如果你不想这样，使用edit -x。如果要再次编辑上次最后编辑的代码，使用edit -p。在上一个特性里，我提到使用hist -n可以很容易的将代码拷贝到编辑器。一个更简单的方法是edit加Python列表的切片（slice）语法。假定hist输出如下::
 
-    In [29]: hist 
-    1 : a = 1 
-    2 : b = 2 
-    3 : c = 3 
-    4 : d = {} 
-    5 : e = [] 
-    6 : 
-    for i in range(20): 
-    e.append(i) 
-    d[i] = b 
+    In [29]: hist
+    1 : a = 1
+    2 : b = 2
+    3 : c = 3
+    4 : d = {}
+    5 : e = []
+    6 :
+    for i in range(20):
+    e.append(i)
+    d[i] = b
 
-    7 : %hist 
+    7 : %hist
 
-现在要将第4，5，6句代码导出到编辑器，只要输入： 
+现在要将第4，5，6句代码导出到编辑器，只要输入：
 
-    edit 4:7 
+    edit 4:7
 
 
-Debugger接口 
+Debugger接口
 -------------
 
-IPython 的另一特性是它与Python debugger的接口。在IPython shell下输入magic关键字pdb就会在产生一个异常时自动开关debugging功能。在自动pdb呼叫启用的情况下，当Python遇到一个未处理的异常时Python debugger就会自动启动。你在debugger中的当前行就是异常发生的那一行。IPython的作者说有时候当他需要在某行代码处debug时，他会在开始debug的地方放一个表达式1/0。启用pdb，在IPython中运行代码。当解释器处理到1/0那一行时，就会产生一个 ZeroDivisionError异常，然后他就在指定的代码处被带到一个debugging session中了。 
+IPython 的另一特性是它与Python debugger的接口。在IPython shell下输入magic关键字pdb就会在产生一个异常时自动开关debugging功能。在自动pdb呼叫启用的情况下，当Python遇到一个未处理的异常时Python debugger就会自动启动。你在debugger中的当前行就是异常发生的那一行。IPython的作者说有时候当他需要在某行代码处debug时，他会在开始debug的地方放一个表达式1/0。启用pdb，在IPython中运行代码。当解释器处理到1/0那一行时，就会产生一个 ZeroDivisionError异常，然后他就在指定的代码处被带到一个debugging session中了。
 
-运行 
+运行
 -------------
-有时候当你在一个交互式shell中时，如果可以运行某个源文件中的内容将会很有用。运行magic关键字run带一个源文件名就可以在IPython解释器中运行一个文件了（例如run <源文件> <运行源文件所需参数>）。参数主要有以下这些： 
+有时候当你在一个交互式shell中时，如果可以运行某个源文件中的内容将会很有用。运行magic关键字run带一个源文件名就可以在IPython解释器中运行一个文件了（例如run <源文件> <运行源文件所需参数>）。参数主要有以下这些：
 
 * -n 阻止运行源文件代码时__name__变量被设为"__main__"。这会防止 ::
 
-    if __name__ == "__main__": 
+    if __name__ == "__main__":
 
-  块中的代码被执行 
+  块中的代码被执行
 
-* -i 源文件就在当前IPython的名字空间下运行而不是在一个新的名字空间中。如果你需要源代码可以使用在交互式session中定义的变量就会很有用。 
+* -i 源文件就在当前IPython的名字空间下运行而不是在一个新的名字空间中。如果你需要源代码可以使用在交互式session中定义的变量就会很有用。
 
-* -p 使用Python的profiler模块运行并分析源代码。使用该选项代码不会运行在当前名字空间。 
+* -p 使用Python的profiler模块运行并分析源代码。使用该选项代码不会运行在当前名字空间。
 
 
 宏
@@ -229,92 +229,92 @@ IPython 的另一特性是它与Python debugger的接口。在IPython shell下
 
 宏允许用户为一段代码定义一个名字，这样你可以在以后使用这个名字来运行这段代码。就象在magic关键字edit中提到的，列表切片法也适用于宏定义。假设有一个历史记录如下::
 
-    In [3]: hist 
-    1: l = [] 
-    2: 
-    for i in l: 
-    print i 
+    In [3]: hist
+    1: l = []
+    2:
+    for i in l:
+    print i
 
-你可以这样来定义一个宏： 
+你可以这样来定义一个宏::
 
-    In [4]: macro print_l 2 
-    Macro `print_l` created. To execute, type its name (without quotes). 
-    Macro contents: 
-    for i in l: 
-    print i 
+    In [4]: macro print_l 2
+    Macro `print_l` created. To execute, type its name (without quotes).
+    Macro contents:
+    for i in l:
+    print i
 
-运行宏:: 
+运行宏::
 
-    In [5]: print_l 
-    Out[5]: Executing Macro... 
+    In [5]: print_l
+    Out[5]: Executing Macro...
 
 在这里，列表l是空的，所以没有东西被输出。但这其实是一个很强大的功能，我们可以赋予列表l某些实际值，再次运行宏就会看到不同的结果::
 
-    In [6]: l = range(5) 
+    In [6]: l = range(5)
 
-    In [7]: print_l 
-    Out[7]: Executing Macro... 
-    0 
-    1 
-    2 
-    3 
-    4 
+    In [7]: print_l
+    Out[7]: Executing Macro...
+    0
+    1
+    2
+    3
+    4
 
-当运行一个宏时就好象你重新输入了一遍包含在宏print_1中的代码。它还可以使用新定义的变量l。由于Python语法中没有宏结构（也许永远也不会有），在一个交互式shell中它更显得是一个有用的特性。 
+当运行一个宏时就好象你重新输入了一遍包含在宏print_1中的代码。它还可以使用新定义的变量l。由于Python语法中没有宏结构（也许永远也不会有），在一个交互式shell中它更显得是一个有用的特性。
 
-环境（Profiles） 
+环境（Profiles）
 ===================================
 
-就象早前提到的那样，IPython安装了多个配置文件用于不同的环境。配置文件的命名规则是ipythonrc-。要使用特定的配置启动IPython，需要这样 :: 
+就象早前提到的那样，IPython安装了多个配置文件用于不同的环境。配置文件的命名规则是ipythonrc-。要使用特定的配置启动IPython，需要这样 ::
 
-    ipython -p 
+    ipython -p
 
-一个创建你自己环境的方法是在$HOME/.ipython目录下创建一个IPython配置文件，名字就叫做ipythonrc- ，这里是你想要的环境的名字。如果你同时进行好几个项目，而这些项目又用到互不相同的特殊的库，这时候每个项目都有自己的环境就很有用了。你可以为每个项目建立一个配置文件，然后在每个配置文件中import该项目中经常用到的模块。 
+一个创建你自己环境的方法是在$HOME/.ipython目录下创建一个IPython配置文件，名字就叫做ipythonrc- ，这里是你想要的环境的名字。如果你同时进行好几个项目，而这些项目又用到互不相同的特殊的库，这时候每个项目都有自己的环境就很有用了。你可以为每个项目建立一个配置文件，然后在每个配置文件中import该项目中经常用到的模块。
 
-使用操作系统的Shell 
+使用操作系统的Shell
 =================================
 
-使用默认的IPython配置文件，有几个Unix Shell命令（当然，是在Unix系统上），cd，pwd和ls都能象在bash下一样工作。运行其它的shell命令需要在命令前加!或!!。使用magic关键字%sc和%sx可以捕捉shell命令的输出。 
+使用默认的IPython配置文件，有几个Unix Shell命令（当然，是在Unix系统上），cd，pwd和ls都能象在bash下一样工作。运行其它的shell命令需要在命令前加!或!!。使用magic关键字%sc和%sx可以捕捉shell命令的输出。
 
 pysh环境可以被用来替换掉shell。使用-p pysh参数启动的IPython，可以接受并执行用户$PATH中的所有命令，同时还可以使用所有的Python模块，Python关键字和内置函数。例如，我想要创建500个目录，命名规则是从d_0_d到d_500_d（译注：呵呵，作者这里犯了个小小的计算错误，你能看出来吗），我可以使用-p pysh启动IPython，然后就象这样::
 
-    jjones@cerberus[foo]|2> for i in range(500): 
-    |.> mkdir d_${i}_d 
-    |.> 
+    jjones@cerberus[foo]|2> for i in range(500):
+    |.> mkdir d_${i}_d
+    |.>
 
 这就会创建500个目录::
 
-    jjones@cerberus[foo]|8> ls -d d* | wc -l 
-    500 
+    jjones@cerberus[foo]|8> ls -d d* | wc -l
+    500
 
-注意这里混合了Python的range函数和Unix的mkdir命令。 
+注意这里混合了Python的range函数和Unix的mkdir命令。
 
-注意，虽然ipython -p pysh提供了一个强大的shell替代品，但它缺少正确的job控制。在运行某个很耗时的任务时按下Ctrl-z将会停止IPython session而不是那个子进程。 
+注意，虽然ipython -p pysh提供了一个强大的shell替代品，但它缺少正确的job控制。在运行某个很耗时的任务时按下Ctrl-z将会停止IPython session而不是那个子进程。
 
-问题和方法 
+问题和方法
 ==============
 
-虽然作为标准Python shell的替换，IPython总的来说很完美。还是有两个问题给我带来了一些麻烦。感谢IPython的开发者，这两个问题都可以通过配置来解决，每个配置都有清晰的文档。 
+虽然作为标准Python shell的替换，IPython总的来说很完美。还是有两个问题给我带来了一些麻烦。感谢IPython的开发者，这两个问题都可以通过配置来解决，每个配置都有清晰的文档。
 
-第一个问题是关于颜色的。在我的一个系统上，我使用的是一个白色背景的xterm。当我使用?和??查询一个对象或模块的信息时，对象的定义会被显示，但看起来好象那些参数丢失了。那是因为在构造函数中的的参数默认显示为白色。我的解决办法是在IPython shell中输入colors LightBG。 
+第一个问题是关于颜色的。在我的一个系统上，我使用的是一个白色背景的xterm。当我使用?和??查询一个对象或模块的信息时，对象的定义会被显示，但看起来好象那些参数丢失了。那是因为在构造函数中的的参数默认显示为白色。我的解决办法是在IPython shell中输入colors LightBG。
 
 第二个问题是关于自动缩进和代码粘贴的。如果autoindent被启用，IPython会对我粘贴的已排好缩进的代码再次应用缩进。例如下面的代码::
 
-    for i in range(10): 
-    for j in range(10): 
-    for k in range(10): 
-    pass 
+    for i in range(10):
+    for j in range(10):
+    for k in range(10):
+    pass
 
 会变成::
 
-    for i in range(10): 
-    for j in range(10): 
-    for k in range(10): 
-    pass 
+    for i in range(10):
+    for j in range(10):
+    for k in range(10):
+    pass
 
-在这里它并不是个问题，因为在它自身中缩进都保持一致。在其它一些情况下（例子一下子举不出来了），可能会成为真正的问题。可以使用magic关键字autoindent来开关自动缩进，告诉IPython不要添加多余的缩进──就象在vim中设置粘贴set paste一样。 
+在这里它并不是个问题，因为在它自身中缩进都保持一致。在其它一些情况下（例子一下子举不出来了），可能会成为真正的问题。可以使用magic关键字autoindent来开关自动缩进，告诉IPython不要添加多余的缩进──就象在vim中设置粘贴set paste一样。
 
-结论 
+结论
 ==============
 
 IPython 并不是囗囗性的，也不是完全创新的。Tab自动补全，历史记录搜索，配置环境，配置文件等都早已在其它shells中存在有些年头了。Python拥有各种级别的自省能力也有一段时间了。但IPython把来自成熟的Unix shell，标准Python shell以及Python语言中的一些最强大的功能整合到了一起。产生出了一个强大的令人难以置信的性能增强工具，我想我会很乐意在接下来的几年中一直使用它。套用阿基米德的话来说，给我一个强大而又灵活的文本编辑器（vim），一个交互式shell（IPython）以及一个语言（Python），我就能撬动整个世界。
diff --git a/devtools/vim.rst b/devtools/vim.rst
index 7bfd7835..afde1d99 100644
--- a/devtools/vim.rst
+++ b/devtools/vim.rst
@@ -26,7 +26,7 @@ Emacs则奉行“Everything at reach”设计哲学，通过强大的扩展性
 ------------------
 
 Vim 有两种模式——Normal 模式和 Insert 模，所有命令都是在 Normal 模式下执行。启动 Vim 后，默认进入 Normal 模式，
-可以按 `i` 键进入 Insert 模式，或者 `s` 删除当前字符并进入 Insert 模式，退出 Insert 模式进入 Normal 按 `ESC` 。 
+可以按 i 键进入 Insert 模式，或者 s 删除当前字符并进入 Insert 模式，退出 Insert 模式进入 Normal 按 ESC 。 
 
 下面的教程中约定 + 表示同时按其左右的按键，小写字母（如 i）表示按该字母一次，大写字母（如 G）表示同时按 shift+g。
 
@@ -59,7 +59,7 @@ Vim 有两种模式——Normal 模式和 Insert 模，所有命令都是在 Nor
 * u 撤销上一步操作
 * zo/zn/zc 折叠/展开代码块
 * :vsp 新建工作区
-* ctrl+w 松手后再按 `方向键` 切换工作区
+* ctrl+w 松手后再按 方向键 切换工作区
 * :MR 选择最近打开的文件（需安装插件）
 * F12 运行当前文件 
 * # 搜索光标处短语
@@ -75,7 +75,7 @@ Vim 有两种模式——Normal 模式和 Insert 模，所有命令都是在 Nor
 * 光标移至行首
 * I 进入插入模式
 * 输入注释符号
-* 双击 `ESC`
+* 双击 ESC
 
 如何配置
 ------------------
diff --git a/favicon.ico b/favicon.ico
index c197200b..72a53a9b 100644
Binary files a/favicon.ico and b/favicon.ico differ
diff --git a/frontend/animate/animate-css.rst b/frontend/animate/animate-css.rst
deleted file mode 100644
index d0b87be2..00000000
--- a/frontend/animate/animate-css.rst
+++ /dev/null
@@ -1,14 +0,0 @@
-.. _animate_css:
-
-===========
-animate.css
-===========
-
-:作者: 王然 kxxoling@gmail.com
-
-简介
-----
-
-`Animate.css <http://github.com/daneden/animate.css>`_ 是一个封装后的 CSS3 动画库，提供了多种动画效果。
-在线演示： `官网 <http://daneden.github.io/animate.css/>`_
-
diff --git a/frontend/animate/textillate-js.rst b/frontend/animate/textillate-js.rst
deleted file mode 100644
index 6cf8487e..00000000
--- a/frontend/animate/textillate-js.rst
+++ /dev/null
@@ -1,16 +0,0 @@
-.. _textillate:
-
-=============
-textillate.js
-=============
-
-:作者: 王然 kxxoling@gmail.com
-
-简介
-----
-
-`textillate.js <http://jschr.github.io/textillate/>`_ 依赖于 `lettering.js <https://github.com/davatron5000/Lettering.js>`_
-和 `animate.css <http://daneden.github.io/animate.css/>`_ ，主要用于文字出现时的动画效果。
-
-
-
diff --git a/frontend/animate/wow-js.rst b/frontend/animate/wow-js.rst
deleted file mode 100644
index 8f196fbb..00000000
--- a/frontend/animate/wow-js.rst
+++ /dev/null
@@ -1,91 +0,0 @@
-.. _wowjs:
-
-=======
-WOW.js
-=======
-
-:作者: 王然 kxxoling@gmail.com
-
-简介
-----
-
-`WOW.js <https://github.com/matthieua/WOW>`_ 是一个基于 `animate.css` 的动画效果库，使用简单。
-
-
-基本使用
----------
-
-`WOW.js` 依赖于 `animate.css <https://daneden.github.io/animate.css/>`_ ，所以使用前需引用 `animate.css` 。 ::
-
-    <link rel="stylesheet" href="css/animate.css">
-    <script src="js/wow.min.js"></script>
-
-初始化 ::
-
-    <script>
-     new WOW().init();
-    </script>
-
-`WOW.js` 根据 `.wow` 类标记需要添加动画效果的元素： ::
-
-    <div class="wow">
-      Content to Reveal Here
-    </div>
-
-再通过特定的类型设置动画类型，比如 `bounceInUp` 可以设置 `自上而下的反弹效果 <http://gh.windrunner.info/z42-doc/animate/wow.html#bounceInUp>`_ ： ::
-
-    <div class="wow bounceInUp">
-      Content to Reveal Here
-    </div>
-
-至此，已经实现了滚动时的动画效果。
-
-
-高级设置
---------
-
-data-wow-duration：动画的持续时间
-data-wow-delay：动画出现的延迟
-data-wow-offset：动画的偏移距离（相对于浏览器底部）
-data-wow-iteration：动画出现的重复次数
-
-::
-
-    <section class="wow slideInLeft" data-wow-duration="2s" data-wow-delay="5s">
-    </section>
-
-    <section class="wow slideInRight" data-wow-offset="10"  data-wow-iteration="10">
-    </section>
-
-
-配置
-----
-
-boxClass: Class name that reveals the hidden box when user scrolls.
-
-animateClass: Class name that triggers the CSS animations (’animated’ by default for the animate.css library)
-
-offset: Define the distance between the bottom of browser viewport and the top of hidden box.
-When the user scrolls and reach this distance the hidden box is revealed.
-
-::
-
-    wow = new WOW(
-      {
-        boxClass:     'wow',      // default
-        animateClass: 'animated', // default
-        offset:       0,          // default
-        mobile:       true,       // default
-        live:         true        // default
-      }
-    )
-    wow.init();
-
-
-相关链接
---------
-
-`英文文档 <http://mynameismatthieu.com/WOW/docs.html>`_
-`官网及示例 <http://mynameismatthieu.com/WOW/index.html>`_
-`GitHub <https://github.com/matthieua/WOW>`_
-
diff --git a/frontend/director.rst b/frontend/director.rst
deleted file mode 100644
index deacf694..00000000
--- a/frontend/director.rst
+++ /dev/null
@@ -1,50 +0,0 @@
-.. _director_:
-
-===========
-director.js
-===========
-
-:作者: 王然 kxxoling@gmail.com
-
-简介
-----
-
-`director <https://github.com/flatiron/director#client-side-routing>`_ 提供前端和后端的
-路由解决方案，可以用于控制前端路由以及页面代码的显示与否。
-
-使用很简单：
-
-.. code-block:: html
-
-    <!DOCTYPE html>
-    <html>
-      <head>
-        <meta charset="utf-8">
-        <title>Director Example</title>
-        <div id="results"></div>
-        <script src="http://www.javascriptoo.com/application/html/js/flatiron/director/director.min.js"></script>
-        <script>
-        var r = document.getElementById('results');
-            var data = {
-                people: [
-                    { 'firstName': 'Clark', 'lastName': 'Kent' },
-                    { 'firstName': 'Bruce', 'lastName': 'Wayne' },
-                    { 'firstName': 'Peter', 'lastName': 'Parker' }
-                ]
-            }
-            var viewPerson = function(personId) { r.innerHTML = data.people[personId].firstName + ' ' + data.people[personId].lastName; };
-            var routes = {'/person/view/:bookId': viewPerson};
-            var router = Router(routes);
-            router.init();
-        </script>
-      </head>
-      <body>
-        <ul>
-          <li><a href="#/person/view/0">Clark</a></li>
-          <li><a href="#/person/view/1">Bruce</a></li>
-          <li><a href="#/person/view/2">Peter</a></li>
-        </ul>
-      </body>
-    </html>
-
-效果预览： `GitHub Pages <http://angelcrunchdev.github.io/z42-doc/director>`_
diff --git a/frontend/fonticon.rst b/frontend/fonticon.rst
deleted file mode 100644
index 2d140873..00000000
--- a/frontend/fonticon.rst
+++ /dev/null
@@ -1,71 +0,0 @@
-.. _fonticon:
-
-=============
-使用 FontIcon
-=============
-
-作者: 王然 kxxoling@gmail.com
-
-
-为什么使用 FontIcon
--------------------
-
-位图图片不能很好地进行缩放，当图片进行放大时会失真（即变模糊），当图片进行缩小时就会浪费掉像素。而且加载每一张图片都需要一次“http请求”，因此也拖慢了整个加载页面的时间。另外，要是没有图片编辑器（软件）的话就很难对图片进行编辑、处理等操作。
-
-字体就不会有以上这些问题，字体可以进行随意缩放并且每一个字符都不需要进行额外的“http请求”。
-
-其它优势：
-
-很容易任意地缩放；
-很容易地改变颜色；
-很容易地产生阴影；
-可以拥有透明效果；
-一般来说，有先进的浏览器支持；
-可以使用CSS来装饰（可以得到CSS很好支持）；
-可以快速转化形态（做出一些变化，如 :hover等）；
-可以做出跟图片一样可以做的事情（改变透明度、旋转度，等）；
-本身体积更小，但携带的信息并没有削减。
-
-扩展阅读：
-
-http://www.w3cplus.com/css3/icon-fonts.html
-
-http://isux.tencent.com/icon-font.html
-
-http://io-meter.com/2014/07/20/replace-icon-fonts-with-svg/
-
-
-如何使用
--------------------
-
-上传自己的图片格式 icon：http://www.iconfont.cn/icons/uploadShow
-
-收藏：点开特定的图标库，将鼠标移动到图标上，点击“购物车按钮”或者图标本身。
-
-下载：点击搜索框左侧的“购物车按钮”，选择“下载至本地”。
-
-在线存储：点击搜索框左侧的“购物车按钮”，选择“存储为项目”即可在线使用。
-存储后会会生成自己的项目，点击“获取在线链接”橙色按钮，即可生成如下 CSS 代码： ::
-
-    @font-face {
-      font-family: 'iconfont';
-      src: url('http://at.alicdn.com/t/font_1425368696_9550455.eot'); /* IE9*/
-      src: url('http://at.alicdn.com/t/font_1425368696_9550455.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
-      url('http://at.alicdn.com/t/font_1425368696_9550455.woff') format('woff'), /* chrome、firefox */
-      url('http://at.alicdn.com/t/font_1425368696_9550455.ttf') format('truetype'), /* chrome、firefox、opera、Safari, Android, iOS 4.2+*/
-      url('http://at.alicdn.com/t/font_1425368696_9550455.svg#iconfont') format('svg'); /* iOS 4.1- */
-    }
-
-复制按钮对应的 Unicode 编码，如 `&#xe600;` 即可在项目中作为文字使用。
-
-在线演示： http://angelcrunchdev.github.io/z42-doc/fonticon.html
-
-注：文件类型请选择 Unicode（默认）而非 FontClass。
-
-
-获取 Icon 素材
--------------------
-
-可以在这些提供免费图标下载的网站下载图片，在 IconFont 网站转换成 Font 后再使用：
-
-http://flaticon.com
diff --git a/frontend/html.rst b/frontend/html.rst
deleted file mode 100644
index 9ee9f2e1..00000000
--- a/frontend/html.rst
+++ /dev/null
@@ -1,279 +0,0 @@
-.. _html:
-
-=====================================================================
-HTML + CSS
-=====================================================================
-
-HTML
-----
-
--  label for属性
-
-绑定表单元素id，实现点击label选中相应radio或checkbox \*
-链接都必须写上href属性， 空链接请写成::
-
-::
-
-    <a href="javascript:void(0)"></a>
-
--  反复出现的按钮图标用 a标签 配合 css设置背景图
-   来实现，不要反复的写img标签（减少代码的冗余度）
-
-CSS
----
-
--  模块分级
-
-   -  全局级
-   -  模块级
-   -  页面级
-
--  分栏布局
-
-例如左宽665px,父容器宽1000px,在layout.css中定义::
-
-    .R665{float: left; margin-left: 688px; width:312px;}
-    .L665{float: left; width: 665px; margin-left: -100%;}
-
-参考:
-
-`双飞翼布局 <http://www.dqqd.me/flying-wing/>`_
-`常见布局 <http://blog.html.it/layoutgala/>`_
-
--  绝对底部
-
-``CSS Sticky Footer <http://paranimage.com/css-sticky-footer/>``\ \_ \*
-
-清除浮动 \* 子选择器
-
-::
-
-    利用父级元素id进行选择
-
--  ``图文混排 <http://dabblet.com/gist/4094139>``\ \_
--  CSS盒模型
--  垂直居中
--  如何制作三角
--  :first-child 和 :last-child
-
-   -  案例 : 圆圈 , padding
-
--  如果内容可变，就不要设置高度
--  写完页面依次检查
-
-   -  对齐
-   -  字体
-
-      -  大小
-      -  粗细
-      -  颜色
-
-   -  留白
-
--  邮件
-
-   ``Css Inliner Tool <http://templates.mailchimp.com/resources/inline-css/>``\ \_
-
--  42web
-
-   -  HTML中如何引用图片
-
-   图片上传至七牛，使用外链地址
-
-   -  CSS中如何引用图片::
-
-        background:url(/css/\_img/xxx)
-
-   -  不要引用站外的图片
-
--  checkbox 和 radio 的 样式
-
-``文字对齐 <http://www.zhangxinxu.com/wordpress/?p=56>``\ \_ \*
-
-不要用空格做间距
-
-
-我们常用的CSS样式
------------------
-
--  按钮
-
-   -  功能
-   -  强调
-
-设计
-----
-
--  对齐
--  留白的一致性
--  粗体
--  字号
-
-javascript
-----------
-
--  获取时间戳::
-
-   (new Date).geTime()
--  在js中取得当前用户::
-
-   $.current\_user
--  $$
-
-例如调用弹窗(可有多个参数)::
-
-::
-
-    $$('SITE/auth/login')
-
--  require
--  $.require
--  $.dialog * 需要登录调用.login\_dialog(参考submit\_project.coffee)
--  $.errtip ::
-
-    err = {}
-
-    if xxx: err.xxx = "xx" if xx : err.xx ="xx"
-
-    ::
-
-       if not errtip.set err:
-          xxxxx
-
-jQuery
-------
-
--  $.extend([deep],target,object)
-
-
-jQuery 自定义扩展
------------------
-
--  $.timeago
-
-接受一个时间戳作为参数,返回距离当前时间描述 \* $.isotime \* $.getJSON1
-\* jsonp 跨域调用 \* $.postJSON1 \* $.html 模版
-
-参考egg\_new.coffee
-
-jQuery UI
----------
-
--  Accordion
--  Datepicker
--  Tagit
-
-CoffeeScript
-------------
-
--  ``在页面中直接写coffee <http://coffeescript.org/#scripts>``\ \_
-
-avalon
-------
-
--  命名规则的修改
-
-   "-"改为"\_"
--  ms\_view
--  操作类似view的复用
--  view与数据结构的模块划分原则（每一个保存的url对应一个view）
--  ``$remove <http://limodou.github.io/avalon-learning/zh_CN/event.html>``\ \_
--  ``$watch <http://limodou.github.io/avalon-learning/zh_CN/watch.html>``\ \_
--  如何定义avalon组建
-
-   -  创建既可以单独使用，也可以在循环中使用的avalon组件
-
-参考ui\_follow.coffee
-
-Firebug
--------
-
--  控制台面板中，点击“保持”按钮，页面重新载入时不清空面板
-
-杂项
-----
-
--  ``七牛剪裁 <http://developer.qiniu.com/docs/v6/api/reference/fop/image/imageview2.html>``\ \_
--  上传文件
--  上传头像
--  地址选择
-
-工具
-----
-
--  Chrome插件
-
-   -  PerfectPixel
-   -  Page Ruler
-
--  Windows
-
-   -  Color Picker
-
-入门篇
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-#. `网页设计师 <https://bitbucket.org/zuroc/42qu-school/src/02ffbde7b7e4/book/%E7%BD%91%E9%A1%B5%E8%AE%BE%E8%AE%A1%E5%B8%88.chm>`_ , 初学者可以从其中 » 循序渐进 章节开始自己的学习之旅
-
-#. `HTML中文手册 <https://bitbucket.org/zuroc/42qu-school/src/02ffbde7b7e4/book/html.chm>`_
-
-#. `CSS中文手册 <https://bitbucket.org/zuroc/42qu-school/src/02ffbde7b7e4/book/css.chm>`_
-
-#. `CSS3 系列教程 <http://www.blueidea.com/tech/web/2009/6460.asp>`_
-
-#. `Javascript教程 <http://www.w3school.com.cn/js/index.asp>`_
-
-#. `15天学会jquery <https://bitbucket.org/zuroc/42qu-school/src/02ffbde7b7e4/book/15%E5%A4%A9%E5%AD%A6%E4%BC%9Ajquery.pdf>`_
-
-#. `jquery中文手册 <https://bitbucket.org/zuroc/42qu-school/src/02ffbde7b7e4/book/jquery.chm>`_
-
-基本库
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-#. `Aliceui 支付宝的前端框架 <http://aliceui.org/>`_
-
-Javascript
-=====================================================================
-
-基本库
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-#. `Jquery <http://jquery.com/>`_
-#. `Jquery UI <http://jqueryui.com/>`_
-#. `director 前端路由库 <https://github.com/flatiron/director>`_
-
-实用库
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-#. `Tag-it 标签 <http://aehlke.github.io/tag-it/examples.html>`_
-#. `imgareaselect图片剪裁 <http://odyniec.net/projects/imgareaselect/>`_
-#. `Highlight文字高亮 <http://unwrongest.com/projects/highlight/>`_
-#. `FancyBox弹出框 <http://fancybox.net/>`_
-#. `Poshy Tip jQuery Plugin 消息提示 <http://vadikom.com/demos/poshytip/>`_
-#. `摄像头拍照 <https://github.com/jhuckaby/webcamjs>`_
-
-
-
-图表库
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-#. `Open Flash Chart <http://teethgrinder.co.uk/open-flash-chart/>`_
-#. `highcharts <http://www.highcharts.com/>`_
-#. `Morris.js <http://oesmith.github.com/morris.js/>`_
-#. `Flot <http://code.google.com/p/flot/>`_
-
-扩展阅读
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-#. `Arale 支付宝的前端开发体系 <http://aralejs.org/>`_
-#. `学习CSS布局  <http://zh.learnlayout.com/>`_
-#. `双飞翼布局  <http://www.dqqd.me/flying-wing/>`_
-#. `常用布局  <http://blog.html.it/layoutgala/>`_
-#. `PNG压缩  <https://tinypng.com/>`_
-#. `JPG压缩  <http://www.jpegmini.com/>`_
-#. `完美的CSS绝对底部  <http://paranimage.com/css-sticky-foot/>`_
-#. `AliceUI <http://aliceui.org/docs/framework.html/>`_
-#. `colorMania取色器  <http://www.blacksunsoftware.com/colormania.html/>`_
-#. `emmet-vim  <http://www.ruanyifeng.com/blog/2013/06/emmet_and_haml.html/>`_
-
-云服务
-=====================================================================
-
-#. `stathat.com 在线统计图表 <https://www.stathat.com>`_
-
diff --git a/frontend/index.rst b/frontend/index.rst
deleted file mode 100644
index c373d168..00000000
--- a/frontend/index.rst
+++ /dev/null
@@ -1,12 +0,0 @@
-===================================
-前端技术
-===================================
-
-
-.. toctree::
-    :glob:
-    :maxdepth: 2
-
-    *
-    animate/*
-
diff --git a/frontend/innerfunc.rst b/frontend/innerfunc.rst
deleted file mode 100644
index 5f9a2d94..00000000
--- a/frontend/innerfunc.rst
+++ /dev/null
@@ -1,12 +0,0 @@
-.. _innner_funcs:
-
-==================================================
-内部函数
-==================================================
-
-
--  前端函数
-
-   -  $$
-   -  $import
-   -  View
diff --git a/frontend/jquery_ext.rst b/frontend/jquery_ext.rst
deleted file mode 100644
index 6538904f..00000000
--- a/frontend/jquery_ext.rst
+++ /dev/null
@@ -1,19 +0,0 @@
-.. _jquery_ext:
-
-=================
-jQuery Extensions
-=================
-
-   -  $.postJSON1
-   -  $.getJSON1
-   -  $.host\_id
-   -  $.current\_user
-   -  $.current\_user\_id
-   -  $.ajax\_submit
-   -  $.errtip
-   -  $.cnenlen
-   -  $.cookie
-   -  $.islogin
-   -  $.login
-   -  $.loader.show
-   -  $.loader.hide
diff --git a/go-note/algorithms.rst b/go-note/algorithms.rst
new file mode 100644
index 00000000..f681f29c
--- /dev/null
+++ b/go-note/algorithms.rst
@@ -0,0 +1,457 @@
+.. _go_algorithms:
+
+Go 算法和数据结构
+=====================================================================
+
+刷题过程中会遇到一些通用数据结构的封装，在这里记录一下。注意因为是刷算法题用的，没有考虑 goroutine 安全，
+不要直接用在并发环境，如果是在生产环境中使用请加锁改造。(没有泛型写起来挺繁琐的)
+
+Stack
+--------------------------------------------------
+
+.. code-block:: go
+
+    type Stack struct {
+        st []int
+    }
+
+    func NewStack() *Stack {
+        return &Stack{st:make([]int,0)}
+    }
+    func (s *Stack) Push(x int) {
+        s.st = append(s.st, x)
+    }
+
+    func (s *Stack) Peek() int{
+        return s.st[len(s.st)-1]
+    }
+
+    func (s *Stack) Pop() int{
+        n := len(s.st)
+        top := s.st[n-1]
+        s.st = s.st[:n-1]
+        return top
+    }
+
+    func (s *Stack) Empty() bool{
+        return len(s.st) == 0
+    }
+
+Queue
+--------------------------------------------------
+
+.. code-block:: go
+
+    type Item struct {
+        Num   int
+        Order int
+    }
+    type Queue struct {
+        items []Item
+    }
+
+    func NewQueue() *Queue {
+        return &Queue{
+            items: make([]Item, 0),
+        }
+    }
+    func (q *Queue) Push(x Item) {
+        q.items = append(q.items, x)
+    }
+
+    func (q *Queue) Pop() Item {
+        x := q.items[0]
+        q.items = q.items[1:]
+        return x
+    }
+
+    func (q *Queue) Front() Item {
+        return q.items[0]
+    }
+
+    func (q *Queue) End() Item {
+        return q.items[len(q.items)-1]
+    }
+
+    func (q *Queue) Empty() bool {
+        return len(q.items) == 0
+    }
+
+Deque 双端队列
+--------------------------------------------------
+
+.. code-block:: go
+
+    import (
+        "container/list"
+        "fmt"
+    )
+
+    // 滑动窗口最大值
+
+    type Deque struct {
+        ll *list.List
+    }
+
+    func NewDeque() *Deque {
+        return &Deque{ll: list.New()}
+    }
+
+    func (dq *Deque) PushFront(x int) {
+        dq.ll.PushFront(x)
+    }
+
+    func (dq *Deque) PushBack(x int) {
+        dq.ll.PushBack(x)
+    }
+
+    func (dq *Deque) Pop() { // remove back
+        dq.ll.Remove(dq.ll.Back())
+    }
+
+    func (dq *Deque) PopFront() { // remove first
+        dq.ll.Remove(dq.ll.Front())
+    }
+
+    func (dq *Deque) Front() int {
+        return dq.ll.Front().Value.(int)
+    }
+
+    func (dq *Deque) Back() int {
+        return dq.ll.Back().Value.(int)
+    }
+
+    func (dq *Deque) Len() int {
+        return dq.ll.Len()
+    }
+
+
+Linked List
+--------------------------------------------------
+
+.. code-block:: go
+
+    package main
+
+    import "fmt"
+
+    // 测试链表。在 redigo 里边使用到了链表作为 pool 的实现
+    type IntList struct {
+        count int
+        // front,back 分别指向第一个和最后一个 node，或者是 nil。front.prev back.next 都是空
+        front, back *Node
+    }
+
+    // 链表节点
+    type Node struct {
+        next, prev *Node
+    }
+
+    func (l *IntList) Count() int {
+        return l.count
+    }
+
+    func (l *IntList) pushFront(node *Node) {
+        node.next = l.front
+        node.prev = nil
+        if l.count == 0 { // note when list is empty
+            l.back = node
+        } else {
+            l.front.prev = node
+        }
+        l.front = node
+        l.count++
+    }
+
+    func (l *IntList) popFront() {
+        first := l.front
+        l.count--
+        if l.count == 0 {
+            l.front, l.back = nil, nil
+        } else {
+            first.next.prev = nil
+            l.front = first.next
+        }
+        first.next, first.prev = nil, nil // clear first
+    }
+
+    func (l *IntList) popBack() {
+        last := l.back
+        l.count--
+        if l.count == 0 {
+            l.front, l.back = nil, nil
+        } else {
+            last.prev.next = nil
+            l.back = last.prev
+        }
+        last.prev, last.next = nil, nil
+    }
+
+    func (l *IntList) Print() {
+        cur := l.front
+        for cur != l.back {
+            fmt.Println(cur)
+            cur = cur.next
+        }
+        if l.back != nil {
+            fmt.Println(l.back)
+        }
+    }
+
+
+Trie 字典树
+--------------------------------------------------
+
+.. code-block:: go
+
+    // Package main provides ...
+    package main
+
+    import "fmt"
+
+    // https://golangbyexample.com/trie-implementation-in-go/
+
+    const (
+        ALBHABET_SIZE = 26
+    )
+
+    type node struct {
+        childrens [ALBHABET_SIZE]*node
+        isWordEnd bool
+    }
+
+    type trie struct {
+        root *node
+    }
+
+    func newTrie() *trie {
+        return &trie{
+            root: &node{},
+        }
+    }
+
+    func (t *trie) insert(word string) {
+        wordLength := len(word)
+        current := t.root
+        for i := 0; i < wordLength; i++ {
+            idx := word[i] - 'a'
+            if current.childrens[idx] == nil {
+                current.childrens[idx] = &node{}
+            }
+            current = current.childrens[idx]
+        }
+        current.isWordEnd = true
+    }
+    func (t *trie) find(word string) bool {
+        wordLength := len(word)
+        current := t.root
+        for i := 0; i < wordLength; i++ {
+            idx := word[i] - 'a'
+            if current.childrens[idx] == nil {
+                return false
+            }
+            current = current.childrens[idx]
+        }
+        if current.isWordEnd {
+            return true
+        }
+        return false
+    }
+
+    func main() {
+        trie := newTrie()
+        words := []string{"zhang", "wang", "li", "zhao"}
+        for i := 0; i < len(words); i++ {
+            trie.insert(words[i])
+        }
+        toFind := []string{"zhang", "wang", "li", "zhao", "gong"}
+        for i := 0; i < len(toFind); i++ {
+            c := toFind[i]
+            if trie.find(c) {
+                fmt.Printf("word[%s] found in trie.\n", c)
+            } else {
+                fmt.Printf("word[%s] not found in trie\n", c)
+            }
+        }
+    }
+
+Lru Cache (不是并发安全的，仅示例)
+--------------------------------------------------
+
+.. code-block:: go
+
+    package main
+
+    import "container/list"
+
+    type LRUCache struct {
+        lis      *list.List
+        m        map[int]*list.Element
+        capacity int
+    }
+
+    // 包装成一个 struct
+    type KV struct {
+        Key int
+        Val int
+    }
+
+    func Constructor(capacity int) LRUCache {
+        return LRUCache{
+            lis:      list.New(),
+            m:        make(map[int]*list.Element),
+            capacity: capacity,
+        }
+    }
+
+    func (this *LRUCache) Get(key int) int {
+        if ele, ok := this.m[key]; ok {
+            this.lis.MoveToFront(ele)
+            return ele.Value.(*KV).Val
+        }
+        return -1
+    }
+
+    func (this *LRUCache) Put(key int, value int) {
+        if ele, ok := this.m[key]; ok {
+            ele.Value.(*KV).Val = value
+            this.lis.MoveToFront(ele)
+            return
+        }
+
+        ele := this.lis.PushFront(&KV{key, value})
+        this.m[key] = ele // map 保存的是 节点信息
+
+        if this.lis.Len() > this.capacity {
+            back := this.lis.Back()
+            delete(this.m, back.Value.(*KV).Key)
+            this.lis.Remove(back)
+        }
+    }
+
+OrderedMap (类似 python collections.OrderedDict)
+--------------------------------------------------
+模拟 python collections.OrderedDict 写的，可以方便的实现 lru 等。注意这里的 order 指的是 key 插入的顺序，不是指 key 字典序。
+
+.. code-block:: go
+
+    package main
+
+    import (
+        "container/list"
+        "fmt"
+    )
+
+    // 按照 key 插入顺序遍历 map，类似 python collections.OrderedDict。注意不是 key 的字典序，而是插入顺序
+    type OrderedMap struct {
+        m  map[string]int
+        me map[string]*list.Element
+        ll *list.List // 记录 key order
+    }
+
+    func NewOrderedMap() *OrderedMap {
+        return &OrderedMap{
+            m:  make(map[string]int),
+            me: make(map[string]*list.Element),
+            ll: list.New(),
+        }
+    }
+
+    func (o *OrderedMap) Set(k string, v int) {
+        if _, found := o.m[k]; !found {
+            e := o.ll.PushBack(k)
+            o.me[k] = e
+        }
+        o.m[k] = v
+    }
+
+    func (o *OrderedMap) Exist(k string) bool {
+        _, found := o.m[k]
+        return found
+    }
+
+    func (o *OrderedMap) Get(k string) int {
+        return o.m[k]
+    }
+
+    func (o *OrderedMap) Delete(k string) {
+        delete(o.m, k)
+
+        node := o.me[k]
+        o.ll.Remove(node)
+        delete(o.me, k)
+    }
+
+    func (o *OrderedMap) Len() int {
+        return len(o.m)
+    }
+
+    // 按照 key 进入顺序返回
+    func (o *OrderedMap) Keys() []string {
+        keys := make([]string, o.ll.Len())
+        i := 0
+        for e := o.ll.Front(); e != nil; e = e.Next() {
+            keys[i] = e.Value.(string)
+            i++
+        }
+        return keys
+    }
+
+Heap 堆
+--------------------------------------------------
+go 自带了一个 ``container/heap`` 模块可以用来实现堆。
+
+.. code-block:: go
+
+    // This example demonstrates an integer heap built using the heap interface.
+    // A heap is a tree with the property that each node is the minimum-valued node in its subtree.
+    // 可以用来实现优先级队列 priority queue
+    package main
+
+    import (
+        "container/heap"
+        "fmt"
+    )
+
+    // An IntHeap is a min-heap of ints.
+    type IntHeap []int
+
+    func (h IntHeap) Len() int           { return len(h) }
+    func (h IntHeap) Less(i, j int) bool { return h[i] < h[j] }
+    func (h IntHeap) Swap(i, j int)      { h[i], h[j] = h[j], h[i] }
+
+    // 最后追加一个元素
+    func (h *IntHeap) Push(x interface{}) {
+        // Push and Pop use pointer receivers because they modify the slice's length,
+        // not just its contents.
+        *h = append(*h, x.(int))
+    }
+
+    // 移除并且返回最后一个元素
+    func (h *IntHeap) Pop() interface{} {
+        old := *h
+        n := len(old)
+        x := old[n-1]
+        *h = old[0 : n-1]
+        return x
+    }
+
+    // This example inserts several ints into an IntHeap, checks the minimum,
+    // and removes them in order of priority.
+    func main() {
+        h := &IntHeap{2, 1, 5}
+        heap.Init(h)
+        fmt.Println(h)
+        heap.Push(h, 3)
+        fmt.Println(h)
+        fmt.Printf("minimum: %d\n", (*h)[0]) // h[0] 最小的元素
+        for h.Len() > 0 {
+            fmt.Printf("%d ", heap.Pop(h))
+        }
+    }
+
+
+参考：
+
+- https://github.com/emirpasic/gods
+
diff --git a/go-note/concurrency_patterns.rst b/go-note/concurrency_patterns.rst
new file mode 100644
index 00000000..79dfa03a
--- /dev/null
+++ b/go-note/concurrency_patterns.rst
@@ -0,0 +1,612 @@
+.. _go_concurrency_patterns:
+
+Go 并发设计模式
+=====================================================================
+
+并发编程常见问题
+--------------------------------------------------
+
+1. 资源共享问题。竞争条件(race condition), 死锁(deadlock)，资源争用(contention)
+
+   - 比如并发读写原生的 map 导致 panic
+   - 读锁重入导致思索。获取读锁的协程，不要二次重入再去获取读锁
+
+2. 协程管理
+3. 通道使用
+
+屏障模式(Barrier Mode)
+--------------------------------------------------
+屏障模式(Barrier Mode)，用来阻塞goroutine直到聚合所有goroutine返回结果，可以用通道实现。使用场景：
+
+1. 多个网络请求并发，聚合结果
+2. 粗粒度任务拆分并发执行，聚合结果
+
+.. code-block:: go
+
+    package main
+
+    import (
+        "fmt"
+        "io/ioutil"
+        "net/http"
+        "time"
+    )
+
+    // 封装屏障模式响应的结构体(一般就是返回值和错误)
+    type BarrierResponse struct {
+        Err    error
+        Resp   string
+        Status int
+    }
+
+    // 执行单个请求，结果放入 channel
+    func doRequest(out chan<- BarrierResponse, url string) {
+        res := BarrierResponse{}
+        client := http.Client{Timeout: time.Duration(3 * time.Second)}
+        resp, err := client.Get(url)
+        if resp != nil {
+            res.Status = resp.StatusCode
+        }
+        if err != nil {
+            res.Err = err
+            out <- res
+            return
+        }
+
+        b, err := ioutil.ReadAll(resp.Body)
+        defer resp.Body.Close()
+        if err != nil {
+            res.Err = err
+            out <- res
+            return
+        }
+        res.Resp = string(b)
+        out <- res //  结果放入通道
+    }
+
+    // 并发请求并聚合结果
+    func Barrier(urls ...string) {
+        n := len(urls)
+        in := make(chan BarrierResponse, n)
+        response := make([]BarrierResponse, n)
+        defer close(in)
+
+        for _, url := range urls {
+            go doRequest(in, url)
+        }
+
+        var hasError bool
+        for i := 0; i < n; i++ {
+            resp := <-in
+            if resp.Err != nil {
+                fmt.Println("Error: ", resp.Err, resp.Status)
+                hasError = true
+            }
+            response[i] = resp
+        }
+        if !hasError {
+            for _, resp := range response {
+                fmt.Println(resp.Status)
+            }
+        }
+    }
+
+    func main() {
+        urls := []string{
+            "https://www.baidu.com",
+            "https://www.weibo.com",
+            "https://www.zhihu.com",
+        }
+        Barrier(urls...)
+    }
+
+未来模式(Future Mode)
+--------------------------------------------------
+Future模式(也称为Promise Mode)。使用 ``fire-and-forget`` 方式，主进程不等子进程执行完就直接返回，然后等到未来执行完的时候再去获取结果。
+未来模式中主goroutine不用等待子goroutine返回的结果，可以先去做其他事情，等未来需要子goroutine结果的时候再来取。
+如果子goroutine还没有返回结果，则一直等待。以下简单的代码示例说明了该模式的原理：
+
+.. code-block:: go
+
+    c := make(chan int)      // future
+    go func() { c <- f() }() // async
+    value := <-c             // await
+
+可以针对 future 模式做一个统一的封装，方便后续使用，代码示例如下：
+
+.. code-block:: go
+
+    /* https://github.com/golang-collections/go-datastructures/blob/59788d5eb259/futures/futures.go
+    Package futures is useful for broadcasting an identical message to a multitude
+    of listeners as opposed to channels which will choose a listener at random
+    if multiple listeners are listening to the same channel.  The future will
+    also cache the result so any future interest will be immediately returned
+    to the consumer.
+    */
+    package main
+
+    import (
+        "fmt"
+        "sync"
+        "time"
+    )
+
+    // Completer is a channel that the future expects to receive
+    // a result on.  The future only receives on this channel.
+    type Completer <-chan interface{}
+
+    // Future represents an object that can be used to perform asynchronous
+    // tasks.  The constructor of the future will complete it, and listeners
+    // will block on getresult until a result is received.  This is different
+    // from a channel in that the future is only completed once, and anyone
+    // listening on the future will get the result, regardless of the number
+    // of listeners.
+    type Future struct {
+        triggered bool // because item can technically be nil and still be valid
+        item      interface{}
+        err       error
+        lock      sync.Mutex
+        wg        sync.WaitGroup
+    }
+
+    // GetResult will immediately fetch the result if it exists
+    // or wait on the result until it is ready.
+    func (f *Future) GetResult() (interface{}, error) {
+        f.lock.Lock()
+        if f.triggered {
+            f.lock.Unlock()
+            return f.item, f.err
+        }
+        f.lock.Unlock()
+
+        f.wg.Wait()
+        return f.item, f.err
+    }
+
+    func (f *Future) setItem(item interface{}, err error) {
+        f.lock.Lock()
+        f.triggered = true
+        f.item = item
+        f.err = err
+        f.lock.Unlock()
+        f.wg.Done()
+    }
+
+    func listenForResult(f *Future, ch Completer, timeout time.Duration, wg *sync.WaitGroup) {
+        wg.Done()
+        select {
+        case item := <-ch:
+            f.setItem(item, nil)
+        case <-time.After(timeout):
+            f.setItem(nil, fmt.Errorf(`Timeout after %f seconds.`, timeout.Seconds()))
+        }
+    }
+
+    // New is the constructor to generate a new future.  Pass the completed
+    // item to the toComplete channel and any listeners will get
+    // notified.  If timeout is hit before toComplete is called,
+    // any listeners will get passed an error.
+    func New(completer Completer, timeout time.Duration) *Future {
+        f := &Future{}
+        f.wg.Add(1)
+        var wg sync.WaitGroup
+        wg.Add(1)
+        go listenForResult(f, completer, timeout, &wg)
+        wg.Wait()
+        return f
+    }
+
+    // 使用示例
+    func main() {
+        c := make(chan interface{})
+
+        go func() {
+            time.Sleep(time.Second)
+            c <- "hehe"
+        }()
+
+        f := New(c, time.Second*3)
+        res, err := f.GetResult()
+        fmt.Println(res, err)
+    }
+
+
+管道模式(Pipeline Mode)
+--------------------------------------------------
+也称作流水线模式，一般有以下几个步骤：
+
+1. 流水线由一道道工序构成，每道工序通过通道把数据传递到下一个工序
+2. 每道工序一般会对应一个函数，函数里有协程和通道，协程一般用于处理数据并把它放入通道中，每道工序会返回这个通道以供下一道工序使用
+3. 最终要有一个组织者（示例中的main()函数）把这些工序串起来，这样就形成了一个完整的流水线，对于数据来说就是数据流
+
+.. code-block:: go
+
+    // 以组装计算机为例。三道工序：配件采购(Buy)-> 配件组装(Build) -> 打包成品(Pack)
+    func Buy(n int) <-chan string {
+        out := make(chan string)
+        go func() {
+            defer close(out)
+            for i := 1; i <= n; i++ {
+                out <- fmt.Sprintf("配件%d", i)
+            }
+        }()
+        return out
+    }
+
+    func Build(in <-chan string) <-chan string {
+        out := make(chan string)
+        go func() {
+            defer close(out)
+            for c := range in {
+                out <- fmt.Sprintf("组装(%s)", c)
+            }
+        }()
+        return out
+    }
+
+    func Pack(in <-chan string) <-chan string {
+        out := make(chan string)
+        go func() {
+            defer close(out)
+            for c := range in {
+                out <- fmt.Sprintf("打包(%s)", c)
+            }
+        }()
+        return out
+    }
+
+    func main() {
+        accessories := Buy(6)
+        computers := Build(accessories)
+        packs := Pack(computers)
+        for p := range packs {
+            fmt.Println(p)
+        }
+    }
+
+.. code-block:: go
+
+    package main
+
+    import "fmt"
+
+    // 工序 1：数组生成器
+    func Generator(max int) <-chan int {
+        out := make(chan int, 100)
+        go func() {
+            for i := 1; i <= max; i++ {
+                out <- i
+            }
+            close(out)
+        }()
+        return out
+    }
+
+    // 工序 2：求整数的平方
+    func Square(in <-chan int) <-chan int {
+        out := make(chan int, 100)
+        go func() {
+            for v := range in {
+                out <- v * v
+            }
+            close(out)
+        }()
+        return out
+    }
+
+    // 工序 3：求和
+    func Sum(in <-chan int) <-chan int {
+        out := make(chan int, 100)
+        go func() {
+            var sum int
+            for v := range in {
+                sum += v
+            }
+            out <- sum
+            close(out)
+        }()
+        return out
+    }
+
+    func main() {
+        arr := Generator(5)
+        squ := Square(arr)
+        sum := <-Sum(squ)
+        fmt.Println(sum)
+    }
+
+
+扇出和扇入模式(Fan-out Fan-in)
+--------------------------------------------------
+扇出(Fan-out)是指多个函数可以从同一个通道读取数据，直到该通道关闭。扇入(Fan-in)是指一个函数可以从多个输入中读取数据并继续进行，
+直到所有输入都关闭。扇出和扇入模式的方法是将输入通道多路复用到一个通道上，当所有输入都关闭时，该通道才关闭。
+扇出的数据流向是发散传递出去，是输出流；扇入的数据流向是汇聚进来，是输入流。
+
+.. image::  ../_image/goweb/concurrency/扇出扇入.png
+
+.. code-block:: go
+
+    // 扇入函数，把多个channel 中的数据发送到一个 channel 中
+    func Merge(ins ...<-chan string) <-chan string {
+        var wg sync.WaitGroup
+        out := make(chan string)
+
+        p := func(in <-chan string) {
+            defer wg.Done()
+            for c := range in {
+                out <- c
+            }
+        }
+
+        wg.Add(len(ins))
+        // 扇入
+        for _, cs := range ins {
+            go p(cs)
+        }
+        go func() {
+            wg.Wait()
+            close(out)
+        }()
+        return out
+    }
+
+    func main() {
+        accessories := Buy(12)
+        computers1 := Build(accessories)
+        computers2 := Build(accessories)
+        computers3 := Build(accessories)
+        computers := Merge(computers1, computers2, computers3)
+        packs := Pack(computers)
+        for p := range packs {
+            fmt.Println(p)
+        }
+    }
+
+
+协程池模式
+--------------------------------------------------
+即便 go 的协程比较轻量，但是当需要操作大量 goroutine 的时候，依然有内存开销和 GC 的压力。可以考虑使用协程池减少频繁创建销毁协程的开销。
+
+.. code-block:: go
+
+    package main
+
+    import (
+        "fmt"
+        "sync"
+        "sync/atomic"
+    )
+
+    // 任务处理器
+    type TaskHandler func(interface{})
+
+    // 任务结构体
+    type Task struct {
+        Param   interface{}
+        Handler TaskHandler
+    }
+
+    // 协程池接口
+    type WorkerPoolImpl interface {
+        AddWorker()
+        SendTask(Task)
+        Release()
+    }
+
+    // 协程池
+    type WorkerPool struct {
+        wg   sync.WaitGroup
+        inCh chan Task
+    }
+
+    func (d *WorkerPool) AddWorker() {
+        d.wg.Add(1)
+        go func() {
+            defer d.wg.Done()
+            for task := range d.inCh {
+                task.Handler(task.Param)
+            }
+        }()
+    }
+
+    func (d *WorkerPool) Release() {
+        close(d.inCh)
+        d.wg.Wait()
+    }
+
+    func (d *WorkerPool) SendTask(t Task) {
+        d.inCh <- t
+    }
+
+    func NewWorkerPool(buffer int) WorkerPoolImpl {
+        return &WorkerPool{
+            inCh: make(chan Task, buffer),
+        }
+    }
+
+    func main() {
+        bufferSize := 100
+        var workerPool = NewWorkerPool(bufferSize)
+        workers := 4
+        for i := 0; i < workers; i++ {
+            workerPool.AddWorker()
+        }
+
+        var sum int32
+        testFunc := func(i interface{}) {
+            n := i.(int32)
+            atomic.AddInt32(&sum, n)
+        }
+
+        var i, n int32
+        n = 100
+        for ; i < n; i++ {
+            task := Task{
+                i,
+                testFunc,
+            }
+            workerPool.SendTask(task)
+        }
+        workerPool.Release()
+        fmt.Println(sum) // 4950
+    }
+
+
+发布订阅模式
+--------------------------------------------------
+基于消息通知的并发设计模式。发送者发送消息，订阅者通过订阅感兴趣的主题(Topic) 接收消息。
+
+.. code-block:: go
+
+    package main
+
+    import (
+        "fmt"
+        "strings"
+        "time"
+    )
+
+    import (
+        "sync"
+    )
+
+    type (
+        //订阅者通道
+        Subscriber chan interface{}
+        //主题函数
+        TopicFunc func(v interface{}) bool
+    )
+
+    //发布者结构体
+    type Publisher struct {
+        // subscribers 是程序的核心，订阅者都会注册在这里，
+        // publisher发布消息的时候也会从这里开始
+        subscribers map[Subscriber]TopicFunc
+        buffer      int           // 订阅者的缓冲区长度
+        timeout     time.Duration // publisher 发送消息的超时时间
+        // m 用来保护 subscribers
+        // 当修改 subscribers 的时候（即新加订阅者或删除订阅者）使用写锁
+        // 当向某个订阅者发送消息的时候（即向某个 Subscriber channel 中写入数据），使用读锁
+        m sync.RWMutex
+    }
+
+    //实例化
+    func NewPublisher(publishTimeout time.Duration, buffer int) *Publisher {
+        return &Publisher{
+            buffer:      buffer,
+            timeout:     publishTimeout,
+            subscribers: make(map[Subscriber]TopicFunc),
+        }
+    }
+
+    //发布者订阅方法
+    func (p *Publisher) Subscribe() Subscriber {
+        return p.SubscribeTopic(nil)
+    }
+
+    //发布者订阅主题
+    func (p *Publisher) SubscribeTopic(topic TopicFunc) Subscriber {
+        ch := make(Subscriber, p.buffer)
+        p.m.Lock()
+        p.subscribers[ch] = topic
+        p.m.Unlock()
+
+        return ch
+    }
+
+    //Delete 删除掉某个订阅者
+    func (p *Publisher) Delete(sub Subscriber) {
+        p.m.Lock()
+        defer p.m.Unlock()
+
+        delete(p.subscribers, sub)
+        close(sub)
+    }
+
+    //发布者发布
+    func (p *Publisher) Publish(v interface{}) {
+        p.m.RLock()
+        defer p.m.RUnlock()
+
+        var wg sync.WaitGroup
+        // 同时向所有订阅者写消息，订阅者利用 topic 过滤消息
+        for sub, topic := range p.subscribers {
+            wg.Add(1)
+            go p.sendTopic(sub, topic, v, &wg)
+        }
+
+        wg.Wait()
+    }
+
+    //Close 关闭 Publisher，删除所有订阅者
+    func (p *Publisher) Close() {
+        p.m.Lock()
+        defer p.m.Unlock()
+
+        for sub := range p.subscribers {
+            delete(p.subscribers, sub)
+            close(sub)
+        }
+    }
+
+    //发送主题
+    func (p *Publisher) sendTopic(sub Subscriber, topic TopicFunc, v interface{}, wg *sync.WaitGroup) {
+        defer wg.Done()
+
+        if topic != nil && !topic(v) {
+            return
+        }
+
+        select {
+        case sub <- v:
+        case <-time.After(p.timeout):
+        }
+    }
+
+    func main() {
+        //实例化
+        p := NewPublisher(100*time.Millisecond, 10)
+        defer p.Close()
+
+        // 订阅者订阅所有消息
+        all := p.Subscribe()
+        //订阅者仅订阅包含 golang 的消息
+        golang := p.SubscribeTopic(func(v interface{}) bool {
+            if s, ok := v.(string); ok {
+                return strings.Contains(s, "golang")
+            }
+            return false
+        })
+
+        //发布消息
+        p.Publish("hello, world!")
+        p.Publish("hello, golang!")
+
+        //加锁
+        var wg sync.WaitGroup
+        wg.Add(2)
+
+        //开启goroutine
+        go func() {
+            for msg := range all {
+                _, ok := msg.(string)
+                fmt.Println(ok)
+            }
+            wg.Done()
+        }()
+
+        //开启goroutine
+        go func() {
+            for msg := range golang {
+                v, ok := msg.(string)
+                fmt.Println(v)
+                fmt.Println(ok)
+            }
+            wg.Done()
+        }()
+
+        p.Close()
+        wg.Wait()
+    }
+
+参考：《Go 语言高级开发与实战》
diff --git a/go-note/design_patterns.rst b/go-note/design_patterns.rst
new file mode 100644
index 00000000..70446480
--- /dev/null
+++ b/go-note/design_patterns.rst
@@ -0,0 +1,89 @@
+.. _go_design_patterns:
+
+Go 设计模式
+=====================================================================
+
+单例模式(Singleton)
+--------------------------------------------------
+.. code-block:: go
+
+    package main
+
+    import (
+        "fmt"
+        "sync"
+    )
+
+    /*
+    go 单例模式实现方式：
+
+    1. 使用 lock，为了并发安全，使用 lock + double check
+    2. 使用 sync.Once (推荐👍)
+    3. 使用 init()。The init function is only called once per file in a package,  so we can be sure that only a single instance will be created
+
+    参考：
+
+    - https://blog.cyeam.com/designpattern/2015/08/12/singleton
+    - https://golangbyexample.com/all-design-patterns-golang/
+    - https://medium.com/golang-issue/how-singleton-pattern-works-with-golang-2fdd61cd5a7f
+    */
+
+    var lock = &sync.Mutex{}
+
+    type single struct {
+    }
+
+    var singleInstance *single
+
+    func getInstance() *single {
+         if singleInstance == nil { // 防止每次调用 getInstance 都频繁加锁
+             lock.Lock()
+             defer lock.Unlock()
+             // double check. 如果超过一个goroutine进入这里，保证只有一个 goroutine 创建
+             if singleInstance == nil {
+                 fmt.Println("Creting Single Instance Now")
+                 singleInstance = &single{}
+             } else {
+                 fmt.Println("Single Instance already created-1")
+             }
+         } else {
+             fmt.Println("Single Instance already created-2")
+         }
+         return singleInstance
+    }
+
+    func main() {
+         for i := 0; i < 100; i++ {
+             go getInstance()
+         }
+         // Scanln is similar to Scan, but stops scanning at a newline and
+         // after the final item there must be a newline or EOF.
+         fmt.Scanln()
+    }
+
+
+.. code-block:: go
+
+    // 推荐使用 once，实现更加简洁优雅
+    var once sync.Once
+
+    type single struct {
+    }
+
+    var singleInstance *single
+
+    func getInstance() *single {
+        once.Do(func() {
+            fmt.Println("Creting Single Instance Now")
+            singleInstance = &single{}
+        })
+        return singleInstance
+    }
+
+参考:
+--------------------------------------------------
+
+- `Go Patterns (github) <https://github.com/tmrts/go-patterns>`_
+- `All Design Patterns in Go (Golang) <https://golangbyexample.com/singleton-design-pattern-go/>`_
+- `设计模式Golang实现 <https://github.com/senghoo/golang-design-pattern/tree/master>`_
+
diff --git a/go-note/index.rst b/go-note/index.rst
new file mode 100644
index 00000000..cfcec4f1
--- /dev/null
+++ b/go-note/index.rst
@@ -0,0 +1,16 @@
+==========================================
+Golang 快速入门 Go For Pythonisa ❤️
+==========================================
+
+
+.. toctree::
+    :glob:
+    :maxdepth: 2
+
+    web
+    tricks
+    mistakes
+    optimize
+    algorithms
+    design_patterns
+    concurrency_patterns
diff --git a/go-note/mistakes.rst b/go-note/mistakes.rst
new file mode 100644
index 00000000..5a89d6d7
--- /dev/null
+++ b/go-note/mistakes.rst
@@ -0,0 +1,52 @@
+.. _gomistakes:
+
+Go 常见错误
+=====================================================================
+
+100 Go Mistakes and How to Avoid Them
+---------------------------------------------------------------
+《100 Go Mistakes and How to Avoid Them》这本书的一些重要内容笔记。
+
+2 Code and project organization
+--------------------------------------------------
+
+2.1 #1: Unintended variable shadowing
+>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+
+.. code-block:: go
+
+	var client *http.Client // 以下代码运行后，client 依然还是 nil
+	if tracing {
+		// Declares a client variable
+		client, err := createClientWithTracing() // 新创建的同名变量覆盖外层同名的 client
+		if err != nil {
+			return err
+		}
+		log.Println(client)
+	} else {
+		client, err := createDefaultClient()
+		if err != nil {
+			return err
+		}
+		log.Println(client)
+	}
+
+    // 解决方式：不要使用 := 赋值一个新变量，需要声明一下 client 和 err
+	var client *http.Client
+	var err error // Declares an err variable
+	if tracing {
+		client, err = createClientWithTracing()
+		if err != nil {
+			return err
+		}
+	} else {
+		// Same logic
+	}
+
+2.5 #5: Interface pollution
+>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+When to use interfaces:
+
+- Common behavior
+- Decoupling
+- Restricting behavior
diff --git a/go-note/optimize.rst b/go-note/optimize.rst
new file mode 100644
index 00000000..a1a2ad62
--- /dev/null
+++ b/go-note/optimize.rst
@@ -0,0 +1,210 @@
+.. _optimize:
+
+Go 性能优化
+=====================================================================
+
+常见的优化手段
+---------------------------------------------------------------
+- 池化技术。比如 sync.Pool 对象池(频繁分配同一类型的多个对象)；协程池(Jeffail/tunny, panjf2000/ants)；连接池(buraksezer/connpool)；内存池等
+- 复用对象。比如 string2bytes
+- 优化反射。https://github.com/goccy/go-reflect
+- 减小锁消耗。减小锁粒度；使用原子操作(atomic)代替互斥锁。读多写少的场景使用读写锁(RWMutex)
+- 减小锁竞争。比如 go-cache/bigcache 等缓存库都是通过分片功能减少锁竞争
+
+优化建议：
+
+- 预分配内存。slice/map 如果预知容量信息，初始化应该提供，减少内存重分配和复制元素的消耗。 ``make([]int, 0, cap); make(map[int]int, cap)``
+- 使用 strings.Builder 拼接大量字符串
+- 使用空的结构体作为占位符，空结构体 struct{} 不占内存。 比如实现 set 使用 map[string]struct{}{}
+- 使用 atomic 代替 sync 包。atomic 包的操作指令级支持
+- struct 合理的布局可以减少内存占用，提升性能(内存对齐)。一个简单的规则是按照字节大小倒序排列。
+- 尽量减少逃逸，将变量限制在栈上，减少堆变量分配，降低 GC 成本，提高程序性能
+
+  - 局部切片尽可能确定长度或者容量。长度和容量尽可能是一个常量，如果是一个变量go无法判断其大小会逃逸到堆上分配
+  - 函数返回指针可以减少拷贝但是会导致内存分配逃逸到堆中。如果要修改原对象或者返回内存比较大的对象，返回指针。对于只读或
+    者占用内存小的对象，可以直接返回值。
+  - 如果返回值的类型可以确定，就不要用 interface{}
+
+string 与 []byte 互转
+---------------------------------------------------------------
+利用了底层 string 和 byte slice 实现的技巧，如果需要大量互转可以使用这种方式。
+
+.. code-block:: go
+
+    /*
+    type StringHeader struct { // reflect.StringHeader
+        Data uintptr
+        Len  int
+    }
+    type SliceHeader struct { // reflect.SliceHeader
+        Data uintptr
+        Len  int
+        Cap  int
+    }
+    */
+
+    // NOTE：注意之后不要修改 string
+    func str2bytes(s string) []byte {
+       x := (*[2]uintptr)(unsafe.Pointer(&s))
+       b := [3]uintptr{x[0], x[1], x[1]}
+       return *(*[]byte)(unsafe.Pointer(&b))
+    }
+
+    func bytes2str(b []byte) string {
+       return *(*string)(unsafe.Pointer(&b))
+    }
+
+
+- `Go性能优化技巧 <https://segmentfault.com/a/1190000005006351>`_
+- `Golang 中 string 与 []byte 互转优化 <https://medium.com/@kevinbai/golang-%E4%B8%AD-string-%E4%B8%8E-byte-%E4%BA%92%E8%BD%AC%E4%BC%98%E5%8C%96-6651feb4e1f2>`_
+- `Go 语言高性能编程 <https://geektutu.com/post/high-performance-go.html>`_
+
+大量字符串拼接
+---------------------------------------------------------------
+字符串在 go 中是不可变对象。大量字符串(一般超过 5 个字符串)拼接不要用 + ，使用 bytes.Buffer 或者 strings.Builder。
+不过对于个数比较少的字符串拼接，直接用 + 效率也很高，比 fmt.Sprintf 更快，所以少量字符串拼接可以放心使用 + 。
+
+.. code-block:: go
+
+    // bytes.Buffer
+    package main
+
+    import (
+        "fmt"
+        "bytes"
+    )
+
+    func main() {
+        var b bytes.Buffer
+
+        b.WriteString("abc")
+        b.WriteString("def")
+
+        fmt.Println(b.String()) // abcdef
+    }
+
+.. code-block:: go
+
+    // strings.Builder
+    package main
+
+    import (
+        "fmt"
+        "strings"
+    )
+
+    func main() {
+        var sb strings.Builder
+        sb.WriteString("First")
+        sb.WriteString("Second")
+        fmt.Println(sb.String())    // FirstSecond
+    }
+
+- `concatenate strings in golang <https://golangdocs.com/concatenate-strings-in-golang>`_
+- `How to efficiently concatenate strings in go <https://stackoverflow.com/questions/1760757/how-to-efficiently-concatenate-strings-in-go>`_
+
+
+更快的随机数
+---------------------------------------------------------------
+Go 内置的 rand.Int()在生成随机数时，为了并发安全底层使用了锁，在高并发常见下会有性能问题。
+可以使用 github.com/valyala/fastrand 等三方库替换。
+
+
+更快的Json序列化
+---------------------------------------------------------------
+可以使用字节开源的 sonic 库  https://github.com/bytedance/sonic 替换内置的 json
+
+
+伪共享问题(false sharing)
+---------------------------------------------------------------
+如果并发更新一个结构体的字段，我们可以通过填充空字节防止字段被 cpu 缓存到一个 cache line 单位中，需要不断同步降低效率。
+可以在 https://github.com/uber-go/ratelimit 中找到一个例子：
+
+.. code-block:: go
+
+    type leakyBucketLimiter struct {
+        state unsafe.Pointer // 是一个状态的指针，用于存储上一次的执行的时间，以及需要 sleep  的时间
+
+        //lint:ignore U1000 Padding is unused but it is crucial to maintain performance
+        // of this rate limiter in case of collocation with other frequently accessed memory.
+        padding [56]byte // cache line size - state pointer size = 64 - 8; created to avoid false sharing.(伪共享)
+        // cpu cache 一般是以 cache line 为单位的，在 64 位的机器上一般是 64 字节
+        // 所以如果我们高频并发访问的数据小于 64 字节的时候就可能会和其他数据一起缓存，其他数据如果出现改变就会导致 cpu 认为缓存失效，这就是 false sharing
+        // 所以在这里为了尽可能提高性能，填充了 56 字节的无意义数据，因为 state 是一个指针占用了 8 个字节，所以 64 - 8 = 56
+
+        perRequest time.Duration // perRequest = 1s / rate，每个请求间隔 1s/perRequest
+        maxSlack   time.Duration // 松弛时间，也就是可以允许的突发流量的大小，默认是 Pre / 10
+    }
+
+
+正确设置容器 CPU 配额
+---------------------------------------------------------------
+容器中运行 Go 程序需要正确设置 GOMAXPROCS，推荐使用 https://github.com/uber-go/automaxprocs 这个库，直接一行代码就可以。
+``import _ "go.uber.org/automaxprocs"``
+
+
+日志延迟序列化
+---------------------------------------------------------------
+经常需要在日志里打印 json 数据，但是免不了序列化的开销。即使你只在 debug 级别下打印，还是要先序列化参数之后传入数据。
+可以使用延迟序列化的方式，这样正式环境下的 debug 日志不会真正序列化，减少 cpu 开销。
+
+.. code-block:: go
+
+    package main
+
+    import (
+        "time"
+
+        "xxxx/logs" // 你们用的日志库
+        "github.com/bytedance/sonic"
+    )
+
+    func GetLog(data interface{}) string {
+        if log, err := sonic.MarshalString(data); err == nil {
+            return log
+        }
+        return ""
+    }
+
+    // 延迟序列化，避免 debug 模式下序列化 json 开销
+    type LazyInfo struct {
+        Data any
+    }
+
+    func (l *LazyInfo) String() string {
+        dataStr, err := sonic.MarshalString(l.Data)
+        if err != nil {
+            panic(err)
+        }
+        return dataStr
+    }
+
+    func NewLazyInfo(d any) *LazyInfo {
+        return &LazyInfo{Data: d}
+    }
+
+    type S struct {
+        a string
+        b string
+    }
+
+    func init() {
+        // logs.SetLevel(logs.LevelDebug) // 测试环境
+        logs.SetLevel(logs.LevelInfo) // 正式环境
+    }
+
+    func main() {
+        s := S{a: "a", b: "b"}
+        logs.Info("info getLog:%s", GetLog(s))
+        logs.Debug("debug getLog:%s", GetLog(s))
+
+        logs.Info("info lazyInfo:%s", NewLazyInfo(s))   // 正式环境使用info
+        logs.Debug("debug lazyInfo:%s", NewLazyInfo(s)) // 正式环境使用 info 级别后，debug 函数参数不会真序列化，减少开销
+
+        time.Sleep(time.Second)
+    }
+
+
+使用 context cache 避免重复下游调用(调用放大)
+---------------------------------------------------------------
+TODO
diff --git a/go-note/tricks.rst b/go-note/tricks.rst
new file mode 100644
index 00000000..c515fcc7
--- /dev/null
+++ b/go-note/tricks.rst
@@ -0,0 +1,1506 @@
+.. _gotricks:
+
+Go 踩坑
+=====================================================================
+
+go初学者常见错误
+---------------------------------------------------------------
+强烈建议 go 新手先过一遍下边这篇文章，go 写多了你一定会遇到很多里边总结的坑。
+
+- `50 Shades of Go: Traps, Gotchas, and Common Mistakes for New Golang Devs  <http://devs.cloudimmunity.com/gotchas-and-common-mistakes-in-go-golang/>`_
+- https://github.com/teivah/100-go-mistakes
+
+Go tricks
+--------------------------------------------------
+
+影子变量(Shadowed variables)
+>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+影子变量的问题可以使用工具 govet 检查出来，建议在流水线加上代码静态检查发现此类问题。
+
+.. code-block:: go
+
+    // https://yourbasic.org/golang/gotcha-shadowing-variables/
+    func main() {
+        n := 0
+        if true {
+            n := 1
+            n++
+        }
+        fmt.Println(n) // 0，注意 if 作用与里边使用 := 赋值隐藏了外部的 n，所以原来的 n 打印还是 0
+        // 如果想要修改 n，直接用 n = 1
+    }
+
+golang cannot refer to unexported field or method
+>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+
+小写开头只能被本 package 访问
+
+- https://stackoverflow.com/questions/24487943/invoke-golang-struct-function-gives-cannot-refer-to-unexported-field-or-method
+
+访问其它Go package中的私有函数(不推荐)
+>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+
+可以写一个公有函数暴露私有方法，不过一般私有方法都是希望隐藏的，就像 python 的下划线虽然不强制，但是不推荐使用。
+
+- https://colobu.com/2017/05/12/call-private-functions-in-other-packages/
+
+Can't load package
+>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+同一个 folder 不要存在不同 package，否则 can't load package
+
+package is folder.  package name is folder name.  package path is folder path.
+
+- https://studygolang.com/articles/8312
+
+
+如何编译期间检查是否实现接口？
+>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+
+.. code-block:: go
+
+    // for pointers to struct
+    type MyType struct{}
+    var _ MyInterface = (*MyType)(nil)  // 很多源码中可以看到这种写法，但是一开始感觉有点奇怪
+    // for struct literals
+    var _ MyInterface = MyType{}
+    // for other types - just create some instance
+    type MyType string
+    var _ MyInterface = MyType("doesn't matter")
+
+- https://splice.com/blog/golang-verify-type-implements-interface-compile-time/
+- https://medium.com/@matryer/golang-tip-compile-time-checks-to-ensure-your-type-satisfies-an-interface-c167afed3aae
+
+
+如何通过反射判断是否实现接口?
+>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+
+.. code-block:: go
+
+    type CustomError struct{}
+
+    func (*CustomError) Error() string {
+        return ""
+    }
+
+    func testImplements() {
+        // 判断某个类型是否实现了接口。
+        // 获取接口类型 reflect.TypeOf((*<interface>)(nil)).Elem()
+        typeOfError := reflect.TypeOf((*error)(nil)).Elem()
+        customErrorPtr := reflect.TypeOf(&CustomError{})
+        customError := reflect.TypeOf(CustomError{})
+
+        fmt.Println(customErrorPtr.Implements(typeOfError)) // true
+        fmt.Println(customError.Implements(typeOfError))    // false
+    }
+
+
+Go 运行单个测试文件报错 undefined？
+>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+
+执行命令时加入这个测试文件需要引用的源码文件，在命令行后方的文件都会被加载到command-line-arguments中进行编译
+
+``go test -v getinfo_test.go  lib.go``
+
+- https://www.cnblogs.com/Detector/p/10010292.html
+- https://stackoverflow.com/questions/14723229/go-test-cant-find-function-in-a-same-package
+
+break 可以在 for,switch,select 生效
+>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+如果在 for 循环中的 switch/select 中使用了 break，实际上 break 的并不是 for，而是对应的switch/select。
+如果需要 break 掉 for 语句，就要使用标签(label)来解决。
+
+.. code-block:: go
+
+        for {
+            select {
+            case <-ch:
+            // do something
+            case <-ctx.Done():
+                break // NOTE: 注意这里 break 的是 select，并不是 for
+            }
+        }
+
+    // 解决方式使用 loop 标签
+    loop:
+        for {
+            select {
+            case <-ch:
+            // do something
+            case <-ctx.Done():
+                break loop // 结束 loop 而不是 select
+            }
+        }
+
+
+Go 循环遍历 []struct 会拷贝值
+>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+注意循环遍历一个结构体切片会拷贝每一个值，修改会不生效。如果想要修改 struct 的值，请使用 slice 下标修改或者用结构体指针。
+(推荐使用下标，因为遍历指针结构对cpu cache 不友好)
+
+.. code-block:: go
+
+    type Cat struct {
+      name string
+    }
+
+    func testSliceAssign() {
+      cats := []Cat{
+        {name: "cat1"},
+        {name: "cat2"},
+      }
+      for _, cat := range cats { // cat 这里是拷贝的值
+        cat.name = "new cat"  // NOTE: 注意这里 cat 是拷贝的值，所以你无法修改 cat。使用下标或者指针
+      }
+      fmt.Println(cats) // 无法修改 [{cat1} {cat2}]
+
+      // 方式1：使用下标(推荐⭐️)
+      for i := range cats {
+        cats[i].name = "new cat"
+      }
+      fmt.Println(cats)
+
+      // 方式2：使用struct 指针 (这里也会拷贝指针，但是因为指向同一个地址，所以也可以修改)
+      pcats := []*Cat{
+        {name: "cat1"},
+        {name: "cat2"},
+      }
+      for _, cat := range pcats {
+        cat.name = "new cat"
+      }
+      for _, cat := range pcats {
+        fmt.Println(cat)
+      }
+    }
+
+
+不要并发读写map，可能导致程序崩溃
+>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+使用内置 map 注意几点:
+
+- 使用 make 初始化。直接声明就赋值会 panic。有时候可能会忘记一些隐含场景，比如：
+
+  - 结构体里有一个 map 成员，直接赋值也会 panic
+  - map 作为函数的命名返回值的时候，直接赋值也会 panic
+
+- 赋值是浅拷贝。深拷贝需要自己复制
+- 内置 map 不要并发写入或者删除，必须加锁。或者使用 sync.Map
+
+如果多个 goroutine 并发对 map 进行读写，必须要同步，否则可能导致进程退出
+
+.. code-block:: go
+
+    // https://blog.golang.org/go-maps-in-action
+    var counter = struct{
+        sync.RWMutex
+        m map[string]int
+    }{m: make(map[string]int)}
+
+    counter.RLock() // locks for reading
+    n := counter.m["some_key"]
+    counter.RUnlock()
+    fmt.Println("some_key:", n)
+
+    counter.Lock() // locks for writing
+    counter.m["some_key"]++
+    counter.Unlock()
+
+
+go 如何实现函数默认值(go本身没提供)
+>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+
+.. code-block:: go
+
+    // https://stackoverflow.com/questions/19612449/default-value-in-gos-method
+    // 可以通过传递零值或者 nil 的方式来判断。
+    // Both parameters are optional, use empty string for default value
+    func Concat1(a string, b int) string {
+      if a == "" {
+        a = "default-a"
+      }
+      if b == 0 {
+        b = 5
+      }
+
+      return fmt.Sprintf("%s%d", a, b)
+    }
+
+    // 或者使用传递结构体的方式，使用结构体的默认零值或者构造函数的初始值
+    type Params struct {
+      a, b, c int
+    }
+
+    func doIt(p Params) int {
+      return p.a + p.b + p.c
+    }
+
+
+go 初始化 slice/map 的区别
+>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+直接看代码，注意 map 赋值之前需要先 make 创建，直接给一个 nil map 赋值会 panic，但是 slice 却可以直接声明然后 append。
+如果是一个 struct 包含了 map，你应该在构造函数里进行 make 初始化，否则直接赋值也会 panic。
+
+.. code-block:: go
+
+    // 初始化一个全局 map 可以用 make，防止第一次赋值 nil map 会 panic
+    // https://nanxiao.gitbooks.io/golang-101-hacks/content/posts/nil-slice-vs-nil-map.html
+    var globalMap = make(map[string]string) // 之后可以在 init() 函数初始化
+
+    func main() {
+            var intSlice []int // 注意可以直接声明一个 slice 然后 append
+            fmt.Println(intSlice)
+            intSlice = append(intSlice, 1)
+            fmt.Println(intSlice)
+
+            // 已知最大容量的情况下，建议 make 初始化，可以避免重新分配内存提升效率
+            maxCap := 3
+            intSlice2 := make([]int, 0, maxCap)
+            fmt.Println(intSlice2)
+
+            m2 := make(map[int]int) // 如果是 map 要先 make 才可以，否则 panic
+            m2[1] = 1 // ok
+            fmt.Println(m2)
+
+            // 直接声明然后赋值就会 panic。有一些 struct 包含了 map 结构体成员，构造函数里需要注意初始化 map，否则直接赋值panic
+            // https://stackoverflow.com/questions/27553399/golang-how-to-initialize-a-map-field-within-a-struct
+            var m1 map[int]int
+            m1[1] = 1          // NOTE: panic ! 注意这样会panic 啊！！!
+            fmt.Println(m1)
+
+            type T struct {
+                m map[int][int]
+            }
+            func NewT() T {
+                return T{m: make(map[int]int)}
+                // return T{m: map[int]int{}}
+            }
+    }
+
+    func testNilMap() {
+        var nilm map[string]int
+        nilm = nil
+        // 直接对 nil map 取值和 获取长度不会 panic
+        fmt.Println(nilm["a"], len(nilm))
+        // 直接对 nil map 遍历也不会 panic
+        for k, v := range nilm {
+            fmt.Println(k, v)
+        }
+        // nilm["hehe"] = 1 // 但是不能赋值，会 panic。必须用 make 或者 empty map 初始化先
+    }
+
+
+go 没有内置的 set 结构
+>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+go 目前没有提供泛型，也没提供一个统一的 set 数据结构。可以使用 map[string]bool 来模拟 set(注意并发安全)。
+或者使用第三方提供的 set 类型。
+
+- https://github.com/deckarep/golang-set
+- https://stackoverflow.com/questions/34018908/golang-why-dont-we-have-a-set-datastructure
+
+编译 tag 的作用
+>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+
+.. code-block:: go
+
+    // +build linux,386 darwin,!cgo
+
+- https://golang.org/pkg/go/build/
+
+Application auto build versioning
+>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+
+给 build 的二进制文件加上版本号，注意如果命令中输出有空格，需要加上单引号。
+这样我们可以每次运行二进制文件的时候打印构建时间，当前的版本等信息。
+
+.. code-block:: go
+
+    // +build linux,386 darwin,!cgo
+    package main
+
+    import "fmt"
+    var xyz string
+    func main() {
+        fmt.Println(xyz)
+    }
+    // $ go run -ldflags "-X main.xyz=abc" main.go
+    // go build -ldflags "-X main.minversion=`date -u +.%Y%m%d.%H%M%S`" service.go
+    // go build  -ldflags "-X 'main.time=$(date -u --rfc-3339=seconds)' -X 'main.git=$(git log --pretty=format:"%h" -1)'"  main.go
+
+- https://stackoverflow.com/questions/11354518/application-auto-build-versioning
+
+
+Go JSON 空值处理的一些坑，看示例
+>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+
+.. code-block:: go
+
+    package main
+
+    import (
+            "encoding/json"
+            "fmt"
+    )
+
+    // https://www.sohamkamani.com/blog/golang/2018-07-19-golang-omitempty/
+    // omitempty 对于0值和，nil，pointer 的处理需要注意下坑。
+
+    func testNormal() {
+            type Dog struct {
+                    Breed    string
+                    WeightKg int
+            }
+            d := Dog{
+                    Breed:    "dalmation",
+                    WeightKg: 45,
+            }
+            b, _ := json.Marshal(d)
+            fmt.Println(string(b)) // {"Breed":"dalmation","WeightKg":45}
+    }
+
+    func testOmit() {
+            type Dog struct {
+                    Breed    string
+                    WeightKg int
+            }
+            d := Dog{
+                    Breed: "pug",
+            }
+            b, _ := json.Marshal(d)
+            fmt.Println(string(b)) //{"Breed":"pug","WeightKg":0}
+            // 注意没填的字段输出0，如果不想输出0呢？比如想输出 null 或者压根不输出这个字段
+    }
+
+    func testOmitEmpty() {
+            type Dog struct {
+                    Breed string
+                    // The first comma below is to separate the name tag from the omitempty tag
+                    WeightKg int `json:",omitempty"`
+            }
+            d := Dog{
+                    Breed: "pug",
+            }
+            b, _ := json.Marshal(d)
+            fmt.Println(string(b)) // {"Breed":"pug"}
+    }
+
+    func testValuesCannotBeOmitted() {
+            type dimension struct {
+                    Height int
+                    Width  int
+            }
+
+            type Dog struct {
+                    Breed    string
+                    WeightKg int
+                    Size     dimension `json:",omitempty"`
+            }
+
+            d := Dog{
+                    Breed: "pug",
+            }
+            b, _ := json.Marshal(d)
+            fmt.Println(string(b)) //{"Breed":"pug","WeightKg":0,"Size":{"Height":0,"Width":0}}
+
+    }
+
+    func testValuesCannotBeOmittedButUsePointer() {
+            type dimension struct {
+                    Height int
+                    Width  int
+            }
+
+            type Dog struct {
+                    Breed    string
+                    WeightKg int
+                    Size     *dimension `json:",omitempty"` //和上一个不同在于这里使用指针
+            }
+
+            d := Dog{
+                    Breed: "pug",
+            }
+            b, _ := json.Marshal(d)
+            fmt.Println(string(b)) // {"Breed":"pug","WeightKg":0}
+
+    }
+
+    // The difference between 0, "" and nil
+    // One issue which particularly caused me a lot a trouble is the case where you want to differentiate between a default value, and a zero value.
+    //
+    // For example, if we have a struct describing a resteraunt, with the number of seated customers as an attribute:
+    func testZeroWillOmit() {
+            type Restaurant struct {
+                    NumberOfCustomers int `json:",omitempty"`
+            }
+
+            d := Restaurant{
+                    NumberOfCustomers: 0,
+            }
+            b, _ := json.Marshal(d)
+            fmt.Println(string(b)) // {}
+            // 输出 {}， 0被省略了
+    }
+
+    func testZeroPointer() {
+            type Restaurant struct {
+                    NumberOfCustomers *int `json:",omitempty"`
+            }
+            d1 := Restaurant{}
+            b, _ := json.Marshal(d1)
+            fmt.Println(string(b)) //Prints: {}
+
+            n := 0
+            d2 := Restaurant{
+                    NumberOfCustomers: &n,
+            }
+            b, _ = json.Marshal(d2)
+            fmt.Println(string(b)) //Prints: {"NumberOfCustomers":0} ，总结一下就是值为 0 的 pointer 也不会省略字段
+    }
+
+    func main() {
+            // testOmit()
+            // testOmitEmpty()
+            // testValuesCannotBeOmitted()
+            // testValuesCannotBeOmittedButUsePointer()
+            testZeroWillOmit()
+
+    }
+
+- https://www.sohamkamani.com/blog/golang/2018-07-19-golang-omitempty/
+- https://ethancai.github.io/2016/06/23/bad-parts-about-json-serialization-in-Golang/
+- https://www.liwenzhou.com/posts/Go/json_tricks_in_go/
+
+Go int/int64/float 和 string 转换示例
+>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+
+.. code-block:: go
+
+    // 推荐一个更加强大的转换库：https://github.com/spf13/cast
+    package main
+
+    import (
+            "fmt"
+            "strconv"
+    )
+    /*
+    或者安装 https://github.com/chubin/cheat.sh
+    curl https://cht.sh/:cht.sh | sudo tee /usr/local/bin/cht.sh
+    chmod +x /usr/local/bin/cht.sh
+
+    之后输入以下语句查询答案：
+    cht.sh go cht.sh go string to float64
+    */
+
+    func main() { // 测试 int 和 string(decimal) 互相转换的函数
+            // https://yourbasic.org/golang/convert-int-to-string/
+            // int -> string
+            sint := strconv.Itoa(97)
+            fmt.Println(sint, sint == "97")
+
+            // byte -> string
+            bytea := byte(1)
+            bint := strconv.Itoa(int(bytea))
+            fmt.Println(bint)
+
+            // int64 -> string
+            sint64 := strconv.FormatInt(int64(97), 10)
+            fmt.Println(sint64, sint64 == "97")
+
+            // int64 -> string (hex) ，十六进制
+            sint64hex := strconv.FormatInt(int64(97), 16)
+            fmt.Println(sint64hex, sint64hex == "61")
+
+            // string -> int
+            _int, _ := strconv.Atoi("97")
+            fmt.Println(_int, _int == int(97))
+
+            // string -> int64
+            _int64, _ := strconv.ParseInt("97", 10, 64)
+            fmt.Println(_int64, _int64 == int64(97))
+
+            // https://stackoverflow.com/questions/30299649/parse-string-to-specific-type-of-int-int8-int16-int32-int64
+            // string -> int32，注意 parseInt 始终返回的是 int64，所以还是需要 int32(n) 强转一下
+            _int32, _ := strconv.ParseInt("97", 10, 32)
+            fmt.Println(_int32, int32(_int32) == int32(97))
+
+            // int32 -> string, https://stackoverflow.com/questions/39442167/convert-int32-to-string-in-golang
+            strconv.FormatInt(int64(i), 10) // fast
+            strconv.Itoa(int(i)) // fast
+            fmt.Sprint(i) // slow
+
+            // int -> int64 ，不会丢失精度
+            var n int = 97
+            fmt.Println(int64(n) == int64(97))
+
+            // string -> float32/float64  https://yourbasic.org/golang/convert-string-to-float/
+            f := "3.14159265"
+            if s, err := strconv.ParseFloat(f, 32); err == nil {
+                fmt.Println(s) // 3.1415927410125732
+            }
+            if s, err := strconv.ParseFloat(f, 64); err == nil {
+                fmt.Println(s) // 3.14159265
+            }
+
+            // float -> string https://yourbasic.org/golang/convert-string-to-float/
+            s := fmt.Sprintf("%f", 123.456)
+    }
+
+Go struct 如何设置默认值
+>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+Go 的结构体成员没法直接设置默认值，使用的是每个类型的默认值，可以 New 构造函数里设置。
+
+.. code-block:: go
+
+    // https://stackoverflow.com/questions/37135193/how-to-set-default-values-in-go-structs
+    //Something is the structure we work with
+    type Something struct {
+         Text string
+         DefaultText string
+    }
+    // NewSomething create new instance of Something
+    func NewSomething(text string) Something {
+       something := Something{}
+       something.Text = text
+       something.DefaultText = "default text"
+       return something
+    }
+
+Go 如何使用枚举值 Enum
+>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+Go没有提供内置的枚举类型，不过可以使用自定义类型和常量值来实现枚举类型。
+并且还可以给自定义的类型定义方法。
+
+.. code-block:: go
+
+    type Base int
+
+    const (
+            A Base = iota
+            C
+            T
+            G
+    )
+
+- https://stackoverflow.com/questions/14426366/what-is-an-idiomatic-way-of-representing-enums-in-go
+- https://blog.learngoprogramming.com/golang-const-type-enums-iota-bc4befd096d3
+
+
+Go 如何断判非空字符串/slice
+>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+
+标准库实际上 ``len(s) != 0`` 和 ``s != ""`` 都有使用，我个人倾向于 ``s != ""`` 看起来更清楚，区分其他容器类型判断的方式。
+比如如果使用 slice 可以使用 len(slice) == 0 判断是否为空。
+
+同时注意空的 slice 和 nil slice 的区别，判断的时候如果是 nil 需要显示判断是否为 nil，而使用 len 判断两者都为 0.
+
+Go 清空 slice 两种方式区别
+>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+
+.. code-block:: go
+
+    // https://yourbasic.org/golang/clear-slice/
+    func testClearSlice() {
+        // 1. 赋值 nil 清空
+        a := []string{"A", "B", "C"}
+        a = nil                        // 这种方式会导致 gc 释放底层数组（假设没有其他引用)
+        fmt.Println(a, len(a), cap(a)) // [] 0 0
+
+        // 2. 使用 s = s[:0] , 注意底层 cap 还是 5
+        b := []string{"A", "B", "C", "D", "E"}
+        b = b[:0]                      // 不会清空底层数组
+        fmt.Println(b, len(b), cap(b)) // [] 0 5
+    }
+
+Go 如何格式化参数
+>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+
+- https://yourbasic.org/golang/fmt-printf-reference-cheat-sheet/
+
+
+命名返回值
+>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+
+go 的返回值可以命名，使用命名返回值有几个用处：
+
+- 可以当成文档，直观的展示返回值的含义
+- 自动初始化为类型的零值
+- 返回的时候不用写很多参数名，直接用 return 就行
+- 如果想要在 defer 中修改返回值，只能使用命名参数。例子如下
+- 缺点：函数里很容易误用声明一个同名的参数就会被被覆盖了(shadow)
+- 函数返回相同类型的两个或三个参数，或者如果从上下文中不清楚结果的含义，使用命名返回，其它情况不建议使用命名返回。
+
+.. code-block:: go
+
+    func namedReturn(i int) (ret int) {
+        ret = i
+        defer func() { ret++ }()
+
+        return
+    }
+
+    func anonReturn(i int) int {
+        ret := i
+        defer func() { ret++ }() // 修改 ret 无效
+        return ret
+    }
+
+    func main() {
+        fmt.Println(namedReturn(0)) // 1
+        fmt.Println(anonReturn(0))  // 0
+    }
+
+
+defer 的一些坑
+>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+
+1. defer 语句的参数是立即计算
+2. defer for 循环中使用注意循环的量级，避免内存泄露
+3. defer 碰到panic也能执行，但是如果直接执行了 os.Exit 退出而不是正常的 return/panic 退出，程序会立刻终止不会执行 defer。遇到服务升级 defer 可能失效
+
+来看一个例子，通过 defer 计算一下执行的时间，错误写法因为 defer 的参数是立即计算的，所以不生效。
+
+.. code-block:: go
+
+    func testDeferLogTime() { // 记录执行的毫秒数
+        start := time.Now()
+        defer func() { fmt.Println(time.Since(start).Milliseconds()) }() // 正确写法输入 1s 。需要用匿名函数包一层
+        // defer fmt.Println(time.Since(start).Milliseconds()) // NOTE: 写法错误，defer 参数及时计算，这样写结果是 0
+        time.Sleep(1 * time.Second)
+    }
+
+解决方法有两种：
+
+1. 传入指针作为参数。这种情况同样适用于指针接收者和值接收者的情况
+2. 使用函数闭包(closure)。把要执行的语句放到一个匿名函数里
+
+
+Go 如何复制map
+>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+注意 go 和其他很多编程语言一样，对于复合结构是浅拷贝，共享底层数据结构。几个变量指向同一个复合结构的时候注意修改一个对其他变量也是可见的。
+
+.. code-block:: go
+
+    // https://stackoverflow.com/questions/23057785/how-to-copy-a-map
+    func copyMap(src map[string]string) map[string]string {
+      res := make(map[string]string)
+      for k, v := range src {
+        res[k] = v
+      }
+      return res
+    }
+
+    func testShareMap() {
+      am := []map[string]string{
+        map[string]string{"a1": "a1", "b1": "b1"},
+        map[string]string{"a2": "a2", "b2": "b2"},
+      }
+      bm := am
+      bm[0]["a1"] = "testbm" // NOTE 这里修改了b，a 里边的也会变。共享 map
+      fmt.Println(am)
+
+      var cm []map[string]string
+      for _, m := range am {
+        cm = append(cm, copyMap(m))
+      }
+      cm[0]["a1"] = "testcm" // will not modify am
+      fmt.Println(am)
+    }
+
+    func main() {
+      testShareMap()
+    }
+
+循环变量与闭包问题 ⚠️
+>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+go 的循环变量是 per-loop 的而不是 per-iteration 绑定的，这个特性导致了很多隐蔽反直觉并且难以排查的bug。
+go 官方目前仍在讨论是否要改善这个问题：https://github.com/golang/go/discussions/56010 。
+最新的 go1.22 版本终于修复了这个问题，可以尝试升级到最新的 go 版本解决。
+
+举三个常见例子防止踩坑：
+
+.. code-block:: go
+
+  package main
+
+  import (
+      "fmt"
+      "time"
+  )
+
+  // 示例1：goroutine 直接使用闭包变量
+  func testForLoopGoroutine() {
+      data := []string{"one", "two", "three"}
+      for _, v := range data {
+          go func() {
+              fmt.Println(v) // 最终打印的都是一样的值，和期望不符
+          }()
+      }
+      // output: three three three
+  }
+
+  // 两种方式解决：1.使用一个临时变量
+  func testForLoopGoroutine1() {
+      data := []string{"one", "two", "three"}
+      for _, v := range data {
+          v := v // 使用一个临时变量 v
+          go func() {
+              fmt.Println(v)
+          }()
+      }
+      // one two three (may wrong order)
+  }
+
+  // 方法2：传给匿名goroutine参数
+  func testForLoopGoroutine2() {
+      data := []string{"one", "two", "three"}
+      for _, v := range data {
+          go func(in string) {
+              fmt.Println(in)
+          }(v)
+      }
+      // one two three (may wrong order)
+  }
+
+  // 示例2：对循环变量取地址
+  type MyStruct struct{ i int }
+
+  func testForLoopPointer() {
+      values := []MyStruct{MyStruct{1}, MyStruct{2}, MyStruct{3}}
+      output := []*MyStruct{}
+
+      for _, v := range values {
+          // v := v // 加上这一行才能打印1，2，3
+          output = append(output, &v) // 如果不用临时变量，始终取到的是同一个地址
+          // 或者用 output = append(output, &values[i]) // 推荐使用下标
+      }
+
+      for _, o := range output {
+          fmt.Println(o.i) // 都打印3
+      }
+  }
+
+  // 示例3：接收者指针
+  type field struct{ name string }
+
+  func (p *field) print() { // 接收者是指针
+      fmt.Println(p.name)
+  }
+
+  func testForLoopPointerReceiver() {
+      data := []field{{"one"}, {"two"}, {"three"}} // 改成 []*field 可以修复
+      for _, v := range data {
+          // v := v       // NOTE：直接这样就可以解决。或者使用 struct 指针，[]*field 初始化
+          go v.print() // print three three three
+      }
+  }
+
+总结一下会出bug的常见情况：
+
+- 在 goroutine 中直接使用循环变量(示例1)
+- 循环中直接对循环变量取地址(示例2)。这种场景建议直接迭代下标，然后从原来的数组获取值
+- 循环变量(非指针)调用函数是一个指针接收者，调用却直接传入循环变量值(示例3)
+
+解决方式：
+
+1. 创建一个临时局部变量(``v:=v``)
+2. 作为参数传入(解决goroutine场景)
+3. for 循环中推荐直接只迭代下标的方式从原来的数组获取值
+
+参考:
+
+- https://nullprogram.com/blog/2014/06/06/ Per Loop vs. Per Iteration Bindings
+
+Failed Type Assertions
+>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+
+.. code-block:: go
+
+    package main
+
+    import "fmt"
+
+    func main() {
+            var data interface{} = "hehe"
+            // NOTE: 这里不要用 同名的 data 变量，比如换成 dataInt
+            if data, ok := data.(int); ok {
+                    fmt.Println("[is an int] value =>", data)
+            } else {
+                    fmt.Println("[not an int] value =>", data)
+                    // NOTE ：注意 data 已经被失败的 type assert 赋值成了0
+            }
+    }
+
+为什么接口判断nil!=nil? interface 和 nil 比较的坑。An interface holding a nil value is not nil
+>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+An interface holding nil value is not nil. An interface equals nil only if both type and value are nil.
+接口的底层实现包含两个部分：type 和 data。type 表示接口存储值的具体类型，data 则存储实际的值。只有当 type 和 data 都为 nil 时，接口才被认为是 nil。
+这个问题可能会导致一些非常隐蔽的 bug。
+
+.. code-block:: go
+
+    package main
+    import "fmt"
+    func main() {
+        var a interface{}
+        fmt.Printf("a == nil is %t\n", a == nil) // a == nil is true
+        var b interface{}
+        var p *int = nil
+        b = p
+        fmt.Printf("b == nil is %t\n", b == nil) // b == nil is false。这里接口的类型并不是空，所以判断不是nil
+    }
+
+    // 在一些场景中如果需要返回 nil，直接显示返回 nil 不容易出错
+    func testInterfaceNilWrong() {
+        doit := func(arg int) interface{} {
+            var result *struct{} = nil
+            if arg > 0 {
+                result = &struct{}{}
+            } // WRONG 错误写法！必须显示返回 nil
+            return result
+        }
+        if res := doit(-1); res != nil {
+            fmt.Println("good result:", res)
+        }
+    }
+
+    func testInterfaceNilRight() {
+        doit := func(arg int) interface{} {
+            var result *struct{} = nil
+            if arg > 0 {
+                result = &struct{}{}
+            } else{
+                return nil // CORRECT 必须显示返回 nil
+            }
+            return result
+        }
+        if res := doit(-1); res != nil {
+            fmt.Println("good result:", res)
+        }
+    }
+
+总结一下，严格遵守几个原则，可以避免出现这种隐蔽的 bug：
+
+1. 如果 interface 被具体类型变量赋值过，无论是否具体变量本身是不是 nil，接口值都是非 nil。
+2. 如果函数返回具体类型，然后又在其他地方需要赋值给接口，那函数应该直接返回接口类型。
+3. 不要写任何可能存在 ``接口 = 具体类型(nil)`` 的代码，如果有 nil 值，直接赋给接口，不要引入中间具体类型
+
+
+逃逸分析
+>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+想要知道变量在 stask 还是 heap 分配使用 ``go run -gcflags -m app.go``
+
+报错：go test flag: flag provided but not defined
+>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+
+- https://stackoverflow.com/questions/29699982/go-test-flag-flag-provided-but-not-defined
+
+
+Go rand 的坑
+>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+如果忘记调用 ``rand.Seed(time.Now().UnixNano())`` 每次重新运行返回结果是相同的，应该在程序初始化的时候比如 init 函数调用一次 seed。
+
+
+Go 循环引用(import cycle)
+>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+项目大了之后如果包没设计好可能会出现循环引用包的问题，导致构建的时候提示 import cycle not allowed 失败。
+可以用 ``go list -f '{\{join .DepsErrors "\n"\}}' <import-path>`` 来获取循环引用信息。
+或者使用第三方工具： ``go get github.com/kisielk/godepgraph`` 安装后执行 ``godepgraph -s import-cycle-example | dot -Tpng -o godepgraph.png``
+
+如何解决循环引用的问题：
+
+- 通过 interface 解耦，打破循环依赖
+- 合理设计包的内容，比如单独提出第三个包供另外两个包引入(mediator design pattern)。(底层的包不要引入上层的包，否则很容易出现循环引用)
+- 使用 go:linkname (不建议)
+
+参考:
+
+- https://jogendra.dev/import-cycles-in-golang-and-how-to-deal-with-them
+- https://www.positioniseverything.net/import-cycle-not-allowed/
+
+Go 结构体相关
+--------------------------------------------------
+
+Go 无法修改结构体的值(值接收者vs指针接收者)
+>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+注意如果需要修改结构体的值，必须使用指针接收者，否则无法修改结构体的值(值拷贝)。指针接收者其实也是拷贝，不过因为拷贝的是
+指针的值，所以能够修改。
+
+.. code-block:: go
+
+    type Student struct {
+        Name string
+    }
+
+    func (s Student) SetName(name string) { // NOTE: 无法修改，接收者是值拷贝
+        s.Name = name
+    }
+
+    func (s *Student) SetNameByPointer(name string) { // 指针接收者才能修改
+        s.Name = name
+    }
+
+    func testChangeName() {
+        s := &Student{Name: "lao wang"}
+        s.SetName("lao li")
+        fmt.Printf("%+v\n", s) // NOTE: 输出还是 lao wang，这里是值拷贝修改不了
+        s.SetNameByPointer("lao li")
+        fmt.Printf("%+v\n", s) // 输出 lao li，修改成功！
+    }
+
+
+总结一下何时使用值接收者或者指针接收者(《100 Go Mistakes and How to Avoid Them-2022》)：
+
+- 必须使用指针接收者
+
+  - 方法需要修改接收者
+  - 方法的接收者包含一个不能拷贝的对象，比如 sync 包的对象
+
+- 应该使用指针接收者：大对象场景，可以避免拷贝开销
+
+- 必须使用值接收者
+
+  - 保证接收者的值不会被修改
+  - 接收者是 map、function、channel，如果是指针接收者会编译错误
+
+- 应该使用值接收者
+
+  - 如果接收者是值，并且不应该被修改
+  - 如果接收者是小的 array 或者struct（仅包含值类型并且没有可修改的值比如time.Time）
+  - 如果接收者是基础类型比如 int、float64、string
+
+
+Go 无法修改值为结构体的map
+>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+
+.. code-block:: go
+
+    func testChangeMapStruct() {
+        type T struct{ Cnt int }
+        m := map[string]T{"a": T{Cnt: 1}, "b": T{Cnt: 2}}
+        for _, v := range m {
+            v.Cnt = 100
+        }
+        fmt.Println(m)
+
+        // 修改值为结构体的 map，需要使用指针
+        m2 := map[string]*T{"a": &T{Cnt: 1}, "b": &T{Cnt: 2}}
+        for _, v := range m2 {
+            v.Cnt = 100
+        }
+        fmt.Println(m2["a"], m2["b"])
+
+        // 或者使用 map[k].v 修改 (也需要value是结构体指针)
+        m3 := map[string]*T{"a": &T{Cnt: 1}, "b": &T{Cnt: 2}}
+        for k := range m3 {
+            m3[k].Cnt = 100
+        }
+        fmt.Println(m3["a"], m3["b"])
+    }
+
+    func main() {
+      testChangeMapStruct()
+    }
+
+
+如何判断一个空结构体(empty struct)
+>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+
+.. code-block:: go
+
+    // 注意需要加上括号，否则 syntax error
+    // https://stackoverflow.com/questions/28447297/how-to-check-for-an-empty-struct
+    if (Session{}) == session  {
+        fmt.Println("is zero value")
+    }
+
+
+如何重写结构体的方法(override)
+>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+go 不是传统的 oop 语言，没有提供直接的支持。可以通过嵌入一个接口的方式来实现。
+go 的匿名嵌入是一种 Pseudo is-a 关系(伪is a)，不能完全等价于子类，没有多态的功能(c++等语言通过虚函数表实现)。go只能通过接口实现多态
+
+.. code-block:: go
+
+    // https://stackoverflow.com/questions/38123911/golang-method-override
+    // golang面向对象分析 https://www.cnblogs.com/457220157-FTD/p/14703692.html
+    package main
+
+    import "fmt"
+
+    type Getter interface {
+        Get() string
+    }
+
+    type Base struct {
+        Getter
+    }
+
+    func (base *Base) Get() string {
+        return "base"
+    }
+
+    func (base *Base) GetName() string {
+        return base.Getter.Get()
+    }
+
+    // user code :
+    type Sub struct {
+        Base
+    }
+
+    func (t *Sub) Get() string {
+        return "Sub"
+    }
+    func New() *Sub {
+        userType := &Sub{}
+        userType.Getter = interface{}(userType).(Getter)
+        return userType
+    }
+
+    func main() {
+        userType := New()
+        fmt.Println(userType.GetName()) // user string
+    }
+
+
+Go 标准库相关
+--------------------------------------------------
+
+Json 序列化/反序列化的坑
+>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+1. 类型嵌入导致序列化字段丢失问题
+
+.. code-block:: go
+
+    // 序列化后会发现 ID 字段神奇地丢失了，因为嵌入Timer导致 Event 实现了 json.Marshaler
+    type Event struct {
+        ID int
+        time.Time
+    }
+    // 解决方式：1. 不要直接嵌入对象，增加一个名字。2. 重新实现 MarshalJSON() 方法
+    type Event struct {
+        ID int
+        Time time.Time
+    }
+
+2. time.Time 字段序列化和反序列化后比较结果不同。
+go 的 time.Time 对象包含日历时钟(Wall time)和单调时钟(Monitonic time)，序列化后在反序列化会丢失单调时钟的值，所以用等号
+比较会不同。解决方式有两种：
+
+- 使用 time.Equal 比较不会比较单调时钟
+- 使用 time.Truncate(0) 去掉单调时钟后再序列化
+
+3. 如果序列化 map 的值是 any，序列化后的数字类型都是 float64
+
+4. Unmarshal 精度丢失问题
+
+.. code-block:: go
+
+    // interface 接收 int64 的unmarshal结果，会丢失精度
+    func testUnmarshalInt64() {
+        res := map[string]interface{}{}
+        data := `{"id": 7444023959772728345}`
+        if err := json.Unmarshal([]byte(data), &res); err != nil {
+            panic(err)
+        }
+        fmt.Printf("%d\n", int64(res["id"].(float64))) // 输出 7444023959772728320
+    }
+
+    // 解决方式： 1. 使用 decoder.UseNumber  2. 使用 string 字段
+    func testUnmarshalInt64RightWay() {
+        res := map[string]interface{}{}
+        data := `{"id": 7444023959772728345}`
+        decoder := json.NewDecoder(strings.NewReader(data))
+        decoder.UseNumber()
+        if err := decoder.Decode(&res); err != nil {
+            panic(err)
+        }
+        fmt.Println(res["id"]) // 输出 7444023959772728345
+    }
+
+
+网络相关
+--------------------------------------------------
+
+获取本机 ip
+>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+
+.. code-block:: go
+
+    package main
+
+    import (
+        "fmt"
+        "log"
+        "net"
+        "time"
+    )
+
+    var localIp string // 用一个全局变量或者缓存，防止高并发的时候重复频繁系统调用
+
+    // GetIPAddr 获取 server IP
+    func GetIPAddr() string {
+        if localIp != "" {
+            // fmt.Printf("use local ip %s\n", localIp)
+            return localIp
+        }
+        addrs, err := net.InterfaceAddrs()
+        if err != nil {
+            return ""
+        }
+
+        for _, addr := range addrs {
+            if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
+                if ipnet.IP.To4() != nil {
+                    localIp = ipnet.IP.String()
+                    return localIp
+                }
+            }
+        }
+        return ""
+    }
+
+    func main() {
+        fmt.Println(GetIPAddr())
+    }
+
+
+Go 网络请求设置 Host 不起作用
+>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+
+.. code-block:: go
+
+    req, err := http.NewRequest("GET", "http://bbb.com/", nil)
+    if err != nil {
+        log.Fatal(err)
+    }
+    req.Host = "aaa.com"
+    // 注意以下不起作用，用 python 习惯使用 header 设置头了，但是 go 里边只能通过 req.Host 设置 host
+    // req.Header.Set("Host", "www.example.org")  // 不起作用！！！ https://github.com/golang/go/issues/29865
+
+Go 错误处理的一些建议
+>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+
+1. 如果封装了原始的 error，应该使用 errors.Is(error value) 和 errors.As(error type)判断
+2. 不要多次处理错误，打日志或者返回 error 不要同时做。内层错误 wrap 之后，外层统一处理打印
+3. 不要忽略错误。如果明确可以忽略也要显示忽略并且加上注释。 ``_ = func()``
+
+
+并发编程
+--------------------------------------------------
+
+数据竞争
+>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+当多个协程同时访问同一块内存区域并且至少有一个是写入操作时就会发生数据竞争。
+
+- 使用 atomic 操作(atomic包)
+- 使用 mutex 保护临界区
+- 通过 channel 通信保证一个变量只能被一个协程更新
+
+Go Context 的坑：错误传播 context
+>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+调用接收context的函数时要小心，要清楚context在什么时候cancel，什么行为会触发cancel。
+笔者最近遇到一个问题是，在框架的 handler 函数返回之前，单独开一个 goroutine 创建一条 mysql 流水，但是handler 函数调用完
+成之后(response被写回client)框架会cancel，导致这个 mysql 传进去了框架函数带过来的 ctx cancel 之后执行失败。
+解决方式就是传一个单独的 context，不要直接透传上游框架的 context。
+
+.. code-block:: go
+
+    func (h *Handler) handleFunc(ctx context.Context, req *Request, resp *Response) error {
+        // ... 其他业务逻辑
+        go func() { // 异步记录流水
+            // 注意，这里不能直接用框架的 ctx，而是需要一个不被 cancel 的 context，否则执行可能会失败，改成 context.TODO()或者有超时的新context
+            if err := postDao.CreatePostCreateRecord(context.TODO(), row); err != nil { // 正确!
+            // if err := postDao.CreatePostCreateRecord(ctx, row); err != nil { // 错误！ ctx 很快被 cancel 导致流程失败
+                log.Errorf("CreatePost postDao.CreatePostCreateRecord err:%+v", err)
+            }
+        }()
+        return nil
+    }
+
+- https://zhuanlan.zhihu.com/p/34417106 《Go Context的踩坑经历》
+
+不要拷贝 sync 包里的对象(比如mutex)
+>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+如果一个结构体包含 sync 包的对象比如 mutex 对象，不要 copy 它。比如如果放到了一个结构体里，必须使用指针接收者防止拷贝。
+以下场景如果包含 sync 包的 Cond/Map/Mutex/RWMutex/Once/Pool/WaitGroup 都要注意：
+
+1. 方法是一个值接收者
+2. sync 包的结构作为函数参数
+3. 函数的参数包含一个sync 结构
+
+.. code-block:: go
+
+    type Counter struct {
+        sync.Mutex // <-- Added a mutex
+        counters   map[string]int
+    }
+
+    func (c Counter) inc(name string) { // NOTE: 错误！ 这里必须使用指针接收者，否则会 copy mutex 成员
+        c.Lock() // <-- Added locking of the mutex
+        defer c.Unlock()
+        c.counters[name]++
+    }
+
+    // 解决方法 1：方法的参数必须是指针接受者
+    func (c *Counter) inc(name string) { // 正确写法！ 用指针接收者
+        c.Lock() // <-- Added locking of the mutex
+        defer c.Unlock()
+        c.counters[name]++
+    }
+
+
+    // 解决方法 2：sync 对象使用指针类型
+    type Counter struct {
+        mu       *sync.Mutex // 使用指针类型
+        counters map[string]int
+    }
+
+    func NewCounter() Counter {
+        return Counter{
+            mu:       &sync.Mutex{},
+            counters: map[string]int{},
+        }
+    }
+
+
+Go 安全编程(panic/内存泄露)
+--------------------------------------------------
+
+Go panic 场景 ⚠️
+>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+在《Go 编程之旅》中总结了一些 panic 场景，写 go 的时候注意下，防止进程退出：
+
+- 数组/切片越界。确保下标在数组长度范围内；确保使用之前是否为 nil 或者空数组
+- 空指针访问。比如访问一个 nil 结构体成员或者嵌套的 nil 成员, ``a.b.c`` 但是 b 是一个空指针就会 panic(嵌套多了有时候不易发现)
+- 过早关闭 HTTP 响应体
+- 除以 0
+- 向已经关闭的 channel 发送消息(或者多次close同一个 channel)
+- 重复关闭 channel
+- 关闭未初始化的 channel
+- 跨协程的 panic 处理。main 协程中无法处理对子协程的 panic
+- sync 计数为负数。(比如 sync.WaitGroup 确保 Add和Done 的次数是对应一致的)
+- 类型断言不匹配。``var a interface{} = 1; fmt.Println(a.(string))`` 会 panic，建议用 ``s,ok := a.(string)``
+- 危险的 map
+
+  - 赋值之前必须用 make 创建。注意访问 map 不存在的 key 不会 panic，而是返回 map 类型对应的零值，但是不能直接声明就赋值，而是要用make创建
+  - 不要并发写原生 map。需要加锁、使用 sync.Map 或者第三方并发安全的 map 比如 patrickmn/go-cache。当你声明一个全局map时，确认它是只读的，如果意外不同携程的函数同时修改它就可能偶现 panic
+
+
+一般禁止业务代码里直接使用 panic，除非以下一些场景：
+
+- 启动阶段的强依赖初始化，比如init, main 可以使用 panic
+- Must 函数。比如 MustMarshal 等
+- 未实现方法标记。 ``panic("need implement")``
+
+参考：
+
+- https://xiaomi-info.github.io/2020/01/20/go-trample-panic-recover/
+- https://zhuanlan.zhihu.com/p/534419732 Golang channel 三大坑，你踩过了嘛？
+
+
+Go 内存泄露场景
+>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+
+- 资源使用后没有 Close。比如 http.body、db.Query.Rows、file文件描述符未关闭等。（实现了 io.Closer 的结构体都应该用完关闭）
+- 切片引用的底层数组，一直没有释放(由于 string 切片时也会共用底层数组，所以使用不当也会造成内存泄漏)。
+
+  - 尽量保证切片只作为局部变量使用，不会被传递到方法外，这样局部变量使用完后就会被回收
+  - 如果不能保证切片作为局部变量且不会被传递，应该对切片进行拷贝。可以用 append 方法或者使用 copy 函数
+
+- time.Ticker 忘记 stop。注意Ticker 和 Timer 是不同的。Timer 只会定时一次，而 Ticker 如果不 Stop，就会一直发送定时。建议用defer 进行stop
+- channel 误用造成的泄露
+
+  - 如果接收者需要在 channel 关闭之前提前退出，为防止内存泄漏，在发送者与接收者发送次数是一对一时，应设置 channel 缓冲队列为 1；``ch := make(chan struct{}, 1)``
+  - 在发送者与接收者的发送次数是多对多时，应使用专门的 stop channel 通知发送者关闭相应 channel。
+
+- 使用了 cgo。为了不让goroutine阻塞，cgo都是单独开一个线程处理，这种场景 runtime 无法管理。如果cgo处理逻辑有阻塞，可能导致线程数保障
+
+参考
+
+- https://zhuanlan.zhihu.com/p/550956060
+- https://www.hitzhangjie.pro/blog/2021-04-14-go%E7%A8%8B%E5%BA%8F%E5%86%85%E5%AD%98%E6%B3%84%E9%9C%B2%E9%97%AE%E9%A2%98%E5%BF%AB%E9%80%9F%E5%AE%9A%E4%BD%8D/
+
+Go goroutine 泄露(堆积)
+>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+泄露场景：
+
+- goroutine由于channel的读/写端退出而一直阻塞，导致goroutine一直占用资源，而无法退出
+- goroutine进入死循环中，导致资源一直无法释放。(比如无法停止的定时器或者 for 循环等)
+- goroutine中的网络操作由于没设置超时短期大量未结束导致goroutine快速累积(慢等待)。（所有的网络 client 一定要设置超时时间）
+
+解决方式:  goroutine 能够终止，goroutine 终止的场景如下：
+
+- 当一个goroutine完成它的工作
+- 由于发生了没有处理的错误
+- 有其他的协程告诉它终止(比如常见的生产者消费者场景，主线程结束之后通知生产者退出)
+
+如何发现：
+
+- 使用开源工具: github.com/uber-go/goleak
+- runtime 协程数量监控：``runtime.NumGoroutine()``
+- pprof: ``pprof.Lookup("goroutine")``
+
+参考：
+
+- https://www.trailofbits.com/post/discovering-goroutine-leaks-with-semgrep
+- https://hoverzheng.github.io/post/technology-blog/go/goroutine-leak%E5%92%8C%E8%A7%A3%E5%86%B3%E4%B9%8B%E9%81%93/
+
+
+redis go tricks
+--------------------------------------------------
+
+redis 连接超时
+>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+默认是没有超时时间的，注意设置超时时间（connect/read/write)。所有的网络 client 都应该设置超时时间(参考P99等值)
+
+- https://ms2008.github.io/2019/07/04/golang-redis-deadlock/
+
+redis 单测如何 mock
+>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+reids mock 可以用 miniredis，以下是一个示例代码
+
+.. code-block:: go
+
+    package main
+
+    import (
+      "fmt"
+      "os"
+      "testing"
+
+      "github.com/alicebob/miniredis"
+      "github.com/go-redis/redis"
+      "github.com/stretchr/testify/assert"
+    )
+
+    var followImpl *Follow
+
+    func setupFollow() {
+      fmt.Println("setup")
+      mr, err := miniredis.Run()
+      if err != nil {
+        panic("init miniredis failed")
+      }
+      client := redis.NewClient(&redis.Options{
+        Addr: mr.Addr(),
+      })
+      _ = client.Set("key", "wang", 0).Err()
+      followImpl = &Follow{client: client}
+    }
+
+    func TestGet(t *testing.T) {
+      val, err := followImpl.Get("key")
+      followImpl.client.Set("key", "2", 0)
+      fmt.Println(val, err)
+      assert.Equal(t, val, "wang")
+    }
+
+    func TestPING(t *testing.T) {
+      PING()
+    }
+
+    func TestMain(m *testing.M) {
+      setupFollow()
+      code := m.Run()
+      os.Exit(code)
+    }
+
+
+Go Web
+--------------------------------------------------
+
+滚动日志
+>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+
+.. code-block:: go
+
+    package main
+
+    import (
+        "fmt"
+        "os"
+        "strings"
+
+        "go.uber.org/zap"
+        "go.uber.org/zap/zapcore"
+        lumberjack "gopkg.in/natefinch/lumberjack.v2"
+    )
+
+    func NewLogger(filePath string, level zapcore.Level, maxSize int, maxBackups int, maxAge int, compress bool, serviceName string) *zap.Logger {
+        core := newCore(filePath, level, maxSize, maxBackups, maxAge, compress)
+        return zap.New(core, zap.AddCaller(), zap.Development(), zap.Fields(zap.String("serviceName", serviceName)))
+    }
+
+    func newCore(filePath string, level zapcore.Level, maxSize int, maxBackups int, maxAge int, compress bool) zapcore.Core {
+        //日志文件路径配置
+        hook := lumberjack.Logger{
+            Filename:   filePath,   // 日志文件路径
+            MaxSize:    maxSize,    // 每个日志文件保存的最大尺寸 单位：M
+            MaxBackups: maxBackups, // 日志文件最多保存多少个备份
+            MaxAge:     maxAge,     // 文件最多保存多少天
+            Compress:   compress,   // 是否压缩
+        }
+        // 设置日志级别
+        atomicLevel := zap.NewAtomicLevel()
+        atomicLevel.SetLevel(level)
+        //公用编码器
+        encoderConfig := zapcore.EncoderConfig{
+            TimeKey:        "time",
+            LevelKey:       "level",
+            NameKey:        "logger",
+            CallerKey:      "linenum",
+            MessageKey:     "msg",
+            StacktraceKey:  "stacktrace",
+            LineEnding:     zapcore.DefaultLineEnding,
+            EncodeLevel:    zapcore.LowercaseLevelEncoder,  // 小写编码器
+            EncodeTime:     zapcore.ISO8601TimeEncoder,     // ISO8601 UTC 时间格式
+            EncodeDuration: zapcore.SecondsDurationEncoder, //
+            EncodeCaller:   zapcore.FullCallerEncoder,      // 全路径编码器
+            EncodeName:     zapcore.FullNameEncoder,
+        }
+        return zapcore.NewCore(
+            zapcore.NewJSONEncoder(encoderConfig),                                           // json 格式编码器配置
+            zapcore.NewMultiWriteSyncer(zapcore.AddSync(os.Stdout), zapcore.AddSync(&hook)), // 打印到控制台和文件
+            atomicLevel, // 日志级别
+        )
+    }
+
+    // 全局 logger 定义
+    var Logger *zap.Logger
+
+    func getZapLevel(level string) zapcore.Level {
+        level = strings.ToLower(level)
+        switch level {
+        case "debug":
+            return zapcore.DebugLevel
+        case "info":
+            return zapcore.InfoLevel
+        case "warn":
+            return zapcore.WarnLevel
+        case "error":
+            return zapcore.ErrorLevel
+        case "dpanic":
+            return zapcore.DPanicLevel
+        case "fatal":
+            return zapcore.FatalLevel
+        default:
+            panic(fmt.Errorf("invalid log level"))
+        }
+    }
+
+    func init() {
+        loglevel := "debug"
+        Logger = NewLogger("./logs/service.log", getZapLevel(loglevel), 128, 30, 7, true, "UploadServer")
+    }
+
+    func main() {
+        Logger.Info("hello go!")
+    }
+
+
+静态检查
+--------------------------------------------------
+
+检查未使用代码
+>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+安装 golangci-lint ``go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.47.3`` (目前用的go.1.18)。
+执行 ``golangci-lint run code_dir --disable-all -E unused``
diff --git a/go-note/web.rst b/go-note/web.rst
new file mode 100644
index 00000000..fac8f744
--- /dev/null
+++ b/go-note/web.rst
@@ -0,0 +1,536 @@
+.. _goweb:
+
+Go 入门和深入
+=====================================================================
+
+Go语言入门和深入
+--------------------------------------------------
+
+- `Go Roadmap <https://github.com/Alikhll/golang-developer-roadmap>`_  学习路线图
+- `Lets Go <https://github.com/PegasusWang/LetsGo>`_  笔者连载的 go 文字和视频教程
+- `A Tour of Go <https://tour.golang.org/welcome/1>`_
+- `Go by Example <https://gobyexample.com>`_
+- `Learn Go with Tests <https://quii.gitbook.io/learn-go-with-tests/>`_
+- `The-way-to-go <https://github.com/Unknwon/the-way-to-go_ZH_CN>`_
+- `golang-developer-roadmap <https://github.com/Alikhll/golang-developer-roadmap>`_
+- `How to Write Go Code <https://golang.org/doc/code.html>`_
+- `Learning-golang <https://github.com/developer-learning/learning-golang>`_
+- `Build Web Application With Golang <https://github.com/astaxie/build-web-application-with-golang>`_
+- `Go 高级编程 <https://chai2010.cn/advanced-go-programming-book/>`_
+- `Go 101 <https://go101.org/article/101.html>`_ 包含了很多基础和高级主题
+- `Mastering Go <https://books.studygolang.com/Mastering_Go_ZH_CN/>`_
+- `Go语言圣经 <https://books.studygolang.com/gopl-zh/>`_
+- `Go安全编程规范 <https://github.com/Tencent/secguide/blob/main/Go%E5%AE%89%E5%85%A8%E6%8C%87%E5%8D%97.md>`_
+- `Go并发编程 <https://lailin.xyz/post/go-training-week3-goroutine.html>`_
+- `Learning notes for golang <https://github.com/jincheng9/go-tutorial>`_
+
+
+Go 常用框架(工具)技术雷达 ❤️
+---------------------------------------------------------------
+技术选型一般选择接口稳定，持续维护，生态相对成熟，star 数量较高，用户广泛的库，坑少一点。
+前后分离时代用 gin 之类的框架写app后台还是挺快的，但是感觉做并发不高的内部后台业务还是用脚本python/php之类的更快。
+以下第三方库均可以通过 google + 关键词搜索到，同一行尽量按照流行程度从前往后列举，默认都是 github 上的包(只写了仓库后缀)。
+也可以去 awesome-go 之类的去查找，然后根据 star 数目等作为参考选用。
+
+- web/rpc框架: gin, grpc, beego, labstack/echo
+- 微服务框架：go-kit, go-micro, karatos(b 站)，go-zero(好未来)，jupiter(斗鱼)
+- 参数验证：go-playground/validator, bytedance/go-tagexpr
+- 单元测试断言：matryer/is, testify/assert, smartystreets/goconvey(bdd 驱动测试), rakyll/gotest(gotest 颜色)
+- 错误处理: pkg/errors, hashicorp/go-multierror(多错误处理), sync/errgroup(多goroutine错误处理)
+- panic处理：thepkg/recover
+- 重试：avast/retry-go
+- json处理转换：go-simplejson/mapstructure，json-iterator/go (比内置的 json 解析快很多), tidwall/gjson(获取 json 值), bytedance/sonic
+- 字典/结构体合并/结构体拷贝/类型转换：imdario/mergo, jinzhu/copier, jmattheis/goverter
+- 配置解析: viper(兼容很多格式)
+- mysql orm: gorm, xorm, sqlx, ent/ent(实体框架), doug-martin/goqu(生成sql)
+- redis: go-redis, redigo
+- Kafka: Shopify/sarama, confluent-kafka-go
+- Elasticsearch: olivere/elastic, elastic/go-elasticsearch
+- mongodb: mongodb/mongo-go-driver
+- id生成器: rx/xid, beinan/fastid, bwmarrin/snowflake, sony/sonyflake, godruoyi/go-snowflake
+- uuid: gofrs/uuid, satori/go.uuid, google/uuid (注意有些会 panic)
+- hash: cespare/xxhash(快速 hash), groupcache/consistenthash(一致性哈希)
+- cache(in memory并发安全): patrickmn/go-cache, allegro/bigcache, golang/groupcache(分布式), coocood/freecache, singleflight(防止缓存击穿), dgraph-io/ristretto, orcaman/concurrent-map(泛型)
+- cache(基于context): ag9920/go-ctxcache (基于context缓存解决重复调用放大问题)
+- cache(lru/lfu/2Q/ARC): hashicorp/golang-lru, bluele/gcache, songangweb/mcache(增强lru)
+- 并发/协程池(star 数从低到高排序)：
+
+  - https://github.com/panjf2000/ants
+  - https://github.com/rafaeldias/async
+  - https://github.com/Jeffail/tunny
+  - https://github.com/benmanns/goworker
+  - https://github.com/buptmiao/parallel
+
+- 原子访问：uber-go/atomic
+- 异步任务队列框架: machinery, gocelery, hibiken/asynq, LMSTFY(美图开源)
+- 分布式/定时任务: robfig/cron, ouiqiang/gocron, distribworks/dkon, shunfei/cronsun
+- 熔断：hystrix-go, eapache/go-resiliency, cep21/circuit, alibaba/sentinel-golang
+- 限流库:
+
+  - web框架限流：ulule/limiter, didip/tollbooth
+  - 令牌桶(token bucket)限流：juju/ratelimit, golang.org/x/time/rate
+  - 漏桶(leaky bucket)限流: uber-go/ratelimit
+
+- 日志: logrus, zap, lumberjack(滚动日志)
+- 链路追踪：opentracing/opentracing-go, uber/jaeger-client-go
+- 调试：go-spew/dlv, kr/pretty
+- 图片处理：h2non/imaginary
+- 网络库/连接池：fatih/pool; panjf2000/gnet, valyala/fasthttp，kavu/go_reuseport
+- websocket: nhooyr.io/websocket, gorilla/websocket
+- http client: levigross/grequests, asmcos/requests, go-resty/resty, gojek/heimdall(重试、熔断)
+- 表格：go-echarts
+- excel(XLSX): 360EntSecGroup-Skylar/excelize, tealeg/xlsx
+- 转换工具：
+
+  - sql2go(sql -> go struct): http://stming.cn/tool/sql2go.html
+  - curl2go(curl -> go http code): https://mholt.github.io/curl-to-go/
+  - Json2go(json -> go struct): https://mholt.github.io/json-to-go/
+
+- 代码检查工具：
+
+  - 静态检查：golangci-lint
+  - goroutine 泄露检查: github.com/uber-go/goleak
+  - 注释工具: github.com/cuonglm/gocmt 自动给导出变量、函数等增加注释
+
+- 热编译工具：gowatch
+- 网络代理：goproxy
+- 命令行处理: spf13/pflag, spf13/cobra
+- 字符串处理工具：huandu/xstrings
+- 通用数据类型转换：spf13/cast
+- HTML 处理/过滤: PuerkitoBio/goquery, microcosm-cc/bluemonday
+- 系统信息收集：shirou/gopsutil
+- go runtime: bmhatfield/go-runtime-metrics(runtime 指标收集)
+- 邮件：gopkg.in/gomail
+- 接口文档生成：swaggo/swag
+- 消息队列：nsqio/nsq
+- 延时队列/时间轮：ouqiang/delay-queue, RussellLuo/timingwheel
+- 分布式kv存储：etcd
+- 用户认证：golang-jwt/jwt,dgrijalva/jwt-go(弃用), authelia/authelia
+- 访问(权限)控制：casbin/casbin
+- 进程控制：uber-go/automaxprocs
+- 地理位置：ip2location/ip2location-go
+- 时间处理：jinzhu/now
+- 金融数字格式化/定点数: leekchan/accounting, shopspring/decimal
+- 分布式事务：yedf/dtm
+- 分布式锁: go-redsync/redsync(redlock算法), bsm/redislock, go-zero/core/stores/redis
+- Zookeeper: go-zookeeper/zk
+- 设计模式：tmrts/go-patterns
+- 数据结构：deckarep/golang-set, emirpasic/gods
+- 通用泛型工具库：thoas/go-funk, bytedance/gg(字节内部广泛使用且开源的泛型库)
+- 通用工具库：duke-git/lancet
+- 深拷贝：mohae/deepcopy, barkimedes/go-deepcopy
+- 采样监控：mosn/holmes(排查OOM等问题)
+- 规则引擎：govaluate, goengine, gorule, diegoholiveira/jsonlogic (基于json存储和解析规则)
+
+工具:
+
+- https://github.com/smallnest/gen gorm struct 生成工具，根据 sql 生成 struct，甚至还可以直接生成增删改查的代码
+- https://mholt.github.io/json-to-go/ json 转 go struct
+- https://protogen.marcgravell.com/decode proto decode 工具
+- https://gopherize.me/  一个好玩的小工具，设计你喜欢的 gopher 形象
+- https://github.com/egonelbre/gophers 各种好玩的 gopher 吉祥物图片
+- https://github.com/jfeliu007/goplantuml 一个自动根据 go 项目生成 uml 图的工具 https://www.dumels.com/
+- https://github.com/TrueFurby/go-callvis 查看 go 的调用关系
+- https://github.com/novalagung/gorep 批量替换引入的包(比如修改了名字或者包名等)
+- https://github.com/better-go/go-template go 生成项目模板
+- https://awesome-go.com/ 寻找你需要的 go 依赖库
+
+Go Books
+---------------------------------------------------------------
+- https://github.com/dariubs/GoBooks
+
+Go 开发工具
+---------------------------------------------------------------
+- Goland IDE
+- Vscode/Sublime/Atom 等常用编辑器结合插件
+- vim/Neovim + vim-go + coc.nvim
+
+博客:
+
+- https://octetz.com/docs/2019/2019-04-24-vim-as-a-go-ide/
+- https://ictar.xyz/2019/05/14/an-overview-of-go-tooling/
+- https://github.com/fedir/go-tooling-cheat-sheet/blob/master/go-tooling-cheat-sheet.pdf
+
+Go 博客教程
+--------------------------------------------------
+- https://yourbasic.org/golang/
+- https://golangbot.com/learn-golang-series/
+- https://golangbot.com/learn-golang-series/
+
+
+Go 设计模式
+--------------------------------------------------
+- https://github.com/senghoo/golang-design-pattern  代码示例
+- https://golangbyexample.com/all-design-patterns-golang/ go 设计模式(考虑并发安全)
+
+
+Go idioms
+--------------------------------------------------
+- https://yourbasic.org/golang/switch-statement/
+
+
+Go 错误处理
+--------------------------------------------------
+- https://github.com/pkg/errors 推荐使用
+- https://github.com/juju/errors
+- https://blog.golang.org/error-handling-and-go
+- https://dave.cheney.net/2016/04/27/dont-just-check-errors-handle-them-gracefully
+- https://zhuanlan.zhihu.com/p/82985617 Golang error 的突围
+- https://cloud.tencent.com/developer/article/1999877 3种方式！Go Error处理最佳实践
+- https://medium.com/@dche423/golang-error-handling-best-practice-cn-42982bd72672 Golang 错误处理最佳实践
+
+Go日志实践
+--------------------------------------------------
+- https://imhanjm.com/2017/05/19/go%20logger%20日志实践/
+
+Go文档查询
+--------------------------------------------------
+- https://gowalker.org
+
+
+GOPROXY 代理
+--------------------------------------------------
+如果有有一些库拉不下来又没有自己的代理，可以试试
+
+export GOPROXY=https://goproxy.io
+
+
+Web/RPC框架
+--------------------------------------------------
+
+- gin
+- grpc
+
+个人推荐使用 gin，当然你可以参考一下 star 选择别的框架
+
+- https://github.com/gin-gonic/contrib gin各种组件
+- https://github.com/e421083458/gin_scaffold gin 脚手架
+- https://github.com/mingrammer/go-web-framework-stars
+
+Gin example
+--------------------------------------------------
+- https://github.com/EDDYCJY/go-gin-example
+- https://github.com/vsouza/go-gin-boilerplate
+- https://github.com/gothinkster/golang-gin-realworld-example-app
+- https://github.com/go-programming-tour-book/blog-service 《go 编程之旅》博客代码示例
+
+gin 实战博客:
+
+- https://segmentfault.com/a/1190000013808421  gin 连载博客
+- https://www.cnblogs.com/xinliangcoder/p/11212573.html logrus日志
+- https://marcoma.xyz/2019/03/17/gin-tutorial-7/
+- https://www.jishuchi.com/books/gin-practice Golang Gin 实践
+
+微服务
+--------------------------------------------------
+微服务框架：
+
+- go kit: https://github.com/go-kit/kit
+- go-micro: https://github.com/micro/go-micro
+- kratos: https://github.com/bilibili/kratos B站go微服务框架
+- go-zero: https://github.com/tal-tech/go-zero 好未来 go 微服务框架
+- jupiter: https://github.com/douyu/jupiter 斗鱼 go 微服务框架
+
+微服务代码示例：
+
+- https://dzone.com/users/3214037/eriklupander.html 介绍 go 微服务实践的一系列博客
+- https://github.com/callistaenterprise/goblog go 微服务代码示例和博客，介绍了微服务各种基础组件
+- https://github.com/yun-mu/MicroServicePractice 微服务实践
+
+Go package (搜索常用的 go 第三方库)
+--------------------------------------------------
+- https://awesome-go.com/
+- https://go-search.org/search?q=redis
+- https://golangrepo.com/
+
+Go项目Layout
+--------------------------------------------------
+- https://github.com/golang-standards/project-layout 标准 go 项目目录组织
+- https://zhengyinyong.com/go-project-layout-design.html
+
+
+单元测试(unittest)
+--------------------------------------------------
+
+`GoMock框架使用指南 <https://www.jianshu.com/p/f4e773a1b11f>`_
+`如何写出优雅的 golang 代码 <https://draveness.me/golang-101>`_
+
+静态语言编写单测相比动态语言要难一些，动态语言中比如 python 可以很容易用 mock.patch 来做属性/方法替换。
+但是静态语言不行，一般难点在于如何去模拟外部依赖(比如数据库/rpc请求，redis 请求等)：
+
+- 接口(go 推荐面向接口编程，否则你很难使用 gomock 来编写单测)
+- mysql: 如何 mock 数据库请求。使用 sqlmock，或者编写 dao 层 interface，然后 mock 这个dao层接口
+- http: 使用 httpmock 来模拟请求返回值
+- redis: 这里我试了下 miniredis 比较好用，基于 go 实现，无需真实的 redis server
+
+也有一种方式在单测环境加入真实的db 和redis（比如 docker），然后单测读取测试环境的数据库来操作。
+这样的好处是可以不使用各种 mock 库，直接操作真实的 mysql，测试代码写起来也更方便。
+
+以下是一些单测相关的库：
+
+- testing: 内置库
+- github.com/stretchr/testify/assert: 用来做断言 assert 方便
+- gomock(mockgen): 静态语言难以像动态语言直接属性替换，所以一般我们基于接口编写代码，然后可以生成接口 mock
+- sqlmock: 如果依赖了数据库 mysql 等，可以使用 sqlmock 模拟数据库返回内容。（或者就在测试环境用真实的 mysql，测试完清理插入的测试数据)
+- httpmock: 用来 mock 调 http 请求
+- github.com/alicebob/miniredis 可以用来 mock redis，无需启动真实的 resdis server。试了下非常好用，也不用使用 mock 和真实的 redis 了。个人强烈推荐
+- bouk/monkey: 通过替换函数指针的方式修改任意函数的实现，如果以上都无法满足需求，可以用这种比较 hack 的方式。可能需要禁止编译器内联优化 ``go test -gcflask=-l ./...``
+- agiledragon/gomonkey: go 语言实现 monkey patch
+- mockey: 字节开源的 go mock 工具库。 https://juejin.cn/post/7159568574699274248
+
+目前项目推荐使用 mockey 和 convey 库，可以方便地实现 mock 和 bdd 行为驱动测试，一个简单的示例如下。
+
+.. code-block:: shell
+
+  // https://juejin.cn/post/7159568574699274248
+  import (
+      "fmt"
+      "testing"
+
+      . "github.com/bytedance/mockey"
+      . "github.com/smartystreets/goconvey/convey"
+  )
+
+  func Foo(in string) string {
+      return in
+  }
+
+  type A struct{}
+
+  func (a A) Foo(in string) string { return in }
+
+  var Bar = 0
+
+  func TestMockXXX(t *testing.T) {
+      PatchConvey("TestMockXXX", t, func() { // 推荐用 PatchConvey 方法，它会帮我们自动释放掉 mock
+          Mock(Foo).Return("c").Build()   // mock函数 (一般对于下游调用、数据库调用、缓存调用等需要 mock 掉，方便测试且跑单测无需依赖下游部署)
+          Mock(A.Foo).Return("c").Build() // mock方法
+          MockValue(&Bar).To(1)           // mock变量
+
+          // convey 提供了 ShouldEqual 等诸多方法用来比对单测结果和各种数据结构
+          So(Foo("a"), ShouldEqual, "c")        // 断言`Foo`成功mock
+          So(new(A).Foo("b"), ShouldEqual, "c") // 断言`A.Foo`成功mock
+          So(Bar, ShouldEqual, 1)               // 断言`Bar`成功mock
+      })
+      // `PatchConvey`外自动释放mock
+      fmt.Println(Foo("a"))        // a
+      fmt.Println(new(A).Foo("b")) // b
+      fmt.Println(Bar)             // 0
+  }
+
+- https://medium.com/@rosaniline/unit-testing-gorm-with-go-sqlmock-in-go-93cbce1f6b5b  (medium.com有阅读次数限制，隐身模式打开似乎就可以了)
+
+
+Go 断点调试器dlv
+---------------------------------------------------------------
+
+.. code-block:: shell
+
+   # 搜索函数，打断点，如果有同名函数的时候比较有用
+   funcs FuncName
+
+   # 打断点断点
+   b main.main
+
+   # 打印变量
+   print val
+
+   # go get -u github.com/derekparker/delve/cmd/dlv
+   dlv debug main.go
+
+   # 加上命令行参数
+   # https://github.com/go-delve/delve/issues/562
+   dlv debug ./cmd/unit-assignment-cli/main.go -- server
+
+   # 如何调试 go 的 coredump 文件。对于一些很偶发的进程退出会比较难排查，可以利用 coredump 文件辅助排查问题：
+   1. 调整ulimit关于core file size的设置，执行 ulimit -c unlimited 将core file size设成无限大小。
+   2. 输出环境变量 export GOTRACEBACK=crash 使得golang进程知道需要产生cash时候的coredump文件。
+   3. 分析 /usr/local/bin/dlv core ./app ./core_app
+   4. 使用命令 goroutine goroutineid 和 bt 打印栈信息
+
+   # dlv 常用命令：
+   break main.go:7  # 在main.go 第 7 行加入断点
+   break runtime.growslice # 函数处打断点
+   continue # 继续执行运行到断点处
+   disassemble # 插件对应的反汇编代码
+   goroutines # 查看当前 goroutines
+   stack(bt) # 查看调用栈信息
+   regs  # 过regs命令可以查看全部的寄存器状态, 可以通过单步执行来观察寄存器的变化
+   locals # 查看当前函数所有变量值
+
+- https://yq.aliyun.com/articles/57578
+
+Go 跨平台编译
+---------------------------------------------------------------
+用 Go 你可以选择自己喜欢的操作系统开发，并跨平台编译程序要发布平台的可执行程序即可。
+Go 通过两个环境变量控制跨平台编译：
+
+- GOOS: 代表要编译的目标操作系统，常见的有 Linux, Windows, Darwin 等
+- GOARCH: 代表要编译的目标处理器架构，常见的有 386, AMD64, ARM64 等
+
+比如现在在用 macOS AMD64 开发，但是想要编译出 Linux AMD64 的可执行程序，只需要执行：
+
+``GOOS=linux GOARCH=amd64 go build main.go``
+
+Go Debug 调试工具
+---------------------------------------------------------------
+- go-spew: 用来打印一些复杂结构方便调试 https://github.com/davecgh/go-spew
+- dlv: 断点调试器
+
+Go vs. Python
+---------------------------------------------------------------
+- http://govspy.peterbe.com/
+
+
+Go Best practice(工程实践)
+---------------------------------------------------------------
+- https://draveness.me/golang-101 如何写出优雅的 golang 代码(好文推荐)
+- https://github.com/golang/go/wiki/CodeReviewComments 作为 effective go 补充
+- https://peter.bourgon.org/go-best-practices-2016/
+- https://dave.cheney.net/practical-go/presentations/qcon-china.html
+- https://golang.org/doc/effective_go.html
+- https://talks.golang.org/2013/bestpractices.slide
+- https://dave.cheney.net/practical-go
+- https://github.com/codeship/go-best-practices
+- https://github.com/uber-go/guide/blob/master/style.md   uber 的 go 规范
+
+- https://12factor.net/zh_cn/
+- https://go-proverbs.github.io go谚语，类似 python 之禅
+- https://the-zen-of-go.netlify.com/ zen of go
+- https://bluxte.net/musings/2018/04/10/go-good-bad-ugly/
+- https://github.com/cristaloleg/go-advice
+- https://dablelv.github.io/go-coding-advice/ Go 编码建议
+
+Go 开发关键技术指南
+---------------------------------------------------------------
+- https://developer.aliyun.com/article/739836 Go 开发关键技术指南
+- https://developer.aliyun.com/article/740696 Go 面向失败编程
+- https://yq.aliyun.com/articles/741747 带着服务器编程金刚经走进 2020 年
+- https://developer.aliyun.com/article/742169  Go 开发关键技术指南 | 敢问路在何方？
+
+Go 常用命令
+---------------------------------------------------------------
+
+.. code-block:: shell
+
+  # 获取 go 的 import 列表 (list import)
+  # https://pmcgrath.net/how-to-get-golang-package-import-list
+  go list -f '{{range $imp := .Imports}}{{printf "%s\n" $imp}}{{end}}' | sort
+  go list -f '{{range $dep := .Deps}}{{printf "%s\n" $dep}}{{end}}' | xargs go list -f '{{if not .Standard}}{{.ImportPath}}{{end}}'
+
+  # 清理模块缓存, GO111MODULE=on 以后，下载的模块内容会缓存在$GOPATH/pkg/mod 目录中： 使用以下命令可清空缓存：
+  go clean --modcache
+
+  # 跨平台编译
+  `GOOS=linux GOARCH=amd64 go build main.go`
+
+  # 强制重新构建(依赖包) 使用 -a。参考：http://c.biancheng.net/view/120.html
+  go build -a main.go
+
+  # go mod why 加上包名查看特定模块被引入的原因，用于查询依赖关系
+  go mod why pakage_name
+
+  # go mod graph
+  # 会显示出go.mod里需要的每个包,都依赖了哪些包。可以用这个工具可视化 https://github.com/PaulXu-cn/go-mod-graph-chart
+  go get -u github.com/PaulXu-cn/go-mod-graph-chart/gmchart
+  go mod graph | gmchart
+
+  # remove any unused package
+  go mod tidy -v
+
+
+Go 数据结构与算法
+---------------------------------------------------------------
+
+- https://golangbyexample.com/all-data-structures-golang/
+- https://github.com/emirpasic/gods
+- https://github.com/Workiva/go-datastructures
+- https://github.com/golang-collections/go-datastructures
+
+博客：
+
+- https://zhuanlan.zhihu.com/p/22803609 redigo demo
+- https://blog.biezhi.me/2018/10/load-config-with-viper.html viper 解析配置
+
+Go 底层实现(源码)
+---------------------------------------------------------------
+- https://draveness.me/golang/concurrency/golang-context.html
+- https://github.com/tiancaiamao/go-internals/tree/master/zh
+- https://zhuanlan.zhihu.com/p/80853548 深度解密Go语言之 scheduler
+- https://github.com/cch123/golang-notes
+- https://draveness.me/golang/  Go 语言设计与实现
+
+Go Profiler
+---------------------------------------------------------------
+- pprof
+- github.com/uber/go-troch: Flame graph profiler for Go programs，火焰图工具，配合压测看性能瓶颈
+- https://cizixs.com/2017/09/11/profiling-golang-program/
+- https://software.intel.com/en-us/blogs/2014/05/10/debugging-performance-issues-in-go-programs
+- https://github.com/wolfogre/go-pprof-practice 实战教程
+
+Go 性能优化
+---------------------------------------------------------------
+- https://github.com/dgryski/go-perfbook
+- https://dave.cheney.net/high-performance-go-workshop/dotgo-paris.html
+- https://stephen.sh/posts/quick-go-performance-improvements
+- https://mp.weixin.qq.com/s/ogtRE_LbllN2Tla97LnFrQ
+- https://zhuanlan.zhihu.com/p/482547957 Go 高性能编程技法
+- https://geektutu.com/post/high-performance-go.html 《Go 语言高性能编程》
+
+Goroutines
+---------------------------------------------------------------
+- https://medium.com/@vigneshsk/how-to-write-high-performance-code-in-golang-using-go-routines-227edf979c3c
+- https://udhos.github.io/golang-concurrency-tricks/
+
+Go 内存泄露(memory leak)
+---------------------------------------------------------------
+- https://go101.org/article/memory-leaking.html
+- https://colobu.com/2019/08/28/go-memory-leak-i-dont-think-so/
+
+Go 反射
+---------------------------------------------------------------
+- https://segmentfault.com/a/1190000016230264 Go Reflect 高级实践
+
+Go 网络编程
+---------------------------------------------------------------
+- https://tumregels.github.io/Network-Programming-with-Go/  一本 go 网络编程入门在线电子书
+
+Go 操作消息队列
+---------------------------------------------------------------
+- https://juejin.cn/post/6999263126713696293 Golang中如何正确的使用sarama包操作Kafka？
+
+Go 并发模式
+---------------------------------------------------------------
+- https://blog.golang.org/pipelines
+
+Go 位操作
+---------------------------------------------------------------
+- https://learnku.com/go/t/23460/bit-operation-of-go
+
+Go 缺陷
+---------------------------------------------------------------
+- https://github.com/ksimka/go-is-not-good
+- `50 Shades of Go: Traps, Gotchas, and Common Mistakes for New Golang Devs  <http://devs.cloudimmunity.com/gotchas-and-common-mistakes-in-go-golang/>`_
+- https://bluxte.net/musings/2018/04/10/go-good-bad-ugly/
+
+Go Leetcode
+---------------------------------------------------------------
+- https://github.com/austingebauer/go-leetcode
+- https://books.halfrost.com/leetcode/ 一本 go leetcode 题解电子书
+
+Go 面试题
+---------------------------------------------------------------
+- `Awesome Go Interview Questions and Answers <https://goquiz.github.io/>`_
+- https://bytemode.github.io/interview/
+- https://www.topgoer.cn/docs/gomianshiti/mianshiti
+
+Go源码阅读
+---------------------------------------------------------------
+除了内置库之外，go 还有很多优秀的源码值得学习。建议用到的一些优秀的第三方库的源码都可以看一下，了解底层实现也方便排查问题。
+
+- https://golang.design/under-the-hood//
+- https://bytemode.github.io/reading/
+- https://docs.kilvn.com/go-internals/ref2.html
+- https://draveness.me/golang/docs/part1-prerequisite/ch01-prepare/golang-debug/
+
+.. image:: ../_image/goweb/gocode阅读.png
diff --git a/idiom/idiom.rst b/idiom/idiom.rst
new file mode 100644
index 00000000..15e9d1d8
--- /dev/null
+++ b/idiom/idiom.rst
@@ -0,0 +1,360 @@
+.. _idiom:
+
+Write Idiom Python
+=====================================================================
+
+
+Python支持链式比较
+---------------------------------------------------------------
+.. code-block:: python
+
+   # bad
+   a = 5
+   if a > 1 and a < 7:
+       pass
+   # good
+   if 1 < a < 7:
+       pass
+
+
+Python交换变量
+---------------------------------------------------------------
+.. code-block:: python
+
+   # bad
+   x = 10
+   y = 5
+   tmp = x
+   x = y
+   y = tmp
+
+   # good
+   x = 10
+   y = 5
+   x, y = y, x
+
+
+Python中替代三目运算符?:
+---------------------------------------------------------------
+.. code-block:: python
+
+   # bad
+   a = 10
+   b = 5
+   if a > b:
+       c = a
+   else:
+       c = b
+   # good
+   c = a if a > b else b
+
+
+拼接字符列表时，用join方法去实现
+---------------------------------------------------------------
+.. code-block:: python
+
+   # bad
+
+
+格式化字符时多使用format函数
+---------------------------------------------------------------
+.. code-block:: python
+
+    # bad
+    name = "tony"
+    age = 100
+    str = "myname : " + name + " my age : " + str(age)
+    str1 = "myname : %s my age : %d" % (name, age)
+    # good
+    str2 = "myname : {} my age {}".format(name, age)
+
+
+使用列表或者字典comprehension
+---------------------------------------------------------------
+.. code-block:: python
+
+    # bad
+    mylist = range(20)
+    odd_list = []
+    for e in mylist:
+        if e % 2 == 1:
+            odd_list.append(e)
+    # good
+    odd_list = [e for e in mylist if e % 2 == 1]
+
+    # bad
+    user_list = [{'name': 'lucy', 'email': 'lucy@g.com'}, {'name': 'lily', 'email': 'lily@g.com'}]
+    user_email = {}
+    for user in user_list:
+        if 'email' in user:
+            user_email[user['name']] = user['email']
+    # good
+    {user['name']: user['email'] for user in user_list if 'email' in user}
+
+
+条件判断时，避免直接和True, False, None进行比较(==)
+---------------------------------------------------------------
+.. code-block:: python
+
+   # bad
+   if l == []:
+       pass
+   # good
+   if l:    # 实际调用l.__len__() == 0
+       pass
+
+   # bad
+   if something == None:
+   # good, None 是单例对象
+   if something is None:
+
+
+使用enumerate代替for循环中的index变量访问
+---------------------------------------------------------------
+.. code-block:: python
+
+   # bad
+   my_container = ['lily', 'lucy', 'tom']
+   index = 0
+   for element in my_container:
+       print '{} {}'.format(index, element)
+       index += 1
+
+   # good
+   for index, element in enumerate(my_container):
+       print '%d %s' % (index, element)
+
+
+避免使用可变(mutable)变量作为函数参数的默认初始化值
+---------------------------------------------------------------
+.. code-block:: python
+
+   # bad
+   def function(l = []):
+    l.append(1)
+    return l
+
+    print function()
+    print function()
+    print function()
+
+    # print
+    [1]
+    [1, 1]
+    [1, 1, 1]
+
+    # good 使用None作为可变对象占位符
+    def function(l=None):
+        if l is None:
+            l = []
+        l.append(1)
+        return l
+
+
+一切皆对象
+---------------------------------------------------------------
+.. code-block:: python
+
+    # bad
+    def print_addition_table():
+        for x in range(1, 3):
+            for y in range(1, 3):
+                print(str(x + y) + '\n')
+
+    def print_subtraction_table():
+        for x in range(1, 3):
+            for y in range(1, 3):
+                print(str(x - y) + '\n')
+
+    def print_multiplication_table():
+            for x in range(1, 3):
+                for y in range(1, 3):
+                    print(str(x * y) + '\n')
+
+    def print_division_table():
+        for x in range(1, 3):
+            for y in range(1, 3):
+                print(str(x / y) + '\n')
+
+    print_addition_table()
+    print_subtraction_table()
+    print_multiplication_table()
+    print_division_table()
+
+    # good, python一切都是对象，可以函数作为参数，类似技巧可以用来简化代码
+    import operator as op
+
+    def print_table(operator):
+        for x in range(1, 3):
+            for y in range(1, 3):
+                print(str(operator(x, y)) + '\n')
+
+    for operator in (op.add, op.sub, op.mul, op.div):
+        print_table(operator)
+
+
+防御式编程EAFP vs LBYL
+---------------------------------------------------------------
+* EAFP：easier to ask forgiveness than permission
+* LBYL：look before you leap
+
+EAFP可以理解成一切按正常的逻辑编码，不用管可能出现的错误，等出了错误再说；而LBYL就是尽可能每写一行代码，都要提前考虑下当前的前置条件是否成立；
+
+.. code-block:: python
+
+    # LBYL
+    def getPersonInfo(person):
+        if person == None:
+            print 'person must be not null!'
+        print person.info
+
+    # EAFP
+    def getPersonInfo(person):
+        try:
+            print person.info
+        except NameError:
+            print 'person must be not null!'
+
+其实用EAFP风格的代码最大的好处是代码逻辑清晰，而LBYL会导致本来两句话说清楚的事，往往因为穿插了很多条件检查的语句使代码逻辑变得混乱。Python社区更提倡EAFP形式的。另外还有一个原因，在高并发场景下， if条件如果是个表达式，会造成一致性问题，这个时候必须用EAFP形式。这个可以参考Glow团队的技术博客[Glow cache structure](http://tech.glowing.com/cn/glow-cache-structure).
+
+
+用dict对象完成switch...case...的功能
+---------------------------------------------------------------
+
+.. code-block:: python
+
+    # bad
+    def apply_operation(left_operand, right_operand, operator):
+        if operator == '+':
+            return left_operand + right_operand
+        elif operator == '-':
+            return left_operand - right_operand
+        elif operator == '*':
+            return left_operand * right_operand
+        elif operator == '/':
+            return left_operand / right_operand
+    # good
+    def apply_operation(left_operand, right_operand, operator):
+        import operator as op
+        operator_mapper = {'+': op.add, '-': op.sub, '*': op.mul, '/': op.truediv}
+        return operator_mapper[operator](left_operand, right_operand)
+
+
+
+访问tuple的数据项时，可以用namedtuple代替index的方式访问
+---------------------------------------------------------------
+
+.. code-block:: python
+
+    # bad
+    rows = [('lily', 20, 2000), ('lucy', 19, 2500)]
+    for row in rows:
+        print '{}`age is {}, salary is {} '.format(row[0], row[1], row[2])
+
+    # good
+    from collections import  namedtuple
+    Employee = namedtuple('Employee', 'name, age, salary')
+    for row in rows:
+        employee = Employee._make(row)
+        print '{}`age is {}, salary is {} '.format(employee.name, employee.age, employee.salary)
+
+
+
+用isinstance来判断对象的类型
+---------------------------------------------------------------
+因为在python中定义变量时，不用像其它静态语言，如java， 要指定其变量数据类型，如int = 4. 但是这并不意味在python中没有数据类型，只是一个变量的数据类型是在运行的时候根据具体的赋值才最终确定。比如下面的代码是计算一个对象的长度值，如果是序列类型（str,list,set,dict）的, 直接调用len方法，如果是True, False, None则返回1，如果是数值的，则返回其int值.
+
+.. code-block:: python
+
+    # bad
+    def get_size(some_object):
+        try:
+            return len(some_object)
+        except TypeError:
+            if some_object in (True, False, None):
+            return 1
+        else:
+            return int(some_object)
+
+    print(get_size('hello'))
+    print(get_size([1, 2, 3, 4, 5]))
+    print(get_size(10.0))
+
+    # good
+    def get_size(some_object):
+        if isinstance(some_object, (list, dict, str, tuple)):
+            return len(some_object)
+        elif isinstance(some_object, (bool, type(None))):
+            return 1
+        elif isinstance(some_object, (int, float)):
+            return int(some_object)
+
+
+用with管理操作资源的上下文环境
+---------------------------------------------------------------
+在一个比较典型的场景里，如数据库操作，我们操作connection时一般要正常关闭连接，而不管是正常退出还是异常退出。如下：
+
+.. code-block:: python
+
+    # bad
+    class Connection(object):
+    def execute(self, sql):
+        raise Exception('ohoh, exception!')
+
+    def close(self):
+        print 'closed the Connection'
+
+    try:
+        conn = Connection()
+        conn.execute('select * from t_users')
+    finally:
+        conn.close()
+
+    # good
+    class Connection(object):
+    def execute(self, sql):
+        raise Exception('ohoh, exception!')
+
+    def close(self):
+        print 'closed the Connection'
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, errorType, errorValue, error):
+        self.close()
+
+    with Connection() as conn:
+        conn.execute('select * from t_users')
+
+
+使用generator返回耗费内存的对象
+---------------------------------------------------------------
+
+.. code-block:: python
+
+   # bad
+   def f():
+       # ...
+       return biglist
+
+   # good
+   def f():
+       # ...
+       for i in biglist:
+           yield i
+
+
+更多资源：
+---------------------------------------------------------------
+
+* `《分享书籍[writing idiomatic python ebook]》 <http://www.cnblogs.com/jcli/p/3624904.html>`_
+* `《Python 3 Patterns, Recipes and Idioms》 <https://media.readthedocs.org/pdf/python-3-patterns-idioms-test/latest/python-3-patterns-idioms-test.pdf>`_
+* `《30个有关Python的小技巧》 <http://blog.jobbole.com/63320/>`_
+* `《Hidden features of Python》 <http://stackoverflow.com/questions/101268/hidden-features-of-python>`_
+* `《Python程序员的10个常见错误》 <http://blog.jobbole.com/68256/>`_
+* `《Python高级编程slide》 <http://www.dongwm.com/archives/fen-xiang-%5B%3F%5D-ge-zhun-bei-gei-gong-si-jiang-pythongao-ji-bian-cheng-de-slide/>`_
+* `《Effective Python》 <https://book.douban.com/subject/26312313/>`_
+* `《编写高质量代码：改善Python程序的91个建议》 <https://book.douban.com/subject/25910544/>`_
+* `《Code Like a Pythonista: Idiomatic Python》 <http://python.net/~goodger/projects/pycon/2007/idiomatic/handout.html>`_
+* `《The Little Book of Python Anti-Patterns》 <http://docs.quantifiedcode.com/python-code-patterns/>`_
diff --git a/index.rst b/index.rst
index af4696f7..361af9c7 100644
--- a/index.rst
+++ b/index.rst
@@ -4,17 +4,21 @@
 
 .. toctree::
     :glob:
-    :maxdepth: 1
+    :maxdepth: 2
 
-    team/*
-    api/*
-    backend/index
+    base/*
+    go-note/index
+    codingstyle/*
+    debug/index
+    codingtools/*
+    idiom/*
+    design/*
+    algorithms/*
     database/index
-    codingguide/index
-    frontend/index
-    innertools/index
     skillstack/index
     devtools/index
-    mobile/index
-    sso/index
+    python-note/index
+    microservice_distribute/index
+    tracing/index
+    memo/*
     *
diff --git a/innertools/42qucc.rst b/innertools/42qucc.rst
deleted file mode 100644
index 31832e43..00000000
--- a/innertools/42qucc.rst
+++ /dev/null
@@ -1,35 +0,0 @@
-.. _42qucc:
-
-==================================================
-42qu.cc 简明实用教程
-==================================================
-
-:作者: 王然 kxxoling@gmail.com
-
-关于 42qu.cc
-============
-
-42qu.cc 是在线的随手贴，可以随意地自定义网址来自己记录或与朋友分享文字。
-
-
-保存
------------------------
-
-42qu.cc 提供在线分享文本文件的功能。在命令行中输入
-
-    42qucc < 某个文件名
-
-即可获得诸如 `http://42qu.cc/6nhxuqpg <http://42qu.cc/6nhxuqpg>`_ 格式的网址，打开该链接即可看到分享的内容。
-
-
-读取
------------------------
-
-读取刚刚相反，将链接中的内容保存在文件中：
-
-    42qucc http://42qu.cc/6nhxuqpg > 文件名
-
-
-自定义网址
-------------
-
diff --git a/innertools/other.rst b/innertools/other.rst
deleted file mode 100644
index 220bf124..00000000
--- a/innertools/other.rst
+++ /dev/null
@@ -1,11 +0,0 @@
-.. _tools:
-
-============
-其它内部工具
-============
-
-
--  html2plim
--  deltem
--  dir\_replace
--  replace\_line
diff --git a/innertools/seafile.rst b/innertools/seafile.rst
deleted file mode 100644
index d61e8fe2..00000000
--- a/innertools/seafile.rst
+++ /dev/null
@@ -1,103 +0,0 @@
-
-.. _seafile:
-
-seafile指南
-=================================================
-
-这里介绍了如何使用seafile共享文件。
-
-获取账户
---------------------------------------------------
-
-发邮件给我moujunqiu@pe.vc(牟俊秋)，写明你的邮箱地址，我会给你开通账户的，密码会随注册邮件发到您的邮箱。
-
-
-从网页登陆
--------------------------------------------------
-
-网页登陆地址是：http://seafile.mushapi.com
-
-客户端安装和使用
---------------------------------------------------
-
-`官网所有版本客户端下载地址 <http://seafile.com/download/>`_
-
-
-`windows版客户端下载地址 <http://seafile.mushapi.com/repo/fe292c8f-1799-43fa-9ed5-e33e17f52e1e/files/?p=/seafile-3.0.4.msi>`_
-
-
-
-seafile支持windows、mac、linux以及苹果和android手机，下载适合的版本并安装。
-
-这里以ubuntu下的客户端为例说明客户端使用方法
-
-1. 登录
-
-   服务器处填入http://seafile.mushapi.com
-
-   填入邮箱和密码
-
-   .. image:: ../_static/login.png
-
-#. 创建资料库
-
-   1. 从网页创建
-
-   #. 从客户端创建
-
-   .. image:: ../_static/main.png
-
-#. 创建和加入群组
-
-   创建和加入群组的操作需要在 **网页** 上进行，所有用户都可以创建群组，群组的创建者自动成为该群组的管理员，其他成员申请加入该群组时需要群组管理员批准。
-
-   1. 创建群组
-         登陆到seafile后，点击上方导航条的 **群组** 然后再点击下拉菜单中的 **所有群组** 。
-
-         .. image:: ../_static/addgroup.png
-
-         在 **我的群组** 中点击 **创建群组** 按钮，输入群组相关信息完成创建。
-
-         .. image:: ../_static/addgroup2.png
-
-   #. 加入群组
-         如何加入群组
-
-   #. 管理员审核加入群组的请求
-         有用户申请加入群组后，群组管理员的首页上会显示一个通知，如下图所示
-
-         .. image:: ../_static/unreadnotify.png
-
-         点击那个小铃铛，打开通知页面，首先点击该用户。
-
-         .. image:: ../_static/notify.png
-
-         然后将该用户添加到你的通讯录里
-
-         .. image:: ../_static/addtogroup.png
-
-         最后在 **群组** 的 **管理** 里从通讯录中将该用户添加到群组。（我知道这里的操作很蠢，但我并没发现别的途径，如果有请告诉我）
-
-         .. image:: ../_static/addusertogroup.png
-
-         .. image:: ../_static/aaaaa.png
-
-#. 共享资料库
-
-   资料库有三种共享方式，共享给个人、共享给群组、共享给机构。
-
-   1. 共享给个人
-         通过个人的邮箱共享，共享之后只有指定的用户可以访问该资料库。
-
-   #. 共享给群组
-         可以通过直接在群组中创建资料库或者将指定的资料库共享给您 **已经加入** 的群组，共享后群组内的所有成员都能访问该资料库。
-
-   #. 共享给机构
-         这里的 *机构* 是指该网站下的所有用户，跟上一条相同也可通过直接在机构中创建和共享已有资料库的形式共享。
-
-#. 使用pc客户端同步资料库
-
-   右单击要同步的资料库，选择‘同步该资料库’选项，未同步的资料库的图标显示为一个云彩，同步完成的显示成一个绿色的对号。
-
-   .. image:: ../_static/sync.png
-
diff --git a/kp.rst b/kp.rst
deleted file mode 100644
index 5d4f13ca..00000000
--- a/kp.rst
+++ /dev/null
@@ -1,389 +0,0 @@
-====================
-程序员知识点考评大纲
-====================
-
-
-
-常用工具
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-Linux
-----------------------------------
-
-基础篇
-..................................
-* locate
-* ag
-* xtail
-
-Gentoo
-----------------------------------
-* emerge --autounmask-write 然后 etc-update 然后 -3
-* emerge =xxx-版本号
-
-自定义的命令
-..................................
-* dirreplace
-* replace_line.py
-* deltmp
-
-高级篇
-..................................
-* dstat
-* dirreplace
-* replace_line.py
-
-
-tmux
------------------------------------
-* Ctrl+B
-
-xshell
------------------------------------
-
-vim
------------------------------------
-* F12
-* MR
-* 批量注释
-* = 排版
-* vsp
-* 替换
-* 替换选中部分
-
-版本控制
------------------------------------
-
-* hg
-
-  - blame
-  - log
-    + -l 
-  - fetch 
-  - st
-  - ignore
-  - diff
-  - grep 
-  - bisect 定位BUG出现的版本 
-  - serve
-  - resolve
-    + -l
-    + -m
-
-42web小工具 :
-
-    ./hg_close_branch  
-    ./hg_update_branch
-
-
-前端
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-HTML
------------------------------------
-* label for属性
-
-  绑定表单元素id，实现点击label选中相应radio或checkbox
-* 链接都必须写上href属性， 空链接请写成:: 
-
-    <a href="javascript:void(0)"></a>
-
-* 反复出现的按钮图标用 a标签 配合 css设置背景图 来实现，不要反复的写img标签（减少代码的冗余度）
-
-CSS
------------------------------------
-* 模块分级
-    * 全局级
-    * 模块级
-    * 页面级
-* 分栏布局
-
-  例如左宽665px,父容器宽1000px,在layout.css中定义::
-
-  .R665{float: left; margin-left: 688px; width:312px;}
-  .L665{float: left; width: 665px; margin-left: -100%;}
-
-  参考:
-
-  `双飞翼布局 <http://www.dqqd.me/flying-wing/>`_
-  `常见布局 <http://blog.html.it/layoutgala/>`_
-
-* 绝对底部
-
-  `CSS Sticky Footer <http://paranimage.com/css-sticky-footer/>`_
-* 清除浮动
-* 子选择器
-    
-    利用父级元素id进行选择
-* `图文混排 <http://dabblet.com/gist/4094139>`_
-* CSS盒模型
-* 垂直居中
-* 如何制作三角
-* :first-child 和 :last-child
-    - 案例 : 圆圈 , padding
-* 如果内容可变，就不要设置高度
-* 写完页面依次检查
-    * 对齐
-    * 字体
-        * 大小
-        * 粗细
-        * 颜色
-    * 留白
-* 邮件
-    `Css Inliner Tool <http://templates.mailchimp.com/resources/inline-css/>`_
-* 42web
-    * HTML中如何引用图片 
-
-    图片上传至七牛，使用外链地址
-    * CSS中如何引用图片::
-
-        background:url(/css/_img/xxx)
-
-    * 不要引用站外的图片 
-* checkbox 和 radio 的 样式 
-
-  `文字对齐 <http://www.zhangxinxu.com/wordpress/?p=56>`_
-* 不要用空格做间距
-
-我们常用的CSS样式
------------------------------------
-* 按钮
-    * 功能
-    * 强调
-
-设计 
------------------------------------
-* 对齐
-* 留白的一致性
-* 粗体
-* 字号
-
-javascript
------------------------------------
-* 获取时间戳::
-
-    (new Date).getTime()
-* 在js中取得当前用户::
-
-    $.current_user
-* $$
-
-  例如调用弹窗(可有多个参数)::
-
-    $$('SITE/auth/login')
-* require
-* $.require
-* $.dialog
-    * 需要登录调用$.login_dialog(参考submit_project.coffee)
-* $.errtip ::
-
-    err = {}
-
-    if xxx:
-     err.xxx = "xx"
-     if xx :  
-        err.xx ="xx"
-
-        if not errtip.set err:
-           xxxxx
-
-jQuery
------------------------------------
-* $.extend([deep],target,object)
-
-jQuery 自定义扩展
------------------------------------
-* $.timeago
-
-  接受一个时间戳作为参数,返回距离当前时间描述
-* $.isotime
-* $.getJSON1
-    * jsonp 跨域调用
-* $.postJSON1
-* $.html 模版
-
-  参考egg_new.coffee
-
-jQuery UI
------------------------------------
-* Accordion
-* Datepicker
-* Tagit
-
-CoffeeScript
------------------------------------
-* `在页面中直接写coffee <http://coffeescript.org/#scripts>`_
-
-avalon
------------------------------------
-* 命名规则的修改
-
-    "-"改为"_"
-* ms_view
-* 操作类似view的复用
-* view与数据结构的模块划分原则（每一个保存的url对应一个view）
-* `$remove <http://limodou.github.io/avalon-learning/zh_CN/event.html>`_
-* `$watch <http://limodou.github.io/avalon-learning/zh_CN/watch.html>`_
-* 如何定义avalon组建
-    * 创建既可以单独使用，也可以在循环中使用的avalon组件
-
-  参考ui_follow.coffee
-
-Firebug
------------------------------------
-* 控制台面板中，点击“保持”按钮，页面重新载入时不清空面板
-
-杂项
------------------------------------
-* `七牛剪裁 <http://developer.qiniu.com/docs/v6/api/reference/fop/image/imageview2.html>`_
-* 上传文件
-* 上传头像
-* 地址选择
-
-Photoshop
------------------------------------
-* `复制psd中的文字 参考psd文字编辑 <http://jingyan.baidu.com/article/fc07f9893db14512ffe5199e.html>`_
-
-工具
------------------------------------
-* Chrome插件 
-    * PerfectPixel 
-    * Page Ruler 
-* Windows
-    * Color Picker
-
-
-后端
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-python
------------------------------------
-* 闭包 
-* 正则表达式
-* collections
-  - defaultdict
-* itertools
-* enum
-  - IntEnum
-* enumerate
-* time.mktime(time.strptime("2007-03-04 21:08:12", "%Y-%m-%d %H:%M:%S"))
-* dateparser
-* python 的 新式类与旧式类 ， 以及super的意义
-
-mongodb
------------------------------------
-* find 
-    - limit
-    - skip
-    - sort
-* delete
-* remove(删除条件)
-* save
-   - 填充默认值
-* upsert
-
-MySQL
------------------------------------
-* get
-* mc_get
-* mc_get_list
-
-Kv
-----------------------------------
-* id_by_value
-* get
-* mc_get
-
-nginx
------------------------------------
-
-mako
------------------------------------
-* this 比如 this.get_argument('q')
-* ${json_encode(xxx)|n}
-
-redis
------------------------------------
-* hset
-* set
-* zset
-* list
-* expire 
-
-mongo
------------------------------------
-* 时间用int保存
-* mongo默认值需要是一个生成函数
-  * pyhton常见的默认值陷阱，以create_time=time()为例
-
-gearman
------------------------------------
-
-supervisor
------------------------------------
-* 线上服务器如何看异常
-
-tornado
------------------------------------
-* 通过编写 Base View简化业务开发
-
-42web
------------------------------------
-* 新建url页面
-* render
-* css，js的引用
-* merge.conf
-* 新建css，js，修改merge.conf需要重启开发服务器
-* View的类型
-* 分页
-* 在页面取得当前用户
-* 搜索
-* 自动补全
-* gearman 异步调用 
-* JsOb
-* rendermail 发送邮件
-* redis key的定义 ， R.
-* model 中 使用绝对路径import以防止redis提示key重复定义
-* import _env 
-* 配置文件 的 定义 与 自适应
-* make.py 生成配置文件
-
-开发习惯
-----------------------------------
-* 修改函数接口后， 用ag查找并修改些调用过的地方
-* 函数命名规则 ：名词在前动词在后 ， 常用命名如下
-
-  - user_new 新建
-  - user_rm 删除
-  - user_dumps 返回一个包含各种相关数据的json对象
-  - user_id_list_by_com_id(limit, offset) 查询
-  - user_new 新建
-  - user_rm 删除
-  - user_dumps 返回一个包含各种相关数据的json对象
-  - user_id_list_by_com_id(limit, offset) 查询
-  - user_id_count_by_com_id
- 
-  我们通常把user_id作为第一个参数 
-
-开发流程
-----------------------------------
-* 表单
-
-  #. 编写静态html页面
-  #. coffeescript完成交互 
-  #. 演示并验收前端页面
-  #. 定义 url 和 json数据
-  #. 完成ajax保存接口 
-  #. 数据回填
-  #. 演示并验收
-  #. code review
-  #. 合并到default 
-  #. 上线到开发服务器
-  #. 合并到online
-  #. 上线到线上服务器 
-
-
-前端
-.................................
-* 确认链接都链接到了正确的页面
-
-
diff --git a/memo/memo.rst b/memo/memo.rst
new file mode 100644
index 00000000..0fe35bc3
--- /dev/null
+++ b/memo/memo.rst
@@ -0,0 +1,1425 @@
+.. _memo:
+
+个人技术备忘录 💻
+=====================================================================
+
+..
+
+  好记性不如烂笔头. - 中国谚语
+
+
+Python
+---------------------------------------------------------------
+.. code-block:: python
+
+   # python起文件服务器
+   python3.4 -m http.server
+   python -m SimpleHTTPServer    # python2
+
+   python -u script.py   # 刷新缓冲，执行脚本重定向结果到文件时候比较有用
+   python -u script.py  | tee script.log # 打印脚本结果，同时输出到日志 (无缓冲可以立马看到输出)
+
+   # logging
+   FATAL(50) > ERROR(40) > WARNING(30) > INFO(20) > DEBUG(10)
+
+   # 使用virtualenv制定python版本
+   virtualenv -p /usr/bin/python2.7 ENV2.7
+
+   # pyenv 安装多个版本的 python : https://github.com/pyenv/pyenv
+   # pyenv-virtualenv https://github.com/pyenv/pyenv-virtualenv
+   # mac install multiple version of python
+   brew install pyenv pyenv-virtualenv
+   pyenv install 3.6.4  # install python3.6.4
+   pyenv virtualenv 3.6.4 v3.6.4
+   pyenv activate v3.6.4
+   pyenv deactivate
+
+   # 格式化 json，这个可以配置在 vim 里用来格式化当前 json 文本
+   cat some.json | python -m json.tool
+
+
+pip/easy_install
+---------------------------------------------------------------
+.. code-block:: python
+
+   # 服务器上有时候没有 root 权限可以试试
+   # https://stackoverflow.com/questions/7465445/how-to-install-python-modules-without-root-access
+
+   # https://stackoverflow.com/questions/12332975/installing-python-module-within-code
+   # pip install
+   import pip
+
+   def install(package):
+       pip.main(['install', package])
+
+   # Example
+   if __name__ == '__main__':
+       install('argh')
+
+
+有时候 pip 安装会比较慢可以更换为豆瓣的源，两种方式
+
+1. ``pip install redis -i https://pypi.doubanio.com/simple``
+2. 或者修改全局 ``vi ~/.pip/pip.conf``
+
+.. code-block:: shell
+
+  [global]
+  timeout = 60
+  index-url = http://pypi.douban.com/simple
+  trusted-host = pypi.douban.com
+
+
+IPython
+---------------------------------------------------------------
+
+.. code-block:: sh
+
+   # ipython 如何使用 autoreload，每次重新修改了文件都得重新重启 ipython 很麻烦，解决方式
+   # https://support.enthought.com/hc/en-us/articles/204469240-Jupyter-IPython-After-editing-a-module-changes-are-not-effective-without-kernel-restart
+   # https://stackoverflow.com/questions/1254370/reimport-a-module-in-python-while-interactive
+   # http://ipython.readthedocs.io/en/stable/config/extensions/autoreload.html
+   In [1]: %load_ext autoreload
+   In [2]: %autoreload 2
+
+
+.. code-block:: python3
+
+   # -*- coding: utf-8 -*-
+
+   # ~/.ipython/profile_default/startup/startup.py
+   # Ned's .startup.py file    ipython 启动加载文件，用来导入一些自定义函数或者模块，方便调试
+   # http://stackoverflow.com/questions/11124578/automatically-import-modules-when-entering-the-python-or-ipython-interpreter
+
+   print("(.startup.py)")
+
+   import datetime as dt
+   import os
+   import pprint
+   import re
+   import sys
+   import time
+   import json
+   import requests as req
+
+   try:
+       import matplotlib.pyplot as plt
+       import pandas as pd
+       from pandas import Series, DataFrame
+       import numpy as np
+   except ImportError:
+       pass
+
+   print("(imported datetime, os, pprint, re, sys, time, json)")
+
+   def _json_dumps(dict_data, indent=4):
+       """用来处理一些包含中文的 json 输出"""
+       print(json.dumps(dict_data, indent=indent, ensure_ascii=False))
+
+   def _repr_dict(d):
+       """https://stackoverflow.com/questions/25118698/print-python-dictionary-with-utf8-values"""
+       print('{%s}' % ',\n'.join("'%s': '%s'" % pair for pair in d.iteritems()))
+
+   def _json_dumps(dict_data, indent=4):
+       """用来处理一些包含中文的 json 输出"""
+       print(json.dumps(dict_data, indent=indent, ensure_ascii=False))
+
+
+   repr_dict = _repr_dict
+   pp = pprint.pprint
+   json_dumps = _json_dumps
+
+.. code-block:: sh
+
+   # http://shawnleezx.github.io/blog/2015/08/03/some-notes-on-ipython-startup-script/
+   """
+   !!! 注意，如果遇到了 TypeError: super(type, obj): obj must be an instance or subtype of type
+   请禁用 autoreload, http://thomas-cokelaer.info/blog/2011/09/382/
+   """
+   from IPython import get_ipython
+   ipython = get_ipython()
+
+   # ipython.magic("pylab")
+   ipython.magic("load_ext autoreload")
+   ipython.magic("autoreload 2")
+
+   # Ipython 技巧，如何查询文档，比如 time.time 方法的文档
+   # https://jakevdp.github.io/PythonDataScienceHandbook/01.01-help-and-documentation.html
+   >>> import time
+   >>> time.time?  # 回车之后可以输出该函数的 docstring 文档
+   >>> time.time??  # 回车之后可以输出该函数的定义
+
+
+Ipdb
+---------------------------------------------------------------
+.. code-block:: sh
+
+   # ~/.pdbrc
+   # https://github.com/gotcha/ipdb/issues/111
+
+   import os
+   alias kk os._exit(0)    # 如果不幸在循环里打了断点，可以用 os._exit(0) 跳出
+
+   alias pd for k in sorted(%1.keys()): print "%s: %s" % (k, (%1[k]))
+
+   # https://stackoverflow.com/questions/21123473/how-do-i-manipulate-a-variable-whose-name-conflicts-with-pdb-commands
+   # 如果 pdb 里的内置命令和内置函数冲突了，可以加上 ! 使用内置函数
+   !next(iter)
+
+Chrome(Mac)
+---------------------------------------------------------------
+.. code-block:: sh
+
+   # 使用 comamnd + l 可以立即定位到 url 输入框
+   # 使用 vimium 或者 surfingkeys 插件可以用 vim 的模式操作 chrome
+   # 用 vimium 如何不用鼠标从 url 输入框回到网页:
+   https://superuser.com/questions/324266/google-chrome-mac-set-keyboard-focus-from-address-bar-back-to-page/324267#324267
+   https://xavierchow.github.io/2016/03/07/vimium-leave-address-bar/
+   # 清理 dns cache, https://superuser.com/questions/203674/how-to-clear-flush-the-dns-cache-in-google-chrome
+   Navigate to chrome://net-internals/#dns # and press the "Clear host cache" button.
+
+   # 收藏夹。注意分类收藏，否则后来会收藏多了比较乱。使用 surfingkeys ab (add bookmark) 和 gb(收藏夹管理) 可以快速操作
+
+   # 黑科技：如何chrome 证书认证（不推荐）
+   在网页中输入 thisisunsafe 或者 badidea 就可以了
+
+   # surfingkeys 插件可以通过T 搜索当前打开的标签，但是打开数量在 tabsThreshold 以下的时候只会高亮 tab 不会打开搜索框
+   # 可以设置这个值 tabsThreshold 只要打开超过该数量的标签就可以使用搜索浏览器标签的功能
+   settings.tabsThreshold = 5;
+
+MacOS
+---------------------------------------------------------------
+.. code-block:: python
+
+   # NOTE: 使用『时间机器』定期备份你的mac 是一个好习惯，笔者买了一个移动硬盘用来定期备份
+   # 文件字符串批量替换，git项目里替换的时候注意指定文件类型，防止破坏git信息
+   # 注意 mac 的 sed 命令和 linux 有区别(比如mac sed -i 后必须有参数) https://unix.stackexchange.com/questions/13711/differences-between-sed-on-mac-osx-and-other-standard-sed
+   # 可以使用 brew install gnu-sed 替换，然后使用 gsed 命令替代
+   find . -name \*.py -exec sed -i '' 's/old/new/g' {} \;
+   # copy that data into the system’s paste buffer
+   cat file.txt | pbcopy
+   # The pbpaste command lets you take data from the system’s paste buffer and write it to standard out.
+   pbcopy < birthday.txt
+   pbpaste | ag name
+   pbpaste > filename
+
+   # updatedb https://superuser.com/questions/109590/whats-the-equivalent-of-linuxs-updatedb-command-for-the-mac
+   sudo /usr/libexec/locate.updatedb
+
+   # homebrew 更换源, https://maomihz.com/2016/06/tutorial-6/
+   cd /usr/local
+   git remote set-url origin git://mirrors.ustc.edu.cn/brew.git
+
+   cd /usr/local/Library/Taps/homebrew/homebrew-core
+   git remote set-url origin git://mirrors.ustc.edu.cn/homebrew-core.git
+
+   # 从终端查 wifi 密码, https://apple.stackexchange.com/questions/176119/how-to-access-the-wi-fi-password-through-terminal
+   security find-generic-password -ga "ROUTERNAME" | grep "password:"
+
+   # XXX.APP已损坏,打不开.你应该将它移到废纸篓 MACOS 10.12 SIERRA，允许“任何来源” https://zhuanlan.zhihu.com/p/135948430
+   sudo spctl --master-disable
+
+   # 使用 mounty 挂载 ntfs 盘，Item "file.mov" is used by Mac OS X and cannot be opened.
+   # https://apple.stackexchange.com/questions/136157/mov-file-in-external-hd-greyed-out-and-wont-open-this-item-is-used-by-mac-o?utm_medium=organic&utm_source=google_rich_qa&utm_campaign=google_rich_qa
+   cd /Volumes/[drive name]
+   xattr -d com.apple.FinderInfo *
+   # or
+   SetFile -c "" -t "" path/to/file.mov
+
+   # mac 使用命令挂载
+   diskutil mount /dev/disk1s2
+   diskutil unmount /dev/disk1s2
+
+   # 使用 rmtrash 删除到 trash，防止危险的 rm 删除命令找不回来。在 bashrc or zshrc alias rm='rmtrash '
+   # 如果是 linux 用户，可以使用 safe-rm https://github.com/kaelzhang/shell-safe-rm
+   # 删除的文件会放到 $HOME/.Trash 方便恢复
+   brew install rmtrash  # npm install -g safe-rm; alias rm='safe-rm'
+
+   # 如何在文件更新之后自动刷新浏览器，需要首先 pip 安装 when-changed
+   alias flush_watch_refresh_chrome=" when-changed -v -r -1 -s ./ osascript -e 'tell application \"Google Chrome\" to tell the active tab of its first window to reload' "
+
+   # 如何启用三指拖移(新版本把改设置移动到了辅助功能，使用三指移动可以方便地移动窗口，一般我会启用提高效率)
+   辅助功能 -> 鼠标与触控板 -> 触控板选项 -> 启用拖移 (之后就能直接三指翻译单词了)
+
+   # 如何解决 mac 突然没有声音的问题(系统 bug，音频守护进程 coreaudiod出了问题)
+   sudo killall coreaudiod
+
+   # mac 如何使用 realpath 显示绝对路径, https://stackoverflow.com/questions/3572030/bash-script-absolute-path-with-os-x
+   # brew install coreutils
+   grealpath file
+
+   # mac trackpad 蓝牙频繁掉线问题。尝试使用 5G wifi 而不是 2.4G
+   # https://apple.stackexchange.com/questions/321948/why-does-my-magic-trackpad-2-randomly-disconnect-and-stop-clicking
+
+   # 软件：pathfinder 如何增加 隔空投送 airdrop 分享文件
+   https://support.cocoatech.com/discussions/problems/126873-full-airdrop-sharing-is-here-for-pf8-and-pf7
+
+   # mac 压缩之后去掉 "_MACOSX" 隐藏文件。https://stackoverflow.com/questions/10924236/mac-zip-compress-without-macosx-folder
+   zip -d filename.zip __MACOSX/\*
+
+   # 解压 windows zip 乱码。https://www.jianshu.com/p/460f9307dadf
+   brew install unar
+   unar -e GBK xx中文xx.zip
+
+   # 删除旧文件 https://tecadmin.net/delete-files-older-x-days/
+   find /var/log -name "*.log" -type f -mtime +30 # 找到 30 天之前修改的文件，指定文件类型 为 log
+   find /var/log -name "*.log" -type f -mtime +30 -delete  # 执行删除
+   find /opt/backup -type f -mtime +30
+
+   # hide or show desktop icon for presentation 隐藏桌面图标
+   alias hide_desktop_icon='defaults write com.apple.finder CreateDesktop -bool false; killall Finder'
+   alias show_desktop_icon='defaults write com.apple.finder CreateDesktop -bool true; killall Finder'
+
+   # mac https://apple.stackexchange.com/questions/54329/can-i-get-the-cpu-temperature-and-fan-speed-from-the-command-line-in-os-x
+   gem install iStats # need
+   istats all
+
+   # mac 休眠 https://www.jianshu.com/p/ec888c3e33dd
+   sudo shutdown -s +10 # 10分钟后休眠
+
+   # mac https://superuser.com/questions/273756/how-to-change-default-app-for-all-files-of-particular-file-type-through-terminal
+   # https://chainsawonatireswing.com/2012/09/19/changing-default-applications-on-a-mac-using-the-command-line-then-a-shell-script/
+   brew install duti # 安装 duti
+   osascript -e 'id of app "calibre.app"' # https://www.hexnode.com/mobile-device-management/help/how-to-find-the-bundle-id-of-an-application-on-mac/
+   duti -x sh # 获取 .sh 文件的默认打开软件，最后一行是id
+   duti -s io.brackets.appshell .md all # 用该 id 的软件打开所有的 md 文件
+
+   osascript -e 'id of app "ebook-viewer.app"' # 安装 calibre 之后，找到附带的电子书浏览软件 id
+   duti -s com.calibre-ebook.ebook-viewer .mobi all # 用 ebook-viewer 打开所有的 mobi
+
+   # legacyScreenSaver memory 内存占用高。装了 fliqlo 翻页时钟屏保， 尝试重新装下 https://fliqlo.com/screensaver/ 最新版
+
+如何发送 mac 通知，可以用来做提示
+
+.. code-block:: python
+
+   # https://stackoverflow.com/questions/17651017/python-post-osx-notification
+   # 配合 crontab 可以用来做一个简单的定时任务提醒功能 57-59 17 * * * python ~/.tmp/noti.py
+
+
+   # ~/.tmp/noti.py
+   import os
+
+   def notify(title, text):
+       os.system(""" osascript -e 'say "家里放点音乐吧"' """)
+       os.system(""" osascript -e 'display notification "{}" with title "{}"' """.format(text, title))
+
+   notify("开会啦", "Go Go Go !!!")
+
+增加终端下光标的移动速度(⭐️ 非常好用)：
+
+.. code-block:: shell
+
+   # mac: 系统设置-> 键盘 -> 修改按键重复到最快，重复前延迟最短。可以让光标在终端里移动更快 (推荐下边的命令修改更快)
+
+   # 增加 terminal 光标移动速度, https://stackoverflow.com/questions/4489885/how-can-i-increase-the-cursor-speed-in-terminal
+   # 终端执行以下三个 defaults 命令后必须重启(亲测有效，速度飞起! 😄) https://medium.com/@juanpaulo/set-blazingly-fast-key-repeats-a87c808ad01d
+
+   # Disable press-and-hold for keys in favor of key repeat
+   defaults write NSGlobalDomain ApplePressAndHoldEnabled -bool false
+   # Set a blazingly fast keyboard repeat rate
+   defaults write NSGlobalDomain KeyRepeat -int 1  # 默认值 2，设置成 1 合适，设置成 0 就太快了，翻页刷新有问题
+   defaults write NSGlobalDomain InitialKeyRepeat -int 10
+
+如何命令行格式化u盘:
+
+.. code-block:: shell
+
+   # https://superuser.com/questions/527657/how-do-you-format-a-2-gb-sd-card-to-fat32-preferably-with-disk-utility
+
+   # 找到你的u 盘
+   diskutil list
+   # 格式化 u 盘，注意 UDISKNAME 必须大写。最后的 /dev/disk2 是上一步找到的 u 盘，千万别写错了
+   sudo diskutil eraseDisk FAT32 UDISKNAME MBRFormat /dev/disk2
+
+增加 time machine 备份速度:
+
+.. code-block:: shell
+
+   # https://huataihuang.gitbooks.io/cloud-atlas/content/develop/mac/time_machine_backup_speed.html
+   sudo sysctl debug.lowpri_throttle_enabled=0 # 关闭限流
+   sudo sysctl debug.lowpri_throttle_enabled=1 # 恢复限流
+
+
+SSH
+-------------
+
+二次验证自动登录跳板机脚本，根据你的密码和服务器配置修改即可。
+
+.. code-block:: python
+
+  #!/bin/sh
+
+  # 有二次验证登录跳板机的时候比较麻烦，可以用这个脚本自动登录跳板机 参考：https://juejin.im/post/5ce760cef265da1b6e657d6f
+  # brew install expect
+  # brew install oath-toolkit
+  # {user} {ip} {yourpassword} {server_qr_token} 替换成对应的 用户名、ip、密码、服务器秘钥 (密码建议定期更换防止安全风险)
+  export LC_CTYPE="en_US.UTF-8"
+  expect -c "
+  spawn ssh user@ip -p22
+  set timeout 3
+  expect  \"user@ip's password:\"
+  set password yourpassword
+  set token \"`oathtool --totp -b -d 6 server_qr_token`\"
+  send \"\$password\$token\r\"
+  interact
+  "
+
+
+Mac 蓝牙耳机(自用索尼 wi1000x)
+---------------------------------------------------------------
+如何给 Macbook 开启 Apt-X 蓝牙音质果更高
+
+- https://www.jianshu.com/p/a1efa561ed9e
+- https://gist.github.com/dvf/3771e58085568559c429d05ccc339219
+
+注意：mac有一个 bug 至今没有修复，cpu 占用高的时候使用蓝牙耳机可能会被莫名其妙修改平衡。声音一边大一边小，去设置-声音里调整一下就好。
+
+`macbook-pro-bluetooth-audio-balance-keeps-changing-by-itself <https://apple.stackexchange.com/questions/280145/macbook-pro-bluetooth-audio-balance-keeps-changing-by-itself>`_
+
+
+Proxy
+---------------------------------------------------------------
+
+mac电脑下设置socks5代理 https://blog.csdn.net/fafa211/article/details/78387899
+
+
+Oh My Zsh
+---------------------------------------------------------------
+.. code-block:: shell
+
+   # Powerlevel9k 是一个强大的 zsh 主题
+   # iTerm2 + Oh My Zsh + Solarized color scheme + Meslo powerline font + [Powerlevel9k] - (macOS)
+   # https://gist.github.com/kevin-smets/8568070
+
+   # https://gist.github.com/dogrocker/1efb8fd9427779c827058f873b94df95
+   # 安装自动补全插件
+   git clone https://github.com/zsh-users/zsh-autosuggestions.git $ZSH_CUSTOM/plugins/zsh-autosuggestions
+   git clone https://github.com/zsh-users/zsh-syntax-highlighting.git $ZSH_CUSTOM/plugins/zsh-syntax-highlighting
+   # nvi ~/.zshrc
+   plugins=(git zsh-autosuggestions zsh-syntax-highlighting)
+
+   # 如何复制上一条命令, https://apple.stackexchange.com/questions/110343/copy-last-command-in-terminal
+   alias lcc='fc -ln -1 | awk "{\$1=\$1}1" ORS="" | pbcopy '
+
+   # 报错：_git:58: _git_commands: function definition file not found
+   # 解决方式：rm ~/.zcompdump*; rm ~/.zplug/zcompdump  # https://github.com/robbyrussell/oh-my-zsh/issues/3996
+   # rm ~/.zcompdump; exec zsh -l  # https://github.com/ohmyzsh/ohmyzsh/issues/3996
+
+Linux(centos/ubuntu)
+---------------------------------------------------------------
+
+.. code-block:: python
+
+    # 查看版本
+    lsb_release -a
+
+    # virtual box虚拟机和windows主机共享目录方法：安装增强工具；win主机设置共享目录例如ubuntu_share；在ubuntu里建立/mnt/share后使用命令：
+
+    sudo mount -t vboxsf ubuntu_share /mnt/share/
+
+    # 映射capslock 为　ctrl
+    setxkbmap -layout us -option ctrl:nocaps
+
+    # 文件字符串批量替换(危险操作最好有 git 版本控制方便回滚)
+    grep oldString -rl /path | xargs sed -i "s/oldString/newString/g"
+
+    # 删除多个文件中包含 patter 的指定行。比如递归删除包含 sleep 的行 (注意这种危险操作最好有 git 版本控制，方便回滚)
+    # https://stackoverflow.com/questions/1182756/remove-line-of-text-from-multiple-files-in-linux
+    find . -name "*.go" -type f | xargs sed -i -e '/sleep/d'
+
+    # 递归删除某一类型文件
+    find . -name "*.bak" -type f -delete
+
+    # 监控某一日志文件变化
+    tail -f t.log
+
+    # 类似mac pbcopy, apt-get install xsel
+    cat README.TXT | xsel
+    cat README.TXT | xsel -b # 如有问题可以试试-b选项
+    xsel < README.TXT
+    # 将readme.txt的文本放入剪贴板
+
+    xsel -c
+    # 清空剪贴板
+
+    # 可以把代码文件贴到paste.ubuntu.com共享，此命令返回一个网址
+    # sudo apt-get install pastebinit; sudo pip install configobj
+    pastebinit -i [filename]
+
+
+    # json格式化输出
+    echo '{"foo": "lorem", "bar": "ipsum"}' | python -m json.tool
+    python -m json.tool my_json.json
+    # 或者apt-get intsall jq
+    jq . <<< '{ "foo": "lorem", "bar": "ipsum"  }'
+
+
+    # 进程相关
+    dmesg | egrep -i -B100 'killed process'   # 查看被杀死进程信息
+    # linux 批量杀掉筛选进程(比如定时脚本多个同时执行，最好限制) https://blog.csdn.net/weiyichenlun/article/details/59108463
+    ps -ef | grep main.py | grep -v grep | awk '{print $2}' | xargs kill -9
+
+    # scp
+    scp someuser@192.168.199.1:/home/someuser/file ./    # 远程机器拷贝到本机
+    scp ./file someuser@192.168.199.1:/home/someuser/    # 拷贝到远程机器
+
+    # tar
+    tar zxvf FileName.tar.gz    # 解压
+    tar zcvf FileName.tar.gz DirName    # 压缩
+
+    # 监控文件变动并且自动增量同步本地文件到服务器对应文件夹（需要先安装 when-changed)
+    when-changed -r -v -1 . rsync -avh --exclude='.git/' --exclude='mydoc/' --exclude='output/' /Users/pegasus/work/code/ XXX@ip:/home/pegasus/work/code/
+
+
+代码搜索用Ag(the silversearcher)/rg, 比ack快
+
+.. code-block:: shell
+
+    # 安装
+    sudo apt-get install silversearcher-ag    # ubuntu
+    brew install ag # mac
+
+    ag string dir/    # search dir
+    ag readme$    # regular expression
+    ag -Q .rb    # Literal Expression Searches, search for the exact pattern。这个选项很有用，特殊字符不用转义了
+    ag string -l    # Listing Files (-l)
+    ag string -i    # Case Insensitive Searches (-i)
+    ag string -G py$    # 搜索应py结尾的文件 (指定文件类型)
+    ag readme -l --ignore-dir=railties/lib    # 忽略文件夹
+    ag readme -l --ignore-dir="*.rb"    # 忽略特性类型文件
+    .agignore    # 用来忽略一些vcs，git等文件。
+
+Centos
+-------------------------------------------------------------
+
+.. code-block:: shell
+
+   # 如何搜索和安装指定版本
+   # https://unix.stackexchange.com/questions/151689/how-can-i-instruct-yum-to-install-a-specific-version-of-package-x
+   yum --showduplicates list golang
+   yum install package-version
+
+crontab
+-------------------------------------------------------------
+分、时、日、月、周
+
+.. code-block:: python
+
+    # 记得bashrc里边
+    EXPORT EDITOR=vim
+    export PYTHONIOENCODING=UTF-8
+
+    # crontab注意：绝对路径；环境变量；
+    0 */5 * * * python -u /root/wechannel/crawler/sougou_wechat/sougou.py >> /root/wechannel/crawler/sougou_wechat/log 2>&1
+    */5 * *  * * /root/pyhome/crawler/lagou/changeip.sh >> /root/pyhome/crawler/lagou/ip.log 2>&1
+
+    # 一个 crontab 表达式工具
+    - https://tooltt.com/crontab/
+    - https://tool.lu/crontab/
+
+
+可以用如下方式执行依赖其他模块的python脚本，用run.sh执行run.py，记得chmod +x可执行权限，运行前执行下sh脚本测试能否成功
+
+.. code-block:: sh
+
+    #!/usr/bin/env bash
+    PREFIX=$(cd "$(dirname "$0")"; pwd)
+    cd $PREFIX
+    source ~/.bashrc
+
+    python -u run.py    # -u 参数强制刷新输出
+    date
+
+
+对于python脚本，可以用如下方式保证同一时间只有一个脚本在运行（一些定时任务同一台机器上多个同时跑可能有问题），可以用
+如下方式限制。（多个机器上应该用分布式锁）
+
+.. code-block:: shell
+
+    #!/usr/bin/env python
+    # -*- coding:utf-8 -*-
+
+    import time
+    # https://stackoverflow.com/questions/380870/make-sure-only-a-single-instance-of-a-program-is-running
+    # 更好的方式使用 tendo
+    # pip install tendo
+    from tendo import singleton
+    me = singleton.SingleInstance() # will sys.exit(-1) if other instance is running
+
+    def main():
+        time.sleep(10)
+        print(time.time())
+
+    if __name__ == '__main__':
+        main()
+
+
+* `《crontab快速参考》 <http://linuxtools-rst.readthedocs.io/zh_CN/latest/tool/crontab.html>`_
+
+
+Iterm2/Terminal
+-------------------------------------------------------------
+
+.. code-block:: sh
+
+   # https://stackoverflow.com/questions/11913990/iterm2-keyboard-shortcut-for-moving-tabs-around
+   # Preferences/Keys 自定义配置使用 Cmd +jk 来在 Iterm2 tab 前后移动，模仿 vim 键位
+
+   # 如何防止 command+w 意外关闭导致工作丢失，这里可以如下设置，每次关闭提醒
+   # Settings -> Profiles -> Session -> Prompt before closing 勾选 Always
+
+   # 如何使用 rz/sz 传文件
+   https://segmentfault.com/a/1190000012166969
+
+   # 如何使用 iterm2 it2copy 从 服务器上用 vim 拷贝文件
+   # https://stackoverflow.com/questions/10694516/vim-copy-mac-over-ssh/10703012
+   1. 安装 iTerm2 Utilities 到服务器。iTerm2 -> Install shell Integratio。后边是 bash or zsh，根据你用的 shell 选择
+    curl -L https://iterm2.com/shell_integration/install_shell_integration_and_utilities.sh | zsh
+   2. 重新登录之后 it2copy 生效
+   3. 在 vim visual 模式选择之后 执行 `:w !it2copy` 即可。或这直接 cat file.txt | it2copy
+
+   # 终端输出乱序。有时候有一些脚本或者软件可能会修改终端配置但是失败后又没有恢复，导致输出乱序，解决如下
+   `stty sane` 或者 `reset`
+
+
+Tmux
+-------------------------------------------------------------
+
+.. code-block:: sh
+
+   # 建议直接用 https://github.com/gpakosz/.tmux 这个强大的 tmux 配置(oh-my-tmux)
+   # 不过注意，如果一开始 tmux.conf.local 里的命令执行失败（比如curl 网络失败）可能导致插件加载失败，注意排查
+
+   # https://wiki.archlinux.org/index.php/tmux
+   tmux rename -t oriname newname
+   tmux att -t name -d               # -d 不同窗口全屏
+
+   # 如果手贱在本机tmux里又ssh到服务器又进入服务器的tmux怎么办(退出 tmux 套娃)
+   c-b c-b d
+
+   # 如果升级了 tmux 之后，使用 tmux 出现 tmux server exited unexpectedly 尝试删除 /tmp 里的 tmux 临时文件
+   # https://github.com/tmux/tmux/issues/2376
+
+   # 技巧：tmux 如何在多个 pane 执行一样的命令。先执行 ctrl + b :
+   ctrl + b :
+   setw synchronize-panes on
+   setw synchronize-panes off
+
+   # Vim style pane selection
+   bind -n C-h select-pane -L
+   bind -n C-j select-pane -D
+   bind -n C-k select-pane -U
+   bind -n C-l select-pane -R
+
+   # https://stackoverflow.com/questions/22138211/how-do-i-disconnect-all-other-users-in-tmux
+   tmux a -dt <session-name>
+
+   # 如何 ssh 后自动 attach 到某个 session。编辑你的 .bashrc or .zshrc
+   if [[ "$TMUX" == "" ]] && [[ "$SSH_CONNECTION" != "" ]]; then
+       # Attempt to discover a detached session and attach it, else create a new session
+       WHOAMI="lens"   # attach 的 session 名称
+       if tmux has-session -t $WHOAMI 2>/dev/null; then
+           tmux -2 attach-session -t $WHOAMI
+       else
+           tmux -2 new-session -s $WHOAMI
+       fi
+   fi
+   # 或者
+   if [[ -z "$TMUX" ]] && [ "$SSH_CONNECTION" != "" ]; then
+       SESSION_NAME="sessionname"
+       tmux attach-session -t $SESSION_NAME || tmux new-session -s $SESSION_NAME
+   fi
+
+   # 问题：Tmux resurrect file not found! 。新版本应该是放到了 ~/.local/share/tmux/resurrect 。老版本在~/.tmux/resurrect
+   function tmux-resurrect-reset-last() {
+       cd ~/.local/share/tmux/resurrect && \
+           ln -f -s $(/bin/ls -t tmux_resurrect_*.txt | head -n 1) last && \
+           /bin/ls -l last
+   }
+
+   # use prefix + m zoom and unzoom panes. https://tao-of-tmux.readthedocs.io/en/latest/manuscript/07-pane.html
+   bind-key -T prefix m resize-pane -Z
+
+
+SSH
+-------------------------------------------------------------
+
+.. code-block:: python
+
+   # https://superuser.com/questions/98562/way-to-avoid-ssh-connection-timeout-freezing-of-gnome-terminal/98565#98565
+   Press Enter, ~, . one after the other to disconnect from a frozen session.
+   # https://unix.stackexchange.com/questions/176547/copy-only-file-details-file-name-size-time-from-remote-machine-in-unix
+   ssh remotemachine  "ls -l /opt/apache../webapps/Context"
+   # 使用 paramiko  库可以实现 ssh client 功能
+   # https://www.digitalocean.com/community/tutorials/how-to-use-fabric-to-automate-administration-tasks-and-deployments
+
+
+Fabric
+-------------------------------------------------------------
+可以用 Fabric 实现一些自动化控制服务器功能。示例 fabfile.py
+
+.. code-block:: python
+
+  # -*- coding: utf-8 -*-
+  import os
+  from fabric.api import run, env, get, local
+
+  """
+  需求：经常忘记开发机 build 完go 二进制文件以后 scp 到本地，导致有时候部署还是老的二进制文件。
+
+  功能：
+  实现监听开发机的二进制文件变动，每一次和本地文件对比，如果有开发机二进制文件大小变了，就拷贝到本地来。
+
+  # pip install fabric==1.14.0
+  # brew install watch
+  mac 下用 watch 用来定期执行命令 watch -n 60 ls
+
+  比如每分钟检查一下开发机上的 FaceFusionServer 是否重新 build 了，然后拉取到本地，可以执行
+  watch -n 30 fab monitor_facefusion_server monitor_uploadserver
+
+  1. http://www.bjhee.com/fabric.html
+  """
+
+  class Bcolors:
+      HEADER = '\033[95m'
+      OKBLUE = '\033[94m'
+      OKGREEN = '\033[92m'
+      WARNING = '\033[93m'
+      FAIL = '\033[91m'
+      ENDC = '\033[0m'
+      BOLD = '\033[1m'
+      UNDERLINE = '\033[4m'
+
+
+  env.hosts = ['dev']
+  env.use_ssh_config = True
+  env.password = ""
+
+
+  def who():
+       run('whoami')
+
+
+  def is_change(remote_path, local_path):
+       """ 根据 md5 判断是否变化，注意 centos 和 mac 命令和结果格式不同
+       centos:
+       md5sum UploadServer
+       e4fccc07eafc7ef97d436c50546e352b  UploadServer
+
+       mac:
+       md5 UploadServer
+       MD5 (UploadServer) = e4fccc07eafc7ef97d436c50546e352b
+
+       :param remote_path: absolute remote server path
+       :param local_path: local path
+       """
+       output = run("md5sum {}".format(remote_path))  # 请保证路径存在，不会判断
+       remote_md5 = output.split()[0].strip()
+       if not os.path.exists(local_path):  # 第一次本地没有文件直接拉取
+           return True
+       local_output = local("md5 {}".format(local_path), capture=True)
+       local_md5 = local_output.split()[-1].strip()
+       return remote_md5 != local_md5
+
+
+  def monitor_uploadserver():
+       remote_path = "/user/work/UploadServer"
+       local_path = "./UploadServer"
+       if is_change(remote_path, local_path):  # 变化了就复制到本地 get(remote, local)，存在会覆盖
+           print(Bcolors.WARNING + "===========%s file changed=========" + Bcolors.ENDC)
+           get(remote_path, local_path)
+           local("chmod +x {}".format(local_path))
+       else:
+           print(Bcolors.HEADER + local_path + " not change" + Bcolors.ENDC)
+
+
+Makefile
+-------------------------------------------------------------
+
+.. code-block:: sh
+
+   # 如何设置子进程环境变量 https://stackoverflow.com/questions/23843106/how-to-set-child-process-environment-variable-in-makefile
+   test: export NODE_ENV = test
+
+Git
+-------------------------------------------------------------
+
+.. code-block:: python
+
+    # .gitconfig配置用如下配置可以使用pycharm的diff和merge工具（已经安装pycharm）
+    [diff]
+        tool = pycharm
+    [difftool "pycharm"]
+        cmd = /usr/local/bin/charm diff "$LOCAL" "$REMOTE" && echo "Press enter to continue..." && read
+    [merge]
+        tool = pycharm
+        keepBackup = false
+    [mergetool "pycharm"]
+        cmd = /usr/local/bin/charm merge "$LOCAL" "$REMOTE" "$BASE" "$MERGED"
+
+    # https://stackoverflow.com/questions/34549040/git-not-displaying-unicode-file-names
+    # git 显示中文文件名，如果你的文件名有中文会好看很多
+    git config --global core.quotePath false
+
+    # 用来review：
+    git log --since=1.days --committer=PegasusWang --author=PegasusWang
+    git log --since="6am" # 查看当天提交
+    git diff commit1 commit2
+
+    # 冲突以后使用远端的版本： NOTE：注意在 git merge 和 git rebase 中 ours/theirs 含义相反!
+    # rebase 场景下，theirs 实际表示的是当前分之
+    # merge 场景下相反，theirs 表示的是远端分之
+    # https://stackoverflow.com/questions/16825849/choose-git-merge-strategy-for-specific-files-ours-mine-theirs
+    # https://bitmingw.com/2017/02/16/git-merge-rebase-ours-and-theirs/
+    git checkout --theirs templates/efmp/campaign.mako
+
+    # 防止http协议每次都要输入密码：
+    git config --global credential.helper 'cache --timeout=36000000'      #秒数
+
+    # 暂存和恢复，当我们需要切分支又暂时不想 git add，可以先把目前的修改暂存起来
+    git stash # 暂存当前的修改
+    git stash apply
+    git stash apply stash@{1}
+    git stash pop # 重新应用储藏并且从堆栈中移走
+    # 显示 git stash 内容 https://stackoverflow.com/questions/7677736/git-diff-against-a-stash
+    git stash list # 展示当前的所有 stash 列表
+    git stash show -p  # see the most recent stash
+    git stash show -p stash@{1}
+
+    # 删除远程分之
+    git push origin --delete {the_remote_branch}
+
+    # 手残 add 完以后输入错了 commit 信息
+    git commit --amend
+    # 修改文件内容并合并到上一次的commit变更当中。比如发现没有修改完全，又改了代码，可以修改之后 add 然后执行
+    git commit --amend --no-edit
+    # 类似的还可以修改上一个提交者的名字 https://stackoverflow.com/questions/750172/how-to-change-the-author-and-committer-name-and-e-mail-of-multiple-commits-in-gi
+    git config --global user.name "you name"
+    git config --global user.email you@domain.com
+    git commit --amend --reset-author
+    # 如果想要修改多个历史提交的 commit 信息，可以使用 git rebase -i 交互式修改。对应的提交行使用 reword 就可以
+
+    # 撤销 add （暂存），此时还没有 commit。比如 add 了不该 add 的文件
+    git reset -- file
+    git reset # 撤销所有的 add
+
+    # 撤销修改
+    git checkout -- file
+
+    # 手残pull错了分支就(pull是先fetch然后merge)。或者 revert 一个失误的 merge
+    git reset --hard HEAD~
+    # 如果 pull 产生了 冲突，可以撤销。
+    git merge --abort
+    # git rebase 同样可以
+    git rebase --abort
+
+    # How to revert Git repository to a previous commit?, https://stackoverflow.com/questions/4114095/how-to-revert-git-repository-to-a-previous-commit
+    git reset --hard 0d1d7fc32
+
+    # 手残直接在master分之改了并且add了
+    git reset --soft HEAD^
+    git branch new_branch # 切到一个新分支去 commit
+    git checkout new_branch
+    git commit -a -m "..."
+    # 或者
+    git reset --soft HEAD^
+    git stash
+    git checkout new_branch
+    git stash pop
+
+    # 如果改了master但是没有add比较简单，三步走
+    git stash
+    git checkout -b new_branch
+    git stash pop
+
+    # rename branch
+    git branch -m <oldname> <newname>
+    git branch -m <newname> # rename the current branch
+
+    # 指定文件类型diff
+    git diff master -- '*.c' '*.h'
+    # 带有上下文的diff
+    git diff master --no-prefix -U999
+
+    # undo add
+    git reset <file>
+    git reset    # undo all
+    # undo git reset https://stackoverflow.com/questions/2510276/how-to-undo-git-reset
+    git reset 'HEAD@{1}'
+
+    # 查看add后的diff
+    git diff --staged
+
+    # http://weizhifeng.net/git-rebase.html
+    # rebase改变历史, 永远不要用在master分之，别人有可能使用你的分之时也不要用
+    # only change history for commits that have not yet been pushed
+    # master has changed since I stared my feature branch, and I want bo bring my branch up to date with master. - Dont't merge. rebase
+    # rebase: finds the merge base; cherry-picks all commits; reassigns the branch pointer.
+    # then git push -f
+    # git rebase --abort
+
+    # 全局 ignore, 对于不同编辑器协作的人比较有用，或者用来单独忽略一些自己建立的测试文件等。
+    # NOTE: git 支持每个子文件夹下有一个自己的 .gitignore，文件路径也是相对当前文件夹
+    git config --global core.excludesfile ~/.gitignore_global  # 全局忽略一些文件
+
+    # 拉取别人远程分支，在 .git/config 里配置好
+    git fetch somebody somebranch
+    git checkout -b somebranch origin/somebranch
+
+    # prune all the dead branches from all the remotes
+    # https://stackoverflow.com/questions/17933401/how-do-i-remove-deleted-branch-names-from-autocomplete?utm_medium=organic&utm_source=google_rich_qa&utm_campaign=google_rich_qa
+    git fetch --prune --all # 清理本地本删除的远程分之，补全的时候很干净，没有已经删除的分之
+
+    # https://stackoverflow.com/questions/1274057/how-to-make-git-forget-about-a-file-that-was-tracked-but-is-now-in-gitignore
+    # https://wildlyinaccurate.com/git-ignore-changes-in-already-tracked-files/
+    # 如果一个文件已经被 git 跟踪但是你之后又不想提交针对它的修改了，可以这么做（比如我想修改一些配置，本地 debug 等）
+    git update-index --assume-unchanged <file>    # 忽略一个已经 tracked 的文件，修改后不会被 commit
+    git update-index --no-assume-unchanged <file>   # undo 上一步
+    # 那如何列出这些文件呢？ https://stackoverflow.com/questions/2363197/can-i-get-a-list-of-files-marked-assume-unchanged
+    git ls-files -v | grep '^[[:lower:]]'
+
+    # https://stackoverflow.com/questions/48341920/git-branch-command-behaves-like-less
+    # 禁止 git brach 的时候使用交互式
+    git config --global pager.branch false
+
+    # git rm file and add, https://stackoverflow.com/questions/9591407/unstage-a-deleted-file-in-git/9591612
+    # this restores the file status in the index
+    git reset -- <file>
+    # then check out a copy from the index
+    git checkout -- <file>
+
+    # 如何恢复一个已经删除的分之, https://stackoverflow.com/questions/3640764/can-i-recover-a-branch-after-its-deletion-in-git
+    git reflog  # 查找对应 commit hash
+    git checkout -b branch-name hash
+
+    # git diff 代码显示 tab 为 4 个空格，比如看 go 代码的时候，git diff 显示 8 个
+    # https://stackoverflow.com/questions/10581093/setting-tabwidth-to-4-in-git-show-git-diff
+    git config --global core.pager 'less -x1,5'
+
+    # git 如何使用不同的 committer，除了每个项目和全局可以设置 gitconfig 里的 user 外，可以使用如下方式
+    # https://stackoverflow.com/questions/4220416/can-i-specify-multiple-users-for-myself-in-gitconfig
+    # global config ~/.gitconfig
+    [user]
+        name = John Doe
+        email = john@doe.tld
+
+    [includeIf "gitdir:~/work/"]
+        path = ~/work/.gitconfig
+
+    # ~/work/.gitconfig
+    [user]
+        email = john.doe@company.tld
+
+    # 从提交历史搜索字符串，比如提交历史中引入了一个新的函数，可以通过这个方式搜索
+    # https://stackoverflow.com/questions/5816134/how-to-find-the-git-commit-that-introduced-a-string-in-any-branch
+    git log -S 'hello world' --source --all
+
+    # 统计xx某某提交了多少代码
+    git log --author="xxx" --pretty=tformat: --numstat | awk '{ add += $1; subs += $2; loc += $1 - $2 } END { printf "added lines: %s, removed lines: %s, total lines: %s\n", add, subs, loc }'
+
+    # 修改上一次提交人。比如一开始 git commiter 配置错了。https://stackoverflow.com/questions/3042437/how-to-change-the-commit-author-for-one-specific-commit
+    git commit --amend --author="Author Name <email@address.com>" --no-edit
+
+    # tags 功能(比如从一个源码的 tag 构建) https://stackoverflow.com/questions/35979642/what-is-git-tag-how-to-create-tags-how-to-checkout-git-remote-tags
+    git tag -l  # 显示所有 tag
+    git checkout tags/<tag> -b <branch>
+
+    # 生成和应用 patch, https://stackoverflow.com/questions/28192623/create-patch-or-diff-file-from-git-repository-and-apply-it-to-another-different
+    git diff tag1..tag2 > mypatch.patch
+    git apply mypatch.patch
+
+    # 删除当前文件夹 git 未跟踪的文件 https://stackoverflow.com/questions/61212/how-do-i-remove-local-untracked-files-from-the-current-git-working-tree
+    git clean -f # 注意危险操作提前确认文件确实可以删除
+
+
+Git 删除大文件
+----------------------------
+Git 只适合代码文本管理，如果误提交了一个二进制文件或者视频等文件将会导致 git 仓库变得非常大。
+你应该在首次提交代码的时候就是用 gitignore 忽略掉大文件，如果已经误提交了，直接删除文件并不会删除 git 历史中的记录。
+需要使用专门的工具来进行删除，目前官方推荐使用 git-filter-repo 了，不再推荐使用 git filter-branch。
+
+.. code-block:: shell
+
+    # ⭐️ git 如何删除提交历史中的大文件(比如很多新手误提交了一个二进制或者视频等大文件)
+    # git 注意不要把二进制大文件，视频文件等放入到版本库，可能会导致 .git 非常大，删了也无济于事
+    find . -executable -type f >>.gitignore # https://stackoverflow.com/questions/5711120/gitignore-without-binary-files
+
+    # git 历史删除大文件。如果你提交了大文件，即使你git rm删除了也会留在 git 的历史记录中，导致.git 文件夹很大
+    # https://stackoverflow.com/questions/8083282/how-do-i-remove-a-big-file-wrongly-committed-in-git
+    # git filter-branch --index-filter "git rm -rf --cached --ignore-unmatch path_to_file" HEAD
+
+    # ⭐️ 推荐使用 git-filter-repo
+    # https://stackoverflow.com/questions/2100907/how-to-remove-delete-a-large-file-from-commit-history-in-the-git-repository
+    # https://www.jianshu.com/p/03bf1bc1b543
+    pip install git-filter-repo # 安装
+    git filter-repo --invert-paths --path-match YOUR_BIG_FILE  # 从提交历史删除大文件
+    git push -f origin master # 因为修改了提交历史，可能需要临时放开一下 master 权限，强行 push 一次
+
+
+Git工作流
+------------
+
+.. code-block:: shell
+
+   git checkout master    # 切到master
+   git pull origin master     # 拉取更新
+   git checkout -b newbranch    # 新建分之，名称最好起个有意义的，比如jira号等
+
+   # 开发中。。。
+   git fetch origin master    # fetch master
+   git rebase origin/master    #
+
+   # 开发完成等待合并到master，推荐使用 rebase 保持线性的提交历史，但是记住不要在公众分之搞，如果有无意义的提交也可以用 rebase -i 压缩提交
+   git rebase -i origin/master
+   git checkout master
+   git merge newbranch
+   git push origin master
+
+   # 压缩提交
+   git rebase -i HEAD~~    # 最近两次提交
+
+
+Git hook
+------------
+比如我们要在每次 commit 之前运行下单测，进入项目的 .git/hooks 目录， "cp pre-commit.sample pre-commit" 修改内容如下:
+
+.. code-block:: bash
+
+    #!/bin/sh
+
+    if git rev-parse --verify HEAD >/dev/null 2>&1
+    then
+        against=HEAD
+    else
+        # Initial commit: diff against an empty tree object
+        against=4b825dc642cb6eb9a060e54bf8d69288fbee4904
+    fi
+
+    # Redirect output to stderr.
+    exec 1>&2
+
+    if /your/path/bin/test:    # 这里添加需要运行的测试脚本
+    then
+        exit 0
+    else
+        exit 1
+    fi
+
+    # If there are whitespace errors, print the offending file names and fail.
+    exec git diff-index --check --cached $against --
+
+
+Gitub
+------------
+克隆 Github 仓库时遇到报措 kex_exchange_identification: Connection closed by remote host。执行 ``ssh -T git@github.com``
+kex_exchange_identification: Connection closed by remote host。 可能是因为某些🪜封禁了 github 端口 22 的连接。修改端口:
+
+.. code-block:: bash
+
+  Host github.com
+      HostName ssh.github.com
+      User git
+      Port 443
+
+
+vim/neovim
+-------------
+
+.. code-block:: vim
+
+    " http://stackoverflow.com/questions/9104706/how-can-i-convert-spaces-to-tabs-in-vim-or-linux
+   :set tabstop=2      " To match the sample file
+   :set noexpandtab    " Use tabs, not spaces
+   :%retab!            " Retabulate the whole file，替换tab为空格
+   map <F4> :%retab! <CR> :w <CR> " 映射一个命令
+
+   "https://www.google.com/url?sa=t&rct=j&q=&esrc=s&source=web&cd=1&cad=rja&uact=8&ved=0ahUKEwjF6JzH8aTRAhXiqVQKHUQBDcIQFggcMAA&url=http%3A%2F%2Fstackoverflow.com%2Fquestions%2F71323%2Fhow-to-replace-a-character-by-a-newline-in-vim&usg=AFQjCNGer9onNl_RExCUdE75ctTvVx8WGA&sig2=WrcRh9RFNvN6bUZoHpJvDg
+   "vim替换成换行符使用\r不是\n
+   " 多行加上引号 http://stackoverflow.com/questions/9055998/vim-add-tag-to-multiple-lines-with-surround-vim"
+   :1,3norm yss"
+
+   # Git 插件
+   Plugin 'tpope/vim-fugitive' # 在 vim 里执行 :Gblame 可以看到当前文件每行代码的提交人和日期，找人背锅或者咨询的神器
+
+   # 直接在 vim 里 diff 文件，比如打开了两个文件
+   :windo diffthis
+   :diffoff!
+
+   # 解决mac vim 中文输入法的问题
+   # https://www.jianshu.com/p/4d81b7e32bff
+   # https://zhuanlan.zhihu.com/p/23939198
+   brew tap daipeihust/tap && brew install im-select # 终端下执行此命令安装 im-select
+   # 然后 vim 配置加入这一行 (ABC 是默认输入法，直接输入 im-select 可以看到你的默认值)
+   `autocmd InsertLeave * :silent !/usr/local/bin/im-select com.apple.keylayout.ABC`
+
+
+   # 如果跳转到跳转之前的位置, https://vi.stackexchange.com/questions/2001/how-do-i-jump-to-the-location-of-my-last-edit
+   # 使用场景：比如在当前函数里使用了logging，发现logging import，我会跳转到文件头去 import logging，编辑完后进入normal模式使用  `` 就可以跳转到之前编辑位置
+   `` which will bring you back to where the cursor was before you made your last jump. See :help `` for more information.
+
+   # 如何编辑远程服务器文件, https://superuser.com/questions/403664/how-can-i-copy-and-paste-text-out-of-a-remote-vim-to-a-local-vim
+   :e scp://user@host/relative/path/from/home.txt
+
+   # 跳转
+   g<c-]> # list all match tag
+
+   # 跳转到上一个 insert 的位置，经常用在修改之后跳转到之前的编辑位置 https://vi.stackexchange.com/questions/2001/how-do-i-jump-to-the-location-of-my-last-edit
+   `^ 或者 '^
+
+   # vim 替换不间断空格，illegal character U+00A0异常解决。https://www.jianshu.com/p/5f9992e5cd47
+   :%s/\%u00a0/ /g
+
+   # set transparent，设置透明，如果iterm2 设置了背景图可以看到
+   :hi normal guibg=000000
+
+   # vim 去掉 ^M 字符（这个字符用 type CTRL-V, then CTRL-M. 打出来）。
+   # 或者 brew isntall dos2unix，然后 dos2unix filename
+   :s/^M$//
+
+
+   # vim 鼠标拖移窗口大小。设置鼠标支持即可。如果不生效：Iterm2->Profiles->Terminal->"Enable mouse reporting" 勾一下
+   # 参考：https://stackoverflow.com/questions/62582721/how-to-fix-restore-mouse-controls-in-tmux-on-iterm2
+   :set mouse=a
+
+   # 如何全局替换多个文件的字符串。使用 far.vim 或者如果安装了 coc.nvim 可以使用 CocSearch 命令
+   :Far foo bar **/*.py
+
+   # iterm2 下 set mouse=a 之后vim/neovim不生效，无法鼠标拖动窗口大小
+   Settings -> Profiles -> Terminal -> Enable mouse reporting
+
+
+* `《vim cheet sheet》 <https://vim.rtorr.com/lang/zh_cn/>`_
+
+
+vim-go/coc.nvim plugin Tips
+-----------------------------------------
+
+.. code-block:: vim
+
+  # 最近一直在开发机服务器上直接用 neovim+vim-go+coc.nvim 写 golang，具有完备开发功能(vim-go借助各种go工具实现)
+  # https://github.com/fatih/vim-go
+  # https://github.com/fatih/vim-go-tutorial  # vim-go 官方教程，最好过一遍
+  let g:go_def_mode='godef'  # 有时候 gopls 有问题可以用 godef 跳转，默认用 gopls
+
+  # 如何生成 interface 接口定义
+  type S struct{}   # cursor 放在 S 上执行 :GoImpl io.Reader
+
+  # 跳转到接口的实现 https://github.com/fatih/vim-go/issues/820
+  :GoDef (或ctrl+]) 跳转到定义，但是如果是接口实现只能跳转到 interface 定义而非 struct 实现。
+  :GoCallees 从函数调用处跳转到接口的真正实现，而不是接口定义 (在方法调用点使用 -> struct 方法实现列表)
+  :GoCallers 找到当前函数被调用的地点 (caller 主调， callee 被调)
+  :GoImplements 获取一个接口方法的所有实现列表。(interface method -> implement method list)
+
+  # 常用的方便命令(命令模式Tab补全), 参考 https://github.com/fatih/vim-go/blob/master/doc/vim-go.txt
+  :GoFmt 格式化，你可以配置 vim-go 直接保存自动执行格式化或者直接执行 GoImports
+  :GoRun, GoTest, GoTestFunc 运行代码和单测
+  :GoMetaLinter 执行 lint，可以配置 .gometalinter.json 忽略一些 lint 错误。https://github.com/PegasusWang/linux_config/blob/master/golang/gometalinter.json
+  :GoRename 快速重构
+  :GoImpl 为 struct 生成接口函数定义(光标放到struct定义上使用)。如果一个 interface 有很多需要实现的函数，比较方便
+  :GoAddTags GoRemoveTags json 快速给 struct field 增加 json tag，支持 visual 模式多选。默认 tag 名是下划线命名
+  :GoKeyify 把无名称初始化的 struct literals 转成包含字段名的初始化方式
+  :GoIfErr 生成 if err 返回值(或者用 snippets)
+  :GoChannelPeers 寻找可能的 channel 发送和接收点
+  :GoFillStruct 给一个 struct 填充默认值
+
+  # 甚至还可以让超过 120 行的代码自动折行，需要安装 https://github.com/segmentio/golines
+  # golines -w -m 120 red_dot.go  # 直接命令行格式化，gofmt 没有长行的折行功能
+  # 在 vim 中使用 golines
+  let g:go_fmt_command = "golines"
+  let g:go_fmt_options = {
+    \ 'golines': '-m 120',
+    \ }
+
+
+  " 以下是 coc.nvim 官方示例定义的快捷键。用好这几个快捷键可以给开发带来极大便利
+  " 跳转到变量定义。normal 模式下在一个变量名上按一下 gd 即可跳转到定义位置，然后ctrl-o 可以快速返回原位置
+  nmap <silent> gd <Plug>(coc-definition)
+  " 跳转到值的类型定义，或者跳转到函数的返回值类型。在你想要快速查找一个类型的结构的时候非常有用
+  nmap <silent> gy <Plug>(coc-type-definition)
+  " 跳转到 interface 接口的对应实现。比如查看go里一个 interface 被哪些 struct 实现了。如果在 struct 名字上使用可以找到当前 struct 实现了哪些 interface
+  nmap <silent> gi <Plug>(coc-implementation)
+  " 打开当前变量、函数等被引用的列表。比如看一个 函数 在哪些地方使用了
+  nmap <silent> gr <Plug>(coc-references)
+
+
+用markdown文件制作html ppt
+-------------------------------------------------------------
+
+.. code-block:: python
+
+   apt-add-repository ppa:brightbox/ruby-ng
+   apt-get update
+   apt-get install ruby2.2
+   gem install slideshow
+   slideshow install deck.js
+   sudo  pip install https://github.com/joh/when-changed/archive/master.zip
+   when-changed rest.md slideshow  build rest.md -t deck.js
+
+   # mac: brew install fswatch, http://stackoverflow.com/questions/1515730/is-there-a-command-like-watch-or-inotifywait-on-the-mac
+   jfswatch -o ~/path/to/watch | xargs -n1 ~/script/to/run/when/files/change.sh
+   fswatch -o ./*.py  | xargs -n1  ./runtest.sh    # 比如写单元测试的时候修改后就让测试执行
+
+   # 也可以使用下边的工具用 Jupyter 做 slideshow，最大的特点是直接在浏览器里敲代码交互演示
+   # Reveal.js - Jupyter/IPython Slideshow Extension, also known as live_reveal
+   # https://github.com/damianavila/RISE
+
+   # 更推荐使用 reveal-md
+   reveal-md slides.md -w
+
+
+PPT 技巧
+-------------------------------------------------------------
+
+.. code-block:: shell
+
+   # 如何粘贴代码到 PPT 里边: 转成 rtf。直接粘贴没有代码高亮，转成 rtf 格式就可以了
+   # https://superuser.com/questions/85948/how-can-i-embed-programming-source-code-in-powerpoint-slide-and-keep-code-highli
+   # pip install Pygments
+   pygmentize -f rtf code.py | pbcopy
+   # 粘贴到 ppt 之后需要选择 “保留源格式”，这样代码才有高亮
+
+
+Benchmark
+-------------------------------------------------------------
+
+.. code-block:: shell
+
+    sudo apt-get install apache2-utils
+    ab -c 并发数量 -n 总数量 url
+
+
+Ffmpeg && youbute-dl
+-------------------------------------------------------------
+
+.. code-block:: shell
+
+   # brew install youtube-dl
+   # https://askubuntu.com/questions/486297/how-to-select-video-quality-from-youtube-dl
+   # http://www.cnblogs.com/faunjoe88/p/7810427.html
+   # 下载视频，支持油管、b 站等
+   youtube-dl -F "http://www.youtube.com/watch?v=P9pzm5b6FFY"
+   youtube-dl -f 22 "http://www.youtube.com/watch?v=P9pzm5b6FFY"
+   youtube-dl -f bestvideo+bestaudio "http://www.youtube.com/watch?v=P9pzm5b6FFY"
+
+   # 目前youtube-dl 貌似不更新了，用 yt-dlp 代替
+   python3 -m pip install --force-reinstall https://github.com/yt-dlp/yt-dlp/archive/master.tar.gz
+   # 比如映射了俩命令下载 MP3 或者 mp4
+   alias download_youtube_mp3='yt-dlp --cookies-from-browser chrome  -x --audio-format mp3'
+   alias download_youtube='yt-dlp --cookies-from-browser chrome -f "bestvideo[ext=mp4]+bestaudio[ext=m4a]" '
+
+   # 转换格式，比如 flv -> mp4 https://superuser.com/questions/624565/ffmpeg-convert-flv-to-mp4-without-losing-quality
+   ffmpeg -i input.flv -codec copy output.mp4
+
+   # 截取视频
+   ffmpeg -i input.mp4 -ss 00:01:00 -to 00:02:00 -c copy output.mp4
+   # https://gist.github.com/PegasusWang/11b9203ffa699cd8f07e29559cc4d055
+   # 截图
+   ffmpeg -ss 00:10:00 -i "Apache Sqoop Tutorial.mp4" -y -f image2 -vframes 1 test.png
+
+   # 提取音频mp3, https://stackoverflow.com/questions/9913032/ffmpeg-to-extract-audio-from-video
+   ffmpeg -i sample.avi -q:a 0 -map a sample.mp3
+
+   # 连接视频
+   $ cat input.txt
+   file '/path/to/file1'
+   file '/path/to/file2'
+   file '/path/to/file3'
+   # 注意用 -safe 0
+   ffmpeg -f concat -safe 0 -i input.txt -c copy output.mp4
+
+   # youtube-dl 下载音频: https://askubuntu.com/questions/178481/how-to-download-an-mp3-track-from-a-youtube-video
+   youtube-dl --extract-audio --audio-format mp3 <video URL>
+   # use socks5 proxy
+   youtube-dl --proxy 'socks5://127.0.0.1:1080' [URL]
+
+   # use aria2 # https://blog.51cto.com/14046599/2348642
+   # brew install aria2
+   youtube-dl https://www.youtube.com/watch?v=zAJUeZ0SNp8 --external-downloader aria2c --external-downloader-args "-x 16 -k 1M"
+
+   # 音频 mp3 处理
+   ffmpeg -i input.mp3 -ss 00:00:00 -t 00:03:00 -acodec copy output.mp3  # 截取mp3
+   ffmpeg -i audio.wav -acodec libmp3lame audio.mp3 # wav to mp3
+
+
+.. code-block:: python
+
+   # 脚本下载 youtube 视频
+   #!/usr/bin/env python
+   # -*- coding:utf-8 -*-
+
+   # pip install youtube_dl，如果报错尝试升级
+   # pip install --upgrade youtube_dl
+
+   """
+   目前 youtube_dl 不再更新，可以用 yt-dlp 代替
+   python3 -m pip install --force-reinstall https://github.com/yt-dlp/yt-dlp/archive/master.tar.gz
+   import yt_dlp as youtube_dl
+   """
+
+   from __future__ import unicode_literals
+   import youtube_dl
+
+
+   class MyLogger(object):
+       def debug(self, msg):
+           pass
+
+       def warning(self, msg):
+           pass
+
+       def error(self, msg):
+           print(msg)
+
+
+   def my_hook(d):
+       if d['status'] == 'finished':
+           print('Done downloading, now converting ...')
+
+
+   ydl_opts = {
+       'format': 'bestaudio/best',
+       'postprocessors': [{
+           'key': 'FFmpegExtractAudio',
+           'preferredcodec': 'mp3',
+           'preferredquality': '192',
+       }],
+       'logger': MyLogger(),
+       'progress_hooks': [my_hook],
+   }
+   with youtube_dl.YoutubeDL(ydl_opts) as ydl:
+       url = 'https://www.youtube.com/watch?v=48VSP-atSeI'
+       ydl.download([url])
+
+Vlog 如何增加字幕
+-------------------------------------------------------------
+
+- https://github.com/BingLingGroup/autosub/blob/dev/docs/README.zh-Hans.md#%E8%AF%AD%E9%9F%B3%E8%BD%AC%E6%96%87%E5%AD%97%E7%BF%BB%E8%AF%91api%E8%AF%B7%E6%B1%82
+- https://www.zhihu.com/question/24717723/answer/290003526
+
+.. code-block:: shell
+
+   # 首先安装 autosub。先安装 brew install ffmpeg
+   pip3 install git+https://github.com/BingLingGroup/autosub.git@alpha ffmpeg-normalize
+   # 使用方式。最后生成 srt 文件，名字为 视频.zh-cn.rst
+   autosub -S zh-cn -D zh-cn -i 视频.mp4
+   # 之后可以使用软件比如 ArcTime 或者之类的软件可以导入并生成新的视频。
+   # 使用 ffmpeg 也可以增加字幕并输出到新的 mp4
+   ffmpeg -i 视频.mp4 -vf subtitles=视频.zh-cn.srt output.mp4
+
+Curl
+-------------------------------------------------------------
+
+.. code-block:: shell
+
+   # 记录 curl 过程, https://askubuntu.com/questions/944788/how-does-curl-print-to-terminal-while-piping
+   # 注意，如果 url 地址里边有 & 符号记得 url 两边加上双引号
+   curl -v http://httpbin.org/headers > t.txt 2>&1
+
+
+* `《Linux工具快速教程》 <https://linuxtools-rst.readthedocs.io/zh_CN/latest/>`_
+* `《slide show》 <http://slideshow-s9.github.io/>`_
+* `《markdown sheet》 <http://commonmark.org/help/>`_
+* `《CONQUERING THE COMMAND LINE》 <http://conqueringthecommandline.com/book/>`_
+
+Pandoc 转换文档格式
+-------------------------------------------------------------
+
+.. code-block:: shell
+
+  # https://pandoc.org/demos.html
+  pandoc -s -o about.md about.rst
+  # markdown 转成 word
+  pandoc -o output.docx -f markdown -t docx filename.md
+  # rst -> github markdown
+  pandoc file.rst -f rst -t gfm -o filename.md
+  # markdown -> epub
+  pandoc xxx.md -o xxx.epub
+
+Calibre 电子书管理工具
+-------------------------------------------------------------
+
+.. code-block:: shell
+
+  # https://www.jianshu.com/p/0bcb92509309
+  # https://snowdreams1006.github.io/myGitbook/advance/export.html
+  # 通过 calibre 提供的二进制工具抓取并且生成电子书
+  ebook-convert draveness.recipe draveness.mobi --output-profile kindle
+
+  # 如何合并多个epub文件
+  # https://www.mobileread.com/forums/showthread.php?t=121691
+  # https://github.com/JimmXinu/EpubMerge/blob/main/epubmerge.py
+  python3 epubmerge.py -o all.epub a.epub b.epub c.epub
+
+
+Gitbook
+-------------------------------------------------------------
+
+.. code-block:: shell
+
+  # gitbook 本地生成电子书 pdf（依赖 calibare 的 ebook-convert)
+  npm install -g gitbook
+  npm install -g gitbook-cli
+  # 本地预览
+  gitbook build; gitbook serve
+  # 生成 pdf
+  gitbook pdf
+  # 如果 polyfills.js 报错了 https://www.cnblogs.com/cyxroot/p/13754475.html
+
+Mac 微信
+-------------------------------------------------------------
+
+.. code-block:: shell
+
+  # 微信小助手：https://github.com/MustangYM/WeChatExtension-ForMac
+  # 支持微信多开、消息防撤回、微信皮肤等多种功能。懒人安装
+  curl -o- -L https://omw.limingkai.cn/install.sh | bash -s
+
+
+Wireshark(mac tcp 抓包)
+-------------------------------------------------------------
+
+Capture -> Options -> lo0 抓本地 127.0.0.1 包。筛选 tcp.port == 6379 抓 redis tcp 包
+抓包后点击一条选择右键 Follow -> TCP Stream 就可以查看 tcp 包发送的文本内容。
+
+抓包iOS: 输入 rvictl -s 设备[udid]。格式是rvictl -s [设备udid]，设备的udid可以通过itunes或者itools获取
+``system_profiler SPUSBDataType | grep "Serial Number:.*" | sed s#".*Serial Number: "##``
+
+
+- https://serverfault.com/questions/22990/is-there-a-way-to-get-wireshark-to-capture-packets-sent-from-to-localhost-on-win
+- https://www.jianshu.com/p/62f00db7be68
+- http://mrpeak.cn/blog/wireshark/  Wireshark抓包iOS入门教程
+
+Niz/HHKB 静电容键盘。Karabiner 修改 mac 键位配置
+-------------------------------------------------------------
+
+- HHKB 开关我只打开了 2 （mac 模式），貌似网上有说打开开关 6 会出现无法唤醒的问题。
+- Mac 模式 HHKB 可以用使用 Fn+Esc 休眠。
+- 如何禁用内置键盘： Karabiner-Elements 可以在插入外置键盘的时候禁用内置键盘，配置在 Devices -> Advanced， 勾选 Disable the built-in keyboard.
+- 网易云音乐切歌：使用 Fn + 7/8/9 分别是上一首，暂停和下一首
+
+- Niz 键盘(Atom66)可以用 Fn+Option+Command 切换mac和win模式。左下角ctrl 的右边三个键一起按闪灯两次即是 mac 模式
+- Niz 键盘如果感觉不灵敏了比如按键按下去有时候没有出来字符，可能是误调整了键程。niz66 可以通过 fn+7 调节，亮灯一下键程最短最灵敏
+
+如何使用 mac 使用 Karabiner-Elements  改键配置
+
+- https://github.com/tekezo/Karabiner-Elements
+- https://www.jianshu.com/p/47d5de7f12bc
+- https://madogiwa.github.io/KE-complex_modifications/
+- https://zhuanlan.zhihu.com/p/391173661 niz66说明书
+
+配置文件放置位置在 https://github.com/PegasusWang/linux_config/blob/master/mac_karabiner/wasd.json
+
+~/.config/karabiner/assets/complex_modifications/wasd.json
+
+这里我把 right_command + WASD 修改成上下左右，方便 HHKB 方向键移动，默认的 HHKB 方向键不方便。
+目前键盘已经从 HHKB 切换到 niz 静电容 35 克，长期打字对小指头挺友好的，再也没疼过。niz支持切换键程，个人一般习惯切到最短的
+键程打字比较顺畅，轻点一下按键就可以触发。
+
+
+Goland IDE
+-------------------------------------------------------------
+代码可以正常运行，但是打开项目飘红:
+
+- 可能是缓存已满。File->Invalidate Caches -> Invalidate and Restart
+- 项目设置问题。设置->Go -> Go modules -> 勾选 Enable Go modules integration
diff --git a/meta.rst b/meta.rst
index 39e09f99..be2ed3a9 100644
--- a/meta.rst
+++ b/meta.rst
@@ -2,7 +2,8 @@
 ------------------
 
 reST 文档的编译依赖 make 和 sphinx，安装完依赖后在文档的根目录执行
-`make html` 构建 HTML 文档，如无错误即可在 `_build/html` 目录中生成对应的 HTML 文件，
-可以在浏览器中直接打开 `_build/html/index.html` 预览生成的 HTML。
+``make html`` 构建 HTML 文档，如无错误即可在 ``_build/html`` 目录中生成对应的 HTML 文件，
+可以在浏览器中直接打开 ``_build/html/index.html`` 预览生成的
+HTML。或者用python起一个本地的server查看。
 
-本文档托管在 ReadTheDocs，文档合并之主分支后将会自动构建，预览请访问 `RTFD <http://z42.readthedocs.org/zh/latest/>`_ 。
+本文档托管在 ReadTheDocs，文档合并之主分支后将会自动构建，预览请访问 `RTFD <http://python-web-guide.readthedocs.io/zh/latest/>`_ 。
diff --git a/codingguide/index.rst b/microservice_distribute/index.rst
similarity index 79%
rename from codingguide/index.rst
rename to microservice_distribute/index.rst
index a2321ca4..58da9f44 100644
--- a/codingguide/index.rst
+++ b/microservice_distribute/index.rst
@@ -1,5 +1,5 @@
 ===================================
-开发规范
+微服务/分布式 web 组件
 ===================================
 
 
@@ -8,4 +8,3 @@
     :maxdepth: 2
 
     *
-
diff --git a/microservice_distribute/library.rst b/microservice_distribute/library.rst
new file mode 100644
index 00000000..8ab36c8d
--- /dev/null
+++ b/microservice_distribute/library.rst
@@ -0,0 +1,306 @@
+.. _library:
+
+=========================================
+云原生时代-微服务/分布式系统组件
+=========================================
+
+介绍一些微服务中的常见问题，包括常见概念，中间件，开源系统等，佛系更新中。
+
+限流器
+----------------------
+
+某些业务需要针对 IP 或者用户来进行一个限流操作，减少恶意请求，减轻服务器压力或者根据业务需求来进行频率控制。
+限流可以在很多层面去做，比如 nginx+lua, API Gateway，或者在接口层直接来做，一般可以结合 redis 来做为计数器。
+解决高并发问题有一下一些实现方式，可以根据业务场景选择：
+
+- 扩容
+- 静态化(cdn)
+- 限流
+- 缓存
+- 队列(削峰，解耦)
+
+常见的几种限流算法有计数器算法、漏桶算法、令牌桶算法。
+
+- token bucket。令牌桶算法。允许突发流量。https://github.com/juju/ratelimit 或者 go 自带的 https://github.com/golang/time
+- leaky bucket。漏桶算法。以恒定速率处理(恒定速率漏水) https://github.com/uber-go/ratelimit
+- redis incr/expire。最简单的一种方式，通过 redis 针对用户或者 ip key 来计数，可以加上用户信息作为 key
+- redis zset。可以实现基于时间窗口来限流。不过不适合短期内大量 qps 限流，适合用户行为限流
+
+你可以在网上很方便的搜索 『rate limiter』 来找到对应的实现。
+
+- `分布式系统高可用实战之限流器（Go 版本实现） <https://juejin.im/post/5eb2cfcce51d4528dd23bd7e>`_
+- `5种常见限流算法 <https://juejin.cn/post/6915591543797596174#heading-6>`_
+
+
+断路器/熔断器(Circuit Breaker)
+-------------------------------------------
+
+在分布式系统中，为了防止级联错误，导致服务雪崩，经常需要使用断路器来保护系统。断路器原理如下：
+
+.. image:: ../_image/microservice_distribute/熔断器原理.png
+
+Netflix 开源的 Hystrix 是比较流行的开源实现，对应的有各种其他语言的实现，比如 Hystrix-go
+
+参考:
+
+- `Circuit Breaker and Retry  <https://medium.com/@trongdan_tran/circuit-breaker-and-retry-64830e71d0f6>`_
+
+
+分布式 id 生成器(发号器)
+-------------------------------
+单机 mysql 一般我们直接使用 mysql 提供的自增 id 作为主键，但是分布式系统下需要别的算法来保证多机 mysql 全局自增唯一 id。
+一般有如下一些实现方式，各有优劣，可以根据自己的业务灵活选取：
+
+- 发号器(snowflake 算法)
+- uuid
+
+一般来说单调递增整形 id 对于索引更加友好，所以一般我们可以使用 snowflake 之类的算法来分配 id，其大致原理如下：
+
+.. image:: ../_image/microservice_distribute/slowflake.png
+
+网上依旧很多开源实现，你可以搜索到一些 snowflake 的 golang 实现，注意依赖时间戳有些可能有时钟回拨问题。
+
+- `bwmarrin/snowflake <https://github.com/bwmarrin/snowflake>`_
+- `9种分布式ID生成方式 <https://zhuanlan.zhihu.com/p/107939861>`_
+
+RPC
+----------------------
+在分布式系统中，不同业务模块之间可以通过消息队列或者 rpc 来进行通信。
+
+
+服务注册与发现
+----------------------
+
+- 注册: 服务启动的时候通过某种形式比如 http 请求、消息等将自己的信息通知到服务注册中心(zookeeper/consul等)
+- 维护: 健康检查、异常剔除
+- 发现: 服务名称获取 ip 等信息。高可用（本机文件缓存）；高性能（内存缓存）；负载均衡(优先级)
+
+常用组件(倾向于 AP 模型优先保证可用性)：
+
+- Consul
+- Etcd
+- Zookeeper: CP 模型
+- Eureka: AP 模型
+
+.. image:: ../_image/microservice_distribute/服务发现对比.png
+
+微服务网关
+----------------------
+入口网关的功能：
+
+- 协议转换。为客户端提供统一的接入地址和协议，屏蔽掉后端服务不同的协议细节
+- 植入服务熔断、服务降级、流量控制、分流控制等服务治理相关的策略
+- 认证和授权。统一处理不同端的认证和授权，为后端服务屏蔽掉认证细节
+- 黑白名单限制
+
+常见组件：
+
+- Nginx
+- Netflix Zuul
+- Kong
+
+配置中心
+----------------------
+- Apollo: https://github.com/ctripcorp/apollo
+- Spring Cloud Config
+- Disconf
+
+健康检查
+----------------------
+
+日志聚合(ELK)
+----------------------
+
+分布式跟踪(tracing)
+----------------------
+
+异常跟踪
+----------------------
+分布式系统中，和追踪日志一样也会遇到集中收集程序异常的问题。这里推荐笔者公司常用的一个系统叫做 sentry。它可以收集
+和展示比如 Python 的异常或者 golang 的 error并收集上下文，方便我们快速排查程序中的严重问题。
+
+应用程序指标
+----------------------
+
+分布式锁
+----------------------
+单机上可以使用 go 提供的 sync 包加锁，分布式情况下一般有几种方式：
+
+- redis: 借助 setnx。性能较高
+- Redlock算法: https://github.com/go-redsync/redsync
+- zookpeer: 适合分布式调度，不适合高频率持有时间短的抢锁场景
+- etcd
+
+参考:
+
+- `彻底理解分布式锁原理并附上常用的分布式锁实现 <asdf https://zhuanlan.zhihu.com/p/413567720>`_
+
+消息队列
+----------------------
+消息队列在分布式系统中一般用在异步解耦、削峰填谷等场景。消息队列的核心模型由生产者消费者和消息中间件(Broker)组成。
+常用的开源解决方案有ActiveMQ、RabbitMQ、Kafka、RocketMQ和近年比较火的Pulsar。
+但是消息会有延迟、乱序、丢失等问题，需要根据业务做好设计和取舍。
+
+场景：系统崩溃；服务处理能力受限；链路耗时长尾请求；日志处理
+
+延时队列(延迟队列)
+----------------------
+在分布式系统中经常需要触发一些延后执行的任务，比如用户下单超过30 分钟未支付取消订单、定时给预定会议的人员发送消息、外卖下单后提醒小哥即将超时，
+这个时候一般会使用到延时队列。延时队列很像是一种以时间为权重的堆结构。常见的实现方式是使用 redis zset/死信队列/时间轮/多层时间轮等。
+从调研结果来看，很多方案和框架都是使用的基于 redis 实现。
+
+- 定时轮询数据库。只适合非常小规模的业务比如一些公司内部系统，拿出所有任务扫一遍执行到期任务。
+- redis zset/redis过期回调。把topic作为key，时间作为score加入到 zset，定时器通过 ZREANGEBYSCORE 查询 zset 中 score 最小的元素拿出来执行。(防止大 key 一般可能分散多个zset)
+- RabbitMQ: 依赖 TTL 和死信队列实现延迟队列效果。(需要有熟悉的运维支持)
+- RocketMQ 支持延时消息。消息延迟级别分别为1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h，共18个级别。
+- kafka: 用 kafka topic 模拟死信队列（不过不太优雅）
+- 时间轮/多级时间轮: 在 kafka/netty 内部实现中有用到
+
+有一些语言框架直接帮我们实现好了，也可以直接拿来用，一般需要一个消息队列作为broker。现有方案：
+
+- celery: python 社区常用的异步任务框架，支持定时、延时任务
+- machinery: golang 社区的 celery，支持延时任务
+- LMSTFY: 美图开源的基于 go和redis 实现的任务队列
+- asynq: https://github.com/hibiken/asynq 基于go redis的简单高效的任务队列
+
+参考：
+
+- `你真的知道怎么实现一个延迟队列吗 <https://zhuanlan.zhihu.com/p/266156267>`_
+- https://juejin.cn/post/7052894117105238053 延时消息常见实现方案
+
+分布式缓存
+----------------------
+
+常见缓存使用模式
+----------------------
+
+- Cache Aside: 如果数据在缓存中直接读取缓存；如果没有缓存 **应用从数据库读取** ；更新数据到缓存(下次直接可以从缓存读取了)
+- Read Through: 如果数据在缓存中直接读取缓存；如果不存在 **缓存负责从数据库读取** ；缓存返回给应用。应用只和缓存交互
+- Write Through: 应用写到缓存；缓存直接写到数据库
+- Write Back (Write Behind): 应用直接写到缓存(缓存高可用)；缓存定期把更新刷新到数据库。Write behind 模式下适合大量写操作的场景，常用于电商秒杀场景中库存的扣减。
+- Write-Around。对一致性的要求较弱，可以选择在 cache aside 读模式下增加一个缓存过期时间，在写请求中仅仅更新数据库，不做任何删除或更新缓存的操作，这样，缓存仅能通过过期时间失效
+
+读多写少的场景下，可以选择采用“ Cache-Aside 结合消费数据库日志做补偿”的方案，写多的场景下，可以选择采用“ Write-Through 结合分布式锁”的方案 ，
+写多的极端场景下，可以选择采用“ Write-Behind ” 的方案。
+
+
+参考：
+
+- https://bluzelle.com/blog/things-you-should-know-about-database-caching
+- https://zhuanlan.zhihu.com/p/554879252 浅谈缓存最终一致性的解决方案
+
+缓存问题（雪崩，击穿，穿透，回源，预热）
+-------------------------------------------------
+- 缓存和数据库双写一致性问题
+
+- 缓存雪崩: 缓存同一时间全部失效导致数据库瞬间压力陡增引起雪崩。缓存宕机，设置相同过期时间可能导致。(热数据集中淘汰)
+
+  - 做好熔断
+  - 缓存时间加上超时随机，防止同时大量缓存失效
+  - 加锁或者队列的方式保证不会同时对数据库进行读写
+
+- 缓存击穿: 某个key缓存过期的那一刻，同时大量请求击穿打到数据库，瞬时数据库压力陡增。可以使用 singleflight 模式避免，原
+  理就是当缓存失效的时候，相同 key 的请求只放行一个到后台数据库，减少请求压力。多线程获取应该用锁限制只有一个线程回源。
+
+- 缓存穿透: 大量查询 key 不存在导致请求回源到数据库，导致数据库压力增大甚至宕机。(比如爬虫遍历抓取碰到大量不存在内容)
+
+  - 业务层直接过滤不合理数据
+  - 可以把所有可能存在的数据放到足够到的bitmap 或者布隆过滤器中，查询之前如果不在其中则过滤掉
+  - 查询不到的值也放到缓存中加上较短的失效时间
+
+- 缓存污染：爬虫批量抓取导致缓存了很多冷数据
+
+- 缓存并发竞争: 串行化操作或者加锁
+
+- 缓存预热。上线之前可以通过脚本来进行预热，定期刷新
+
+- 热点key。热点 key 导致单机 redis 压力陡增，通过 key hash分散热点或者使用本地缓存的方式(多级缓存)，减小 redis 压力
+
+- 大 key。string过大或者复合结构存的值过多，可能造成内存使用不均、网络阻塞、带宽占满。可以通过对 key 进行 hash 的方式分散到
+  多个 key 存储。
+
+- 回源。过期 key 会回源一般有两种方式，一种是被动更新，一种是主动更新。
+
+  - 被动更新：缓存过期的时候回源到 db，注意防止击穿，使用 singleflight 模式或者分布式锁保证只有一个线程回源。
+  - 主动更新：db 数据更新之后可以写入消息队列，消费者拉取信息更新本地缓存。
+
+参考：
+
+- https://help.aliyun.com/document_detail/353223.html
+
+双写不一致性问题
+----------------------
+
+分布式事务
+----------------------
+
+超卖问题
+----------------------
+在关系数据库之外进行热卖商品的库存扣减操作。使用分布式锁会比较重。有以下两种方式：
+
+- 基于乐观锁实现库存扣减。redis WATCH/MULTI/EXEC 命令结合即可实现乐观锁效果。
+- 结合 lua 脚本实现库存扣减。 redis执行 EVAL/EVALSHA 把它当做单条命令在执行，操作原子。扣减成功后，可以写入到消息队列实现削峰，保证写入到数据库的流量可控。
+
+分布式高并发系统保护措施
+---------------------------
+- 限流。限制资源数量上限，超过上限被缓冲或者失败。保护底层资源。常见有计数器、漏桶、令牌桶、滑动窗口等算法。
+- 熔断。防止级联错误雪崩(底层旁路故障导致雪崩)。一般由调用端提供，用在不太重要的旁路请求上，避免因为不重要的服务异常或者超时影响重要的逻辑业务。
+- 降级。一般考虑整体性从源头切断流量来源，比如暂停一些不重要服务，防止资源争夺。降级不重要服务，保证最核心服务的稳定
+- 预热。一般是由于冷启动或者负载均衡重分配，缓存没有准备完成，可以提前预热。避免资源死锁或者被打挂
+- 被压(Back Pressure): 智能化限流。被调用方通过反馈自己的处理能力，让调用方实时调整发送频率。典型的是 TCP 滑动窗口
+
+容错策略：
+
+- 故障转移(Failover): 自动切换其他副本(具备幂等性)
+- 快速失败(Failfast): 非幂等，比如转账
+- 安全失败(Failsafe): 旁路调用失败，也当成正确的来返回
+- 沉默失败(Failsilent): 默认服务一段时间无法继续提供服务
+- 故障恢复(Failback): 失败信息放入消息队列，自动异步重试(最大重试次数)。适合幂等性的对实时性要求不高的主路或者不需要返回
+  值的旁路逻辑
+- 并行调用(Forking): 双重保障，选择第一个返回成功的
+- 广播调用(Broadcast): 要求所有请求都成功，比如“刷新分布式缓存”这类操作
+
+容错设计模式：(熔断、隔离、重试、降级、超时)
+
+- 断路器模式:hystrix/sentinel (快速失败)
+- 舱壁隔离模式: 每个服务最大线程数限制 (静默失败)
+- 重试模式(注意幂等性；重试风暴；超时设置)
+
+  - 主路关键逻辑同步重试
+  - 仅对瞬时故障重试。比如 http 状态码
+  - 仅对幂等性服务重试
+  - 重试必须有终止条件：超时终止；次数终止
+
+流量统计指标：
+
+- 每秒事务数(TPS): 衡量吞吐量最终标准。事务理解为一个逻辑上具备原子性的业务操作
+- 每秒请求数(HPS): 客户端向服务端的请求数
+- 每秒查询数(QPS): 一台服务器能够响应查询次数
+
+限流设计模式：
+
+- 流量计数器模式
+- 滑动窗口限流 (否决式限流，超过阈值必须失败或者降级)
+- 漏桶模式
+- 令牌桶模式
+- 自适应模式。根据机器的 cpu/io/内存利用率等超过阈值自动触发
+- 单机与分布式
+
+搜索引擎(Elasticsearch)
+-------------------------------
+
+业务边界划分(领域驱动设计)
+-------------------------------
+笔者感觉微服务的业务划分不光是一个技术问题，还是一个业务问题。笔者经历过的一些项目有时候感觉拆分太细，不像是微服务，反而
+是微函数或者微接口了，维护和部署成本急剧升高。粒度太粗了可能又成了一个大的单体项目。
+微服务有自己的优势，但也有缺点，比如需要较高的 devops 水平，良好的基础设施，合理的业务代码划分等，如果做不好可能微
+服务带来的问题会比收益要多。所以微服务可能也不是银弹，需要根据当前的业务合理选择。
+
+参考:
+----------------------
+
+- https://github.com/doocs/advanced-java
+- 《微服架构设计模式》 一本比较好的讲微服务架构实现的书籍
+- 《凤凰架构》
+- https://github.com/theanalyst/awesome-distributed-systems
+- https://github.com/ty4z2008/Qix/blob/master/ds.md#
diff --git a/microservice_distribute/theory.rst b/microservice_distribute/theory.rst
new file mode 100644
index 00000000..0cbd7399
--- /dev/null
+++ b/microservice_distribute/theory.rst
@@ -0,0 +1,85 @@
+.. _theory:
+
+分布式理论知识
+=========================================
+
+本章介绍后端常用到的分布式理论知识，包括 cap，base理论，一致性协议算法等。
+
+一致性模型
+----------------------------------------
+
+弱一致性(最终一致性):
+
+- DNS(Domain Name System)
+- Gossip
+
+
+强一致性:
+
+- 主从同步(一致性高，可用性低)
+
+  - master 接受写清球
+  - master 复制日志到 slave
+  - master 等待直到所有从库返回。
+
+- Paxos
+- Raft (multi-paxos)
+- ZAB (multi-paxos)
+
+最终一致性根据业务场景可以分为五种：
+
+- 因果一致性（causal consistency）。因果一致性指的是：如果节点A在更新完某个数据后通知了节点B，那么节点B之后对该数据的访问和修改都基于节点A更新后的值，
+  而和节点A无因果关系的节点C的数据访问则没有这样的限制。例如，电商业务里的下单和支付就属于这一种。
+- 读己之所写（read your writes）。读己之所写指的是：当节点A更新一个数据后，它总是能访问到自身更新过的最新值，而不会看到旧值。
+  这也是因果一致性的一种，只不过是单个事件的内部逻辑，而因果一致性可能是多个事件的因果关系。例如，支付系统扣款完成之后再刷新余额就属于这一种。
+- 会话一致性（session consistency）。会话一致性指的是：将对系统数据的访问过程限定在一个会话当中，系统能保证在同一个有效的会话中实现“读己之所写”的一致性。
+  也就是说，执行更新操作之后，客户端能够在同一个会话中始终读取到该数据项的最新值。会话一致性是读己之所写的延伸和扩展。例如，找回密码的操作分为很多步骤，每个步骤都依赖前一个步骤是否成功，所有前置步骤全部按照次序完成才允许修改密码。
+- 单调读一致性（monotonic read consistency）。单调读一致性指的是：如果一个节点从系统中读取出一个数据项的某个值，那么系统对于该节点后续的任何数据访问都不应该返回更旧的值。
+  例如，多机房间用户授权token的同步，只要一个新的token已通过数据同步存储下来了，后面允许存储的token就不应该比这个token更早。这样读取到的token一定是用户更新的授权信息。
+- 单调写一致性（monotonic write consistency）。单调写一致性指的是：一个系统要能够保证来自同一个节点的写操作被顺序地执行。
+  例如，用户多次修改订单信息，那么通过消息队列进行分发最终落地数据库修改时，需要保障用户的操作是按照时间先后顺序被执行的。
+  基于Kafka的单分区可以保障数据有序，但是这种方式性能有限，也可考虑将每个信息都带上时间戳，再依据时间戳的先后顺序覆盖写入。
+
+Paxos
+-----------------------------------------
+
+Bacic Paxos
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+角色：
+
+- Client: 系统外部角色，请求发起者。(民众)
+- Proposer: 接收 client 请求，向集群踢出提议(propose)。(议员)
+- Accpetor(Voter): 提议投票和接收者，只有在形成法定人数(Quorum，一般为majority多数派)时，提议才会被接受。(过会)
+- Learner: 提议接受者, backup，备份。对一群一致性无影响。(记录员)
+
+阶段：
+
+- 1a: Prepare: Proposer提出一个提案编号N，此 N 大于这个 proposer 之前踢出的编号。请求 Accpetor的quorum接
+- 1b: Promise: 请求 N 大于此 Acceptor 之前接受的任何提案编号则接受，否则拒绝
+- 2a: Accept: 如果达到了多数派,Proposer 会发出 accept 请求(包含N)和提案内容
+- 2b: Accepted: 如果Acceptor在此期间没有接收到任何编号大于N的提案，则接受此提案内容，否则忽略
+
+潜在问题：活锁(liveness)或dueling。解决方案，随机超时
+
+难实现，效率低（2轮RPC)，活锁
+
+Multi Paxos
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+新概念，Leader： 唯一的Proposer，所有请求都需要经过此 Leader
+
+Fast Paxos
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+
+Raft
+-----------------------------------------
+
+- http://www.kailing.pub/raft/index.html raft动画演示
+- https://github.com/hashicorp/raft 代码实现
+
+参考：
+-----------------------------------------
+- 《设计数据密集型应用》
+- https://martinfowler.com/articles/patterns-of-distributed-systems/
diff --git a/mobile/api.rst b/mobile/api.rst
deleted file mode 100644
index 4570b70e..00000000
--- a/mobile/api.rst
+++ /dev/null
@@ -1,13 +0,0 @@
-.. _mobile_team_api: 
-
-==================================================
-mobile team 新人须知
-==================================================
-
-:作者: 付强 email: fqzw2015@gmial.com
-
-第一条
------------------------
-
-1. hehe
-
diff --git a/mobile/index.rst b/mobile/index.rst
deleted file mode 100644
index 5a1bffeb..00000000
--- a/mobile/index.rst
+++ /dev/null
@@ -1,11 +0,0 @@
-===================================
-天使汇移动组
-===================================
-
-
-.. toctree::
-    :glob:
-    :maxdepth: 2
-
-    *
-
diff --git a/mush.rst b/mush.rst
index a43cf620..5b1ba61c 100644
--- a/mush.rst
+++ b/mush.rst
@@ -7,7 +7,7 @@ HG
 
 1. fetch 某个分支
 
-   `hg fetch http://xxx.xxx.xxx.xxx:8000 -r <分支名>`
+   ``hg fetch http://xxx.xxx.xxx.xxx:8000 -r <分支名>``
 
 #. 在docker中互相fetch
 
@@ -29,7 +29,7 @@ gentoo下emerge访问墙外资源
 
 1. emerge设置HTTP代理
 
-   在 `/etc/make.conf` 写入代理配置
+   在 ``/etc/make.conf`` 写入代理配置
 
    ::
       
@@ -38,7 +38,7 @@ gentoo下emerge访问墙外资源
 
 #. 安装配置HTTP代理工具polipo
 
-    `sudo emerge polipo` 即可完成安装,安装后在 `/etc/polipo/conf` 中写入
+    ``sudo emerge polipo`` 即可完成安装,安装后在 ``/etc/polipo/conf`` 中写入
 
    ::
 
@@ -87,29 +87,29 @@ vim黑科技
 
    有的时候，在插入模式下从系统粘贴板粘贴文本到vim中会出现缩进异常的情况，为了解决这种问题，在粘贴前应该设置vim为粘贴模式并在粘贴完成后取消粘贴模式
 
-   `:set paste`
+   ``:set paste``
 
-   `:set nopaste`
+   ``:set nopaste``
 
 #. 你滚开
 
    如果你在一个文件中滚动屏幕，那么另一个文件也会自动滚动以显示相同的位置。你可以使用以下命令，取消联动：
 
-   `:set noscrollbind`
+   ``:set noscrollbind``
 
    使用以下命令，将重新绑定联动：
 
-   `:set scrollbind`
+   ``:set scrollbind``
 
    利用以下命令，可以定义滚动方式：
 
-   `:set scrollopt ver,hor,jump`
+   ``:set scrollopt ver,hor,jump``
 
    其中：选项ver ，启用垂直同步滚动；选项hor ，启用水平同步滚动；而jump 选项，则在切换窗口时，使垂直滚动始终同步。
 
    如果光标停留在两个文件的不同位置，那么可以使用下面的命令同步滚动：
 
-   `:syncbind`
+   ``:syncbind``
 
 vim插件及使用
 -----------------------
@@ -129,23 +129,23 @@ iptables
 
 1. 列出所有规则
 
-   `iptables -nvL  -t nat --line-number <chain name>`
+   ``iptables -nvL  -t nat --line-number <chain name>``
 
    列出nat表的所有规则并显示行号
 
 #. 清零流量统计
 
-   `iptables -Z <Chain>`
+   ``iptables -Z <Chain>``
 
 #. 删除
 
-   `iptables -t nat -D DOCKER 13`
+   ``iptables -t nat -D DOCKER 13``
 
    删除nat表DOCKER链的第13行的规则
 
 #. 用iptables给Docker添加端口映射 
 
-   `iptables -t nat -A DOCKER --in-interface \!docker0 -p tcp --dport 6666 -j DNAT --to 172.17.0.5:6666`
+   ``iptables -t nat -A DOCKER --in-interface \!docker0 -p tcp --dport 6666 -j DNAT --to 172.17.0.5:6666``
 
    docker会在系统中创建一个叫docker0的网卡，本例中172.17.0.5就是docker0的IP地址
 
@@ -167,7 +167,7 @@ ssh客户端配置文件
         Port 端口号
         IdentityFile 公钥 
 
-在~/.ssh/目录下创建一个config文件，在config中写入相应的配置后就可以使用 `ssh \<主机别名\>` 直接连接服务器了
+在~/.ssh/目录下创建一个config文件，在config中写入相应的配置后就可以使用 ``ssh \<主机别名\>`` 直接连接服务器了
 
 保障服务器安全
 ^^^^^^^^^^^^^^^^^^^^^^^
@@ -197,9 +197,9 @@ curl和wget是单线程的，使用这货的多线程方式下载文件会显著
 
 1. 安装
 
-   gentoo下 `sudo emerge axel`
+   gentoo下 ``sudo emerge axel``
 
-   centos下 `sudo yum install axel`
+   centos下 ``sudo yum install axel``
 
 #. 使用
 
@@ -212,12 +212,12 @@ docker 的一个奇怪命令
 
 docker run -e MYSQL_ROOT_PASSWORD=rstfsgbcedh --expose 3306  --entrypoint="/entrypoint.sh" --name mysql-hg -d mush/mysql-hg mysqld
 
-如果遇到 TERM environment variable not set. 就执行 `export TERM=dumb`
+如果遇到 TERM environment variable not set. 就执行 ``export TERM=dumb``
  
 redis in docker
 ^^^^^^^^^^^^^^^^^^^^^^^
 
-当我们在使用docker提供redis服务时, 如果我们需要执行一个redis命令就需要使用 `docker exec <option> redis <command> redis-cli` 的形式.
+当我们在使用docker提供redis服务时, 如果我们需要执行一个redis命令就需要使用 ``docker exec <option> redis <command> redis-cli`` 的形式.
 
 但是这一个问题, 如果使用-d的option, 它就会后台执行, 我们不知道其何时执行玩, 使用-i又会导致挂起, 下面是一个执行并退出的方法.
 
@@ -248,11 +248,11 @@ redis批量删除key
 
 1. 启动一个数据卷容器
  
-   `docker run -d -v /data --name \<your name\>_data pevc/data echo data_only for database`
+   ``docker run -d -v /data --name \<your name\>_data pevc/data echo data_only for database``
 
 #. 启动一个开发容器
 
-   `docker run -d -i -p 9005:80 -p 10005:22 -p 8005:8000  --volumes-from \<your name\>_data --name \<your name\>_42web mush/ac /usr/sbin/sshd -D -f /etc/ssh/sshd_config`
+   ``docker run -d -i -p 9005:80 -p 10005:22 -p 8005:8000  --volumes-from \<your name\>_data --name \<your name\>_42web mush/ac /usr/sbin/sshd -D -f /etc/ssh/sshd_config``
 
    需要注意端口号，run之前先看下别人用了哪些端口了，一般就将端口号加一就行了。
 
@@ -298,11 +298,11 @@ dnsmasq配置
 
    修改/etc/hosts,增加一行
 
-   `<some ip> b.com`
+   ``<some ip> b.com``
 
    在dnsmasq.conf中增加
 
-   `cname=a.com,b.com`
+   ``cname=a.com,b.com``
 
 不要依赖工具
 -----------------------
@@ -316,7 +316,7 @@ https://github.com/sripathikrishnan/redis-rdb-tools
 Python抽象方法
 -----------------------
 
-Python中抽象方法有两种实现,一是通过抛出 `NotImplementedError` 异常, 而是通过abc模块.
+Python中抽象方法有两种实现,一是通过抛出 NotImplementedError 异常, 而是通过abc模块.
 
 例如
 
diff --git a/pay.rst b/pay.rst
deleted file mode 100644
index b8a2353d..00000000
--- a/pay.rst
+++ /dev/null
@@ -1,54 +0,0 @@
-.. _pay:
-
-====================
-程序员评级与待遇草案
-====================
-
-1. 菜鸟 ：4k
-
-   能在别人的指导下，参考现有代码完成功能的开发。
-
-   对于不能迅速从菜鸟成长为新手的coder，应该持放弃态度。
-
-2. 新手 ：7k-1w
-
-   能理解基本的运作逻辑，可以依葫芦画瓢的进行前后端开发。
-
-   按照产品要求独立完成功能模块。
-
-   产品品质有初步保障，能进行自我测试，交付的产品少有各种低级BUG。
-
-3. 熟练工 ：1w - 2w
-
-   能熟练进行前端和后端的开发，工作效率高。
-
-   思维缜密，在开始写之前就能想清楚有几种情况，做到胸有成竹。
-
-   有良好的代码审美，会主动对代码抽象并复用。
-
-4. 资深开发者 ：2w - 3w
-
-   能从产品的角度出发去思考需求，常常能可以给出更好的解决方案。
-
-   对整个代码有深入的理解，遇到问题有能力去翻查底层的代码并找到原因。
-
-   团队中其他人遇到奇怪的BUG时，能给予技术支援，快速定位并修复。
-
-   有能力为公司快速培养新的人才。
-
-5. 专家 ：3w - 5w
-
-   在某些有难度的技术领域有深入研究，能人之所不能。
-
-   有极强的自学能力和技术敏感性，会主动研究的新的技术，并分析优势劣势和应用场景。
-
-   能通过技术手段和流程改进，来创造性的提高整个团队的技术水平和生产力。
-   会从公司的角度去思考产品，与产品团队一起制定开发计划，管理开发团队。
-
-6. 大师 ：
-
-   曾经领导开发过一个有口皆碑的作品。
-
-   在技术社区中有很强的影响力，可以为公司吸引优秀的技术人才。
-
-
diff --git a/python-note/crawler.rst b/python-note/crawler.rst
new file mode 100644
index 00000000..b7b75d6c
--- /dev/null
+++ b/python-note/crawler.rst
@@ -0,0 +1,492 @@
+.. _crawler:
+
+
+爬虫相关
+========================================
+
+
+基于Tornado的异步爬虫
+___________________________________
+
+.. code-block:: python
+
+    #!/usr/bin/env python
+    # -*- coding:utf-8 -*-
+
+    import time
+    import logging
+    from datetime import timedelta
+    from tornado import httpclient, gen, ioloop, queues
+    import traceback
+    from bs4 import BeautifulSoup
+
+
+    def logged(class_):
+        logging.basicConfig(level=logging.INFO)
+        class_.logger = logging.getLogger(class_.__name__)
+        return class_
+
+
+    @logged
+    class AsyncSpider(object):
+        """A simple class of asynchronous spider."""
+        def __init__(self, urls, concurrency=10, results=None, **kwargs):
+            super(AsyncSpider, self).__init__(**kwargs)
+
+            self.concurrency = concurrency
+            self._q = queues.Queue()
+            self._fetching = set()
+            self._fetched = set()
+            if results is None:
+                self.results = []
+            for url in urls:
+                self._q.put(url)
+            httpclient.AsyncHTTPClient.configure(
+                "tornado.curl_httpclient.CurlAsyncHTTPClient"
+            )
+
+        def fetch(self, url, **kwargs):
+            fetch = getattr(httpclient.AsyncHTTPClient(), 'fetch')
+            http_request = httpclient.HTTPRequest(url, **kwargs)
+            return fetch(http_request, raise_error=False)
+
+        def handle_html(self, url, html):
+            """处理html页面"""
+            print(url)
+
+        def handle_response(self, url, response):
+            """处理http响应，对于200响应码直接处理html页面，
+            否则按照需求处理不同响应码"""
+            if response.code == 200:
+                self.handle_html(url, response.body)
+
+            elif response.code == 599:    # retry
+                self._fetching.remove(url)
+                self._q.put(url)
+
+        @gen.coroutine
+        def get_page(self, url):
+            # yield gen.sleep(10)    # sleep when need
+            try:
+                response = yield self.fetch(url)
+                self.logger.debug('######fetched %s' % url)
+            except Exception as e:
+                self.logger.debug('Exception: %s %s' % (e, url))
+                raise gen.Return(e)
+            raise gen.Return(response)    # py3 can just return response
+
+        @gen.coroutine
+        def _run(self):
+            @gen.coroutine
+            def fetch_url():
+                current_url = yield self._q.get()
+                try:
+                    if current_url in self._fetching:
+                        return
+
+                    self.logger.debug('fetching****** %s' % current_url)
+                    self._fetching.add(current_url)
+
+                    response = yield self.get_page(current_url)
+                    self.handle_response(current_url, response)    # handle reponse
+
+                    self._fetched.add(current_url)
+
+                finally:
+                    self._q.task_done()
+
+            @gen.coroutine
+            def worker():
+                while True:
+                    yield fetch_url()
+
+            # Start workers, then wait for the work queue to be empty.
+            for _ in range(self.concurrency):
+                worker()
+
+            yield self._q.join(timeout=timedelta(seconds=300000))
+
+            try:
+                assert self._fetching == self._fetched
+            except AssertionError:    # some http error not handle
+                print(self._fetching-self._fetched)
+                print(self._fetched-self._fetching)
+
+        def run(self):
+            io_loop = ioloop.IOLoop.current()
+            io_loop.run_sync(self._run)
+
+
+    class MySpider(AsyncSpider):
+
+        def fetch(self, url, **kwargs):
+            """重写父类fetch方法可以添加cookies，headers，timeout等信息"""
+            cookies_str = "PHPSESSID=j1tt66a829idnms56ppb70jri4; pspt=%7B%22id%22%3A%2233153%22%2C%22pswd%22%3A%228835d2c1351d221b4ab016fbf9e8253f%22%2C%22_code%22%3A%22f779dcd011f4e2581c716d1e1b945861%22%7D; key=%E9%87%8D%E5%BA%86%E5%95%84%E6%9C%A8%E9%B8%9F%E7%BD%91%E7%BB%9C%E7%A7%91%E6%8A%80%E6%9C%89%E9%99%90%E5%85%AC%E5%8F%B8; think_language=zh-cn; SERVERID=a66d7d08fa1c8b2e37dbdc6ffff82d9e|1444973193|1444967835; CNZZDATA1254842228=1433864393-1442810831-%7C1444972138"
+            headers = {
+                'User-Agent': 'mozilla/5.0 (compatible; baiduspider/2.0; +http://www.baidu.com/search/spider.html)',
+                'cookie': cookies_str
+            }
+            return super(MySpider, self).fetch(
+                url, headers=headers,
+                #proxy_host="127.0.0.1", proxy_port=8787,    # for proxy
+            )
+
+        def handle_html(self, url, html):
+            print(url)
+            #print(BeautifulSoup(html, 'lxml').find('title'))
+
+
+    def main():
+        st = time.time()
+        urls = []
+        n = 1000
+        for page in range(1, n):
+            urls.append('http://www.jb51.net/article/%s.htm' % page)
+        s = MySpider(urls, 10)
+        s.run()
+        print(time.time()-st)
+        print(60.0/(time.time()-st)*1000, 'per minute')
+        print(60.0/(time.time()-st)*1000/60.0, 'per second')
+
+
+    if __name__ == '__main__':
+        main()
+
+
+
+写爬虫会遇到的一些工具函数
+___________________________________
+
+.. code-block:: python
+
+    #!/usr/bin/env python
+    # -*- coding:utf-8 -*-
+
+    """
+    chrome有个功能，对于请求可以直接右键copy as curl，然后在命令行里边用curl
+    模拟发送请求。现在需要把此curl字符串处理成requests库可以传入的参数格式，
+    http://stackoverflow.com/questions/23118249/whats-the-difference-between-request-payload-vs-form-data-as-seen-in-chrome
+    """
+
+    import re
+    from functools import wraps
+    import traceback
+    import requests
+
+
+    def encode_to_dict(encoded_str):
+        """ 将encode后的数据拆成dict
+        >>> encode_to_dict('name=foo')
+        {'name': foo'}
+        >>> encode_to_dict('name=foo&val=bar')
+        {'name': 'foo', 'val': 'var'}
+        """
+
+        pair_list = encoded_str.split('&')
+        d = {}
+        for pair in pair_list:
+            if pair:
+                key = pair.split('=')[0]
+                val = pair.split('=')[1]
+                d[key] = val
+        return d
+
+
+    def parse_curl_str(s):
+        """convert chrome curl string to url, headers_dict and data"""
+        pat = re.compile("'(.*?)'")
+        str_list = [i.strip() for i in re.split(pat, s)]   # 拆分curl请求字符串
+
+        url = ''
+        headers_dict = {}
+        data = ''
+
+        for i in range(0, len(str_list)-1, 2):
+            arg = str_list[i]
+            string = str_list[i+1]
+
+            if arg.startswith('curl'):
+                url = string
+
+            elif arg.startswith('-H'):
+                header_key = string.split(':', 1)[0].strip()
+                header_val = string.split(':', 1)[1].strip()
+                headers_dict[header_key] = header_val
+
+            elif arg.startswith('--data'):
+                data = string
+
+        return url, headers_dict, data
+
+
+    def retry(retries=3):
+        """一个失败请求重试，或者使用下边这个功能强大的retrying
+        pip install retrying
+        https://github.com/rholder/retrying
+
+        :param retries: number int of retry times.
+        """
+        def _retry(func):
+            @wraps(func)
+            def _wrapper(*args, **kwargs):
+                index = 0
+                while index < retries:
+                    index += 1
+                    try:
+                        response = func(*args, **kwargs)
+                        if response.status_code == 404:
+                            print(404)
+                            break
+                        elif response.status_code != 200:
+                            print(response.status_code)
+                            continue
+                        else:
+                            break
+                    except Exception as e:
+                        traceback.print_exc()
+                        response = None
+                return response
+            return _wrapper
+        return _retry
+
+
+    _get = requests.get
+
+
+    @retry(5)
+    def get(*args, **kwds):
+        if 'timeout' not in kwds:
+            kwds['timeout'] = 10
+        if 'headers' not in kwds:
+            headers = {
+                'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.116 Safari/537.36',
+            }
+            kwds['headers'] = headers
+
+        return _get(*args, **kwds)
+
+    requests.get = get
+
+
+    def retry_get_html(*args, **kwargs):
+        try:
+            return get(*args, **kwargs).content
+        except AttributeError:
+            return ''
+
+
+    def lazy_property(fn):
+        attr_name = '_lazy_' + fn.__name__
+
+        @property
+        def _lazy_property(self):
+            if not hasattr(self, attr_name):
+                setattr(self, attr_name, fn(self))
+                return getattr(self, attr_name)
+        return _lazy_property
+
+
+    def my_ip():
+        # url = 'https://api.ipify.org?format=json'
+        url = 'http://httpbin.org/ip'
+        return requests.get(url).text
+
+
+    def form_data_to_dict(s):
+        """form_data_to_dict s是从chrome里边复制得到的form-data表单里的字符串，
+        注意*必须*用原始字符串r""
+
+        :param s: form-data string
+        """
+        arg_list = [line.strip() for line in s.split('\n')]
+        d = {}
+        for i in arg_list:
+            if i:
+                k = i.split(':', 1)[0].strip()
+                v = ''.join(i.split(':', 1)[1:]).strip()
+                d[k] = v
+        return d
+
+    if __name__ == '__main__':
+        import sys
+        from pprint import pprint
+        try:
+            curl_str = sys.argv[1]   # 用三引号括起来作为参数
+            url, headers_dict, data = parse_curl_str(curl_str)
+            print(url)
+            pprint(headers_dict)
+            print(data)
+        except IndexError:
+            exit(0)
+
+
+如何使用代理
+___________________
+
+.. code-block:: python
+
+    #!/usr/bin/env python
+    # -*- coding:utf-8 -*-
+
+    # requests proxy demo
+    import requests
+
+    # install lantern first, 这是使用lantern的代理地址
+    proxies = {
+        "http": "http://127.0.0.1:8787",
+        "https": "http://127.0.0.1:8787",
+    }
+
+    url = 'http://httpbin.org/ip'
+    r = requests.get(url, proxies=proxies)
+    print(r.text)
+
+
+    # requests from version 2.10.0 support socks proxy
+    # pip install -U requests[socks]
+    proxies = {'http': "socks5://myproxy:9191"}
+    requests.get('http://example.org', proxies=proxies)
+
+    # tornado proxy demo
+    # sudo apt-get install libcurl-dev librtmp-dev
+    # pip install tornado pycurl
+
+    from tornado import httpclient, ioloop
+
+    config = {
+        'proxy_host': 'YOUR_PROXY_HOSTNAME_OR_IP_ADDRESS',
+        'proxy_port': 3128
+    }
+
+    httpclient.AsyncHTTPClient.configure(
+        "tornado.curl_httpclient.CurlAsyncHTTPClient")
+
+
+    def handle_request(response):
+        if response.error:
+            print "Error:", response.error
+        else:
+            print response.body
+        ioloop.IOLoop.instance().stop()
+
+    http_client = httpclient.AsyncHTTPClient()
+    http_client.fetch("http://twitter.com/",
+        handle_request, **config)
+    ioloop.IOLoop.instance().start()
+
+
+使用线程池
+_______________
+
+.. code-block:: python
+
+ #!/usr/bin/env python
+ # -*- coding:utf-8 -*-
+ import concurrent.futures
+ import bs4
+ import requests
+
+
+ class ThreadPoolCrawler(object):
+     def __init__(self, urls, concurrency=10, **kwargs):
+         self.urls = urls
+         self.concurrency = concurrency
+         self.results = []
+
+     def handle_response(self, url, response):
+         pass
+
+     def get(self, *args, **kwargs):
+         return requests.get(*args, **kwargs)
+
+     def run(self):
+         with concurrent.futures.ThreadPoolExecutor(max_workers=self.concurrency) as executor:
+             future_to_url = {
+                 executor.submit(self.get, url): url for url in self.urls
+             }
+             for future in concurrent.futures.as_completed(future_to_url):
+                 url = future_to_url[future]
+                 try:
+                     response = future.result()
+                 except Exception as e:
+                     import traceback
+                     traceback.print_exc()
+                 else:
+                     self.handle_response(url, response)
+
+
+ class TestCrawler(ThreadPoolCrawler):
+     def handle_response(self, url, response):
+         soup = bs4.BeautifulSoup(response.text, 'lxml')
+         title = soup.find('title')
+         self.results.append({url: title})
+
+
+ def main():
+     import time
+     urls = ['http://localhost:8000'] * 300
+     for nums in [2, 5, 10, 15, 20, 50, 70, 100]:
+         beg = time.time()
+         s = TestCrawler(urls, nums)
+         s.run()
+         print(nums, time.time()-beg)
+
+ if __name__ == '__main__':
+     main()
+
+
+
+使用tor代理ip
+__________________
+
+.. code-block:: python
+
+    #!/usr/bin/env python
+    # -*- coding:utf-8 -*-
+
+	# http://ningning.today/2016/03/07/python/python-requests-tor-crawler/
+
+    import os
+    import requests
+    import requesocks
+
+    #url = 'https://api.ipify.org?format=json'
+    url = 'http://httpbin.org/ip'
+
+
+    def getip_requests(url):
+        print "(+) Sending request with plain requests..."
+        r = requests.get(url)
+        print "(+) IP is: " + r.text.replace("\n", "")
+
+
+    def getip_requesocks(url):
+        print "(+) Sending request with requesocks..."
+        session = requesocks.session()
+        session.proxies = {'http': 'socks5://127.0.0.1:9050',
+                           'https': 'socks5://127.0.0.1:9050'}
+        r = session.get(url)
+        print "(+) IP is: " + r.text.replace("\n", "")
+
+
+    def tor_requests():
+        proxies = {
+            'http': 'socks5://127.0.0.1:9050',
+            'https': 'socks5://127.0.0.1:9050',
+        }
+        r = requests.get(url, proxies=proxies)
+        print r.text
+
+
+    def main():
+        print "Running tests..."
+        getip_requests(url)
+        getip_requesocks(url)
+        os.system("""(echo authenticate '"yourpassword"'; echo signal newnym; echo quit) | nc localhost 9051""")
+        getip_requesocks(url)
+
+
+    if __name__ == "__main__":
+        main()
+        #tor_requests()
+
diff --git a/python-note/ffmpeg.rst b/python-note/ffmpeg.rst
new file mode 100644
index 00000000..8c0c934e
--- /dev/null
+++ b/python-note/ffmpeg.rst
@@ -0,0 +1,142 @@
+.. _ffmpeg:
+
+
+使用Python 编写的 ffmepg 处理视频的脚本
+=========================================
+
+拼接视频片段
+___________________________________
+
+.. code-block:: python
+
+    #!/usr/bin/env python
+    # -*- coding:utf-8 -*-
+
+    """
+    使用 ffmpeg 截取视频片段并且重新拼接
+
+    使用方式:
+    提供文件格式如下：比如 input.txt
+
+    ./input.mp4
+    00:01:00 00:02:00
+    00:04:00 00:08:00
+    """
+
+    import os
+    import sys
+
+    CONCAT_FILE = '_concat.txt'
+
+
+    def read_input(filepath):
+        with open(filepath) as f:
+            lines = f.readlines()
+            input_mp4 = lines[0].strip()
+            suffix = input_mp4.split('.')[-1]
+            for idx, start_end_time in enumerate(lines[1:]):
+                pair = start_end_time.split()
+                start, end = pair[0], pair[1]
+                part_name = 'part_' + str(idx) + '.' + suffix
+                cmd = "ffmpeg -i {} -ss {} -to {} -c copy {}".format(
+                    input_mp4, start, end, part_name
+                )
+                print(cmd)
+                os.system(cmd)
+                yield part_name
+
+
+    def write_part_to_file(part_list):
+        dir_path = os.path.dirname(os.path.realpath(__file__))
+        filepath_list = []
+        for part_name in part_list:
+            print(part_name)
+            path = os.path.join(dir_path, part_name)
+            filepath_list.append(path)
+
+        with open(CONCAT_FILE, 'w') as f:
+            for path in filepath_list:
+                f.write("file '{}'\n".format(path))
+        return filepath_list
+
+
+    def concat_video():
+        cmd = "ffmpeg -f concat -safe 0 -i {} -c copy output.mp4".format(CONCAT_FILE)
+        os.system(cmd)
+
+
+    def remove(filepath_list):
+        """移除中间文件"""
+        for path in filepath_list + [CONCAT_FILE]:
+            if os.path.exists(path):
+                os.remove(path)
+
+
+    def main():
+        try:
+            inputfile = sys.argv[1]
+        except KeyError:
+            print('must need input.txt')
+        partnames = list(read_input(inputfile))
+        filepath_list = write_part_to_file(partnames)
+        concat_video()
+        remove(filepath_list)
+
+
+    if __name__ == '__main__':
+        main()
+
+
+
+递归统计一个文件夹下的所有视频时长
+___________________________________________
+
+.. code-block:: python
+
+    # -*- coding: utf-8 -*-
+
+    """
+    递归统计一个目录下所有视频文件的总时长。比如笔者用来统计课程的内容总时长。
+
+    pip install moviepy
+
+    如果安装报错，尝试升级
+
+    pip install --upgrade setuptools
+
+    使用方法：
+    python compute_duration.py --path ~/Movies/ --type .mp4
+
+    参考：https://blog.csdn.net/qq_22210253/article/details/86684658
+    """
+
+    import os
+    import datetime
+    import argparse
+    from moviepy.editor import VideoFileClip
+
+
+    def main():
+        parser = argparse.ArgumentParser(
+            description='Compute Total Time of a Series of Videos')
+        parser.add_argument("--path", metavar="PATH", default=".",
+                            help="the root path of the videos(default: .)")
+        parser.add_argument("--type", metavar="TYPE", default=".mkv",
+                            help="the type of the videos(default: .mkv)")
+        args = parser.parse_args()
+        filelist = []
+        for a, b, c in os.walk(args.path):
+            for name in c:
+                fname = os.path.join(a, name)
+                if fname.endswith(args.type):
+                    filelist.append(fname)
+        ftime = 0.0
+        for file in sorted(filelist):
+            clip = VideoFileClip(file)
+            print("{}: {}秒".format(file, clip.duration))
+            ftime += clip.duration
+        print("%d seconds: " % ftime, str(datetime.timedelta(seconds=ftime)))
+
+
+    if __name__ == "__main__":
+        main()
diff --git a/python-note/flaskapi.rst b/python-note/flaskapi.rst
new file mode 100644
index 00000000..6ba1c499
--- /dev/null
+++ b/python-note/flaskapi.rst
@@ -0,0 +1,140 @@
+.. _flaskapi:
+
+
+用来测试接口的 flask server
+========================================
+
+Restful规范
+___________________________________
+
+.. code-block:: python
+
+    """
+    RESTFul url 规范，参考：http://www.pythondoc.com/flask-restful/second.html
+
+    ==========  ===============================================  =============================
+    HTTP 方法   URL                                              动作
+    ==========  ===============================================  ==============================
+    GET         http://[hostname]/todo/api/v1.0/tasks            检索任务列表
+    GET         http://[hostname]/todo/api/v1.0/tasks/[task_id]  检索某个任务
+    POST        http://[hostname]/todo/api/v1.0/tasks            创建新任务
+    PUT         http://[hostname]/todo/api/v1.0/tasks/[task_id]  更新任务
+    DELETE      http://[hostname]/todo/api/v1.0/tasks/[task_id]  删除任务
+    ==========  ================================================ =============================
+
+
+    命名规范：
+
+    # 我们使用 SomeListHandler 命名而不是复数形式方便区分，比如 VideosHandler 和 VideoHandler 不容易区分
+    """
+    class TaskListHandler(BaseHandler):
+        def get(self):
+            """获取列表"""
+            pass
+
+        def post(self):
+            """创建一条新的记录"""
+            pass
+
+
+    class TaskHandler(BaseHandler):
+        def get(self, _id):
+            """获取一条记录的信息"""
+            pass
+
+        def put(self, _id):
+            """更新一条记录的信息"""
+            pass
+
+        def delete(self, _id):
+            """删除一条记录"""
+            pass
+
+
+测试 server
+___________________________________
+
+.. code-block:: python
+
+    # pip install flask Flask-RESTful
+    from flask import Flask
+    from flask_restful import reqparse, abort, Api, Resource
+    from flask import request
+
+    app = Flask(__name__)
+    api = Api(app)
+
+    TODOS = {
+        'todo1': {'task': 'build an API'},
+        'todo2': {'task': '?????'},
+        'todo3': {'task': 'profit!'},
+    }
+
+
+    def abort_if_todo_doesnt_exist(todo_id):
+        if todo_id not in TODOS:
+            abort(404, message="Todo {} doesn't exist".format(todo_id))
+
+
+    parser = reqparse.RequestParser()
+    parser.add_argument('task')
+
+
+    # Todo
+    # shows a single todo item and lets you delete a todo item
+    class Todo(Resource):
+        def get(self, todo_id):
+            abort_if_todo_doesnt_exist(todo_id)
+            return TODOS[todo_id]
+
+        def delete(self, todo_id):
+            abort_if_todo_doesnt_exist(todo_id)
+            del TODOS[todo_id]
+            return '', 204
+
+        def put(self, todo_id):
+            args = parser.parse_args()
+            task = {'task': args['task']}
+            TODOS[todo_id] = task
+            return task, 201
+
+
+    # TodoList
+    # shows a list of all todos, and lets you POST to add new tasks
+    class TodoList(Resource):
+        def get(self):
+            return TODOS
+
+        def post(self):
+            print(request.data)
+            args = parser.parse_args()
+            todo_id = int(max(TODOS.keys()).lstrip('todo')) + 1
+            todo_id = 'todo%i' % todo_id
+            TODOS[todo_id] = {'task': args['task']}
+            return TODOS[todo_id], 201
+
+
+    class TaskList(Resource):
+        def post(self):
+            print(request.data)
+            return {"id": 1}
+
+
+    class Ping(Resource):
+        def get(self):
+            return 'pong'
+
+    ##
+    # Actually setup the Api resource routing here
+    ##
+    api.add_resource(Ping, '/')
+    api.add_resource(TodoList, '/todos')
+    api.add_resource(Todo, '/todos/<todo_id>')
+    api.add_resource(TaskList, '/tasks')
+
+
+    if __name__ == '__main__':
+        app.run(debug=True)  # port=5000
+
+
+
diff --git a/python-note/funny.rst b/python-note/funny.rst
new file mode 100644
index 00000000..8e78fef8
--- /dev/null
+++ b/python-note/funny.rst
@@ -0,0 +1,34 @@
+.. _funny:
+
+Just For Fun
+========================================
+
+.. code-block:: python
+
+    # -*- coding: utf-8 -*-
+
+    """生成 ascii 字体
+    pip install colorama termcolor pyfiglet
+
+    """
+
+    import sys
+
+    from colorama import init
+    init(strip=not sys.stdout.isatty()) # strip colors if stdout is redirected
+    from termcolor import cprint
+    from pyfiglet import figlet_format
+
+    # cprint(figlet_format('missile!', font='starwars'), 'yellow', 'on_red', attrs=['bold'])
+    cprint(figlet_format(u'docker vim', width=300))
+
+    """
+
+     ____        _   _                  __        __   _          ____       _     _
+    |  _ \ _   _| |_| |__   ___  _ __   \ \      / /__| |__      / ___|_   _(_) __| | ___
+    | |_) | | | | __| '_ \ / _ \| '_ \   \ \ /\ / / _ \ '_ \    | |  _| | | | |/ _` |/ _ \
+    |  __/| |_| | |_| | | | (_) | | | |   \ V  V /  __/ |_) |   | |_| | |_| | | (_| |  __/
+    |_|    \__, |\__|_| |_|\___/|_| |_|    \_/\_/ \___|_.__/     \____|\__,_|_|\__,_|\___|
+           |___/
+
+    """
diff --git a/python-note/index.rst b/python-note/index.rst
new file mode 100644
index 00000000..feda7ea3
--- /dev/null
+++ b/python-note/index.rst
@@ -0,0 +1,15 @@
+===========================
+个人Python笔记 🐍
+===========================
+
+
+.. toctree::
+    :glob:
+    :maxdepth: 2
+
+    crawler
+    funny
+    ffmpeg
+    flaskapi
+    pdf
+    uncurl
diff --git a/python-note/pdf.rst b/python-note/pdf.rst
new file mode 100644
index 00000000..b584bf01
--- /dev/null
+++ b/python-note/pdf.rst
@@ -0,0 +1,103 @@
+.. _pdf:
+
+Python 批量合并 pdf
+========================================
+
+.. code-block:: python
+
+    """
+    合并多个 pdf 到一个，使用方式：放到当前文件夹下运行。 pyPdf 库有点老了
+    """
+
+    import os.path
+
+    from pyPdf import PdfFileReader, PdfFileWriter # pip install pyPdf
+
+
+    def get_pdf_files(dst_dir):
+        paths = []
+        for root, dirs, files in os.walk(dst_dir):
+            for filespath in files:
+                if filespath.endswith('.pdf'):  # pdf file
+                    abspath = os.path.join(root, filespath)
+                    paths.append(abspath)
+        return paths
+
+
+    ##########################合并同一个文件夹下所有PDF文件########################
+    def merge_pdf(dst_dir, outfile, sort=True):
+        output = PdfFileWriter()
+        curpage = 0
+        pdf_paths = sorted(get_pdf_files(dst_dir)) if sort else get_pdf_files(dst_dir)
+        for each in pdf_paths:
+            print(each)
+            reader = PdfFileReader(file(each, "rb"))
+            # 如果pdf文件已经加密，必须首先解密才能使用pyPdf
+            if reader.isEncrypted == True:
+                reader.decrypt("map")
+            # 获得源pdf文件中页面总数
+            page_count = reader.getNumPages()
+            curpage += page_count
+            print(page_count)
+
+            for iPage in range(0, page_count):
+                output.addPage(reader.getPage(iPage))
+
+        print("All Pages Number:" + str(curpage))
+        outputStream = file(dst_dir + outfile, "wb")
+        output.write(outputStream)
+        outputStream.close()
+
+
+    def main():
+        merged = "all.pdf"
+        merge_pdf("./", merged)
+
+
+    if __name__ == '__main__':
+        main()
+
+
+推荐下边这个 pikepdf 库来批量操作 pdf，pyPdf 库挺久没有更新了
+
+.. code-block:: python
+
+    # -*- coding: utf-8 -*-
+
+    import os.path
+    from pikepdf import Pdf, OutlineItem  # pip install pikepdf
+
+
+    def get_pdf_files(dst_dir):
+        paths = []
+        for root, dirs, files in os.walk(dst_dir):
+            for filespath in files:
+                if filespath.endswith('.pdf'):  # pdf file
+                    abspath = os.path.join(root, filespath)
+                    paths.append(abspath)
+        return paths
+
+
+    def merge_pdf(dst_dir, outfile, sort=True):
+        # https://pikepdf.readthedocs.io/en/latest/topics/pages.html#merge-concatenate-pdf-from-several-pdfs
+        pdf_paths = sorted(get_pdf_files(dst_dir)) if sort else get_pdf_files(dst_dir)
+        pdf = Pdf.new()
+        page_count = 0
+        with pdf.open_outline() as outline:
+            for path in pdf_paths:
+                src = Pdf.open(path)
+                print("merging:" + src.filename)  # 打印一下进度
+                oi = OutlineItem(os.path.basename(src.filename), page_count)  # 增加目录
+                outline.root.append(oi)
+                page_count += len(src.pages)
+                pdf.pages.extend(src.pages)
+        pdf.save(outfile)
+
+
+    def main():
+        merged = "all.pdf"
+        merge_pdf("./", merged)
+
+
+    if __name__ == '__main__':
+        main()
diff --git a/python-note/uncurl.rst b/python-note/uncurl.rst
new file mode 100644
index 00000000..99370ccc
--- /dev/null
+++ b/python-note/uncurl.rst
@@ -0,0 +1,109 @@
+.. _uncurl:
+
+Uncurl
+========================================
+
+.. code-block:: python
+
+    import argparse
+    import json
+    import re
+    import shlex
+    from collections import OrderedDict, namedtuple
+
+    from six.moves import http_cookies as Cookie
+
+    parser = argparse.ArgumentParser()
+    parser.add_argument('command')
+    parser.add_argument('url')
+    parser.add_argument('-d', '--data')
+    parser.add_argument('-b', '--data-binary', default=None)
+    parser.add_argument('-X', default='')
+    parser.add_argument('-H', '--header', action='append', default=[])
+    parser.add_argument('--compressed', action='store_true')
+    parser.add_argument('--insecure', action='store_true')
+
+    BASE_INDENT = " " * 4
+
+    ParsedContext = namedtuple('ParsedContext', ['method', 'url', 'data', 'headers', 'cookies', 'verify'])
+
+    def parse_context(curl_command):
+        method = "get"
+
+        tokens = shlex.split(curl_command)
+        parsed_args = parser.parse_args(tokens)
+
+        post_data = parsed_args.data or parsed_args.data_binary
+        if post_data:
+            method = 'post'
+
+        if parsed_args.X:
+            method = parsed_args.X.lower()
+
+        cookie_dict = OrderedDict()
+        quoted_headers = OrderedDict()
+
+        for curl_header in parsed_args.header:
+            if curl_header.startswith(':'):
+                occurrence = [m.start() for m in re.finditer(':', curl_header)]
+                header_key, header_value = curl_header[:occurrence[1]], curl_header[occurrence[1] + 1:]
+            else:
+                header_key, header_value = curl_header.split(":", 1)
+
+            if header_key.lower() == 'cookie':
+                cookie = Cookie.SimpleCookie(header_value)
+                for key in cookie:
+                    cookie_dict[key] = cookie[key].value
+            else:
+                quoted_headers[header_key] = header_value.strip()
+
+        return ParsedContext(
+            method=method,
+            url=parsed_args.url,
+            data=post_data,
+            headers=quoted_headers,
+            cookies=cookie_dict,
+            verify=parsed_args.insecure
+        )
+
+
+    def parse(curl_command):
+        parsed_context = parse_context(curl_command)
+
+        data_token = ''
+        if parsed_context.data:
+            data_token = '{}data=\'{}\',\n'.format(BASE_INDENT, parsed_context.data)
+
+        verify_token = ''
+        if parsed_context.verify:
+            verify_token = '\n{}verify=False'
+
+        formatter = {
+            'method': parsed_context.method,
+            'url': parsed_context.url,
+            'data_token': data_token,
+            'headers_token': "{}headers={}".format(BASE_INDENT, dict_to_pretty_string(parsed_context.headers)),
+            'cookies_token': "{}cookies={}".format(BASE_INDENT, dict_to_pretty_string(parsed_context.cookies)),
+            'security_token': verify_token
+        }
+
+        return """
+    import requests
+
+    url = "http://localhost:22222"
+    url = "{url}"
+
+    resp = requests.{method}(
+        url,
+    {data_token}{headers_token},
+    {cookies_token},{security_token}
+    )
+    print(resp.text)
+    """.format(**formatter)
+
+    def dict_to_pretty_string(the_dict, indent=4):
+        if not the_dict:
+            return "{}"
+
+        return ("\n" + " " * indent).join(
+            json.dumps(the_dict, sort_keys=True, indent=indent, separators=(',', ': ')).splitlines())
diff --git a/requirements.txt b/requirements.txt
index 483a4e96..c0fe7924 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1 +1,6 @@
-sphinx_rtd_theme
+Sphinx==6.2.1
+sphinx-rtd-theme==1.2.2
+sphinx-autobuild==2021.3.14
+docutils==0.18.1
+readthedocs-sphinx-search==0.3.1
+Jinja2==3.1.2
diff --git a/skillstack/backend.rst b/skillstack/backend.rst
deleted file mode 100644
index fb392f04..00000000
--- a/skillstack/backend.rst
+++ /dev/null
@@ -1,10 +0,0 @@
-.. _backend_skill:
-
-============
-后端技能
-============
-
-
--  Python
--  Mako
--  Plim
diff --git a/skillstack/frontend.rst b/skillstack/frontend.rst
deleted file mode 100644
index 2aab94cf..00000000
--- a/skillstack/frontend.rst
+++ /dev/null
@@ -1,15 +0,0 @@
-.. _frontend_skill:
-
-==================================================
-前端库
-==================================================
-
-
--  Avalon
--  jQuery
--  jQuer-UI
--  FancyBox
--  SCSS/SASS
--  Stylus
--  CoffeeScript
--  Semantic UI
diff --git a/skillstack/hyperestraier.rst b/skillstack/hyperestraier.rst
deleted file mode 100644
index 6b6af5a3..00000000
--- a/skillstack/hyperestraier.rst
+++ /dev/null
@@ -1,59 +0,0 @@
-.. hyper_estraier:
-
-==================================================
-Hyper Estraier 简明教程
-==================================================
-
-:作者: tonghs tonghuashuai@gmail.com
-
-About
-----------------------
-Hyper Estraier is a full-text search system. You can search lots of documents for some documents including specified words. If you run a web site, it is useful as your own search engine for pages in your site. Also, it is useful as search utilities of mail boxes and file servers.
-
-http://fallabs.com/hyperestraier/
-
-目前 天使汇 网站的全文搜索部分使用 Hyper Estraier 实现。
-
-Install
-----------------------
-::
-
-    emerge hyperestraier
-
-Configuration
-----------------------
-1. 建立工作目录::
-
-    mkdir /mnt/data1/42web
-
-#. 设置路径读写权限
-#. 在工作目录下执行::
-
-    estmaster init .
-
-#. 删除工作目录下的配置文件，并执行::
-
-    ln -s ~/42web/zapp/SITE/misc/config/hyperestraier/_conf .
-
-#. 启动 hyperestraier::
-
-    estmaster start .
-
-#. 启动服务::
-
-    zapp/SITE/model/rpc/server/run.py
-    zapp/SITE/model/rpc/server/search.py
-
-#. 访问 http://域名:1978，进入 web 管理页面（默认用户名密码 admin admin）
-
-#. 建立索引数据库 ob0 ob10000 ob20000
-
-   单击 Manage Nodes，在出现的页面填写 node 信息，第一个文本框为 name，第二个文本框为 label。
-
-Other
-----------------------
-* 用户管理
-
-Select Manage Users. There are input boxes for user name, password, flags, full name and miscellaneous information at bottom. Enter following data for new user: "clint", "tnilc", "s", "Clint Eastwood" and "Dirty Harry". Flag "s" denotes user with super user privileges. User name is limited to alphanumeric characters only.
-
-详细见：http://fallabs.com/hyperestraier/nguide-en.html 中的 Administration Interface 章节
diff --git a/skillstack/rst.rst b/skillstack/rst.rst
index 93b2524f..38544228 100644
--- a/skillstack/rst.rst
+++ b/skillstack/rst.rst
@@ -244,7 +244,7 @@ REST 是松散的文本结构，使用预定义的模式描述文章的结构。
 程序引用体例
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
-可以 用 `.. code-block::` 追加各种语法高亮声明::
+可以 用 ``.. code-block::`` 追加各种语法高亮声明::
 
 
     .. code-block:: python
@@ -513,7 +513,7 @@ rST排版技巧
 上下标号
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
-有时要进行数学/化学的表示,在 html 中就需要上/下标( `<sub>` , `<sup>`) 的表达,
+有时要进行数学/化学的表示,在 html 中就需要上/下标( ``<sub>`` , ``<sup>``) 的表达,
 rST 中当然也有::
 
   H\ :sub:`2`\ O
@@ -566,9 +566,3 @@ E = mc\ :sup:`2`
     - 列表项目1    
     - 列表项目2
     - ...
-
-
-
-
-
-
diff --git a/skillstack/shell.rst b/skillstack/shell.rst
index 30ba0c85..7c1116bb 100644
--- a/skillstack/shell.rst
+++ b/skillstack/shell.rst
@@ -52,21 +52,13 @@ tree 以树形显示当前目录结构
 * tree 以树形结构显示当前目录下所有文件和目录
 * tree -d 仅显示目录
 * tree -L <num> 限制目录的最大深度
+* tree tree --du -h  同时显示文件大小
 
 ag 搜索
 
 * ag <something> 在目录所有文件中寻找 something
 * ag <something> <--python> 在所有 Python 文件中搜索 something
 
-`autojump <https://github.com/joelthelion/autojump>`
+``autojump <https://github.com/joelthelion/autojump>``
 
 * j <dir> 根据最近工作目录记录跳转到最合适的位置。
-
-
-其它批处理命令
-----------------------------------------------
-
-:ref:`42qucc`
-
-deltmp
-递归删除目录中所有 vim 临时文件。
diff --git a/skillstack/vps.rst b/skillstack/vps.rst
deleted file mode 100644
index d9f5a4fa..00000000
--- a/skillstack/vps.rst
+++ /dev/null
@@ -1,194 +0,0 @@
-.. _vps_tutorial:
-
-==================================================
-VPS 开发服务器初始配置
-==================================================
-
-:作者: 王然 kxxoling@gmail.com
-
-注：部分内容已失效
-
-
-将域名配置到 DNSPOD
------------------------
-
-首先需要注册一个 DNSPOD 账号，并拥有一个未使用的一级域名，没有的话需要在域名提供商
-（比如 `GoDaddy <http://godaddy.com>`_ ）那里购买。
-
-* 在 DNSPOD 中添加你已注册的域名信息
-* 添加一级域名记录以及泛域名记录，并指向你的 VPS IP
-* 在域名购买商那里将域名解析服务器修改为 DNSPOD 提供的域名解析服务器
-
-.. raw:: html
-
-    <script type="text/javascript">
-      document.createElement('video');document.createElement('audio');document.createElement('track');
-    </script>
-    <link href="//vjs.zencdn.net/4.6/video-js.css" rel="stylesheet">
-    <script src="//vjs.zencdn.net/4.6/video.js"></script>
-    <video id="example_video_1" class="video-js vjs-default-skin"
-      controls preload="auto" width="640" height="264"
-      data-setup='{"example_option":true}'>
-      <source src="//dn-pevc.qbox.me/DNSPOD.mp4" type='video/mp4' />
-    </video>
-
-
-配置 VPS
------------------------
-
-根据邮件提示以 root 用户登录 VPS（ssh root@your.domain），系统已为你创建了一个叫做 zz 的用户，现在为他创建密码：::
-
-	passwd zz
-
-根据提示完成后退出 ssh。
-
-接下来配置无密码登录 VPS。首次使用 ssh 密钥登录需要在本地创建 ssh 密钥：
-
-Mac & Linux: `ssh-keygen` ，该命令会在 home 目录 .ssh 文件夹中创建两个文件：
-id_rsa 和 id_rsa.pub。形式如下：
-
-将密钥拷贝到服务器：
-
-Linux ::
-
-    `ssh-copy-id zz@your.domain`
-
-Mac 下等价命令 ::
-
-    `cat ~/.ssh/id_rsa.pub | ssh zz@your.domain "mkdir ~/.ssh; cat - >> authorized_keys"`
-
-
-windows用户可以参考 :ref:`xshell` 配置xshell
-
-
-配置开发环境
-------------------------
-
-使用 zz 用户登录 VPS，并在本地生成密钥 `ssh-keygen`
-
-将 `~/.ssh/id_rsa.pub` 中全部内容（公钥）复制到 `BitBucket`。 https://bitbucket.org/account/user/youritbucket-b-id/ssh-keys/。
-
-fork `42web 源代码 <https://bitbucket.org/zuroc/42web>`_
-
-修改 ~/.hgrc 文件，在此设置全局用户名&邮箱
-
-修改 ~/42web/.hg/hgrc 文件，修改修改为如下::
-
-	[paths]
-	default = ssh://hg@bitbucket.org/your-bitbucket-id/42web
-	zsp = ssh://hg@bitbucket.org/zuroc/42web
-
-接下来将代码更新到最新（要求当前目录在 hg 版本控制的目录）::
-    hg fetch zsp
-
-如何 pull request
-
-
-运行初始化脚本
--------------------------
-
-在 ~/42web/zapp/SITE/misc/once 目录中运行脚本 ./init.sh
-（~/42web/zapp/SITE/misc/once/init.sh ）
-
-
-在 ~/42web/zapp/SITE/misc/config/_host/主机名.py 文件修改为如下内容，并注意将 HOST 修改为自己的域名::
-
-    import _env
-    from z42 import config
-
-    def prepare(o):
-        config.SMTP.SENDER = config.SMTP.USERNAME = 'postmaster@42.sendcloud.org'
-        config.SMTP.PASSWORD = '密码见Google Docs'
-        config.SMTP.HOST = 'smtpcloud.sohu.com'
-        config.HOST = "hi-acg.com"
-        config.MYSQL_PASSWORD = "密码见Google Docs"
-        config.MYSQL_HOST = "127.0.0.1"
-        config.MYSQL_USER = "zz"
-
-
-    def finish(o):
-        pass
-
-
-因为用户配置的优先级 > 主机配置 > 全局
-
-启动开发脚本，首次运行会比较缓慢。运行完成后请勿关闭此脚本::
-
-    ~/42web/zapp/SITE/dev.sh
-
-如开发过程中涉及到邮件功能，需要启另外一个脚本::
-
-    python ~/42web/zapp/SITE/model/gearman/server/run.py
-
-（如需要启动 Gearman 请输入 `sudo service gearmand restart` ）
-
-
-配置数据库
----------------------------
-
-在 `42web/zapp/SITE/misc/backup/mysql/` 目录下运行以下语句修改数据库::
-
-    mysql -h127.0.0.1 -uzz -prstfsgbcedh zz_42web < zz_42web.sql
-
-
-配置 SMTP
--------------------------
-
-修改配置文件中的邮件服务器配置。
-
-全局配置：42web/z42/config/default.py
-个人配置：42web/zapp/SITE/misc/config/_host/主机名.py
-
-如个人配置与全局配置冲突，个人配置将覆盖全局配置。可以在 42web 目录下运行 `ipython` ，并输入
-`from z42.config import SMTP; print SMTP.HOST; print SMTP.PASSWD` 测试是否覆盖配置成功。
-
-最后，修改 42web/z42/web/smtp.py 中的邮箱并运行，测试是否配置成功。
-
-
-配置 phpMyAdmin 访问域名
------------------------------------------------
-
-修改文件 `/etc/nginx/config/phpmyadmin.conf` ，将其中 server_name 修改为自己想用的二级域名，比如 admin.hi-acg.com。
-
-
-安装 RockMongo
----------------------------------------------
-
-下载 RockMongo 源代码::
-
-    sudo git clone https://github.com/iwind/rockmongo.git /var/www/rockmongo
-
-会将源代码下载在 /var/www 目录。
-
-下载依赖::
-
-    sudo emerge =dev-php/pecl-mongo-1.5.1 --autounmask-write
-    sudo etc-update
-    -3
-    y
-    sudo emerge =dev-php/pecl-mongo-1.5.1
-
-添加配置文件： `sudo vi /etc/nginx/conf/rockmongo.conf` 并添加以下内容::
-
-    server{
-        listen       80;
-        server_name  mongo.kanrss.com;       #这里修改为自己的域名
-        index  index.php;
-        root  /var/www/rockmongo;
-    
-        location ~ \.php$ {
-            root  /var/www/rockmongo;
-    
-            fastcgi_pass  127.0.0.1:9000;
-            fastcgi_index index.php;
-            include /etc/nginx/fastcgi.conf;
-            include        fastcgi_params;
-        }
-    }
-
-再修改 rockmongo 配置文件 /var/www/rockmongo/config.php 中变量 $MONGO["servers"][$i]["mongo_auth"] 和 $MONGO["servers"][$i]["control_auth"] 为 false，以配置 rockmongo 无密码登录。
-
-重新启动 php-fpm ：`sudo service php-fpm restart`
-
-Nginx 重新加载文件：`sudo service nginx reload`
-
diff --git a/sso/example.rst b/sso/example.rst
deleted file mode 100644
index 4343ddc9..00000000
--- a/sso/example.rst
+++ /dev/null
@@ -1,42 +0,0 @@
-SSO App 示例
-============
-
-注册 App
---------
-
-注册脚本如下：
-
-.. code:: python
-
-    import requests
-
-
-    def get_token():
-        host = 'b.dev'                              # App 域名
-        app_name = 'test app'                       # App 名
-        sso_host = 'sso.%s' % host                  # App 的 SSO 域名
-        sync_url = 'http://%s/sso_sync' % host      # 数据同步接口
-        login_url = 'http://%s/sso_login' % host    # 登录回调接口
-
-        r = requests.get(SSO_REGISTER_URL, params=dict(
-                         o='["%s","%s","%s","%s","%s"]'
-                         % (app_name, host, sso_host, sync_url, login_url)))
-        return r.text
-
-
-    if __name__ == '__main__':
-        print get_token()
-
-注册成功则会获得这样的结果：
-
-::
-
-    [9912683,"VlO9Yt0J_fTFRxrEONVY6s4JZCK2myxwGTeNxfw7Dh-O4cx9WsHMUvDFJQOWtKks"]
-
-其中 ``9912683`` 是应用的
-``app_id``\ ，\ ``VlO9Yt0J_fTFRxrEONVY6s4JZCK2myxwGTeNxfw7Dh-O4cx9WsHMUvDFJQOWtKks``
-是 App 的身份认证 token。
-
-身份验证
---------
-
diff --git a/sso/index.rst b/sso/index.rst
deleted file mode 100644
index c151932c..00000000
--- a/sso/index.rst
+++ /dev/null
@@ -1,11 +0,0 @@
-===================================
-SSO
-===================================
-
-
-.. toctree::
-    :glob:
-    :maxdepth: 2
-
-    *
-
diff --git a/sso/sso.rst b/sso/sso.rst
deleted file mode 100644
index 2479daac..00000000
--- a/sso/sso.rst
+++ /dev/null
@@ -1,163 +0,0 @@
-单点登录系统（SSO）使用教程
-===========================
-
-概念介绍
---------
-
-SSO（Single
-Sign-on）单一登录系统，用于实现不同软件系统中的用户信息同步。
-
-本 SSO 系统通过 JSONP
-实现跨域名登录支持，提供用户基础信息、登录历史的存储。 App
-在存储这些数据时需要向 SSO 服务器发送请求以更新数据。 App
-无需存储用户数据，用户数据存储在 SSO 上，App 只需要存储用户数据版本号。
-
-流程介绍
---------
-
-名词解释：
-
--  sso：提供 SSO 服务的服务器
--  app：使用 SSO 服务的应用（暂时仅支持网站）
--  client：客户端，通常是浏览器。
-
-开发之前
---------
-
-依赖
-~~~~
-
-1. 第三方库：\ ``jQuery1.11``\ ，\ ``json2.js``\ ，\ ``jquery.fancybox.js``\ ，\ ``avalon.js``
-2. 本地 CSS：\ ``css/_lib/fancybox*``\ ，\ ``css/SSO/*``
-3. 本地 JS/CoffeeScript
-   依赖：\ ``js/_lib/cookie.js``\ ，\ ``async.coffee``\ ，\ ``jquery_ext.coffee``\ ，\ ``jquery.poshytip.js``\ ，\ ``uploader.coffee``
-   (为个人设置提供的图片上传功能)
-
-注册应用
-~~~~~~~~
-
-在 SSO 服务器上注册应用，并提供 App 名称、App 域名，SSO
-域名（建议统一使用 ``sso.[App 域名]``\ ）、数据同步地址、登录地址。
-
-使用 GET 方法注册应用：GET
-
-::
-
-    http://sso.tech2ipo.com/rpc/app.new?o=["app_name","app_host","app_sso_host","app_sync_url","app_sso_login_url"]
-
-例如：为 b.com 注册应用的 URL 可以是这样的：
-
-::
-
-    http://sso.b.com/rpc/app.new?o=["SITE B","b.com","sso.b.com","http://b.com/rpc/sso.sync","http://b.com/rpc/sso.login"]
-
-相应的，服务器会给返回一个 ``app_id`` 以及对应的
-``app_token``\ ，作为你的身份认证标志。返回的结果类似这样：
-
-::
-
-    [9912662,"wuVsjhBuBXDwv3wpTo5itE1WwtvjFXaozBNsce52keJw5J5_sOpPe-I8Ge5YuqPK"]
-
-
-DNS 设置
-~~~~~~~~
-
-将网站的 app\_sso\_host 的 CNAME 指向 ``sso.tech2ipo.com``\ ，可以使用
-dig 命令查看 CNAME 是否设置成功。
-
-
-简单的流程图
-~~~~~~~~~~~~
-
-.. figure:: ../_image/uml.png
-   :alt: uml
-
-   uml
-
-
-身份验证
-^^^^^^^^
-
-因为存在 Client 端到 App
-端的数据更新操作，所以需要相应的身份验证操作以保证安全。sso 在设计时采用
-session 进行身份验证，
-
-简单实现
---------
-
--  `Python 示例 <https://github.com/kxxoling/ac-sso-demo>`__
-
-参数说明
---------
-
-App 需要实现注册时提交的 ``app_sync_url`` 和 ``app_sso_login_url`` 的
-GET 方法调用。
-
-
-app\_sso\_login\_url
-====================
-
-``app_sso_login_url`` 的作用是接收 SSO
-服务器的登录成功回调，并为用户设置登录状态。
-
-``app_sso_login_url`` 接收的参数为
-session、user\_info\_id、expires\_days，App 使用 app\_token decode
-session 获取 sso\_user\_id，如果成功则说明用户已登录，App 需要再根据
-sso\_user\_id 从自己的数据库中查询对应的
-app\_user\_id，并为用户设置已登录的状态，对应 cookie 和 session
-的过期时间应该设置为 expires\_days。
-
-app\_sync\_url
-====================
-
-``app_sync_url`` 提供一个数据同步接口，由 Client 端向 App
-端同步数据。数据的同步永远是单向的。
-
-``app_sync_url`` 接收的参数为
-sso\_id、user\_info\_id、mail、ico、name、sign、phone，其中仅有前两者为必需项，后四项如无则留空。
-
-注意事项
---------
-
-urlsafe base64 encode
-~~~~~~~~~~~~~~~~~~~~~
-
-由于 base64 encode 的字符在存放在 url 中时会被再次
-encode，导致数据失真，因此需要注意下 encode/decode 的实现，例如 Python
-中可以直接导入：
-
-.. code:: python
-
-    from base64 import urlsafe_b64encode, urlsafe_b64decode
-
-PHP 示例：
-
-.. code:: php
-
-    function urlsafe_b64encode($input) {
-      return strtr(base64_encode($input), '+/=', '-_,');
-    }
-
-    function urlsafe_b64decode($input) {
-      return base64_decode(strtr($input, '-_,', '+/='));
-    }
-
-配置文件
-^^^^^^^^
-
-配置见示例代码： `Python
-示例 <https://github.com/kxxoling/ac-sso-demo>`__
-
-路线图
-------
-
-其他计划开发中的功能：
-
--  多账户系统（参照 DNSPOD 和 Google）
--  IM、私信及系统通知（参照雪球）
--  友邻推荐系统（整合到通知系统）
--  各种社交账号登录（参照多说）
--  头像剪裁
--  移动网页适配以及移动应用开发的 API（登录、聊天）
--  插件、扩展（eg：鼠标移动到姓名上悬浮出现个人资料；点击姓名弹出展示卡片）
-
diff --git a/team/newbie.rst b/team/newbie.rst
deleted file mode 100644
index 215bd1ab..00000000
--- a/team/newbie.rst
+++ /dev/null
@@ -1,43 +0,0 @@
-.. _newbie:
-
-程序部 : 新人/兼职入职流程
-=====================================================================
-
-
-安装其他工具
---------------------------------------
-
-Windows 用户需要安装 `PuTTy <http://www.chiark.greenend.org.uk/~sgtatham/putty/download.html>`_
-或者 `Tunnelier <http://www.bitvise.com/download-area>`_ 以连接开发服务器，
-Mac 用户推荐安装 `item2 <http://www.iterm2.com/>`_ 。
-
-
-翻墙填写新人登记表
-----------------------------
-
-请翻墙填写 `登记表 <https://docs.google.com/forms/d/1jxsdVHvMbsZJTt66NI3xJ-uSCeE34aZcX0HswQE1S0A/viewform?usp=send_form>`_
-
-
-账号准备等
-----------------------------
-
-* 使用公司邮箱（如没有请联系 HR）注册 `Slack <https://slack.com/>`_ ，并 `修改备注名为真实姓名 <https://pevc.slack.com/account/profile>`_ 。
-* 注册 `Slack <https://slack.com/>`_ 并加入 PE.VC 团队，推荐使用公司邮箱注册，可直接加入 Team。其它邮箱需要联系我来邀请。
-* 注册 BitBucket 账号
-* 加入 PE.VC 以及程序部QQ群：11757089（公司）239812971（程序部）
-* 注册 tower.im 账号，并申请加入 `PE.VC 程序部 <https://tower.im/teams/6ebf47dc59164e0c9985fec76a8ad0e6/members>`_
-* 申请 `程序部共享文件夹 <https://drive.google.com/folderview?id=0B_jlnK8Ton9RM0k3Y0Q3dWJJNVk&usp=sharing_eil>`_ 共享权限
-* 申请加入 `程序部 GoogleGroups <https://groups.google.com/forum/#!forum/ac-coder>`_
-
-
-根据本文档配置开发环境以及熟悉常用开发工具
----------------------------------------------------------------
-
-有不清楚的地方及时向我反馈。
-
-
-常用账号及密码
-------------------------------
-
-常用账号及密码都公开在 `Google Docs <https://docs.google.com/document/d/1Fb3YuRhSc8VLCDndPAYlbaM0cb46LTgbYU7ZpfXGjkU/>`_ 上，
-如无法访问请申请权限。
diff --git a/innertools/index.rst b/tracing/index.rst
similarity index 86%
rename from innertools/index.rst
rename to tracing/index.rst
index e4a66dd9..5d00a589 100644
--- a/innertools/index.rst
+++ b/tracing/index.rst
@@ -1,5 +1,5 @@
 ===================================
-内部工具
+后台服务搭建
 ===================================
 
 
diff --git a/tracing/sentry.rst b/tracing/sentry.rst
new file mode 100644
index 00000000..ba112d68
--- /dev/null
+++ b/tracing/sentry.rst
@@ -0,0 +1,90 @@
+.. _sentry:
+
+==================================================
+Sentry 搭建
+==================================================
+
+Sentry 简介
+-----------
+Sentry 是一个 Python 社区中使用广泛的错误收集系统，可以用来收集并归类 Python 异常/Go error 等信息，在知乎/腾讯内部等都有使用。
+你可以把它类比成错误收集的 ELK，虽然它也可以展示收集的日志，不过出于性能等因素考虑，一般只用来收集错误和异常，通过官方提供的开源版本，
+可以很容易使用 docker 运行它。
+
+- 官网：https://sentry.io/welcome/
+- 优势：相比在浩如烟海的日志中搜索异常信息，使用 Sentry 你可以更加容易发现程序中的严重错误并及时修复，同时附带上有用的上下文信息帮助你排查异常。
+
+安装 Docker 和 Docker Compose
+-----------------------------
+
+.. code:: sh
+
+   curl -sSL https://get.daocloud.io/docker | sh
+
+   sudo easy_install pip
+   sudo pip3 install docker-compose==1.24.1 -i https://pypi.doubanio.com/simple 
+   sudo pip3 install docker-compose -i https://pypi.doubanio.com/simple 
+
+pip 如果安装某个包有问题：
+
+``sudo pip install requests --ignore-installed requests``
+
+DockerHub 加速器
+----------------
+
+参考：https://cloud.tencent.com/document/product/457/9113
+
+``sudo vi /etc/docker/daemon.json``
+
+.. code:: json
+
+   {
+      "registry-mirrors": [
+          "https://mirror.ccs.tencentyun.com"
+     ]
+   }
+
+重启docker
+
+.. code:: sh
+
+   sudo systemctl daemon-reload
+   sudo systemctl restart docker
+
+使用 ``docker info`` 检查 Registry Mirrors 是否正确。
+
+使用官方安装脚本
+----------------
+
+https://develop.sentry.dev/self-hosted/
+
+.. code:: sh
+
+   wget https://github.com/getsentry/onpremise/archive/20.11.0.zip
+   unzip 20.11.0.zip
+   cd onpremise-20.11.0/
+   sudo ./install.sh
+
+启动服务
+--------
+
+::
+
+   # https://develop.sentry.dev/self-hosted/#productionalizing
+   # 启动服务
+   docker-compose up -d
+
+   # 重启
+   docker-compose restart web worker cron sentry-cleanup
+
+   # 或者直接重启所有服务
+   docker-compose restart
+
+创建管理员
+----------
+
+执行 ``./install.sh``
+安装脚本的时候会提示是否创建用户，如果跳过了还可以单独创建：
+
+.. code:: sh
+
+   sudo docker-compose run --rm web createuser
diff --git a/tutorial.rst b/tutorial.rst
deleted file mode 100644
index 6bb232df..00000000
--- a/tutorial.rst
+++ /dev/null
@@ -1,166 +0,0 @@
-.. _tutorial:
-
-===================================
-应用开发教程
-===================================
-
-熟悉项目目录结构
------------------------------------
-
-目录结构 ::
-
-    -z42
-        -z42  ------配置和公用库
-        -zapp ------所有应用目录
-            -ANGELCRUNCH -------- AngelCrunch网站目录
-                -html ----------- Mako 模板和静态 HTML 目录
-                -_html ---------- Mako 模板缓存路径
-                -plim ----------- Plim 模板目录
-                -css  ----------- CSS 文件目录
-                -coffee --------- CoffeeScript
-                -javascript ----- JavaScript 库和 CoffeeScript 编译生产的文件目录
-                -model ---------- Python 对象
-                -view ----------- Python 应用视图
-                -dev.sh --------- 应用启动脚本
-            -...         -------- 其它网站目录
-
-对于 CoffeeScript、JavaScript、CSS、SASS、SCSS，其文件目录下的 `_lib` 用于存放第三方库/框架。
-
-
-启动开发服务器
------------------------------------
-
-dev.sh
-
-restart.sh
-
-
-
-创建项目所需文件
------------------------------------
-
-app 目录& `__init__.py`
-
-_env.py
-
-_route.py
-
-root.py
-
-j.py
-
-
-
-配置路由&编写视图
------------------------------------
-
-View 与 `tornado.web.RequestHandler`
-
-\@route
-
-.. code-block:: python
-
-    # root.py
-    ...
-    @route
-    class hello_world(View):
-        def get(self):
-            o = 'world'
-            self.render()
-    ...
-
-
-第一个 Mako 文件
------------------------------------
-
-.. code-block:: mako
-
-    <h1 id="hello" class="mako plim" data-height="10">Hello, ${ o }</h1>
-    <div class="d1">
-        <div class="d2">
-            <span>div</span>
-        </div>
-    </div>
-    <%include file="file.html"/>
-
-
-
-第一个 Plim 文件
------------------------------------
-
-.. code-block:: slim
-
-    h1#hello.mako.plim data-height=10
-        Hello, ${ o }
-    .d1
-        .d2
-            span
-                |div
-    -include file.html
-
-
-
-第一个 SCSS/SASS 文件
------------------------------------
-
-SCSS ::
-
-    $font-stack:    Helvetica, sans-serif;
-    $primary-color: #333;
-
-    body {
-      font: 100% $font-stack;
-      color: $primary-color;
-    }
-
-    nav {
-      ul {
-        margin: 0;
-        padding: 0;
-        list-style: none;
-      }
-
-      li { display: inline-block; }
-
-      a {
-        display: block;
-        padding: 6px 12px;
-        text-decoration: none;
-      }
-    }
-
-
-第一个 CoffeeScript 文件
------------------------------------
-
-.. code-block:: coffeescript
-
-    obj = {
-        a: 1
-        b: 2
-        c： ->
-            alert 'Hello World!'
-    }
-
-输出：
-
-.. code-block:: javascript
-
-    var obj = {
-        a: 1,
-        b: 2,
-        c: function(){
-            alert('Hello World');
-        }
-    }
-
-
-
-重启开发服务器
------------------------------------
-
-添加 CoffeeScript/JavaScript/CSS/SASS/SCSS 文件后需要手动重启服务器：
-
-1. 结束当前 dev.sh： `Ctrl-c`
-
-2. 运行 dev.sh
