From 0b7c2cbc999cd19718543e1a1f654e7583854a08 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 23 May 2019 11:58:46 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E8=83=A1=E6=80=9D=E4=B9=B1?= =?UTF-8?q?=E6=83=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 + index.html | 16 +- src/md/2017-06-05-sql-basic.md | 2370 ++++++++--------- ...9-\345\233\236\345\244\264\347\234\213.md" | 122 +- ...2017-11-25-\351\235\242\350\257\225ofo.md" | 90 +- ...42\350\257\225\346\226\227\351\261\274.md" | 240 +- src/md/2018-01-19-ISIS-after-read.md | 130 +- ...54\347\232\204\345\214\272\345\210\253.md" | 76 +- src/md/2018-02-01-golang-concurrent.md | 326 +-- src/md/2018-02-02-golang-basic.md | 240 +- src/md/2018-02-02-golang-controlflow.md | 228 +- src/md/2018-02-02-golang-method.md | 404 +-- src/md/2018-02-02-golang-moretype.md | 530 ++-- src/md/2018-02-04-golang-reflect.md | 506 ++-- src/md/2018-02-05-golang-flequenct-command.md | 408 +-- src/md/2018-02-08-golang-make-dir-problem.md | 344 +-- ...42\350\257\225\345\220\216\346\204\237.md" | 128 +- .../2018-02-10-Golang-defer-panic-recover.md | 602 ++--- ...32\344\274\232\345\220\216\346\204\237.md" | 58 +- src/md/2018-02-13-golang-channel.md | 580 ++-- ...2018-02-17-golang-variable-memory-order.md | 474 ++-- src/md/2018-02-26-golang-slice-operation.md | 496 ++-- ...02-27-the-go-programing-languange-note1.md | 1018 +++---- src/md/2018-03-02-bitmap-realize-in-golang.md | 220 +- ...03-03-the-go-programming-language-note2.md | 256 +- ...03-04-the-go-programming-language-note3.md | 908 +++---- ...03-05-the-go-programming-language-note4.md | 368 +-- src/md/2018-03-07-golang-unsafe.md | 338 +-- src/md/2018-03-08-why-golang.md | 84 +- src/md/2018-03-21-mysql-protocol.md | 444 +-- src/md/2018-04-01-utf8-in-golang.md | 884 +++--- ...05\345\255\230\345\257\271\351\275\220.md" | 466 ++-- src/md/2018-04-04-super-in-python.md | 756 +++--- ...47\345\255\220\346\225\260\347\273\204.md" | 200 +- ...20\344\270\262\351\227\256\351\242\230.md" | 586 ++-- .../2018-06-10-\346\200\273\347\273\223.md" | 414 +-- ...36\344\271\240\346\200\273\347\273\223.md" | 270 +- ...53\346\216\222\346\200\235\350\267\257.md" | 428 +-- ...1\345\244\232\345\260\221\344\270\2521.md" | 262 +- ...55\345\244\247\345\260\217\347\253\257.md" | 214 +- src/md/2018-09-16-50GB_URL.md | 270 +- src/md/2018-09-22-knapsack.md | 748 +++--- ...13\346\213\233\347\273\223\346\235\237.md" | 122 +- src/md/2018-10-10-RWLock-with-CAS.md | 482 ++-- ...02\345\270\270\345\244\204\347\220\206.md" | 226 +- ...54\347\247\221\346\257\225\344\270\232.md" | 27 + ...76\346\200\247\351\227\256\351\242\230.md" | 116 +- 47 files changed, 9253 insertions(+), 9224 deletions(-) create mode 100644 "src/md/2019-05-22-\346\234\254\347\247\221\346\257\225\344\270\232.md" diff --git a/README.md b/README.md index 807c897..7b73267 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,8 @@ ## 文章 +[本科毕业](./src/md/2019-05-22-本科毕业.md) + [异常处理机制原理](./src/md/2018-10-10-异常处理.md) [读写锁的实现](./src/md/2018-10-10-RWLock-with-CAS.md) diff --git a/index.html b/index.html index 1dc0ad6..572f829 100644 --- a/index.html +++ b/index.html @@ -1,9 +1,9 @@ - - - - - - -

Redirect....

- + + + + + + +

Redirect....

+ \ No newline at end of file diff --git a/src/md/2017-06-05-sql-basic.md b/src/md/2017-06-05-sql-basic.md index 3d05700..2e20f26 100644 --- a/src/md/2017-06-05-sql-basic.md +++ b/src/md/2017-06-05-sql-basic.md @@ -1,1185 +1,1185 @@ ---- -layout: post -title: "sql 基础" -date: 2017-06-05 09:00:05 +0800 -categories: sql ---- - -[SQL 快速参考](http://www.runoob.com/sql/sql-quickref.html) - -# SELECT - -## SELECT DISTINCT - -在一个表中,一个列可能包含多个重复中值,通过 DISTINCT 仅仅列出不同的值 - -SELECT DISTINCT column_name, column_name FROM table_name; - - - -## WHERE 子句中的运算符 - -+ = 等于 - -+ <> 不等于 - -+ `> 大于 - -+ < 小于 - -+ `>= 大于等于 - -+ <= 小于等于 - -+ BETWEEN 在某个范围内 - -+ LIKE 搜索某种模式 - -+ IN 指定针对某个列的多个可能值 - - - -## SELECT TOP - -用于规定要返回的记录的数目,尤其对于拥有大量记录的大型表非常有用。 - -```SQL - -# SQL Server - -SELECT TOP number | percent column_name from table_name; - -# MySQL - -SELECT column_name from table_name LIMIT number; - -# Oracle - -SELECT column_name from table_name WHERE ROWNUM <= number; - -``` - - - -## LIKE - -LIKE 操作符用于在 WHERE 子句中搜索列中的特定模式 - -其中可以使用 % 通配符 - -```SQL - -SELECT * FROM table_name WHERE column_name LIKE '%key%"'; - -SELECT * FROM table_name WHERE column_name NOT LIKE 'xx'; - -``` - - - -## REGEXP - -通过使用 REGEXP 关键字可以使用正则表达式匹配 - -```SQL - -SELECT * from table_name WHERE column_name REGEXP 'regex_expression'; - -``` - - - -## IN - -IN 操作符可以在 WHERE 子句判断是否在一个集合中 - -```SQL - -SELECT * FROM table_name WHERE column_name (NOT) IN (value1, value2, ..., valueN); - -``` - -## AND OR - -AND 关键字等价与逻辑 && - -OR 关键字等价与逻辑 || - -```SQL - -SELECT * FROM table_name WHERE expression1 AND | OR expression2; - -``` - - - -## BETWEEN - -BETWEEN 关键字可以过滤值在某个一个范围内 - -值的范围可以是数值、文本、日期 - -```SQL - -SELECT * FROM table_name WHERE column_name (NOT) BETWEEM value1 AND value2; - -# 文本 - -SELECT * FROM table_name WHERE name BETWEEN 'A' AND 'H'; - -# 时间 - -SELECT * FROM table_name WHERE date BETWEEN '2017-02-26' AND now(); - -``` - -> 请注意,在不同的数据库中,BETWEEN 操作符会产生不同的结果! -> 在某些数据库中,BETWEEN 选取介于两个值之间但不包括两个测试值的字段。 -> 在某些数据库中,BETWEEN 选取介于两个值之间且包括两个测试值的字段。 -> 在某些数据库中,BETWEEN 选取介于两个值之间且包括第一个测试值但不包括最后一个测试值的字段。 -> 因此,请检查您的数据库是如何处理 BETWEEN 操作符 - - -## 时间 - -时间格式可以使用文本插入,也可以使用 now() 等时间函数插入 - -```SQL - -INSERT INTO time_tabel (date) values ('2017-02-28'); - -INSERT INTO time_table (date) values (now()); - -``` - - - -## JOIN - -> 在所有的 JOIN 子句中,如果没有加 GROUP BY | ORDER BY 等排序命令,得到的结果均是以右表原本的记录的顺序,一一与左表进行匹配 - - - -## INNER JOIN - -使用左表去依次匹配右表的记录,只显示成功匹配的记录 - -![](http://www.runoob.com/wp-content/uploads/2013/09/img_innerjoin.gif) - - - -```SQL - -SELECT * FROM w.name, a.date FROM websites w JOIN access_log ON w.id=a.site_id ORDER a.count; - -``` - - - -## LEFT JOIN - -> LEFT JOIN 关键字从左表(table1)返回所有的行,即使右表(table2)中没有匹配。如果右表中没有匹配,则结果为 NULL。 - -![](http://www.runoob.com/wp-content/uploads/2013/09/img_leftjoin.gif) - - - -![](http://www.runoob.com/wp-content/uploads/2013/09/left-join1.jpg) - - - -## RIGHT JOIN - -> RIGHT JOIN 关键字从右表 table2 返回所有的行,即使左表中没有与之匹配。如果左表中没有与之匹配,则返回 NULL。 - -![](http://www.runoob.com/wp-content/uploads/2013/09/img_rightjoin.gif) - - - -![](http://www.runoob.com/wp-content/uploads/2013/09/right-join1.jpg) - - - -## FULL OUTER JOIN - -FULL OUTER JOIN 相当于是 LEFT JOIN + RIGHT JOIN。返回左表和右表的所有结果,如果左表中的数据在右表中没有与之对应的就返回 NULL,如果右表中的数据在左表没有与之对应的就返回 NULL。 - -![](http://www.runoob.com/wp-content/uploads/2013/09/img_fulljoin.gif) - - - -> **MySQL 不支持 FULL OUTER JOIN。** - - - -## UNION - -UNION 操作符合并两个或者多个 SELECT 语句的结果。 - -> 注意:UNION 内部的每个 SELECT 语句必须拥有相同数量的列。咧业必须拥有相似的数据类型(经过尝试大多数都可以转化为字符串类型)。 - -```SQL - -SELECT column_name FROM table1 - -UNION - -SELECT column_name FROM tables; - -``` - -![](http://www.runoob.com/wp-content/uploads/2013/09/union1.jpg) - -> 注释:默认的,UNION 操作符会选取不同的值。如果允许重复,请使用 UNION ALL - -```SQL - -SELECT column_name FROM table1 - -UNION ALL - -SELECT column_name FROM table2; - -``` - -![](http://www.runoob.com/wp-content/uploads/2013/09/union2.jpg) - -> 注释:UNION 结果集中的列名总是等于 UNION 中第一个 SELECT 语句的列名。 - - - -## MySQL 复制数据到新表 - -```SQL - -CREATE TABLE new_table SELECT column_name (AS new_column_name) FROM old_table WHERE expression; - -``` - -通过 AS 语句来修改新表中字段名 - -> 复制数据只是单纯地复制数据以及数据类型到新表,但是各种外健主键约束等需要手动加。 - - - -## SELECT INTO - -把从一个表中的查询结果存储到新表中 - -可以使用 AS 子句来应用新名称 - -```SQL - -SELECT column_name (AS new_column_name) - -INTO newtable - -FROM table1; - -``` - -> MySQL 并不支持 SELECT ... INTO 语句。 - -> 但是支持 INSERT INTO ... SELECT - - - -## INSERT INTO ... SELECT - -```SQL - -INSERT INTO table2 - -SELECT column_name FROM table1; - -# 或者指定列 - -INSERT INTO table2 (column_name) - -SELECT column_name FROM table1; - -``` - - - -## CREATE DATABASE - -创建数据库 - -```SQL - -CREATE DATABASE dbname; - -``` - - - -## DROP DATABASE - -删除数据库 - -```SQL - -DROP DATABASE dbname; - -``` - -## UNIQUE - -```SQL - -CREATE TABLE Person( - -P_id INT NOT NULL, - -LastName VARCHAR(255) NOT NULL, - -FirstName VARCHAR(255), - -Address VARCHAR(255), - -City VARCHAR(255), - -# 在创建表时添加唯一约束 - -CONSTRAINT uc_PersonID UNIQUE(P_id, LastName) - -); - - - -# 或者直接修改表 - -ALTER TABLE Person ADD CONSTRAINT UNIQUE(P_id, LastName); - -``` - -撤销 UNIQUE 约束 - -+ MySQL: - -```SQL - -ALTER TABLE Person DROP INDEX uc_PersonID; - -``` - -+ SQL Server / Oracle - -```SQL - -ALTER TABLE Person DROP CONSTRAINT uc_PersonID; - -``` - - - -## PRIMARY KEY - -PRIMARY KEY = NOT NULL + UNIQUE - -```SQL - -# 创建表时声明主键 - -CREATE TABLE Person( - -P_id INT NOT NULL, - -LastName VARCHAR(255) NOT NULL, - -FirstName VARCHAR(255), - -Address VARCHAR(255), - -City VARCHAR(255), - -# pk_PersonID 是主键,其有 P_id + LastName 组成 - -CONSTRAINT pk_PersonID PRIMARY KEY (P_id, LastName) - -); - - - -# 在表创建后加入主键 - -ALTER TABLE Person ADD CONSTRAINT pk_PersonID PRIMARY KEY(P_id, LastName); - -``` - -撤销 PRIMARY KEY 约束 - -**MySQL** - -```SQL - -# PRIMARY KEY 唯一,所以无需指定 - -ALTER TABLE Person DROP PRIMARY KEY; - -``` - -**SQL Server / Oracle** - -```SQL - -ALTER TABLE Person DROP CONSTRAINT pk_PersonID; - -``` - - - -## SHOW CREATE TABLE - -查看表信息 - -```SQL - -SHOW CREATE TABLE table_name; - -``` - - - -## FORENGN KEY - -FORENGN KEY 约束用于预防破坏表之间连接的行为。 - -FORENGN KEY 约束能防治非法数据插入外健列,因为它必须是指向的那个表中的值之一。 - -**MySQL** - -```SQL - -CREATE TABLE orders( - -id INT NOT NULL PRIMARY KEY, - -orderNo INT NOT NULL, - -p_id int, - -# p_id 连接到 persons 表的 id - -FOREIGN KEY (p_id) REFERENCES persons(id) - -); - -``` - -**SQL Server | Oracle** - -```SQL - -CREATE TABLE Orders -( -id int NOT NULL PRIMARY KEY, -orderNo int NOT NULL, -p_id int FOREIGN KEY REFERENCES persons(p_id) -); - -``` - -如需命名 FOREIGN KEY 约束,并定义多个列的 FOREIGN KEY 约束 - -```SQL - -CREATE TABLE orders( - -id INT NOT NULL, - -orderNo INT NOT NULL, - -p_id INT, - -PRIMARY KEY (id), - -# 外健名称为 fk_perOrders - -CONSTRAINT fk_perOrders FOREIGN KEY (p_id) REFERENCES persons(id) - -); - -``` - -添加 FOREIGN KEY 约束 - -**MySQL** - -```SQL - -ALTER TABLE orders ADD FORENGN KEY (p_id) REFERENCES persons(id); - -``` - -**MySQL | SQL Server | Oracle** - -```SQL - -ALTER TABLE orders ADD CONSTRAINT foreign_key_name FOREIGN KEY (p_id) - - REFERENCES persons(id); - -``` - -撤销 FOREIGN KEY 约束 - -> `SHOW CREATE TABLE orders;` 可以查看对应的外健名 - - - -**MySQL** - -```SQL - -ALTER TABLE orders DROP FOREIGN KEY foreign_key_name; - -``` - -**SQL Server | Oracle** - -```SQL - -ALTER TABLE orders DROP CONSTRAINT foreign_key_name; - -``` - - - -## CHECK - -“所有的存储引擎均对CHECK子句进行分析,但是忽略CHECK子句。” -The CHECK clause is parsed but ignored by all storage engines. - -```SQL - -CREATE TABLE persons( - -id NOT NULL, - -name VARCHAR(255) NOT NULL, - -ADD CONSTRAINT chk_person CHECK (id > 0 AND name <> 'xxx' ) - -); - -# 在创建表后添加 CHECK 约束 - -ALTER TABLE persons ADD CONSTRAINT chk_person CHECK(id > 0 AND name <> 'xxx'); - -``` - - - -## DEFAULT - -DEFAULT 约束可以跟的是一个函数,如 now(); - -在创建表后添加 DEFAULT 约束 - -**MySQL | SQL Server** - -```SQL - -ALTER TABLE persons - -ALTER city SET DEFAULT 'New York'; - -``` - -**Oracle** - -```SQL - -ALTER TABLE persons - -MODIFY city DEFAULT 'New York'; - -``` - -撤销 DEFAULT 约束 - -```SQL - -ALTER TABLE persons - -ALTER COLUMN city DROP DEFAULT; - -``` - - - -## INDEX - -CREATE INDEX 语句在表中创建索引,再不读取整个表的情况下,索引使书库库应用程序可以更快地查找到数据。 - -> 注释:更新一个包含索引的表需要比一个没有索引的表花费更多的时间,所以只在常常被所搜的列上创建索引。 - - - -在表中创建一个简单索引,允许使用重复的值。 - -```SQL - -CREATE INDEX index_name ON table_name (column_name); - -``` - -在表中创建一个唯一的索引,两个行不能拥有相同的索引值。 - -```SQL - -CREATE UNIQUE INDEX index_name ON table_name (column_name); - -``` - -如果需要索引不止一个列,可以如下: - -```SQL - -CREATE INDEX index_name ON table_name (column_name_1, column_name_2); - -``` - -撤销 INDEX - -> index_name 可以通过 `SHOW CREATE TABLE table_name;` 查得 - -```SQL - -DROP INDEX index_name ON table_name; - -# MySQL 上还可以这样: - -ALTER TABLE table_name DROP INDEX index_name; - -``` - - - -## 删除表中的所有数据,并且不删除表 - -+ 方法1 - -```SQL - -DELETE FROM table_name; - -``` - -+ 方法2 - -```SQL - -TURNCATE TABLE table_name; - -``` - - - -## 对表中的列进行操作 - -1. 添加列 - -```SQL - -ALTER TABLE table_name ADD column_name datatype; - -``` - -2.删除列 - -```SQL - -ALTER TABLE table_name DROP COLUMN column_name; - -``` - -3.修改列 - -```SQL - -ALTER TABLE table_name MODIFY column_name datatype; - -``` - - - -## 修改 AUTO_INCREMENET 序列的起始值 - -**MySQL** - -```SQL - -ALTER TABLE table_name AUTO_INCREMENT=start_number; - -``` - -**SQL Server** - -```SQL - -CREATE TABLE persons( - -# start_number 起始值 interval_number 间隔 - -id INT IDENTITY(start_number, interval_number) PRIMARY KEY - -); - -``` - -**Oracle** - -需要创建 SEQUENCE 对象 - -```SQL - -CREATE SEQUENCE seq - -MINVALUE 1 - -START WITH 1 - -INCREMENT BY 1 - -# 缓存10个值提高性能 - -CACHE 10 - -``` - -插入SQL - -```SQL - -INSERT INTO table_name(id, column1, column2) VALUES (seq.nextval, xxx, xxx); - -``` - - - -## 视图 VIEW - -视图是基于SQL语句的结果集的可视化表 - -创建视图 - -```SQL - -CREATE VIEW view_name AS - -SELECT column_name(s) FROM table1; - -``` - -调用视图 - -```SQL - -SELECT * FROM view_name; - -``` - -修改视图 - -```SQL - -CREATE OR REPLACE VIEW view_name AS - -SELECT column(s) FROM table1; - -``` - -撤销视图 - -```SQL - -DROP VIEW view_name; - -``` - - - -## Date 函数 - -MySQL Date 函数 - - -| 函数 | 功能 | 实例 | -| :----------------------------------------------------: | :------------------------------------------------------------------: | --------------------------------------------: | -| NOW() | 返回当前日期和时间 | 2017-02-26 16:23:16 | -| CURDATE() | 返回当前日期 | 2017-02-26 | -| CURTIME() |返回当前时间 | 16:23:16 | -| DATE(date) | 提取日期或者日期/时间表达式的日期部分 | DATE(NOW()) ==> 16:23:16 | -| EXTRACT(unit FROM date)|通过unit指定返回date中年/天/周/日/时/分/秒等| EXTREACT(YEAR FROM now()) ==> 2017| -| DATE_ADD(date, INTERVAL expr type) | 向日期添加指定的时间间隔 | DATE_ADD(NOW(), INTERVAL 1 DAY) ==> 2017-02-27 16:23:16| -| DATE_SUB(date, INTERVAL expr type) | 向日期减去指定的时间间隔 | DATE_SUB(NOW(), INTERVAL 1 DAY) ==> 2017-02-26 16:23:16| -| DATEDIFF(date1, date2) | 返回 date1 - date2 的天数 | DATEDIFF('2017-02-27', '2017-02-28') ==> -1 | -| DATE_FORMAT(date, format) | 以不同的格式输出日期(自定义) | DATE_FORMAT('2017-02-28 16:23:16', '%b %d %Y %h:%i %p') ==> FEB 26 2017 4:23 pm| - - - -**MySQL 日期格式** - -| 类型 | 格式 | -|-------------------|:----------------------------:| -| DATE | YYYY-MM-DD | -| DATETIME | YYYY-MM-DD HH:MM:SS | -| TIMESTAMP | YYYY-MM-DD HH:MM:SS | -| YEAR | YYYY 或 YY | - - - -> 2017-02-26 与 2017-02-26 00:00:00 并不会匹配 - -> 提示:如果希望查询简单且容易维护,请不要将日期中使用时间部分。 - - - -## NULL - -NULL 值与其他值不一样,NULL 无法使用运算符来测试 NULL 值,比如:= , < , <> - -判断某个值是否是 NULL 使用 IS NULL - -判断某个值是否不是 NULL 使用 IS NOT NULL - -```SQL - -# IS NULL - -SELECT * FROM persons WHERE city IS NULL; - -# IS NOT NULL - -SELECT * FROM persons WHERE city IS NOT NULL; - -``` - -> 部分函数、数学或者逻辑运算与 NULL 运算后会得到 NULL 值。 - - - -如果我们希望把 NULL 在运算中用某个数据来代替 ,我们可以这样做: - -**SQL Server** - -```SQL - -SELECT ISNULL(NULL, 0); # return 0 - -``` - -**Oracle** - -```SQL - -SELECT NVL(NULL, ' '); # return ' ' - -``` - -**MySQL** - -```SQL - -SELECT IFNULL(NULL, 1); # return 1 - -# OR - -SELECT COALESCE(NULL, 'ABC'); # return 'abc' - -``` - - - -## SQL 数据类型 - -SQL 通用数据类型 - - - -| 数据类型 | 描述| -|-------------|:--------:| -|CHARACTER(n) | 字符/字符串。固定长度 n。 | -|VARCHAR(n) 或 CHARACTER VARYING(n) | 字符/字符串。可变长度。最大长度 n。 | -|BINARY(n) | 二进制串。固定长度 n。 | -|BOOLEAN | 存储 TRUE 或 FALSE 值 | -|VARBINARY(n) 或 BINARY VARYING(n) | 二进制串。可变长度。最大长度 n。 | -|INTEGER(p) | 整数值(没有小数点)。精度 p。 | -|SMALLINT | 整数值(没有小数点)。精度 5。 | -|INTEGER | 整数值(没有小数点)。精度 10。 | -|BIGINT| 整数值(没有小数点)。精度 19。 | -|DECIMAL(p,s) | 精确数值,精度 p,小数点后位数 s。例如:decimal(5,2) 是一个小数点前有 3 位数小数点后有 2 位数的数字。 | -|NUMERIC(p,s) | 精确数值,精度 p,小数点后位数 s。(与 DECIMAL 相同) | -|FLOAT(p) | 近似数值,尾数精度 p。一个采用以 10 为基数的指数计数法的浮点数。该类型的 size 参数由一个指定最小精度的单一数字组成。 | -|REAL| 近似数值,尾数精度 7。 | -|FLOAT| 近似数值,尾数精度 16。 | -|DOUBLE PRECISION| 近似数值,尾数精度 16。 | -|DATE| 存储年、月、日的值。 | -|TIME| 存储小时、分、秒的值。 | -|TIMESTAMP| 存储年、月、日、小时、分、秒的值。 | -|INTERVAL| 由一些整数字段组成,代表一段时间,取决于区间的类型。 | -|ARRAY| 元素的固定长度的有序集合 | -|MULTISET| 元素的可变长度的无序集合 | -|XML| 存储 XML 数据 | - - - -**SQL 数据类型在不同平台的区别 ** - - - -|数据类型| Access| SQLServer| Oracle| MySQL| PostgreSQL| -|-----------|:-------------:|:----------------:|:-----------:|:-----------:|:-------------------| -|boolean| Yes/No |Bit| Byte| N/A| Boolean| -|integer| Number (integer) |Int| Number| Int Integer| Int Integer | -|float| Number (single) |Float| Real| Number| Float| Numeric| -|currency| Currency| Money| N/A| N/A| Money| -|string (fixed)| N/A| Char| Char| Char| Char| -|string (variable)| Text (<256) Memo (65k+) |Varchar | Varchar Varchar2| Varchar| Varchar| -|binary object |OLE Object Memo| Binary (fixed up to 8K) Varbinary (<8K) Image (<2GB) | Long Raw |Blob Text | Binary Varbinary | - - - -## MySQL 数据类型 - -MySQL 主要有三种类型:Text(文本) 、 Number(数字) 、 Date/Time (日期/时间) - -**Text** - - - -|数据类型| 描述| -|------------|:-----------| -|CHAR(size)| 保存固定长度的字符串(可包含字母、数字以及特殊字符)。在括号中指定字符串的长度。最多 255 个字符。 | -|VARCHAR(size)| 保存可变长度的字符串(可包含字母、数字以及特殊字符)。在括号中指定字符串的最大长度。最多 255 个字符。**注释:如果值的长度大于 255,则被转换为 TEXT 类型。**| -|TINYTEXT| 存放最大长度为 255 个字符的字符串。 | -|TEXT| 存放最大长度为 65,535 个字符的字符串。 | -|BLOB| 用于 BLOBs(Binary Large OBjects)。存放最多 65,535 字节的数据。 | -|MEDIUMTEXT| 存放最大长度为 16,777,215 个字符的字符串。 | -|MEDIUMBLOB| 用于 BLOBs(Binary Large OBjects)。存放最多 16,777,215 字节的数据。 | -|LONGTEXT| 存放最大长度为 4,294,967,295 个字符的字符串。 | -|LONGBLOB| 用于 BLOBs (Binary Large OBjects)。存放最多 4,294,967,295 字节的数据。 | -|ENUM(x,y,z,etc.)| 允许您输入可能值的列表。可以在 ENUM 列表中列出最大 65535 个值。如果列表中不存在插入的值,则插入空值。注释:这些值是按照您输入的顺序排序的。可以按照此格式输入可能的值: ENUM('X','Y','Z')| - |SET| 与 ENUM 类似,不同的是,SET 最多只能包含 64 个列表项且 SET 可存储一个以上的选择。 | - - - -**Number** - - - -|数据类型| 描述| -|-----------|:---------| -|TINYINT(size)| -128 到 127 常规。0 到 255 无符号*。在括号中规定最大位数。 | -|SMALLINT(size)| -32768 到 32767 常规。0 到 65535 无符号*。在括号中规定最大位数。 | -|MEDIUMINT(size)| -8388608 到 8388607 普通。0 to 16777215 无符号*。在括号中规定最大位数。 | -|INT(size)| -2147483648 到 2147483647 常规。0 到 4294967295 无符号*。在括号中规定最大位数。 | -|BIGINT(size)| -9223372036854775808 到 9223372036854775807 常规。0 到 18446744073709551615 无符号*。在括号中规定最大位数。 | -|FLOAT(size,d)| 带有浮动小数点的小数字。在 size 参数中规定最大位数。在 d 参数中规定小数点右侧的最大位数。 | -|DOUBLE(size,d) |带有浮动小数点的大数字。在 size 参数中规定最大位数。在 d 参数中规定小数点右侧的最大位数。 | -|DECIMAL(size,d)| 作为字符串存储的 DOUBLE 类型,允许固定的小数点。在 size 参数中规定最大位数。在 d 参数中规定小数点右侧的最大位数。 | - -> 这些整数类型拥有额外的选项 UNSIGNED。通常,整数可以是负数或正数。如果添加 UNSIGNED 属性,那么范围将从 0 开始,而不是某个负数。 - - - -**Date** - - - -|数据类型| 描述| -|-----------|:----------| -|DATE()| 日期。格式:YYYY-MM-DD **注释:**支持的范围是从 '1000-01-01' 到 '9999-12-31'| -|DATETIME()| *日期和时间的组合。格式:YYYY-MM-DD HH:MM:SS **注释:**支持的范围是从 '1000-01-01 00:00:00' 到 '9999-12-31 23:59:59'| -|TIMESTAMP() |*时间戳。TIMESTAMP 值使用 Unix 纪元('1970-01-01 00:00:00' UTC) 至今的秒数来存储。格式:YYYY-MM-DD HH:MM:SS **注释:**支持的范围是从 '1970-01-01 00:00:01' UTC 到 '2038-01-09 03:14:07' UTC| -|TIME()| 时间。格式:HH:MM:SS **注释:**支持的范围是从 '-838:59:59' 到 '838:59:59'| -|YEAR()| 2 位或 4 位格式的年。**注释:**4 位格式所允许的值:1901 到 2155。2 位格式所允许的值:70 到 69,表示从 1970 到 2069。| - - > 即便 DATETIME 和 TIMESTAMP 返回相同的格式,它们的工作方式很不同。在 INSERT 或 UPDATE 查询中,TIMESTAMP 自动把自身设置为当前的日期和时间。TIMESTAMP 也接受不同的格式,比如 YYYYMMDDHHMMSS、YYMMDDHHMMSS、YYYYMMDD 或 YYMMDD。 - - - -# SQL 函数 - - - -## SQL Aggregate 函数 - -+ AVG() - 返回平均值 - -`SELECT site_id, count FROM access_log WHERE count > (SELECT AVG(count) FROM access_log);` - -+ COUNT() - 返回行数 - -+ FIRST() - 返回第一条记录 **注释:**只有 MS Access 支持 FIRST() - -+ LAST() - 返回最后一条记录 **注释:**只有 MS Access 支持 LAST() - -+ MAX() - 返回最大值 - -`SELECT MAX(column_name) FROM table_name;` - -+ MIN() - 返回最小值 - - `SELECT MIN(column_name) FROM table_name;` - -+ SUM() - 返回总和 - - `SELECT SUM(column_name) FROM table_name;` - - - -## SQL Scalar 函数 - -+ UCASE() - 将某个字段转换为大写 - - `SELECT UCASE(column_name) FROM table_name;` - -+ LCASE() - 将某个字段转换为小写 - - `SELECT LCASE(column_name) FROM table_name;` - -+ MID() - 从某个文本字段提取字符 - -`SELECT MID('WORK HARD', 1, 4); # return WORK` - -**注释:**在 MySQL 中是前闭后闭, 字符串是从第 **1** 位开始算 - -+ LEN() - 返回某个文本字段的长度 - -**注释:**在MySQL 中是 LENGTH(),一个汉字长度为3,一个英文长度为1 - -+ ROUND() - 对某个数值字段进行指定小数位数的四舍五入 - -`SELECT ROUND(column_name,decimals) FROM table_name;` - -**注释:** ROUND 返回值被转化为一个 BIGINT! - -+ NOW() - 返回当前的系统日期和时间 - -+ FORMAT() - 格式化某个字段的显示方式 - - - -### COUNT(column_name) - -```SQL - -SELECT COUNT(column_name) FROM table1; - -``` - -返回指定列的数目,NULL 不计数 - -```SQL - -SELECT COUNT(DISTINCT column_name) FROM table1; - -``` - -返回指定列不同值的数目 - - - - - -### 只需要第一条数据 - -**SQL Server** - -```SQL - -SELECT TOP 1 column_name FROM table1 ORDER BY column_name ASC; - -``` - -**MySQL** - -```SQL - -SELECT column_name FROM table1 ORDER BY column_name ASC - -LIMIT 1; # LIMIT number 指定最多只返回多少条记录 - -``` - -**Oracle** - -```SQL - -SELECT column_name FROM table1 ORDER BY column_name ASC - -WHERE ROWNUM <= 1; - -``` - - - -### 只需要最后一条记录 - -**SQL Server** - -```SQL - -SELECT TOP 1 column_name FROM table - -ORDER BY column_name DESC; - -``` - -**MySQL** - -```SQL - -SELECT column_name FROM table1 ORDER BY column_name DESC - -LIMIT 1; - -``` - -**Oracle** - -```SQL - -SELECT column_name FROM table1 ORDER BY column_name DESC - -WHERE ROWNUM <= 1; - -``` - - - -## GROUP BY - -GROUP BY 语句用于结合聚合函数,根据一个或多个列对结果集进行分组。 - -```SQL - -SELECT column_name, aggregate_function(column_name) -FROM table_name -WHERE column_name operator value - -# 相同的 column_name 的行将会被归为一行 -GROUP BY column_name; - -``` - -![](http://www.runoob.com/wp-content/uploads/2013/09/groupby2.jpg) - - - -## HAVING - -SQL 中增加 HAVING 子句的原因是:WHERE 关键字无法与聚合函数一起使用,HAVING 子句就是用来与聚合函数一起使用的。 - -![](http://www.runoob.com/wp-content/uploads/2013/09/having2.jpg) - - - -## DATE_FORMAT() - -![](http://www.runoob.com/wp-content/uploads/2013/09/formate1.jpg) - +--- +layout: post +title: "sql 基础" +date: 2017-06-05 09:00:05 +0800 +categories: sql +--- + +[SQL 快速参考](http://www.runoob.com/sql/sql-quickref.html) + +# SELECT + +## SELECT DISTINCT + +在一个表中,一个列可能包含多个重复中值,通过 DISTINCT 仅仅列出不同的值 + +SELECT DISTINCT column_name, column_name FROM table_name; + + + +## WHERE 子句中的运算符 + ++ = 等于 + ++ <> 不等于 + ++ `> 大于 + ++ < 小于 + ++ `>= 大于等于 + ++ <= 小于等于 + ++ BETWEEN 在某个范围内 + ++ LIKE 搜索某种模式 + ++ IN 指定针对某个列的多个可能值 + + + +## SELECT TOP + +用于规定要返回的记录的数目,尤其对于拥有大量记录的大型表非常有用。 + +```SQL + +# SQL Server + +SELECT TOP number | percent column_name from table_name; + +# MySQL + +SELECT column_name from table_name LIMIT number; + +# Oracle + +SELECT column_name from table_name WHERE ROWNUM <= number; + +``` + + + +## LIKE + +LIKE 操作符用于在 WHERE 子句中搜索列中的特定模式 + +其中可以使用 % 通配符 + +```SQL + +SELECT * FROM table_name WHERE column_name LIKE '%key%"'; + +SELECT * FROM table_name WHERE column_name NOT LIKE 'xx'; + +``` + + + +## REGEXP + +通过使用 REGEXP 关键字可以使用正则表达式匹配 + +```SQL + +SELECT * from table_name WHERE column_name REGEXP 'regex_expression'; + +``` + + + +## IN + +IN 操作符可以在 WHERE 子句判断是否在一个集合中 + +```SQL + +SELECT * FROM table_name WHERE column_name (NOT) IN (value1, value2, ..., valueN); + +``` + +## AND OR + +AND 关键字等价与逻辑 && + +OR 关键字等价与逻辑 || + +```SQL + +SELECT * FROM table_name WHERE expression1 AND | OR expression2; + +``` + + + +## BETWEEN + +BETWEEN 关键字可以过滤值在某个一个范围内 + +值的范围可以是数值、文本、日期 + +```SQL + +SELECT * FROM table_name WHERE column_name (NOT) BETWEEM value1 AND value2; + +# 文本 + +SELECT * FROM table_name WHERE name BETWEEN 'A' AND 'H'; + +# 时间 + +SELECT * FROM table_name WHERE date BETWEEN '2017-02-26' AND now(); + +``` + +> 请注意,在不同的数据库中,BETWEEN 操作符会产生不同的结果! +> 在某些数据库中,BETWEEN 选取介于两个值之间但不包括两个测试值的字段。 +> 在某些数据库中,BETWEEN 选取介于两个值之间且包括两个测试值的字段。 +> 在某些数据库中,BETWEEN 选取介于两个值之间且包括第一个测试值但不包括最后一个测试值的字段。 +> 因此,请检查您的数据库是如何处理 BETWEEN 操作符 + + +## 时间 + +时间格式可以使用文本插入,也可以使用 now() 等时间函数插入 + +```SQL + +INSERT INTO time_tabel (date) values ('2017-02-28'); + +INSERT INTO time_table (date) values (now()); + +``` + + + +## JOIN + +> 在所有的 JOIN 子句中,如果没有加 GROUP BY | ORDER BY 等排序命令,得到的结果均是以右表原本的记录的顺序,一一与左表进行匹配 + + + +## INNER JOIN + +使用左表去依次匹配右表的记录,只显示成功匹配的记录 + +![](http://www.runoob.com/wp-content/uploads/2013/09/img_innerjoin.gif) + + + +```SQL + +SELECT * FROM w.name, a.date FROM websites w JOIN access_log ON w.id=a.site_id ORDER a.count; + +``` + + + +## LEFT JOIN + +> LEFT JOIN 关键字从左表(table1)返回所有的行,即使右表(table2)中没有匹配。如果右表中没有匹配,则结果为 NULL。 + +![](http://www.runoob.com/wp-content/uploads/2013/09/img_leftjoin.gif) + + + +![](http://www.runoob.com/wp-content/uploads/2013/09/left-join1.jpg) + + + +## RIGHT JOIN + +> RIGHT JOIN 关键字从右表 table2 返回所有的行,即使左表中没有与之匹配。如果左表中没有与之匹配,则返回 NULL。 + +![](http://www.runoob.com/wp-content/uploads/2013/09/img_rightjoin.gif) + + + +![](http://www.runoob.com/wp-content/uploads/2013/09/right-join1.jpg) + + + +## FULL OUTER JOIN + +FULL OUTER JOIN 相当于是 LEFT JOIN + RIGHT JOIN。返回左表和右表的所有结果,如果左表中的数据在右表中没有与之对应的就返回 NULL,如果右表中的数据在左表没有与之对应的就返回 NULL。 + +![](http://www.runoob.com/wp-content/uploads/2013/09/img_fulljoin.gif) + + + +> **MySQL 不支持 FULL OUTER JOIN。** + + + +## UNION + +UNION 操作符合并两个或者多个 SELECT 语句的结果。 + +> 注意:UNION 内部的每个 SELECT 语句必须拥有相同数量的列。咧业必须拥有相似的数据类型(经过尝试大多数都可以转化为字符串类型)。 + +```SQL + +SELECT column_name FROM table1 + +UNION + +SELECT column_name FROM tables; + +``` + +![](http://www.runoob.com/wp-content/uploads/2013/09/union1.jpg) + +> 注释:默认的,UNION 操作符会选取不同的值。如果允许重复,请使用 UNION ALL + +```SQL + +SELECT column_name FROM table1 + +UNION ALL + +SELECT column_name FROM table2; + +``` + +![](http://www.runoob.com/wp-content/uploads/2013/09/union2.jpg) + +> 注释:UNION 结果集中的列名总是等于 UNION 中第一个 SELECT 语句的列名。 + + + +## MySQL 复制数据到新表 + +```SQL + +CREATE TABLE new_table SELECT column_name (AS new_column_name) FROM old_table WHERE expression; + +``` + +通过 AS 语句来修改新表中字段名 + +> 复制数据只是单纯地复制数据以及数据类型到新表,但是各种外健主键约束等需要手动加。 + + + +## SELECT INTO + +把从一个表中的查询结果存储到新表中 + +可以使用 AS 子句来应用新名称 + +```SQL + +SELECT column_name (AS new_column_name) + +INTO newtable + +FROM table1; + +``` + +> MySQL 并不支持 SELECT ... INTO 语句。 + +> 但是支持 INSERT INTO ... SELECT + + + +## INSERT INTO ... SELECT + +```SQL + +INSERT INTO table2 + +SELECT column_name FROM table1; + +# 或者指定列 + +INSERT INTO table2 (column_name) + +SELECT column_name FROM table1; + +``` + + + +## CREATE DATABASE + +创建数据库 + +```SQL + +CREATE DATABASE dbname; + +``` + + + +## DROP DATABASE + +删除数据库 + +```SQL + +DROP DATABASE dbname; + +``` + +## UNIQUE + +```SQL + +CREATE TABLE Person( + +P_id INT NOT NULL, + +LastName VARCHAR(255) NOT NULL, + +FirstName VARCHAR(255), + +Address VARCHAR(255), + +City VARCHAR(255), + +# 在创建表时添加唯一约束 + +CONSTRAINT uc_PersonID UNIQUE(P_id, LastName) + +); + + + +# 或者直接修改表 + +ALTER TABLE Person ADD CONSTRAINT UNIQUE(P_id, LastName); + +``` + +撤销 UNIQUE 约束 + ++ MySQL: + +```SQL + +ALTER TABLE Person DROP INDEX uc_PersonID; + +``` + ++ SQL Server / Oracle + +```SQL + +ALTER TABLE Person DROP CONSTRAINT uc_PersonID; + +``` + + + +## PRIMARY KEY + +PRIMARY KEY = NOT NULL + UNIQUE + +```SQL + +# 创建表时声明主键 + +CREATE TABLE Person( + +P_id INT NOT NULL, + +LastName VARCHAR(255) NOT NULL, + +FirstName VARCHAR(255), + +Address VARCHAR(255), + +City VARCHAR(255), + +# pk_PersonID 是主键,其有 P_id + LastName 组成 + +CONSTRAINT pk_PersonID PRIMARY KEY (P_id, LastName) + +); + + + +# 在表创建后加入主键 + +ALTER TABLE Person ADD CONSTRAINT pk_PersonID PRIMARY KEY(P_id, LastName); + +``` + +撤销 PRIMARY KEY 约束 + +**MySQL** + +```SQL + +# PRIMARY KEY 唯一,所以无需指定 + +ALTER TABLE Person DROP PRIMARY KEY; + +``` + +**SQL Server / Oracle** + +```SQL + +ALTER TABLE Person DROP CONSTRAINT pk_PersonID; + +``` + + + +## SHOW CREATE TABLE + +查看表信息 + +```SQL + +SHOW CREATE TABLE table_name; + +``` + + + +## FORENGN KEY + +FORENGN KEY 约束用于预防破坏表之间连接的行为。 + +FORENGN KEY 约束能防治非法数据插入外健列,因为它必须是指向的那个表中的值之一。 + +**MySQL** + +```SQL + +CREATE TABLE orders( + +id INT NOT NULL PRIMARY KEY, + +orderNo INT NOT NULL, + +p_id int, + +# p_id 连接到 persons 表的 id + +FOREIGN KEY (p_id) REFERENCES persons(id) + +); + +``` + +**SQL Server | Oracle** + +```SQL + +CREATE TABLE Orders +( +id int NOT NULL PRIMARY KEY, +orderNo int NOT NULL, +p_id int FOREIGN KEY REFERENCES persons(p_id) +); + +``` + +如需命名 FOREIGN KEY 约束,并定义多个列的 FOREIGN KEY 约束 + +```SQL + +CREATE TABLE orders( + +id INT NOT NULL, + +orderNo INT NOT NULL, + +p_id INT, + +PRIMARY KEY (id), + +# 外健名称为 fk_perOrders + +CONSTRAINT fk_perOrders FOREIGN KEY (p_id) REFERENCES persons(id) + +); + +``` + +添加 FOREIGN KEY 约束 + +**MySQL** + +```SQL + +ALTER TABLE orders ADD FORENGN KEY (p_id) REFERENCES persons(id); + +``` + +**MySQL | SQL Server | Oracle** + +```SQL + +ALTER TABLE orders ADD CONSTRAINT foreign_key_name FOREIGN KEY (p_id) + + REFERENCES persons(id); + +``` + +撤销 FOREIGN KEY 约束 + +> `SHOW CREATE TABLE orders;` 可以查看对应的外健名 + + + +**MySQL** + +```SQL + +ALTER TABLE orders DROP FOREIGN KEY foreign_key_name; + +``` + +**SQL Server | Oracle** + +```SQL + +ALTER TABLE orders DROP CONSTRAINT foreign_key_name; + +``` + + + +## CHECK + +“所有的存储引擎均对CHECK子句进行分析,但是忽略CHECK子句。” +The CHECK clause is parsed but ignored by all storage engines. + +```SQL + +CREATE TABLE persons( + +id NOT NULL, + +name VARCHAR(255) NOT NULL, + +ADD CONSTRAINT chk_person CHECK (id > 0 AND name <> 'xxx' ) + +); + +# 在创建表后添加 CHECK 约束 + +ALTER TABLE persons ADD CONSTRAINT chk_person CHECK(id > 0 AND name <> 'xxx'); + +``` + + + +## DEFAULT + +DEFAULT 约束可以跟的是一个函数,如 now(); + +在创建表后添加 DEFAULT 约束 + +**MySQL | SQL Server** + +```SQL + +ALTER TABLE persons + +ALTER city SET DEFAULT 'New York'; + +``` + +**Oracle** + +```SQL + +ALTER TABLE persons + +MODIFY city DEFAULT 'New York'; + +``` + +撤销 DEFAULT 约束 + +```SQL + +ALTER TABLE persons + +ALTER COLUMN city DROP DEFAULT; + +``` + + + +## INDEX + +CREATE INDEX 语句在表中创建索引,再不读取整个表的情况下,索引使书库库应用程序可以更快地查找到数据。 + +> 注释:更新一个包含索引的表需要比一个没有索引的表花费更多的时间,所以只在常常被所搜的列上创建索引。 + + + +在表中创建一个简单索引,允许使用重复的值。 + +```SQL + +CREATE INDEX index_name ON table_name (column_name); + +``` + +在表中创建一个唯一的索引,两个行不能拥有相同的索引值。 + +```SQL + +CREATE UNIQUE INDEX index_name ON table_name (column_name); + +``` + +如果需要索引不止一个列,可以如下: + +```SQL + +CREATE INDEX index_name ON table_name (column_name_1, column_name_2); + +``` + +撤销 INDEX + +> index_name 可以通过 `SHOW CREATE TABLE table_name;` 查得 + +```SQL + +DROP INDEX index_name ON table_name; + +# MySQL 上还可以这样: + +ALTER TABLE table_name DROP INDEX index_name; + +``` + + + +## 删除表中的所有数据,并且不删除表 + ++ 方法1 + +```SQL + +DELETE FROM table_name; + +``` + ++ 方法2 + +```SQL + +TURNCATE TABLE table_name; + +``` + + + +## 对表中的列进行操作 + +1. 添加列 + +```SQL + +ALTER TABLE table_name ADD column_name datatype; + +``` + +2.删除列 + +```SQL + +ALTER TABLE table_name DROP COLUMN column_name; + +``` + +3.修改列 + +```SQL + +ALTER TABLE table_name MODIFY column_name datatype; + +``` + + + +## 修改 AUTO_INCREMENET 序列的起始值 + +**MySQL** + +```SQL + +ALTER TABLE table_name AUTO_INCREMENT=start_number; + +``` + +**SQL Server** + +```SQL + +CREATE TABLE persons( + +# start_number 起始值 interval_number 间隔 + +id INT IDENTITY(start_number, interval_number) PRIMARY KEY + +); + +``` + +**Oracle** + +需要创建 SEQUENCE 对象 + +```SQL + +CREATE SEQUENCE seq + +MINVALUE 1 + +START WITH 1 + +INCREMENT BY 1 + +# 缓存10个值提高性能 + +CACHE 10 + +``` + +插入SQL + +```SQL + +INSERT INTO table_name(id, column1, column2) VALUES (seq.nextval, xxx, xxx); + +``` + + + +## 视图 VIEW + +视图是基于SQL语句的结果集的可视化表 + +创建视图 + +```SQL + +CREATE VIEW view_name AS + +SELECT column_name(s) FROM table1; + +``` + +调用视图 + +```SQL + +SELECT * FROM view_name; + +``` + +修改视图 + +```SQL + +CREATE OR REPLACE VIEW view_name AS + +SELECT column(s) FROM table1; + +``` + +撤销视图 + +```SQL + +DROP VIEW view_name; + +``` + + + +## Date 函数 + +MySQL Date 函数 + + +| 函数 | 功能 | 实例 | +| :----------------------------------------------------: | :------------------------------------------------------------------: | --------------------------------------------: | +| NOW() | 返回当前日期和时间 | 2017-02-26 16:23:16 | +| CURDATE() | 返回当前日期 | 2017-02-26 | +| CURTIME() |返回当前时间 | 16:23:16 | +| DATE(date) | 提取日期或者日期/时间表达式的日期部分 | DATE(NOW()) ==> 16:23:16 | +| EXTRACT(unit FROM date)|通过unit指定返回date中年/天/周/日/时/分/秒等| EXTREACT(YEAR FROM now()) ==> 2017| +| DATE_ADD(date, INTERVAL expr type) | 向日期添加指定的时间间隔 | DATE_ADD(NOW(), INTERVAL 1 DAY) ==> 2017-02-27 16:23:16| +| DATE_SUB(date, INTERVAL expr type) | 向日期减去指定的时间间隔 | DATE_SUB(NOW(), INTERVAL 1 DAY) ==> 2017-02-26 16:23:16| +| DATEDIFF(date1, date2) | 返回 date1 - date2 的天数 | DATEDIFF('2017-02-27', '2017-02-28') ==> -1 | +| DATE_FORMAT(date, format) | 以不同的格式输出日期(自定义) | DATE_FORMAT('2017-02-28 16:23:16', '%b %d %Y %h:%i %p') ==> FEB 26 2017 4:23 pm| + + + +**MySQL 日期格式** + +| 类型 | 格式 | +|-------------------|:----------------------------:| +| DATE | YYYY-MM-DD | +| DATETIME | YYYY-MM-DD HH:MM:SS | +| TIMESTAMP | YYYY-MM-DD HH:MM:SS | +| YEAR | YYYY 或 YY | + + + +> 2017-02-26 与 2017-02-26 00:00:00 并不会匹配 + +> 提示:如果希望查询简单且容易维护,请不要将日期中使用时间部分。 + + + +## NULL + +NULL 值与其他值不一样,NULL 无法使用运算符来测试 NULL 值,比如:= , < , <> + +判断某个值是否是 NULL 使用 IS NULL + +判断某个值是否不是 NULL 使用 IS NOT NULL + +```SQL + +# IS NULL + +SELECT * FROM persons WHERE city IS NULL; + +# IS NOT NULL + +SELECT * FROM persons WHERE city IS NOT NULL; + +``` + +> 部分函数、数学或者逻辑运算与 NULL 运算后会得到 NULL 值。 + + + +如果我们希望把 NULL 在运算中用某个数据来代替 ,我们可以这样做: + +**SQL Server** + +```SQL + +SELECT ISNULL(NULL, 0); # return 0 + +``` + +**Oracle** + +```SQL + +SELECT NVL(NULL, ' '); # return ' ' + +``` + +**MySQL** + +```SQL + +SELECT IFNULL(NULL, 1); # return 1 + +# OR + +SELECT COALESCE(NULL, 'ABC'); # return 'abc' + +``` + + + +## SQL 数据类型 + +SQL 通用数据类型 + + + +| 数据类型 | 描述| +|-------------|:--------:| +|CHARACTER(n) | 字符/字符串。固定长度 n。 | +|VARCHAR(n) 或 CHARACTER VARYING(n) | 字符/字符串。可变长度。最大长度 n。 | +|BINARY(n) | 二进制串。固定长度 n。 | +|BOOLEAN | 存储 TRUE 或 FALSE 值 | +|VARBINARY(n) 或 BINARY VARYING(n) | 二进制串。可变长度。最大长度 n。 | +|INTEGER(p) | 整数值(没有小数点)。精度 p。 | +|SMALLINT | 整数值(没有小数点)。精度 5。 | +|INTEGER | 整数值(没有小数点)。精度 10。 | +|BIGINT| 整数值(没有小数点)。精度 19。 | +|DECIMAL(p,s) | 精确数值,精度 p,小数点后位数 s。例如:decimal(5,2) 是一个小数点前有 3 位数小数点后有 2 位数的数字。 | +|NUMERIC(p,s) | 精确数值,精度 p,小数点后位数 s。(与 DECIMAL 相同) | +|FLOAT(p) | 近似数值,尾数精度 p。一个采用以 10 为基数的指数计数法的浮点数。该类型的 size 参数由一个指定最小精度的单一数字组成。 | +|REAL| 近似数值,尾数精度 7。 | +|FLOAT| 近似数值,尾数精度 16。 | +|DOUBLE PRECISION| 近似数值,尾数精度 16。 | +|DATE| 存储年、月、日的值。 | +|TIME| 存储小时、分、秒的值。 | +|TIMESTAMP| 存储年、月、日、小时、分、秒的值。 | +|INTERVAL| 由一些整数字段组成,代表一段时间,取决于区间的类型。 | +|ARRAY| 元素的固定长度的有序集合 | +|MULTISET| 元素的可变长度的无序集合 | +|XML| 存储 XML 数据 | + + + +**SQL 数据类型在不同平台的区别 ** + + + +|数据类型| Access| SQLServer| Oracle| MySQL| PostgreSQL| +|-----------|:-------------:|:----------------:|:-----------:|:-----------:|:-------------------| +|boolean| Yes/No |Bit| Byte| N/A| Boolean| +|integer| Number (integer) |Int| Number| Int Integer| Int Integer | +|float| Number (single) |Float| Real| Number| Float| Numeric| +|currency| Currency| Money| N/A| N/A| Money| +|string (fixed)| N/A| Char| Char| Char| Char| +|string (variable)| Text (<256) Memo (65k+) |Varchar | Varchar Varchar2| Varchar| Varchar| +|binary object |OLE Object Memo| Binary (fixed up to 8K) Varbinary (<8K) Image (<2GB) | Long Raw |Blob Text | Binary Varbinary | + + + +## MySQL 数据类型 + +MySQL 主要有三种类型:Text(文本) 、 Number(数字) 、 Date/Time (日期/时间) + +**Text** + + + +|数据类型| 描述| +|------------|:-----------| +|CHAR(size)| 保存固定长度的字符串(可包含字母、数字以及特殊字符)。在括号中指定字符串的长度。最多 255 个字符。 | +|VARCHAR(size)| 保存可变长度的字符串(可包含字母、数字以及特殊字符)。在括号中指定字符串的最大长度。最多 255 个字符。**注释:如果值的长度大于 255,则被转换为 TEXT 类型。**| +|TINYTEXT| 存放最大长度为 255 个字符的字符串。 | +|TEXT| 存放最大长度为 65,535 个字符的字符串。 | +|BLOB| 用于 BLOBs(Binary Large OBjects)。存放最多 65,535 字节的数据。 | +|MEDIUMTEXT| 存放最大长度为 16,777,215 个字符的字符串。 | +|MEDIUMBLOB| 用于 BLOBs(Binary Large OBjects)。存放最多 16,777,215 字节的数据。 | +|LONGTEXT| 存放最大长度为 4,294,967,295 个字符的字符串。 | +|LONGBLOB| 用于 BLOBs (Binary Large OBjects)。存放最多 4,294,967,295 字节的数据。 | +|ENUM(x,y,z,etc.)| 允许您输入可能值的列表。可以在 ENUM 列表中列出最大 65535 个值。如果列表中不存在插入的值,则插入空值。注释:这些值是按照您输入的顺序排序的。可以按照此格式输入可能的值: ENUM('X','Y','Z')| + |SET| 与 ENUM 类似,不同的是,SET 最多只能包含 64 个列表项且 SET 可存储一个以上的选择。 | + + + +**Number** + + + +|数据类型| 描述| +|-----------|:---------| +|TINYINT(size)| -128 到 127 常规。0 到 255 无符号*。在括号中规定最大位数。 | +|SMALLINT(size)| -32768 到 32767 常规。0 到 65535 无符号*。在括号中规定最大位数。 | +|MEDIUMINT(size)| -8388608 到 8388607 普通。0 to 16777215 无符号*。在括号中规定最大位数。 | +|INT(size)| -2147483648 到 2147483647 常规。0 到 4294967295 无符号*。在括号中规定最大位数。 | +|BIGINT(size)| -9223372036854775808 到 9223372036854775807 常规。0 到 18446744073709551615 无符号*。在括号中规定最大位数。 | +|FLOAT(size,d)| 带有浮动小数点的小数字。在 size 参数中规定最大位数。在 d 参数中规定小数点右侧的最大位数。 | +|DOUBLE(size,d) |带有浮动小数点的大数字。在 size 参数中规定最大位数。在 d 参数中规定小数点右侧的最大位数。 | +|DECIMAL(size,d)| 作为字符串存储的 DOUBLE 类型,允许固定的小数点。在 size 参数中规定最大位数。在 d 参数中规定小数点右侧的最大位数。 | + +> 这些整数类型拥有额外的选项 UNSIGNED。通常,整数可以是负数或正数。如果添加 UNSIGNED 属性,那么范围将从 0 开始,而不是某个负数。 + + + +**Date** + + + +|数据类型| 描述| +|-----------|:----------| +|DATE()| 日期。格式:YYYY-MM-DD **注释:**支持的范围是从 '1000-01-01' 到 '9999-12-31'| +|DATETIME()| *日期和时间的组合。格式:YYYY-MM-DD HH:MM:SS **注释:**支持的范围是从 '1000-01-01 00:00:00' 到 '9999-12-31 23:59:59'| +|TIMESTAMP() |*时间戳。TIMESTAMP 值使用 Unix 纪元('1970-01-01 00:00:00' UTC) 至今的秒数来存储。格式:YYYY-MM-DD HH:MM:SS **注释:**支持的范围是从 '1970-01-01 00:00:01' UTC 到 '2038-01-09 03:14:07' UTC| +|TIME()| 时间。格式:HH:MM:SS **注释:**支持的范围是从 '-838:59:59' 到 '838:59:59'| +|YEAR()| 2 位或 4 位格式的年。**注释:**4 位格式所允许的值:1901 到 2155。2 位格式所允许的值:70 到 69,表示从 1970 到 2069。| + + > 即便 DATETIME 和 TIMESTAMP 返回相同的格式,它们的工作方式很不同。在 INSERT 或 UPDATE 查询中,TIMESTAMP 自动把自身设置为当前的日期和时间。TIMESTAMP 也接受不同的格式,比如 YYYYMMDDHHMMSS、YYMMDDHHMMSS、YYYYMMDD 或 YYMMDD。 + + + +# SQL 函数 + + + +## SQL Aggregate 函数 + ++ AVG() - 返回平均值 + +`SELECT site_id, count FROM access_log WHERE count > (SELECT AVG(count) FROM access_log);` + ++ COUNT() - 返回行数 + ++ FIRST() - 返回第一条记录 **注释:**只有 MS Access 支持 FIRST() + ++ LAST() - 返回最后一条记录 **注释:**只有 MS Access 支持 LAST() + ++ MAX() - 返回最大值 + +`SELECT MAX(column_name) FROM table_name;` + ++ MIN() - 返回最小值 + + `SELECT MIN(column_name) FROM table_name;` + ++ SUM() - 返回总和 + + `SELECT SUM(column_name) FROM table_name;` + + + +## SQL Scalar 函数 + ++ UCASE() - 将某个字段转换为大写 + + `SELECT UCASE(column_name) FROM table_name;` + ++ LCASE() - 将某个字段转换为小写 + + `SELECT LCASE(column_name) FROM table_name;` + ++ MID() - 从某个文本字段提取字符 + +`SELECT MID('WORK HARD', 1, 4); # return WORK` + +**注释:**在 MySQL 中是前闭后闭, 字符串是从第 **1** 位开始算 + ++ LEN() - 返回某个文本字段的长度 + +**注释:**在MySQL 中是 LENGTH(),一个汉字长度为3,一个英文长度为1 + ++ ROUND() - 对某个数值字段进行指定小数位数的四舍五入 + +`SELECT ROUND(column_name,decimals) FROM table_name;` + +**注释:** ROUND 返回值被转化为一个 BIGINT! + ++ NOW() - 返回当前的系统日期和时间 + ++ FORMAT() - 格式化某个字段的显示方式 + + + +### COUNT(column_name) + +```SQL + +SELECT COUNT(column_name) FROM table1; + +``` + +返回指定列的数目,NULL 不计数 + +```SQL + +SELECT COUNT(DISTINCT column_name) FROM table1; + +``` + +返回指定列不同值的数目 + + + + + +### 只需要第一条数据 + +**SQL Server** + +```SQL + +SELECT TOP 1 column_name FROM table1 ORDER BY column_name ASC; + +``` + +**MySQL** + +```SQL + +SELECT column_name FROM table1 ORDER BY column_name ASC + +LIMIT 1; # LIMIT number 指定最多只返回多少条记录 + +``` + +**Oracle** + +```SQL + +SELECT column_name FROM table1 ORDER BY column_name ASC + +WHERE ROWNUM <= 1; + +``` + + + +### 只需要最后一条记录 + +**SQL Server** + +```SQL + +SELECT TOP 1 column_name FROM table + +ORDER BY column_name DESC; + +``` + +**MySQL** + +```SQL + +SELECT column_name FROM table1 ORDER BY column_name DESC + +LIMIT 1; + +``` + +**Oracle** + +```SQL + +SELECT column_name FROM table1 ORDER BY column_name DESC + +WHERE ROWNUM <= 1; + +``` + + + +## GROUP BY + +GROUP BY 语句用于结合聚合函数,根据一个或多个列对结果集进行分组。 + +```SQL + +SELECT column_name, aggregate_function(column_name) +FROM table_name +WHERE column_name operator value + +# 相同的 column_name 的行将会被归为一行 +GROUP BY column_name; + +``` + +![](http://www.runoob.com/wp-content/uploads/2013/09/groupby2.jpg) + + + +## HAVING + +SQL 中增加 HAVING 子句的原因是:WHERE 关键字无法与聚合函数一起使用,HAVING 子句就是用来与聚合函数一起使用的。 + +![](http://www.runoob.com/wp-content/uploads/2013/09/having2.jpg) + + + +## DATE_FORMAT() + +![](http://www.runoob.com/wp-content/uploads/2013/09/formate1.jpg) + diff --git "a/src/md/2017-10-19-\345\233\236\345\244\264\347\234\213.md" "b/src/md/2017-10-19-\345\233\236\345\244\264\347\234\213.md" index adbf895..5bcf014 100644 --- "a/src/md/2017-10-19-\345\233\236\345\244\264\347\234\213.md" +++ "b/src/md/2017-10-19-\345\233\236\345\244\264\347\234\213.md" @@ -1,62 +1,62 @@ ---- -layout: post -title: "2017-10-19 回头看" -date: 2017-10-19 09:00:05 +0800 -categories: 回顾 ---- - -# See The Big Picture. - -做事情要站在更高的角度看问题,See the whole picture,才能够理解当前做事情的意义,不然会认为自己的工作毫无意义。 - -# 回头看 - -我武汉理工学生,今年大三。大学一直过得忙忙碌碌,就像打了鸡血,怀着创业梦,但是至今为止没有开启我的事业,有出没入。 - -由于想创业,我学习过一些管理、经济学以及编程技能(我软件工程专业),关于管理、经济学,我就只是停留在阅读某些热门书籍,激起我对企业的热情的是德鲁克的《创新与企业家精神》,我喝了德鲁克不少的“鸡汤”,他的思想令我兴奋,但上一次阅读该方面的书籍已经是大二上了。 - -我编程起步不较慢,大一下才正式开始学习编程(注:武汉理工大学大一上不允许带电脑 `unfriendly`)。先是学习了JAVA,学习JAVA的原因是什么,应该是当初听了一个师兄说未来需要学。 - -学了JAVA基础后,那时候网上比较多Android教程,比如慕课网、极客学院。我跟着慕课网的教程开始学习Android,没有阅读过书籍深入了解Android,学习Android过程更多只是停留在UI上,如何布局等知识。后面学深入一点,就跟着《Android权威指南》(推荐),该书本是通过做一个APP的思路去教授知识,我也跟着做了几个小APP。这种教学方式很适合入门者,能够快速看到成果(APP),能够坚持学习下去。 - -后来我进入了实验室,我对Android的学习也就停止了,转而投向了JAVA WEB开发。至于为什么是JAVA WEB,很大的原因就是有一个丰富JAVA WEB项目经验的广东老乡兼师兄带,后来也来了一个朋友一起学习JAVA WEB。说实话,学习JAVA WEB的过程非常枯燥,由于急于求成,“吃快餐”,在[易佰教程](http://www.yiibai.com)、[菜鸟教程](http://www.runoob.com/)上学习有关JAVA WEB、数据库方面的知识。没有深入理解其中的原理,很多代码都只是按照教程执行,照葫芦画瓢。 - -## 第一次外包 - -大二下有一天,我接到一个电话叫去谈外包的问题。这个项目叫[店小二365](http://www.dianxiaoer365.com),没错这个网站已经不存在了,因为该团队前几个星期宣布创业失败了。本来这项目是由上面提到的带我的师兄接的,后来他要毕业了,要忙各种事情,后来这外包就落到了我头上来,甲方创业团队是武汉理工大学的一个创业团队,产生了一个由于电商的想法,分级会员体制(微商+“传销”,具体游戏规则我就不透露)。 - -对于新手来说,能够接到外包是很兴奋的,而且对方还是创业团队。第一次接外包,当初很多需求都没有弄清楚,甲方不懂软件工程,我们当初也不懂,于是我找了两个伙伴(一个是上面提到跟我一个学JAVA WEB的朋友,另一个做前端的是我舍友)。开启了大量逃课的日子(即使不做外包,我也会逃课),在实验室里面吭哧吭哧地写代码,没有明确需求,甲方对于UI方面就是说就按照聚美优品做,Deadline为6个星期后。 - -后来由于需求不明确等问题,我们的代码改了很多遍,也没有项目经历,甚至出现了`git conflit`。接入支付宝、阿里云短信接口、邮箱服务等也遇到了不少问题,做事太着急,从网上找来了一个demo就往上面跑,到底其工作原理也没弄清楚,有时候遇到了Google也无法回答的问题(我们不会搜),我们只能够请教师兄,还被师兄训了说没有耐心看官方文档。从此以后我认识到耐心阅读官方文档的重要性,至少要把其基本运作原理弄明白。由于缺乏Linux方面的知识,在部署服务器上也遇到了不少问题。做外包的期间很累。 - -上述的第一个外包对我影响极大,无论是软件工程方面的(虽然我们只是一个小团队,但是也遇到了很多问题),还是责任心(接了一个任务,即使不想再做下去了,也要对当初自己的选择、对方负责)。甲方负责人很用心,也很有耐心,能够包容我们的错误,如果能再选择一次,我还是会接这个外包。 - -## 第一次实习 - -大二下的暑假,留在了实验室老师公司实习,使用Python + ZeroMQ + TinyDB + Electron + Vue.js + Element UI,做了一个数据库方面的客户端管理系统,这个软件是一个可以在局域网内共享信息、桌面版的前后端通信模型。在真正写代码之前,我们有一个月的时间来学习,准备其他方面的知识,在这段时间我们参加了互联网+比赛。 - -需求:有一些做生物信息方面研究的人,他们拥有海量的数据,数据可能分布在多个移动磁盘、服务器磁盘阵列上。如何管理海量数据成了问题,他们需要一个软件去帮助管理数据,查看磁盘、服务器上文件的变更(增删改)状态。 - -这次开发中我们开发团队中有6个人(都是大二),Project Manager是老师的研究生,吸取了上一次外包的教训,我们尽可能详细地弄清楚需求,但是还是没有需求文档。很多需求,PM也不是很清楚,更多的是靠我们大家的想象力和参考别的软件类似功能如何实现。 - -这个过程也遇到了很多软件工程方面的问题,前后端开发不同步,大家拥有不同意见没达到共识,数据结构与算法方面没有做好文档,导致后面需要用到的时候需要阅读之前代码,效率极低,没有对意外情况(xxx病了)做好准备。虽然遇到了很多问题,但整个过程下来还是觉得很高兴的。 - -经过这个项目后,我发现需要阅读更多关于软件工程方面的书籍,比如《人月神话》(至今没看完)。 - -## 搭建博客 - -计划与舍友(外包中做前端的)一起搭建个人博客,在锻炼到技术的同时,能够拥有自己的网站为自己做营销。site: [whoyoung.me](whoyoung.me) - -说起这博客,在做第一个外包之前我们已经在动工,后来因为接了外包和暑假实习原因,一直把这个博客拖到了大三上(至今没有完成,还有想法需要实现)。博客的前后端也经历过改版,由最开始的服务器端渲染到现在的SPA(单页应用),前端也引入了Vue.js框架,服务器端由JAVA SpringMVC到Python Flask,数据库由MySQL到MongoDB。 - -作为技术人员,除了提高自己的技术水平外,还应该多学习如何营销个人,《软技能》中John Sonmez 提到了程序员应该多学习除编码外的其他知识,比如营销、健身、理财、学习等。 - -我的普通话比较差,口音很重,语文水平差,所以表达能力(口头表达、书面表达)一直是我的弱项。在比赛、组织中表达能力是很重要的,比如一个比赛答辩,上台的机会都是留给表达能力好的人。在今后,应该多锻炼自己关于表达能力。 - -## 纠结 - -由于做了好几个关于Web后台的项目,目前对WEB开发有点疲乏感,有想法向转数据或者机器学习应用方面,目前在学习coursera上吴恩达的机器学习课程。 - -而工作中一般做数据或者机器学习方面的都是研究生等有研究背景的人才,我又不想继续读研究生,距离我找工作时候剩下来的时间只剩下不到一年的时间了。昨晚和一位目前在饿了么工作的师兄聊天,他给的建议是找到自己真正热爱的方向,不要看到哪个方向火就往哪个方向转。 - +--- +layout: post +title: "2017-10-19 回头看" +date: 2017-10-19 09:00:05 +0800 +categories: 回顾 +--- + +# See The Big Picture. + +做事情要站在更高的角度看问题,See the whole picture,才能够理解当前做事情的意义,不然会认为自己的工作毫无意义。 + +# 回头看 + +我武汉理工学生,今年大三。大学一直过得忙忙碌碌,就像打了鸡血,怀着创业梦,但是至今为止没有开启我的事业,有出没入。 + +由于想创业,我学习过一些管理、经济学以及编程技能(我软件工程专业),关于管理、经济学,我就只是停留在阅读某些热门书籍,激起我对企业的热情的是德鲁克的《创新与企业家精神》,我喝了德鲁克不少的“鸡汤”,他的思想令我兴奋,但上一次阅读该方面的书籍已经是大二上了。 + +我编程起步不较慢,大一下才正式开始学习编程(注:武汉理工大学大一上不允许带电脑 `unfriendly`)。先是学习了JAVA,学习JAVA的原因是什么,应该是当初听了一个师兄说未来需要学。 + +学了JAVA基础后,那时候网上比较多Android教程,比如慕课网、极客学院。我跟着慕课网的教程开始学习Android,没有阅读过书籍深入了解Android,学习Android过程更多只是停留在UI上,如何布局等知识。后面学深入一点,就跟着《Android权威指南》(推荐),该书本是通过做一个APP的思路去教授知识,我也跟着做了几个小APP。这种教学方式很适合入门者,能够快速看到成果(APP),能够坚持学习下去。 + +后来我进入了实验室,我对Android的学习也就停止了,转而投向了JAVA WEB开发。至于为什么是JAVA WEB,很大的原因就是有一个丰富JAVA WEB项目经验的广东老乡兼师兄带,后来也来了一个朋友一起学习JAVA WEB。说实话,学习JAVA WEB的过程非常枯燥,由于急于求成,“吃快餐”,在[易佰教程](http://www.yiibai.com)、[菜鸟教程](http://www.runoob.com/)上学习有关JAVA WEB、数据库方面的知识。没有深入理解其中的原理,很多代码都只是按照教程执行,照葫芦画瓢。 + +## 第一次外包 + +大二下有一天,我接到一个电话叫去谈外包的问题。这个项目叫[店小二365](http://www.dianxiaoer365.com),没错这个网站已经不存在了,因为该团队前几个星期宣布创业失败了。本来这项目是由上面提到的带我的师兄接的,后来他要毕业了,要忙各种事情,后来这外包就落到了我头上来,甲方创业团队是武汉理工大学的一个创业团队,产生了一个由于电商的想法,分级会员体制(微商+“传销”,具体游戏规则我就不透露)。 + +对于新手来说,能够接到外包是很兴奋的,而且对方还是创业团队。第一次接外包,当初很多需求都没有弄清楚,甲方不懂软件工程,我们当初也不懂,于是我找了两个伙伴(一个是上面提到跟我一个学JAVA WEB的朋友,另一个做前端的是我舍友)。开启了大量逃课的日子(即使不做外包,我也会逃课),在实验室里面吭哧吭哧地写代码,没有明确需求,甲方对于UI方面就是说就按照聚美优品做,Deadline为6个星期后。 + +后来由于需求不明确等问题,我们的代码改了很多遍,也没有项目经历,甚至出现了`git conflit`。接入支付宝、阿里云短信接口、邮箱服务等也遇到了不少问题,做事太着急,从网上找来了一个demo就往上面跑,到底其工作原理也没弄清楚,有时候遇到了Google也无法回答的问题(我们不会搜),我们只能够请教师兄,还被师兄训了说没有耐心看官方文档。从此以后我认识到耐心阅读官方文档的重要性,至少要把其基本运作原理弄明白。由于缺乏Linux方面的知识,在部署服务器上也遇到了不少问题。做外包的期间很累。 + +上述的第一个外包对我影响极大,无论是软件工程方面的(虽然我们只是一个小团队,但是也遇到了很多问题),还是责任心(接了一个任务,即使不想再做下去了,也要对当初自己的选择、对方负责)。甲方负责人很用心,也很有耐心,能够包容我们的错误,如果能再选择一次,我还是会接这个外包。 + +## 第一次实习 + +大二下的暑假,留在了实验室老师公司实习,使用Python + ZeroMQ + TinyDB + Electron + Vue.js + Element UI,做了一个数据库方面的客户端管理系统,这个软件是一个可以在局域网内共享信息、桌面版的前后端通信模型。在真正写代码之前,我们有一个月的时间来学习,准备其他方面的知识,在这段时间我们参加了互联网+比赛。 + +需求:有一些做生物信息方面研究的人,他们拥有海量的数据,数据可能分布在多个移动磁盘、服务器磁盘阵列上。如何管理海量数据成了问题,他们需要一个软件去帮助管理数据,查看磁盘、服务器上文件的变更(增删改)状态。 + +这次开发中我们开发团队中有6个人(都是大二),Project Manager是老师的研究生,吸取了上一次外包的教训,我们尽可能详细地弄清楚需求,但是还是没有需求文档。很多需求,PM也不是很清楚,更多的是靠我们大家的想象力和参考别的软件类似功能如何实现。 + +这个过程也遇到了很多软件工程方面的问题,前后端开发不同步,大家拥有不同意见没达到共识,数据结构与算法方面没有做好文档,导致后面需要用到的时候需要阅读之前代码,效率极低,没有对意外情况(xxx病了)做好准备。虽然遇到了很多问题,但整个过程下来还是觉得很高兴的。 + +经过这个项目后,我发现需要阅读更多关于软件工程方面的书籍,比如《人月神话》(至今没看完)。 + +## 搭建博客 + +计划与舍友(外包中做前端的)一起搭建个人博客,在锻炼到技术的同时,能够拥有自己的网站为自己做营销。site: [whoyoung.me](whoyoung.me) + +说起这博客,在做第一个外包之前我们已经在动工,后来因为接了外包和暑假实习原因,一直把这个博客拖到了大三上(至今没有完成,还有想法需要实现)。博客的前后端也经历过改版,由最开始的服务器端渲染到现在的SPA(单页应用),前端也引入了Vue.js框架,服务器端由JAVA SpringMVC到Python Flask,数据库由MySQL到MongoDB。 + +作为技术人员,除了提高自己的技术水平外,还应该多学习如何营销个人,《软技能》中John Sonmez 提到了程序员应该多学习除编码外的其他知识,比如营销、健身、理财、学习等。 + +我的普通话比较差,口音很重,语文水平差,所以表达能力(口头表达、书面表达)一直是我的弱项。在比赛、组织中表达能力是很重要的,比如一个比赛答辩,上台的机会都是留给表达能力好的人。在今后,应该多锻炼自己关于表达能力。 + +## 纠结 + +由于做了好几个关于Web后台的项目,目前对WEB开发有点疲乏感,有想法向转数据或者机器学习应用方面,目前在学习coursera上吴恩达的机器学习课程。 + +而工作中一般做数据或者机器学习方面的都是研究生等有研究背景的人才,我又不想继续读研究生,距离我找工作时候剩下来的时间只剩下不到一年的时间了。昨晚和一位目前在饿了么工作的师兄聊天,他给的建议是找到自己真正热爱的方向,不要看到哪个方向火就往哪个方向转。 + 到底要不要转使得我很纠结,希望大家能够给点意见。 \ No newline at end of file diff --git "a/src/md/2017-11-25-\351\235\242\350\257\225ofo.md" "b/src/md/2017-11-25-\351\235\242\350\257\225ofo.md" index 3386bb6..b4895b7 100644 --- "a/src/md/2017-11-25-\351\235\242\350\257\225ofo.md" +++ "b/src/md/2017-11-25-\351\235\242\350\257\225ofo.md" @@ -1,46 +1,46 @@ ---- -layout: post -title: "面试ofo后端工程师后感" -date: 2017-11-26 09:00:05 +0800 -categories: 面经 ---- - -# 面试缘由 - -我今年武汉理工大三软件工程学生,我在这文章《回顾大学两年》对大学两年进行了回顾,由于对目前需要掌握的知识点和未来从事的方向并不是很明确,听从了实验室中师兄的建议,先去尝试一下招聘,感受一下被吊打的感觉,听听面试官给点意见。 - -# ofo校园招聘宣讲 - -昨天晚上,ofo来到了武汉理工进行校园招聘宣讲。其中说到了很多ofo的价值观、理念,“**共享单车的原创者&领骑者**”,他们的价值观确实是很动人。 - -由于我是学后端开发的,已经学了后端近一年,做了好几个项目,使用过 JAVA 的 Spring + SpringMVC + Hibernate + MySQL 做过一个外包项目;使用过 Python 的 Flask + MongoDB 搭建过[个人博客](http://whoyoung.me/),[github仓库](https://github.com/g10guang/whoyoungblog);使用过 ZeroMQ 来搭建桌面应用(采用的是B/S架构,所以也是前后端的实现逻辑)。整个学习过程使用开源框架多,很少接触到底层计算机网络和数据结构与算法实现,所以在用别人的轮子多后,就感觉很难提升,想过转机器学习和大数据方向。 - -当晚ofo中的校招技术负责人给了我很多建议和指导意见,首先他指出了现在机器学习和大数据方面的火热,薪资待遇都很好,但是假如校招出去竞争对手都是有三年研究该方面经验的研究生,所以本科生在机器学习和大数据上,学历和科研能力都没有竞争优势。他建议我先打好基础,把基本的数据结构和算法弄清楚,打好数学的底子,语言和框架只是其次,最要的是基础知识够扎实。(《剑指offer》里提到,面试官会关注应聘者的5种素质:扎实的基础知识、能写高质量的代码、分析问题时思路清晰、能优化时间、空间效率以及学习沟通等软能力)。 - -他当晚给了我一张面试的直通卡,我当时确实很兴奋,因为当晚的技术方面应聘者很少,可能是ofo在宣传方面没有做好准备,或者是优秀的人已经拿到了心仪的**offer**。 - -![ofo面试直通卡](https://pic2.zhimg.com/v2-595df3d29f22f2d1165fb4ff7276e00d_b.jpg) - -# 面试经历 - -我当天晚上还纠结了到底应不应该去,毕竟我不是应届生,变相在浪费面试官的时间,而且自己准备得不充分,去了也只有被吊打的机会了。但最后我还是决定去体验学习一番,也是积累面试经验和为了面试官给的指导意见。事实证明,去了还是很值得,了解到了很多东西,也清晰了未来方向。 - -面试官用1分钟看我的简历,并让我做个简短的自我介绍。然后他问了我4个问题,我一个都没回答上。。。。。。 - -![ofo面试题](https://pic4.zhimg.com/v2-9aaed32abb15e559a6980a2014b27b1b_b.jpg) - -至于前面3个我确实是不了解,因为我本科写代码一直都是用别人的轮子,特别是使用了 Python 以后,就一直停留在使用轮子的状态,也没太多了解底层实现以及不同实现方法的区别。使用轮子的好处是可以把精力集中在业务逻辑上,缺点是出了问题不好解决,因为不懂底层是如何实现,也就无法对性能瓶颈进行优化。 - -最后一道题,面试官给了5分钟让我想个思路,我一开始想到的解决方案是先把数组重新排序,然后使用二分查找。但是他说假如这个数组有 100 万个元素,那这样岂不是很浪费空间?我当时就懵逼了(《剑指offer》提到,当面试官问了很简单的问题的时候,应聘者应该注重细节,写出完整、鲁棒的代码;如果遇到复杂问题,应聘者可以通过画图、举具体例子分析和分解复杂问题等方法先理清思路再动手编程),最后面试官给了我答案。 - -4个问题没答上后,他确定我没戏后,很热心地给我讲解了很多发展方面的问题。 - -他说我的简历上写了几个项目,但是对基本的东西却不了解,我能够去一个不错的公司,但是很快就会遇到上升瓶颈,因为核心的问题我无法解决;对 MySQL 底层引擎不了解,不熟悉网络,那么我也很难具有架构的能力。一般大公司会对应聘的基础知识是否扎实很在意,而外包公司在意应聘者是否熟悉某种特定语言和具体框架。 - -从事互联网方向的同学,一开始需要尽可能找一个大平台(公司),因为如果以后跳槽或者从事其他领域,这个平台背景就相当于学历,默认大平台出来的员工不会差。而且大平台有足够完善的上升培养机制,只要能力够强就有机会接触更优秀的人才和得到向上发展的机会。除了做技术,还可以发掘自己软实力,未来有机会可以做 **leader** 或者 **PM** 之类的岗位。 - -他在我离开前让我加他微信,说他感觉我还是很困惑,让我有问题可以咨询他。 - -# 总结 - +--- +layout: post +title: "面试ofo后端工程师后感" +date: 2017-11-26 09:00:05 +0800 +categories: 面经 +--- + +# 面试缘由 + +我今年武汉理工大三软件工程学生,我在这文章《回顾大学两年》对大学两年进行了回顾,由于对目前需要掌握的知识点和未来从事的方向并不是很明确,听从了实验室中师兄的建议,先去尝试一下招聘,感受一下被吊打的感觉,听听面试官给点意见。 + +# ofo校园招聘宣讲 + +昨天晚上,ofo来到了武汉理工进行校园招聘宣讲。其中说到了很多ofo的价值观、理念,“**共享单车的原创者&领骑者**”,他们的价值观确实是很动人。 + +由于我是学后端开发的,已经学了后端近一年,做了好几个项目,使用过 JAVA 的 Spring + SpringMVC + Hibernate + MySQL 做过一个外包项目;使用过 Python 的 Flask + MongoDB 搭建过[个人博客](http://whoyoung.me/),[github仓库](https://github.com/g10guang/whoyoungblog);使用过 ZeroMQ 来搭建桌面应用(采用的是B/S架构,所以也是前后端的实现逻辑)。整个学习过程使用开源框架多,很少接触到底层计算机网络和数据结构与算法实现,所以在用别人的轮子多后,就感觉很难提升,想过转机器学习和大数据方向。 + +当晚ofo中的校招技术负责人给了我很多建议和指导意见,首先他指出了现在机器学习和大数据方面的火热,薪资待遇都很好,但是假如校招出去竞争对手都是有三年研究该方面经验的研究生,所以本科生在机器学习和大数据上,学历和科研能力都没有竞争优势。他建议我先打好基础,把基本的数据结构和算法弄清楚,打好数学的底子,语言和框架只是其次,最要的是基础知识够扎实。(《剑指offer》里提到,面试官会关注应聘者的5种素质:扎实的基础知识、能写高质量的代码、分析问题时思路清晰、能优化时间、空间效率以及学习沟通等软能力)。 + +他当晚给了我一张面试的直通卡,我当时确实很兴奋,因为当晚的技术方面应聘者很少,可能是ofo在宣传方面没有做好准备,或者是优秀的人已经拿到了心仪的**offer**。 + +![ofo面试直通卡](https://pic2.zhimg.com/v2-595df3d29f22f2d1165fb4ff7276e00d_b.jpg) + +# 面试经历 + +我当天晚上还纠结了到底应不应该去,毕竟我不是应届生,变相在浪费面试官的时间,而且自己准备得不充分,去了也只有被吊打的机会了。但最后我还是决定去体验学习一番,也是积累面试经验和为了面试官给的指导意见。事实证明,去了还是很值得,了解到了很多东西,也清晰了未来方向。 + +面试官用1分钟看我的简历,并让我做个简短的自我介绍。然后他问了我4个问题,我一个都没回答上。。。。。。 + +![ofo面试题](https://pic4.zhimg.com/v2-9aaed32abb15e559a6980a2014b27b1b_b.jpg) + +至于前面3个我确实是不了解,因为我本科写代码一直都是用别人的轮子,特别是使用了 Python 以后,就一直停留在使用轮子的状态,也没太多了解底层实现以及不同实现方法的区别。使用轮子的好处是可以把精力集中在业务逻辑上,缺点是出了问题不好解决,因为不懂底层是如何实现,也就无法对性能瓶颈进行优化。 + +最后一道题,面试官给了5分钟让我想个思路,我一开始想到的解决方案是先把数组重新排序,然后使用二分查找。但是他说假如这个数组有 100 万个元素,那这样岂不是很浪费空间?我当时就懵逼了(《剑指offer》提到,当面试官问了很简单的问题的时候,应聘者应该注重细节,写出完整、鲁棒的代码;如果遇到复杂问题,应聘者可以通过画图、举具体例子分析和分解复杂问题等方法先理清思路再动手编程),最后面试官给了我答案。 + +4个问题没答上后,他确定我没戏后,很热心地给我讲解了很多发展方面的问题。 + +他说我的简历上写了几个项目,但是对基本的东西却不了解,我能够去一个不错的公司,但是很快就会遇到上升瓶颈,因为核心的问题我无法解决;对 MySQL 底层引擎不了解,不熟悉网络,那么我也很难具有架构的能力。一般大公司会对应聘的基础知识是否扎实很在意,而外包公司在意应聘者是否熟悉某种特定语言和具体框架。 + +从事互联网方向的同学,一开始需要尽可能找一个大平台(公司),因为如果以后跳槽或者从事其他领域,这个平台背景就相当于学历,默认大平台出来的员工不会差。而且大平台有足够完善的上升培养机制,只要能力够强就有机会接触更优秀的人才和得到向上发展的机会。除了做技术,还可以发掘自己软实力,未来有机会可以做 **leader** 或者 **PM** 之类的岗位。 + +他在我离开前让我加他微信,说他感觉我还是很困惑,让我有问题可以咨询他。 + +# 总结 + 在大平台也能够遇到好的导师,比如向面试官一样nice的导师,会对职业发展很有帮助。如果你像我一样迷茫,建议你也参加一下招聘,了解一下公司的需求;有些人感受到自己竞争力不足选择读研;有些人对未来的职业有了更好的规划。 \ No newline at end of file diff --git "a/src/md/2017-12-2-\351\235\242\350\257\225\346\226\227\351\261\274.md" "b/src/md/2017-12-2-\351\235\242\350\257\225\346\226\227\351\261\274.md" index bc1231a..7e50959 100644 --- "a/src/md/2017-12-2-\351\235\242\350\257\225\346\226\227\351\261\274.md" +++ "b/src/md/2017-12-2-\351\235\242\350\257\225\346\226\227\351\261\274.md" @@ -1,120 +1,120 @@ ---- -layout: post -title: "面试斗鱼后端工程师后感" -date: 2017-12-01 09:00:05 +0800 -categories: 面经 ---- - -前两天去了面试ofo的后端工程师,被吊打,具体经历可以查看[《面试ofo后端工程师后感》](https://zhuanlan.zhihu.com/p/30449067)。 - -昨天我参加了斗鱼的校园招聘笔试以及面试,从下午一点半一直战斗到晚上的六点半,特别是面试过程中,对精神消耗很大,一个会议室狭小、密闭的空间里,一直进行面试。技术一面、技术二面、三面(我不确定第三面是否是HR可能更加倾向于技术团队的管理)、HR四面。 - -# 笔试 - -HR给每一个人相应的笔试题,前端、PHP后端、Golang后端、Java后端、产品经理、数据分析等等。我应聘的是Java后端,前几道题目主要是关于 Java 语言的基础([接口与抽象类的区别](http://www.importnew.com/12399.html)、抽象类与普通类的区别,[创建对象的方式](http://www.importnew.com/22405.html),多线程,[基础类型](http://www.runoob.com/java/java-basic-datatypes.html)以及对应封装类)、MySQL 的表操作(同时使用 JOIN ON / [CASE WHEN](http://www.mysqltutorial.org/mysql-case-function/) / GROUP BY / WHERE / SELECT 等操作)、前端 JS 响应 select 下拉条代码、字符串操作、排序去重算法。 - -其中 MySQL 题目如下: - -+ 有 A 表,其中记录着每个学生的每一科目的成绩(name、score、subject); -+ 有 B 表,其中记录着每个科目占总成绩比例(subject、percent); -+ 求在 0~59、60~89、90~100的学生比例 - - - -对应的 SQL 查询语句: - -```sql -SELECT - sum( - CASE - WHEN stuScores.score BETWEEN 0 AND 59 THEN 1 - ELSE 0 - END) / count(*) AS 0t059, - sum( - CASE - WHEN stuScores.score BETWEEN 60 AND 89 THEN 1 - ELSE 0 - END) / count(*) AS 60to89, - sum( - CASE - WHEN stuScores.score BETWEEN 90 AND 100 THEN 1 - ELSE 0 - END) / count(*) AS 90to100 -FROM (SELECT sum(A.score * B.percent) as score - FROM A JOIN B ON A.subject = B.subject GROUP BY A.name) as stuScores; -``` - -没有了解过 JS 对于前端的响应事件,没有写出代码,只留下一句声明:**不了解 JavaScript** - -对字符串的对称翻转("01234567" 转化为 "32107654"),Java 中 String 对象是不可改变的,所以每次修改 String 中的内容,都会重新创建一个新的 String 对象,所以这条题目我猜面试官想要看到的是把 String 转化为 char[] 进行操作。 - -最后一题,把随机序列排序并去重,[1, 3, 4, 2, 3, 4] 转化为 [1, 2, 3, 4],最后还要求算出时间以及空间复杂度。我的想法是使用平衡二叉树进行排序,在排序的过程中将去重操作一并完成,无奈忘记了平衡二叉树的调整算法。 - -使用平衡二叉树算法时间空间复杂度: - -``` -时间复杂度 = O(nlog(n)) -空间复杂度 = O(n) -``` - -还可以对数组先使用快速排序等算法,然后再开创另一个新的数组,从头到尾一个一个插到新的数组,记录上一个插入的数字,如果下次要插入的数字等于上一次插入的数字就放弃,否则就插入到新的数组里面。 - -``` -时间复杂度 = O(nlog(n)) -空间复杂度 = O(n) -``` - -# 技术一面 - -面试官在看我简历的时候,顺带让我也进行自我介绍。部分问题我忘了,他问了我大概这么几个问题: - - -+ MySQL 有哪些数据引擎以及谈谈他们的区别、应用场景。[Innodb 和 MyISAM 的区别](https://segmentfault.com/a/1190000008227211) -+ MySQL 有哪些数据类型。[char 和 varchar 区别](https://dev.mysql.com/doc/refman/5.7/en/char.html) -+ 谈谈[7层网络协议](https://baike.baidu.com/item/%E7%BD%91%E7%BB%9C%E4%B8%83%E5%B1%82%E5%8D%8F%E8%AE%AE) -+ [TCP三次握手,四次分手](http://blog.csdn.net/youxiansanren/article/details/52435239) -+ GitHub 上除了简历上写的两个项目,还有没有其他项目。[我的 Github](https://github.com/g10guang) -+ 做项目选择的技术的原因,如为什么在博客中选择 MongoDB 而不是 MySQL 等 -+ HTTP 状态码,4xx 和 5xx 大家都很熟悉,他提到了部分 3xx,比如 [301 和 302 的区别](http://veryyoung.me/blog/2015/08/24/difference-between-301-and-302.html),正确地使用 3xx 可以控制缓存以及搜索引擎优化 SEO - -面试官面试我的时候,其实很多问题我都了解过,但是没有深入了解过,回答的时候感觉底气也不是很足。 - - -# 技术二面 - -同样的套路,在看简历的时候让我进行自我介绍。 - -二面中,面试官问了很多我简历写的项目上的一些技术细节(实现的模式、原理),比如电商平台的[抢购功能](https://mp.weixin.qq.com/s?__biz=MjM5ODYxMDA5OQ==&mid=2651959391&idx=1&sn=fb28fd5e5f0895ddb167406d8a735548&scene=21#wechat_redirect)、博客评论中的楼中楼、如何确保数据安全同步以及更多的实现业务逻辑的思路。 - - -他的问题,没有问到技术的细节,而是其中的道理,大概是大道至简,只要把其中的原理弄清楚了,那么将思路方法通过其他编程语言或者框架的难度不大。我仍然记得,ofo面试管对我说的,如果公司只要招一个对语言或者框架熟悉的人才,那么花几个月对新员工进行培训就可以做到,更加重要的是扎实的技术知识,基础不扎实会成为个人成长的瓶颈。个人感觉确实是这样的,我从 Java Web 往 Python Web 转,只在两个星期就上手了,成本相对低。 - -在面试中,我主动问了一些关于笔试题的一些答案,或者面试官认为有没有更优的实现方案。比如上述 CASE WHEN 的解法就是他教我的,数组的排序并去重也给出了他的意见。针对 SQL 我还提问了真正项目开发中,会不会写存储过程等 SQL 编程。他给出公司中的 DBA 不建议写存储过程,因为这样会将业务逻辑往 DB 转,不仅增加数据库压力,而且会在业务逻辑改变时需要更新多个 DB 的存储过程,成本很高,而且可维护性低。 - -因为斗鱼公司在高峰时,有上千万人同时在线观看斗鱼视频,我对他们的高并发处理很感兴趣,咨询他们的技术解决方案。他列举了很多 CDN 缓存、[反向代理](https://www.zhihu.com/question/24723688)、浏览器端缓存、分布式等,还有列举一个曾经使用 PHP 无法解决的性能问题,转到 Golang 就很好地解决了性能问题。现在确实很多互联网方面的公司在往 Golang 转,[Why Golang](https://medium.com/@kevalpatel2106/why-should-you-learn-go-f607681fad65)。 - -既然抛出了 PHP(脚本语言)和 Golang(编译语言),我就问面试官在脚本语言和编译语言中的选择问题,他给我的答案是公司鼓励多语言发展,语言只是实现工具,不要被语言局限了视野,只要打好基础,学习一门新语言是很轻松的,具体开发中要根据具体情况选择合适的语言。 - -PS:面试官没有问技术细节一部分原因很可能是我简历上写的项目都是用 Java Python 方面技术写的,而面试官是主要从事 PHP 开发的。 - -# 三面 + HR 四面 - -我不确定三面是从事什么的,进行车轮战,这时候我的头脑已经不是很清楚了,他进门的时候对他自己进行了简短的自我介绍,但是我没有听清楚,我也尝试过看他的工牌,但是看不清楚。 - -面试官一开始坐下来,跟我聊了聊技术、项目方面的问题,他说进来之前,看过[我的博客](https://whoyoung.me/),说我的博客是自己搭建的,而不是使用第三方平台或者是使用开源模板搭建的,然后就对我的博客产生了兴趣,比如对博客会不会进行再开发之类的深入问了几个问题,我提到了未来可能添加一个资源聚合功能,从知乎等爬取优质网站,放到我们博客上。他跟着问了一些爬虫的是否合法以及数据安全等问题,问我有没有对自己博客采取反爬虫等机制。接着他问了我平时使用的操作系统,以及有没有使用阿里云等云服务器(我PC使用 [Linux Mint](https://linuxmint.com/),在腾讯云购买了两个云服务器,学生优惠很大,建议计算机类大学生去申请资源)。 - -接着他跟我聊了聊公司内的氛围、团建活动、健身器材等,描述斗鱼公司在光谷人均消费500RMB下的都尝了一遍,我更关心健身器材,因为我有保持健身,失望的是,他们居然没有举重等无氧运动设备,但他补充说,斗鱼正在建立自己的园区(4年不到建立园区,可见斗鱼发展之快),斗鱼很快会有更加健全的健身设施。而且看得出他很喜欢现在的工作,对公司的价值观很认同,表示自己与公司正在同步成长,可见在认同的公司中工作是多么重要的,表示斗鱼公司有很多内部分享和参加技术峰会,让内部员工有更多的机会学习成长。 - -他表示除了技术方面,还要多了解点其他东西,多学点东西,对技术要有热情、专研精神。和他还是聊得很愉快的,而且他也很友善,问我职业发展规划怎样的,而且给出了他的建议。他也问了有关我毕业设计等问题。 - -HR四面,没有聊太多东西,可能更多的是了解个人性格等,她也要求我对个人进行简短的介绍(非技术方面,更多的是学习状态和性格),谈谈我对斗鱼公司的了解。令我惊讶的是,她也和我谈项目,问我最愿意拿出来和她分享的是哪一个项目之类的问题。谈了没多久,时间确实已经很晚了,她让我先回去等电话通知。 - -# 总结 - -1. 不要把自己不熟悉的内容写到简历上,不要不懂装懂,否则面试官可能针对它不断发问 -2. 需要通过自我介绍和简历引导面试官往自己熟悉的方面发问 -3. 需要打好基本功,数据结构与算法、操作系统、网络协议以及所使用技术 -4. 面试前需要对公司有一个大概了解,比如公司产品、服务以及技术 -5. 敢于问为什么,在面试结束前,可以请面试官解答一些问题 -6. 平时学习要把其中的“道”弄清楚 -7. 最好有一两个项目可以让面试官发问,不然面试官自由发挥,后果自负 +--- +layout: post +title: "面试斗鱼后端工程师后感" +date: 2017-12-01 09:00:05 +0800 +categories: 面经 +--- + +前两天去了面试ofo的后端工程师,被吊打,具体经历可以查看[《面试ofo后端工程师后感》](https://zhuanlan.zhihu.com/p/30449067)。 + +昨天我参加了斗鱼的校园招聘笔试以及面试,从下午一点半一直战斗到晚上的六点半,特别是面试过程中,对精神消耗很大,一个会议室狭小、密闭的空间里,一直进行面试。技术一面、技术二面、三面(我不确定第三面是否是HR可能更加倾向于技术团队的管理)、HR四面。 + +# 笔试 + +HR给每一个人相应的笔试题,前端、PHP后端、Golang后端、Java后端、产品经理、数据分析等等。我应聘的是Java后端,前几道题目主要是关于 Java 语言的基础([接口与抽象类的区别](http://www.importnew.com/12399.html)、抽象类与普通类的区别,[创建对象的方式](http://www.importnew.com/22405.html),多线程,[基础类型](http://www.runoob.com/java/java-basic-datatypes.html)以及对应封装类)、MySQL 的表操作(同时使用 JOIN ON / [CASE WHEN](http://www.mysqltutorial.org/mysql-case-function/) / GROUP BY / WHERE / SELECT 等操作)、前端 JS 响应 select 下拉条代码、字符串操作、排序去重算法。 + +其中 MySQL 题目如下: + ++ 有 A 表,其中记录着每个学生的每一科目的成绩(name、score、subject); ++ 有 B 表,其中记录着每个科目占总成绩比例(subject、percent); ++ 求在 0~59、60~89、90~100的学生比例 + + + +对应的 SQL 查询语句: + +```sql +SELECT + sum( + CASE + WHEN stuScores.score BETWEEN 0 AND 59 THEN 1 + ELSE 0 + END) / count(*) AS 0t059, + sum( + CASE + WHEN stuScores.score BETWEEN 60 AND 89 THEN 1 + ELSE 0 + END) / count(*) AS 60to89, + sum( + CASE + WHEN stuScores.score BETWEEN 90 AND 100 THEN 1 + ELSE 0 + END) / count(*) AS 90to100 +FROM (SELECT sum(A.score * B.percent) as score + FROM A JOIN B ON A.subject = B.subject GROUP BY A.name) as stuScores; +``` + +没有了解过 JS 对于前端的响应事件,没有写出代码,只留下一句声明:**不了解 JavaScript** + +对字符串的对称翻转("01234567" 转化为 "32107654"),Java 中 String 对象是不可改变的,所以每次修改 String 中的内容,都会重新创建一个新的 String 对象,所以这条题目我猜面试官想要看到的是把 String 转化为 char[] 进行操作。 + +最后一题,把随机序列排序并去重,[1, 3, 4, 2, 3, 4] 转化为 [1, 2, 3, 4],最后还要求算出时间以及空间复杂度。我的想法是使用平衡二叉树进行排序,在排序的过程中将去重操作一并完成,无奈忘记了平衡二叉树的调整算法。 + +使用平衡二叉树算法时间空间复杂度: + +``` +时间复杂度 = O(nlog(n)) +空间复杂度 = O(n) +``` + +还可以对数组先使用快速排序等算法,然后再开创另一个新的数组,从头到尾一个一个插到新的数组,记录上一个插入的数字,如果下次要插入的数字等于上一次插入的数字就放弃,否则就插入到新的数组里面。 + +``` +时间复杂度 = O(nlog(n)) +空间复杂度 = O(n) +``` + +# 技术一面 + +面试官在看我简历的时候,顺带让我也进行自我介绍。部分问题我忘了,他问了我大概这么几个问题: + + ++ MySQL 有哪些数据引擎以及谈谈他们的区别、应用场景。[Innodb 和 MyISAM 的区别](https://segmentfault.com/a/1190000008227211) ++ MySQL 有哪些数据类型。[char 和 varchar 区别](https://dev.mysql.com/doc/refman/5.7/en/char.html) ++ 谈谈[7层网络协议](https://baike.baidu.com/item/%E7%BD%91%E7%BB%9C%E4%B8%83%E5%B1%82%E5%8D%8F%E8%AE%AE) ++ [TCP三次握手,四次分手](http://blog.csdn.net/youxiansanren/article/details/52435239) ++ GitHub 上除了简历上写的两个项目,还有没有其他项目。[我的 Github](https://github.com/g10guang) ++ 做项目选择的技术的原因,如为什么在博客中选择 MongoDB 而不是 MySQL 等 ++ HTTP 状态码,4xx 和 5xx 大家都很熟悉,他提到了部分 3xx,比如 [301 和 302 的区别](http://veryyoung.me/blog/2015/08/24/difference-between-301-and-302.html),正确地使用 3xx 可以控制缓存以及搜索引擎优化 SEO + +面试官面试我的时候,其实很多问题我都了解过,但是没有深入了解过,回答的时候感觉底气也不是很足。 + + +# 技术二面 + +同样的套路,在看简历的时候让我进行自我介绍。 + +二面中,面试官问了很多我简历写的项目上的一些技术细节(实现的模式、原理),比如电商平台的[抢购功能](https://mp.weixin.qq.com/s?__biz=MjM5ODYxMDA5OQ==&mid=2651959391&idx=1&sn=fb28fd5e5f0895ddb167406d8a735548&scene=21#wechat_redirect)、博客评论中的楼中楼、如何确保数据安全同步以及更多的实现业务逻辑的思路。 + + +他的问题,没有问到技术的细节,而是其中的道理,大概是大道至简,只要把其中的原理弄清楚了,那么将思路方法通过其他编程语言或者框架的难度不大。我仍然记得,ofo面试管对我说的,如果公司只要招一个对语言或者框架熟悉的人才,那么花几个月对新员工进行培训就可以做到,更加重要的是扎实的技术知识,基础不扎实会成为个人成长的瓶颈。个人感觉确实是这样的,我从 Java Web 往 Python Web 转,只在两个星期就上手了,成本相对低。 + +在面试中,我主动问了一些关于笔试题的一些答案,或者面试官认为有没有更优的实现方案。比如上述 CASE WHEN 的解法就是他教我的,数组的排序并去重也给出了他的意见。针对 SQL 我还提问了真正项目开发中,会不会写存储过程等 SQL 编程。他给出公司中的 DBA 不建议写存储过程,因为这样会将业务逻辑往 DB 转,不仅增加数据库压力,而且会在业务逻辑改变时需要更新多个 DB 的存储过程,成本很高,而且可维护性低。 + +因为斗鱼公司在高峰时,有上千万人同时在线观看斗鱼视频,我对他们的高并发处理很感兴趣,咨询他们的技术解决方案。他列举了很多 CDN 缓存、[反向代理](https://www.zhihu.com/question/24723688)、浏览器端缓存、分布式等,还有列举一个曾经使用 PHP 无法解决的性能问题,转到 Golang 就很好地解决了性能问题。现在确实很多互联网方面的公司在往 Golang 转,[Why Golang](https://medium.com/@kevalpatel2106/why-should-you-learn-go-f607681fad65)。 + +既然抛出了 PHP(脚本语言)和 Golang(编译语言),我就问面试官在脚本语言和编译语言中的选择问题,他给我的答案是公司鼓励多语言发展,语言只是实现工具,不要被语言局限了视野,只要打好基础,学习一门新语言是很轻松的,具体开发中要根据具体情况选择合适的语言。 + +PS:面试官没有问技术细节一部分原因很可能是我简历上写的项目都是用 Java Python 方面技术写的,而面试官是主要从事 PHP 开发的。 + +# 三面 + HR 四面 + +我不确定三面是从事什么的,进行车轮战,这时候我的头脑已经不是很清楚了,他进门的时候对他自己进行了简短的自我介绍,但是我没有听清楚,我也尝试过看他的工牌,但是看不清楚。 + +面试官一开始坐下来,跟我聊了聊技术、项目方面的问题,他说进来之前,看过[我的博客](https://whoyoung.me/),说我的博客是自己搭建的,而不是使用第三方平台或者是使用开源模板搭建的,然后就对我的博客产生了兴趣,比如对博客会不会进行再开发之类的深入问了几个问题,我提到了未来可能添加一个资源聚合功能,从知乎等爬取优质网站,放到我们博客上。他跟着问了一些爬虫的是否合法以及数据安全等问题,问我有没有对自己博客采取反爬虫等机制。接着他问了我平时使用的操作系统,以及有没有使用阿里云等云服务器(我PC使用 [Linux Mint](https://linuxmint.com/),在腾讯云购买了两个云服务器,学生优惠很大,建议计算机类大学生去申请资源)。 + +接着他跟我聊了聊公司内的氛围、团建活动、健身器材等,描述斗鱼公司在光谷人均消费500RMB下的都尝了一遍,我更关心健身器材,因为我有保持健身,失望的是,他们居然没有举重等无氧运动设备,但他补充说,斗鱼正在建立自己的园区(4年不到建立园区,可见斗鱼发展之快),斗鱼很快会有更加健全的健身设施。而且看得出他很喜欢现在的工作,对公司的价值观很认同,表示自己与公司正在同步成长,可见在认同的公司中工作是多么重要的,表示斗鱼公司有很多内部分享和参加技术峰会,让内部员工有更多的机会学习成长。 + +他表示除了技术方面,还要多了解点其他东西,多学点东西,对技术要有热情、专研精神。和他还是聊得很愉快的,而且他也很友善,问我职业发展规划怎样的,而且给出了他的建议。他也问了有关我毕业设计等问题。 + +HR四面,没有聊太多东西,可能更多的是了解个人性格等,她也要求我对个人进行简短的介绍(非技术方面,更多的是学习状态和性格),谈谈我对斗鱼公司的了解。令我惊讶的是,她也和我谈项目,问我最愿意拿出来和她分享的是哪一个项目之类的问题。谈了没多久,时间确实已经很晚了,她让我先回去等电话通知。 + +# 总结 + +1. 不要把自己不熟悉的内容写到简历上,不要不懂装懂,否则面试官可能针对它不断发问 +2. 需要通过自我介绍和简历引导面试官往自己熟悉的方面发问 +3. 需要打好基本功,数据结构与算法、操作系统、网络协议以及所使用技术 +4. 面试前需要对公司有一个大概了解,比如公司产品、服务以及技术 +5. 敢于问为什么,在面试结束前,可以请面试官解答一些问题 +6. 平时学习要把其中的“道”弄清楚 +7. 最好有一两个项目可以让面试官发问,不然面试官自由发挥,后果自负 diff --git a/src/md/2018-01-19-ISIS-after-read.md b/src/md/2018-01-19-ISIS-after-read.md index 4dd1c40..35e150f 100644 --- a/src/md/2018-01-19-ISIS-after-read.md +++ b/src/md/2018-01-19-ISIS-after-read.md @@ -1,66 +1,66 @@ ---- -layout: post -title: "《黑旗:ISIS的崛起》读后感" -date: 2018-02-02 09:00:05 +0800 -categories: 读书心得 ---- - -如果这个世界上不是存在一个叫做ISIS的组织,那么我肯定以为该书所描述的只是虚构的故事,一切就像美国或者香港的犯罪电影,中央情报局、各国领袖、武器、间谍等等。 - -我是从比尔盖茨微信公众号中了解到有该书籍的存在的,能够引起我的兴趣的就是一个恐怖组织的头目--扎卡维是如何具有如此巨大的魅力能够让人们跟随他从事各种恐怖袭击,甚至是自杀式恐怖行为。 - -而我从书中得到的答案是: - -+ 关心手下 -+ 对宗教的痴迷 -+ 散播恐惧,通过恐惧控制他人 -+ 勇敢等个人气质 -+ 各国在国际上宣扬他的恐怖行为,变相在帮扎卡维做广告(美国天价的通缉令) - -本书主角是扎卡维,一个出生在约旦小村庄里面的小混混,早年辍学,暴利倾向,曾在监狱中待过好几年,最后因为约旦换领导人(父死子承),实行大赦免,不小心把扎卡维以及他的早年精神导师放出监狱。(PS:当局者最后发现名单上有扎卡维名字的时候并不希望赦免他,因为他实在太恐怖,但是因为赦免名单上已经有了国王的签名,一字千金) - -扎卡维在监狱中的日子,在宗教上第一位导师麦格迪西(此人是监狱中的囚犯老大)的影响下,开始迷上了《古兰经》等伊斯兰教经典。但是麦格迪西所秉承的宗教信仰与扎卡维最后自身的极端宗教是两码事,后来师徒两人还会在互联网上发表意见批评对方的做法。 - -扎卡维热爱锻炼,在监狱中通过打磨石头为哑铃,体格非常强壮,他非常关心监狱中的伙伴。在监狱中还发生了一起重要的事件,囚犯们发动政变,推翻麦格迪西的地位,推荐扎卡维为新的领袖。监狱中的医生回忆道,扎卡维很少说话,但是他就像是能够通过眼神就控制一切的人,一看就是领袖。 - -ISIS的前身以及现在的领导人都是从监狱中培育出来的宗教极端分子,作者描述说,监狱中把重犯和轻犯统一关在一起,重犯们通过宣扬极端宗教思想,最后把轻犯也影响为极端宗教分子。其中ISIS的现领导人巴格达迪宣称,宗教思想最发达的是监狱,他在发展ISIS的过程中,还把监狱中的囚犯放出,将他们加入到ISIS组织中去。 - -扎卡维等人通过“圣战”来号召各地的勇士,他还想与“基地”组织合作,壮大自己的实力。“基地”组织是本-拉登控制的恐怖组织,遍布世界各地。可整个过程中,扎卡维并没有得到本拉登的赏识,直到美国征服开出通缉令,扎卡维的身价与本拉登的身价平齐。 - -本拉登身边养着不少宗教学者,他们的任务就是负责使用宗教知识,来解释本拉登旗下的“基地”组织的种种恐怖行为,通过宗教上的信仰,误导年轻的宗教分子走上极端,加入“基地”组织。但是本拉登本人受到全世界的通缉,除了拍摄视频外不能在任何地方露面,他只能够通过邮件等方式来下达命令。本拉登与扎卡维都是非常注重个人和所在组织的形象的,因为这会关乎他们会不会得到新人加入和来自国际上的融资。 - -通过宗教的力量,ISIS等恐怖组织得到世界上各地的人赞助,其中他们还会在Facebook、Twitter等网站上**融资**,有一些有钱人甚至飞到本地直接将钱送到他们手上,最为回报ISIS以该人的名字命名某组织。 - -扎卡维是通过伊拉克的宗教内战来赢得自己的地位,他挑拨离间多个宗教派,使他们内战,而扎卡维则是坐收渔人之利。他通过视频散布自己斩首美国人的视频来宣扬自己,公然挑战世界警察美国。当时美国的总统是布什,他每年在伊拉克上花费大量金钱去维护伊拉克的稳定。伊拉克沙漠下有大量的石油,而一旦这些石油全部由恐怖分子控制,石油可是工业的血液,黑色的金子,石油一旦失手,这就会造成世界大灾难,或者这也是为什么目前各国大力发展新能源的原因吧。还有一个原因就是战争会造成大量的移民,他们会涌向别人发达的、稳定的国家,比如西欧、北美。人口暴增,对于任何一个国家来说都是灾难,而后特朗普的移民新政就是为了驱逐非法移民。 - -扎卡维是在布什的任职期间斩获的,本拉登是在奥巴马期间斩获的。奥巴马与布什采用的军事策略不一样,他在选举时就声明不会干涉中东政治,因为布什每年花费大量纳税人的钱去做世界警察,纳税人非常不讨喜。而奥巴马说把这些钱都拿去做医改,每人都能够得到医疗保险,纳税人特别是社会底层的人们就欢喜了。(PS:医改已经被特朗普废除) - -还有一个有趣的现象就是,政府没有这么多的金钱去和恐怖组织做斗争,极端分子在恐怖组织一年赚到的钱比在军队中多得多。 - -扎卡维只是通过号召人们前来参加“圣战”,利用了伊拉克当地人对美国军队的仇恨,因为伊拉克当地人都人为是美国人带来的战争,他们都非常痛恨美国人。扎卡维通过宗教信仰,给极端分子的仅仅只是一个承诺,他们死后就会进入天堂,而被他们炸死的美国人则下地狱。仅仅是这样一个宗教上的承诺,就吸引了无数人投奔他,自愿参加人弹行为。(人弹就是将炸弹绑在自己身上,自杀式的行为,在伊拉克是不允许搜索女人的身体的,女人会穿得很严密,所以女人就是一个人弹的好选择) - -而人们后来发掘,扎卡维的真正目的不是报复美国人,因为扎卡维是约旦人,他前往伊拉克的原因就是这里动乱适合他的组织发展。所以他后来的恐怖行动中,有不少是针对伊拉克当地人,女人、孩子,这时候人们发现,扎卡维只是单纯地想要制造恐惧来控制人们,伊拉克渐渐地沦为了无政府装态,各势力都想要获得统治地位,不仅仅只有扎卡维。 - -而美国情报局是通过不断地捕获扎卡维的手下,通过拷问来得到情报,最终得知扎卡维还有另一位精神导师,他们每隔7~10天就会见面,美国军队通过跟踪该导师,找到了扎卡维的藏身之地,使用了空袭将扎卡维击毙。 - -一旦军方人员被捕,大部分都会背扎卡维亲自执刑,非常残暴。 - -极端宗教分子虽然在行事的时候非常勇敢、无情,有时甚至可以执行自杀式的任务,但是在被捕获后,有一些扎卡维的亲信都诚实地交待了情报。(PS:或者美国中央情报局的拷问有学问,能够诱导对人一步步突出真言) - -而扎卡维死后,其组织的势力并没有消去,他的意志被传承下来,最后发展为现在的伊斯兰国。(PS:伊斯兰国和伊斯兰没有任何关系) - -ISIS的真正崛起是在阿拉伯之春的爆发,其战地是在叙利亚。叙利亚人民希望当政者下台,国际上的国家领袖以及当时的奥巴马也发出声明需要叙利亚的当政者在xxxx-xx-xx日前下台,可是当政者不愿意下台(谁不贪婪权利地位呢?),从而引发了战争,这就提供了ISIS发展壮大的好机会,这情况就像扎卡维时期的伊拉克。 - -恐怖组织的主要金钱来源: - -+ 中东地区丰富的石油 -+ 罂粟等毒品种植 -+ 国家上的募捐 -+ 人口贩卖 -+ 掠夺财富 - -**引发我的思考:** - -上述这么多的恐怖行为大部分都是借助了极端宗教的力量而达到的,无论是“圣战”的号召还是“上天堂,下地狱”的承诺。在**《人类简史》**和**《未来简史》**中都极大地强调了宗教的作用,宗教一开始的存在是为了解释某个当时人们还无法的解释的现象,或者帮助人们克服内心的恐惧。人类发展了这么旧,几乎每个民族都有各自的信仰,其中数基督教、伊斯兰教、佛教比较普遍,但是随着知识的发展,人类能够利用已有的知识去解释各种现象,而不是借助宗教去讲故事,我们年青一代似乎也没有如此宗教信仰,那么未来宗教会如何发展?未来的人会遗忘宗教吗? - +--- +layout: post +title: "《黑旗:ISIS的崛起》读后感" +date: 2018-02-02 09:00:05 +0800 +categories: 读书心得 +--- + +如果这个世界上不是存在一个叫做ISIS的组织,那么我肯定以为该书所描述的只是虚构的故事,一切就像美国或者香港的犯罪电影,中央情报局、各国领袖、武器、间谍等等。 + +我是从比尔盖茨微信公众号中了解到有该书籍的存在的,能够引起我的兴趣的就是一个恐怖组织的头目--扎卡维是如何具有如此巨大的魅力能够让人们跟随他从事各种恐怖袭击,甚至是自杀式恐怖行为。 + +而我从书中得到的答案是: + ++ 关心手下 ++ 对宗教的痴迷 ++ 散播恐惧,通过恐惧控制他人 ++ 勇敢等个人气质 ++ 各国在国际上宣扬他的恐怖行为,变相在帮扎卡维做广告(美国天价的通缉令) + +本书主角是扎卡维,一个出生在约旦小村庄里面的小混混,早年辍学,暴利倾向,曾在监狱中待过好几年,最后因为约旦换领导人(父死子承),实行大赦免,不小心把扎卡维以及他的早年精神导师放出监狱。(PS:当局者最后发现名单上有扎卡维名字的时候并不希望赦免他,因为他实在太恐怖,但是因为赦免名单上已经有了国王的签名,一字千金) + +扎卡维在监狱中的日子,在宗教上第一位导师麦格迪西(此人是监狱中的囚犯老大)的影响下,开始迷上了《古兰经》等伊斯兰教经典。但是麦格迪西所秉承的宗教信仰与扎卡维最后自身的极端宗教是两码事,后来师徒两人还会在互联网上发表意见批评对方的做法。 + +扎卡维热爱锻炼,在监狱中通过打磨石头为哑铃,体格非常强壮,他非常关心监狱中的伙伴。在监狱中还发生了一起重要的事件,囚犯们发动政变,推翻麦格迪西的地位,推荐扎卡维为新的领袖。监狱中的医生回忆道,扎卡维很少说话,但是他就像是能够通过眼神就控制一切的人,一看就是领袖。 + +ISIS的前身以及现在的领导人都是从监狱中培育出来的宗教极端分子,作者描述说,监狱中把重犯和轻犯统一关在一起,重犯们通过宣扬极端宗教思想,最后把轻犯也影响为极端宗教分子。其中ISIS的现领导人巴格达迪宣称,宗教思想最发达的是监狱,他在发展ISIS的过程中,还把监狱中的囚犯放出,将他们加入到ISIS组织中去。 + +扎卡维等人通过“圣战”来号召各地的勇士,他还想与“基地”组织合作,壮大自己的实力。“基地”组织是本-拉登控制的恐怖组织,遍布世界各地。可整个过程中,扎卡维并没有得到本拉登的赏识,直到美国征服开出通缉令,扎卡维的身价与本拉登的身价平齐。 + +本拉登身边养着不少宗教学者,他们的任务就是负责使用宗教知识,来解释本拉登旗下的“基地”组织的种种恐怖行为,通过宗教上的信仰,误导年轻的宗教分子走上极端,加入“基地”组织。但是本拉登本人受到全世界的通缉,除了拍摄视频外不能在任何地方露面,他只能够通过邮件等方式来下达命令。本拉登与扎卡维都是非常注重个人和所在组织的形象的,因为这会关乎他们会不会得到新人加入和来自国际上的融资。 + +通过宗教的力量,ISIS等恐怖组织得到世界上各地的人赞助,其中他们还会在Facebook、Twitter等网站上**融资**,有一些有钱人甚至飞到本地直接将钱送到他们手上,最为回报ISIS以该人的名字命名某组织。 + +扎卡维是通过伊拉克的宗教内战来赢得自己的地位,他挑拨离间多个宗教派,使他们内战,而扎卡维则是坐收渔人之利。他通过视频散布自己斩首美国人的视频来宣扬自己,公然挑战世界警察美国。当时美国的总统是布什,他每年在伊拉克上花费大量金钱去维护伊拉克的稳定。伊拉克沙漠下有大量的石油,而一旦这些石油全部由恐怖分子控制,石油可是工业的血液,黑色的金子,石油一旦失手,这就会造成世界大灾难,或者这也是为什么目前各国大力发展新能源的原因吧。还有一个原因就是战争会造成大量的移民,他们会涌向别人发达的、稳定的国家,比如西欧、北美。人口暴增,对于任何一个国家来说都是灾难,而后特朗普的移民新政就是为了驱逐非法移民。 + +扎卡维是在布什的任职期间斩获的,本拉登是在奥巴马期间斩获的。奥巴马与布什采用的军事策略不一样,他在选举时就声明不会干涉中东政治,因为布什每年花费大量纳税人的钱去做世界警察,纳税人非常不讨喜。而奥巴马说把这些钱都拿去做医改,每人都能够得到医疗保险,纳税人特别是社会底层的人们就欢喜了。(PS:医改已经被特朗普废除) + +还有一个有趣的现象就是,政府没有这么多的金钱去和恐怖组织做斗争,极端分子在恐怖组织一年赚到的钱比在军队中多得多。 + +扎卡维只是通过号召人们前来参加“圣战”,利用了伊拉克当地人对美国军队的仇恨,因为伊拉克当地人都人为是美国人带来的战争,他们都非常痛恨美国人。扎卡维通过宗教信仰,给极端分子的仅仅只是一个承诺,他们死后就会进入天堂,而被他们炸死的美国人则下地狱。仅仅是这样一个宗教上的承诺,就吸引了无数人投奔他,自愿参加人弹行为。(人弹就是将炸弹绑在自己身上,自杀式的行为,在伊拉克是不允许搜索女人的身体的,女人会穿得很严密,所以女人就是一个人弹的好选择) + +而人们后来发掘,扎卡维的真正目的不是报复美国人,因为扎卡维是约旦人,他前往伊拉克的原因就是这里动乱适合他的组织发展。所以他后来的恐怖行动中,有不少是针对伊拉克当地人,女人、孩子,这时候人们发现,扎卡维只是单纯地想要制造恐惧来控制人们,伊拉克渐渐地沦为了无政府装态,各势力都想要获得统治地位,不仅仅只有扎卡维。 + +而美国情报局是通过不断地捕获扎卡维的手下,通过拷问来得到情报,最终得知扎卡维还有另一位精神导师,他们每隔7~10天就会见面,美国军队通过跟踪该导师,找到了扎卡维的藏身之地,使用了空袭将扎卡维击毙。 + +一旦军方人员被捕,大部分都会背扎卡维亲自执刑,非常残暴。 + +极端宗教分子虽然在行事的时候非常勇敢、无情,有时甚至可以执行自杀式的任务,但是在被捕获后,有一些扎卡维的亲信都诚实地交待了情报。(PS:或者美国中央情报局的拷问有学问,能够诱导对人一步步突出真言) + +而扎卡维死后,其组织的势力并没有消去,他的意志被传承下来,最后发展为现在的伊斯兰国。(PS:伊斯兰国和伊斯兰没有任何关系) + +ISIS的真正崛起是在阿拉伯之春的爆发,其战地是在叙利亚。叙利亚人民希望当政者下台,国际上的国家领袖以及当时的奥巴马也发出声明需要叙利亚的当政者在xxxx-xx-xx日前下台,可是当政者不愿意下台(谁不贪婪权利地位呢?),从而引发了战争,这就提供了ISIS发展壮大的好机会,这情况就像扎卡维时期的伊拉克。 + +恐怖组织的主要金钱来源: + ++ 中东地区丰富的石油 ++ 罂粟等毒品种植 ++ 国家上的募捐 ++ 人口贩卖 ++ 掠夺财富 + +**引发我的思考:** + +上述这么多的恐怖行为大部分都是借助了极端宗教的力量而达到的,无论是“圣战”的号召还是“上天堂,下地狱”的承诺。在**《人类简史》**和**《未来简史》**中都极大地强调了宗教的作用,宗教一开始的存在是为了解释某个当时人们还无法的解释的现象,或者帮助人们克服内心的恐惧。人类发展了这么旧,几乎每个民族都有各自的信仰,其中数基督教、伊斯兰教、佛教比较普遍,但是随着知识的发展,人类能够利用已有的知识去解释各种现象,而不是借助宗教去讲故事,我们年青一代似乎也没有如此宗教信仰,那么未来宗教会如何发展?未来的人会遗忘宗教吗? + 待未来有空阅读《未来简史》或许能够得到答案,其作者是以色列著名的历史学家、宗教学家赫拉利。 \ No newline at end of file diff --git "a/src/md/2018-01-20-\345\274\200\345\217\221\350\275\257\344\273\266\345\222\214\350\204\232\346\234\254\347\232\204\345\214\272\345\210\253.md" "b/src/md/2018-01-20-\345\274\200\345\217\221\350\275\257\344\273\266\345\222\214\350\204\232\346\234\254\347\232\204\345\214\272\345\210\253.md" index ac7500a..e6ab0dd 100644 --- "a/src/md/2018-01-20-\345\274\200\345\217\221\350\275\257\344\273\266\345\222\214\350\204\232\346\234\254\347\232\204\345\214\272\345\210\253.md" +++ "b/src/md/2018-01-20-\345\274\200\345\217\221\350\275\257\344\273\266\345\222\214\350\204\232\346\234\254\347\232\204\345\214\272\345\210\253.md" @@ -1,38 +1,38 @@ ---- -layout: post -title: "开发软件和小脚本的区别" -date: 2018-01-20 09:00:05 +0800 -categories: 心得 ---- - -近日开发了一个武汉理工大学抢课软件,断断续续开发该软件花了不少时间,[Github仓库](https://github.com/g10guang/WHUT_Courses_System) - -**以下是本文的小定义:** - -+ 小脚本:容错性很差,是给专门人使用的,需要使用人对脚本逻辑有一定了解,比如Python脚本 -+ 软件:开放给大众使用,需要提供好的 UI,需要很好的容错性,需要反馈给用户信息 - -如果只是供笔者用于的小脚本,完成抢课逻辑并不需要多少的代码量,可能几个小时分析问题,写几十行小脚本就完事了。但是我萌生了一个开发一个帮助同学抢课的软件,免得同学在抢课时分候着电脑、手机、平板点点点,最后却什么都没得到。 - -项目中需要爬取选课系统中的信息,就需要用到了爬虫,于是我屁颠屁颠地邀请懂爬虫的友人参与我的开发中。 - -我是平常研究的后端开发比较多,但是软件需要提供用户 UI,于是我匆匆忙忙地学习了TkInter的基本用法,随机开始了设计UI,由于不熟悉,开发过程也是不断地Google。很快用户界面开发得差不多了,但是由于Python的GIL存在的,为了提供用户体验,不让界面一卡一卡的,我又去研究了Python的多进程以及多线程(PS:采用的思路是抢课任务分配到新进程中进行,而且每添加一个新的任务就创建新的线程去执行) - - -其后爬虫的第一个方案似乎出了点问题,而且使用了Scrapy对于本项目有点笨重,笔者后面就去学了 requests+BeautifulSoup 作为简单的爬虫,去爬取特定的信息,requests模拟登陆拿到响应的html页面,BeautifulSoup 解析页面提取相关信息。 - -由于学校选课网站是外包项目,html写得非常凌乱(还有注释~~),每个选课的表格还不一样,还要为专门为某个选课做特殊处理。(可能这就是选课系统的防爬虫的措施吧~~) - -幸好,requests与BeatifulSoup都提供了良好的文档和API,整个爬虫开发从学习到完成也就是花了两天时间而已。但是依然无法做到百分百的覆盖,我只能说爬虫在非抢课期间能够成功执行,但是如果在真正抢课期间,选课系统随时宕机,爬虫也就无法保证一定能正确无误地抓去数据。 - -怎么样能够提升用于体验,提供用户真正需要的功能,这确实是需要大量的思考。这也就是为什么腾讯、知乎等企业招聘员工要求应聘者需要有产品sense。 - -整个过程下来,感觉开发一个给予大众使用的软件成本非常高,怪不得web中要强调前后端分离,这样才能够做到前端能够注重用户体验,后端能够专注业务逻辑。整个团队的开发效率才会有所提升,术业有专攻,也不需要有那么一群人每个人都精通所有,(即使是前端也有专注与写JS逻辑、CSS样式)但付出的就是沟通成本。 - -**最后思考:** - -一个网站防爬虫是多么重要,之前看朋友爬美团、携程的数据非常有趣,真实显示的数据与HTML中的数据相差是非常大的,他们之间有一些转换规则,或者是显示的价格是从图片中一个一个数字扣下来的,用心良苦,这给爬虫爬取有用的信息带来了很大的难度。有兴趣的朋友可以去尝试一下。 - -之前老师上课说真正项目中有一大半代码都是防御型代码,为了保证代码的鲁棒性。 - -如果这个世界每个人都那么遵守规则,这个社会做锁的成本会低了多少。 +--- +layout: post +title: "开发软件和小脚本的区别" +date: 2018-01-20 09:00:05 +0800 +categories: 心得 +--- + +近日开发了一个武汉理工大学抢课软件,断断续续开发该软件花了不少时间,[Github仓库](https://github.com/g10guang/WHUT_Courses_System) + +**以下是本文的小定义:** + ++ 小脚本:容错性很差,是给专门人使用的,需要使用人对脚本逻辑有一定了解,比如Python脚本 ++ 软件:开放给大众使用,需要提供好的 UI,需要很好的容错性,需要反馈给用户信息 + +如果只是供笔者用于的小脚本,完成抢课逻辑并不需要多少的代码量,可能几个小时分析问题,写几十行小脚本就完事了。但是我萌生了一个开发一个帮助同学抢课的软件,免得同学在抢课时分候着电脑、手机、平板点点点,最后却什么都没得到。 + +项目中需要爬取选课系统中的信息,就需要用到了爬虫,于是我屁颠屁颠地邀请懂爬虫的友人参与我的开发中。 + +我是平常研究的后端开发比较多,但是软件需要提供用户 UI,于是我匆匆忙忙地学习了TkInter的基本用法,随机开始了设计UI,由于不熟悉,开发过程也是不断地Google。很快用户界面开发得差不多了,但是由于Python的GIL存在的,为了提供用户体验,不让界面一卡一卡的,我又去研究了Python的多进程以及多线程(PS:采用的思路是抢课任务分配到新进程中进行,而且每添加一个新的任务就创建新的线程去执行) + + +其后爬虫的第一个方案似乎出了点问题,而且使用了Scrapy对于本项目有点笨重,笔者后面就去学了 requests+BeautifulSoup 作为简单的爬虫,去爬取特定的信息,requests模拟登陆拿到响应的html页面,BeautifulSoup 解析页面提取相关信息。 + +由于学校选课网站是外包项目,html写得非常凌乱(还有注释~~),每个选课的表格还不一样,还要为专门为某个选课做特殊处理。(可能这就是选课系统的防爬虫的措施吧~~) + +幸好,requests与BeatifulSoup都提供了良好的文档和API,整个爬虫开发从学习到完成也就是花了两天时间而已。但是依然无法做到百分百的覆盖,我只能说爬虫在非抢课期间能够成功执行,但是如果在真正抢课期间,选课系统随时宕机,爬虫也就无法保证一定能正确无误地抓去数据。 + +怎么样能够提升用于体验,提供用户真正需要的功能,这确实是需要大量的思考。这也就是为什么腾讯、知乎等企业招聘员工要求应聘者需要有产品sense。 + +整个过程下来,感觉开发一个给予大众使用的软件成本非常高,怪不得web中要强调前后端分离,这样才能够做到前端能够注重用户体验,后端能够专注业务逻辑。整个团队的开发效率才会有所提升,术业有专攻,也不需要有那么一群人每个人都精通所有,(即使是前端也有专注与写JS逻辑、CSS样式)但付出的就是沟通成本。 + +**最后思考:** + +一个网站防爬虫是多么重要,之前看朋友爬美团、携程的数据非常有趣,真实显示的数据与HTML中的数据相差是非常大的,他们之间有一些转换规则,或者是显示的价格是从图片中一个一个数字扣下来的,用心良苦,这给爬虫爬取有用的信息带来了很大的难度。有兴趣的朋友可以去尝试一下。 + +之前老师上课说真正项目中有一大半代码都是防御型代码,为了保证代码的鲁棒性。 + +如果这个世界每个人都那么遵守规则,这个社会做锁的成本会低了多少。 diff --git a/src/md/2018-02-01-golang-concurrent.md b/src/md/2018-02-01-golang-concurrent.md index 8b00791..5912b8e 100644 --- a/src/md/2018-02-01-golang-concurrent.md +++ b/src/md/2018-02-01-golang-concurrent.md @@ -1,163 +1,163 @@ ---- -layout: post -title: "Golang 并发基础" -date: 2018-02-06 09:00:05 +0800 -categories: golang ---- - -# goroutine - -> A goroutine is a light weight thread managed by the Go runtime. - -```golang -go f(x, y, z) -``` - -`f, x, y, z` 的计算会发生在当前 goroutine,但执行在另一个新的 goroutine - -goroutine 运行在同一个地址空间,所以访问共享内存时需要同步 - -# channel - -Go 哲学不要使用共享内存通信,而是通过 channel 通信来达到共享内存的目的 - -channel 是用于 goroutine 之间的通信 - -通道是一个通信管道,可以通过通道操作符 `<-` 来接收,发送消息(PS: 估计类似与进程管道进行通信) - -表现同时也像队列,先进先出。普通 channel 可以用来做同步控制(synchronization),因为 channel 的一方必须等另一方准备完成才能进行读写,比如: - -Sender 把一个数据放进 channel,Reader 还未进行读,此时 Sender 再次把数据放进 channel 就会阻塞 Sender,知道 Reader 把数据从 channel 读取出来 - -一个 goroutine 可以写和读 channel,但是不能够读自己写的数据,读自己写的数据将会发生阻塞,如果所有 goroutine 都 asleep 那么 go runtime 认为出现了死锁 - -```golang -ch <- v // 发送 v 到 channel ch -v := <-ch // 从 channel ch 接收一个值,并且赋予 v -``` - -数据流动方向就是箭头方向,`<-` - -创建 channel - -```golang -ch := make(chan int) -``` - -默认情况下,直到对方准备好,才开始发送和接收消息。这提供了 goroutine 之间的同步机制,而不是采用特殊的锁或条件变量 - -# buffered channel - -channel 可以是有缓存的,提高缓存的长度用来初始化 channel - -```golang -ch := make(chan int, 2) -``` - -> 向满的 buffered channel 中输入数据,或者是向空 buffered channel 中读取数据,会发生错误 - -# Range and Close - -发送者可以 `close` 关闭 channel,表明没有再多的元素被发送; - -接受者可以测试一个 channel 是否已经关闭 `v, ok := <- ch` - -> 只有发送者才能够关闭 channel,向已经关闭了的 channel 发送数据会引发 `panic` - -`ok == false` 如果 channel 中没有更多元素,channel 已经被关闭 - -使用 for 循环读取 channel 中的数据,直到 channel 被挂壁 - -`for i := range ch` - -> 不像文件,关闭 channel 并不是必须的。只有当需要通知接受者没有更多的元素时,才需要主动关闭 channel,例如通知接受者停止 `for i := range ch` 循环 - -读取 buffered channel 目前元素个数 `length` 和可以容纳的最多元素个数 `capability` - -```golang -len(ch) -cap(ch) -``` - -从 channel 中不断发送和读取 fibonacci 数列 - -```golang -func fibonacci(n int, ch chan int) { - x, y := 0, 1 - for i := 0; i < n; i++ { - ch <- y - x, y = y, x + y - } - close(ch) -} - -func main() { - ch := make(chan int, 10) - go fibonacci(cap(ch), ch) - // 不断地读取元素,直到 channel closed - for i := range ch { - fmt.Println(i) - } -} -``` - -# select - -select 从多个 channel 中随机选取一个可读或者可写的 channel,执行该 case。 - -```golang -func fibonacci(c, quit chan int) { - x, y := 0, 1 - for { - select { - case c <- y: - x, y = y, x+y - // 从 quit 通道中读取元素,但是没有发生赋值 - case <-quit: - fmt.Println("quit") - return - } - } -} - -func main() { - c := make(chan int) - quit := make(chan int) - go func() { - // 从 c 中读取 10 个元素,然后向 quit 中发送信号,让程序退出 - for i := 0; i < 10; i++ { - fmt.Println(<-c) - } - quit <- 0 - }() - fibonacci(c, quit) -} -``` - -## time.After - -用于控制超时 select - -```golang -select { - case v := <-ch: - doSomething() - // time.After 可以保证一定时间后 channel 即可通信 - case <-time.After(1 * time.Second): - timeout() -} -``` - -# sync.Mutex - -sync.Mutex 用于同步,比如上锁等操作 - -```golang -mux.Lock() // 上锁 -defer mux.Unlock() // 解锁 -``` - -goroutine 不是 thread,每一个 goroutine 都拥有一个自己的调用栈。goroutine 开销很低,可以同时开启上千个 goroutine。 - -有可能一个 thread 中有上千个 goroutine。goroutine 多路动态复用 thread,也就是说真正执行计算的还是 thread,但是一个 thread 可能利用异步等技术来 concurrent 并发多个 goroutine 执行,并且减少切换 thread 上下文的成本 - +--- +layout: post +title: "Golang 并发基础" +date: 2018-02-06 09:00:05 +0800 +categories: golang +--- + +# goroutine + +> A goroutine is a light weight thread managed by the Go runtime. + +```golang +go f(x, y, z) +``` + +`f, x, y, z` 的计算会发生在当前 goroutine,但执行在另一个新的 goroutine + +goroutine 运行在同一个地址空间,所以访问共享内存时需要同步 + +# channel + +Go 哲学不要使用共享内存通信,而是通过 channel 通信来达到共享内存的目的 + +channel 是用于 goroutine 之间的通信 + +通道是一个通信管道,可以通过通道操作符 `<-` 来接收,发送消息(PS: 估计类似与进程管道进行通信) + +表现同时也像队列,先进先出。普通 channel 可以用来做同步控制(synchronization),因为 channel 的一方必须等另一方准备完成才能进行读写,比如: + +Sender 把一个数据放进 channel,Reader 还未进行读,此时 Sender 再次把数据放进 channel 就会阻塞 Sender,知道 Reader 把数据从 channel 读取出来 + +一个 goroutine 可以写和读 channel,但是不能够读自己写的数据,读自己写的数据将会发生阻塞,如果所有 goroutine 都 asleep 那么 go runtime 认为出现了死锁 + +```golang +ch <- v // 发送 v 到 channel ch +v := <-ch // 从 channel ch 接收一个值,并且赋予 v +``` + +数据流动方向就是箭头方向,`<-` + +创建 channel + +```golang +ch := make(chan int) +``` + +默认情况下,直到对方准备好,才开始发送和接收消息。这提供了 goroutine 之间的同步机制,而不是采用特殊的锁或条件变量 + +# buffered channel + +channel 可以是有缓存的,提高缓存的长度用来初始化 channel + +```golang +ch := make(chan int, 2) +``` + +> 向满的 buffered channel 中输入数据,或者是向空 buffered channel 中读取数据,会发生错误 + +# Range and Close + +发送者可以 `close` 关闭 channel,表明没有再多的元素被发送; + +接受者可以测试一个 channel 是否已经关闭 `v, ok := <- ch` + +> 只有发送者才能够关闭 channel,向已经关闭了的 channel 发送数据会引发 `panic` + +`ok == false` 如果 channel 中没有更多元素,channel 已经被关闭 + +使用 for 循环读取 channel 中的数据,直到 channel 被挂壁 + +`for i := range ch` + +> 不像文件,关闭 channel 并不是必须的。只有当需要通知接受者没有更多的元素时,才需要主动关闭 channel,例如通知接受者停止 `for i := range ch` 循环 + +读取 buffered channel 目前元素个数 `length` 和可以容纳的最多元素个数 `capability` + +```golang +len(ch) +cap(ch) +``` + +从 channel 中不断发送和读取 fibonacci 数列 + +```golang +func fibonacci(n int, ch chan int) { + x, y := 0, 1 + for i := 0; i < n; i++ { + ch <- y + x, y = y, x + y + } + close(ch) +} + +func main() { + ch := make(chan int, 10) + go fibonacci(cap(ch), ch) + // 不断地读取元素,直到 channel closed + for i := range ch { + fmt.Println(i) + } +} +``` + +# select + +select 从多个 channel 中随机选取一个可读或者可写的 channel,执行该 case。 + +```golang +func fibonacci(c, quit chan int) { + x, y := 0, 1 + for { + select { + case c <- y: + x, y = y, x+y + // 从 quit 通道中读取元素,但是没有发生赋值 + case <-quit: + fmt.Println("quit") + return + } + } +} + +func main() { + c := make(chan int) + quit := make(chan int) + go func() { + // 从 c 中读取 10 个元素,然后向 quit 中发送信号,让程序退出 + for i := 0; i < 10; i++ { + fmt.Println(<-c) + } + quit <- 0 + }() + fibonacci(c, quit) +} +``` + +## time.After + +用于控制超时 select + +```golang +select { + case v := <-ch: + doSomething() + // time.After 可以保证一定时间后 channel 即可通信 + case <-time.After(1 * time.Second): + timeout() +} +``` + +# sync.Mutex + +sync.Mutex 用于同步,比如上锁等操作 + +```golang +mux.Lock() // 上锁 +defer mux.Unlock() // 解锁 +``` + +goroutine 不是 thread,每一个 goroutine 都拥有一个自己的调用栈。goroutine 开销很低,可以同时开启上千个 goroutine。 + +有可能一个 thread 中有上千个 goroutine。goroutine 多路动态复用 thread,也就是说真正执行计算的还是 thread,但是一个 thread 可能利用异步等技术来 concurrent 并发多个 goroutine 执行,并且减少切换 thread 上下文的成本 + diff --git a/src/md/2018-02-02-golang-basic.md b/src/md/2018-02-02-golang-basic.md index c4cffdd..6d5603f 100644 --- a/src/md/2018-02-02-golang-basic.md +++ b/src/md/2018-02-02-golang-basic.md @@ -1,120 +1,120 @@ ---- -layout: post -title: "Golang 基础" -date: 2018-02-02 09:00:05 +0800 -categories: golang ---- - -# Exported Name - -在 package 中,有且只有以大写开头的变量、函数、类型会被外面所访问 - -# Basic types - -Go 基本类型 - -``` -bool - -string - -int int8 int16 int32 int64 -uint uint8 uint16 uint32 uint64 uintptr - -byte // alias for uint8 - -rune // alias for int32 - // represents a Unicode code point - -float32 float64 - -complex64 complex128 -``` - -int uint uintptr 是 32 位如果当前系统为 32 位,64 位如果当前系统为 64 位 - - -# 输出格式 - -[fmt说明](https://golang.org/pkg/fmt/) - -+ `%T` 类型 -+ `%v` 值 - -# 初始值 - -+ 0 for number -+ false for bool -+ "" for string - -# Type Conversion 类型转化 - -在 Go 中没有默认类型转化机制,如果类型不匹配将会抛出错误 - -基本类型转化 `T(v)` T 是类型, v 是值,如: - -```golang -i := 10 -var f float = float(i) -// var f float = i # error -``` - -不同类型之间不能进行运算,甚至不能够进行比较 `int + float` error - -# const - -常量与变量声明不一致 - -```golang -// 变量,使用 := -var i := 1 -// 常量,使用 = -const Pi = 3.14 -``` - -常量还可以作为枚举 - - -常量中比较特殊的关键字 `iota` - -Numeric constants are high-precision values. Go 将保留所有精确值 - -```golang -package main - -import "fmt" - -const ( - // Create a huge number by shifting a 1 bit left 100 places. - // In other words, the binary number that is 1 followed by 100 zeroes. - Big = 1 << 100 - // Shift it right again 99 places, so we end up with 1<<1, or 2. - Small = Big >> 99 -) - -func needInt(x int) int { return x*10 + 1 } -func needFloat(x float64) float64 { - return x * 0.1 -} - -func main() { - fmt.Println(needInt(Small)) - fmt.Println(needFloat(Small)) - fmt.Println(needFloat(Big)) -} -``` - -output: -``` -21 -0.2 -1.2676506002282295e+29 -``` - - -## new make - -new(T) 创建某个地址空间,返回对应的地址,然后将属性初始化为 zero value - -make() 创建 slice / map / chan,并且完成对该类型的初始化,返回的是引用 - +--- +layout: post +title: "Golang 基础" +date: 2018-02-02 09:00:05 +0800 +categories: golang +--- + +# Exported Name + +在 package 中,有且只有以大写开头的变量、函数、类型会被外面所访问 + +# Basic types + +Go 基本类型 + +``` +bool + +string + +int int8 int16 int32 int64 +uint uint8 uint16 uint32 uint64 uintptr + +byte // alias for uint8 + +rune // alias for int32 + // represents a Unicode code point + +float32 float64 + +complex64 complex128 +``` + +int uint uintptr 是 32 位如果当前系统为 32 位,64 位如果当前系统为 64 位 + + +# 输出格式 + +[fmt说明](https://golang.org/pkg/fmt/) + ++ `%T` 类型 ++ `%v` 值 + +# 初始值 + ++ 0 for number ++ false for bool ++ "" for string + +# Type Conversion 类型转化 + +在 Go 中没有默认类型转化机制,如果类型不匹配将会抛出错误 + +基本类型转化 `T(v)` T 是类型, v 是值,如: + +```golang +i := 10 +var f float = float(i) +// var f float = i # error +``` + +不同类型之间不能进行运算,甚至不能够进行比较 `int + float` error + +# const + +常量与变量声明不一致 + +```golang +// 变量,使用 := +var i := 1 +// 常量,使用 = +const Pi = 3.14 +``` + +常量还可以作为枚举 + + +常量中比较特殊的关键字 `iota` + +Numeric constants are high-precision values. Go 将保留所有精确值 + +```golang +package main + +import "fmt" + +const ( + // Create a huge number by shifting a 1 bit left 100 places. + // In other words, the binary number that is 1 followed by 100 zeroes. + Big = 1 << 100 + // Shift it right again 99 places, so we end up with 1<<1, or 2. + Small = Big >> 99 +) + +func needInt(x int) int { return x*10 + 1 } +func needFloat(x float64) float64 { + return x * 0.1 +} + +func main() { + fmt.Println(needInt(Small)) + fmt.Println(needFloat(Small)) + fmt.Println(needFloat(Big)) +} +``` + +output: +``` +21 +0.2 +1.2676506002282295e+29 +``` + + +## new make + +new(T) 创建某个地址空间,返回对应的地址,然后将属性初始化为 zero value + +make() 创建 slice / map / chan,并且完成对该类型的初始化,返回的是引用 + diff --git a/src/md/2018-02-02-golang-controlflow.md b/src/md/2018-02-02-golang-controlflow.md index 3c63e97..4b3cfa7 100644 --- a/src/md/2018-02-02-golang-controlflow.md +++ b/src/md/2018-02-02-golang-controlflow.md @@ -1,115 +1,115 @@ ---- -layout: post -title: "Golang 控制流" -date: 2018-02-06 09:00:05 +0800 -categories: golang ---- - -# for - loop - -以下代码省略 condition statement 将会引起无限循环 - -```golang -for ;; { - fmt.Println("hello world") -} -``` - -# if - -相对与其他语言,Go 中支持 if 像 for-loop 一样先执行 init,init 中声明的变量只能在 `if {} else {}` 中使用 - -```golang -if v:=method(); v == 0 { - statment() -} -// cannot use v from here -``` - -# switch - -switch 是一连串 if-else 的简写,但是 Go 中 switch 与 C java 不同,Go 中只有命中的 case 会被执行。所以 break 相当于出现在每个 case 之后 - -```golang -func test_switch() { - fmt.Println("Go runs on") - switch os := runtime.GOOS; os { - case "darwin": - fmt.Println("OS X") - case "linux": - fmt.Println("Linux") - default: - fmt.Printf("%s\n", os) - } -} -``` - -swithc without the condition == 选择第一个 true case,空 switch 有利于书写很长的 if-else 语句 - -```golang -func switch3() { - t := time.Now() - // without condition equals to select true - switch { - case t.Hour() < 12: - fmt.Println("Good morning") - case t.Hour() < 17: - fmt.Println("Good afternoon") - default: - fmt.Println("Good evening") - - } -} -``` - -不想 C java 中 case 紧随一定要是常量,Go 中 case 后可以跟随任何变量,甚至函数 - -# fallthrough - -switch-case 中强制执行命中 case 的下一条 case - -```golang -func main() { - // 空 switch 则相当于寻找第一个为 true 的 case - switch { - case 1+1 == 2: - fmt.Println("1+1=2") - fallthrough - case 1 == 2: - fmt.Println("fallthrough") - fallthrough - default: - fmt.Println("default") - } -} -``` - -output: - -``` -1+1=2 -fallthrough -default -``` - -# defer - -defer 语句能够推迟某些语句的执行,直到 return 语句附近 - -```golang -func defer1() { - defer fmt.Println("world") - defer fmt.Println("AAAA") - - fmt.Println("Hello") -} -``` - -defer 将语句放到最后执行,然后将语句执行的顺序完全相反。其背后是通过栈(FILO)实现的。 - -output: -``` -Hello -AAAA -world +--- +layout: post +title: "Golang 控制流" +date: 2018-02-06 09:00:05 +0800 +categories: golang +--- + +# for - loop + +以下代码省略 condition statement 将会引起无限循环 + +```golang +for ;; { + fmt.Println("hello world") +} +``` + +# if + +相对与其他语言,Go 中支持 if 像 for-loop 一样先执行 init,init 中声明的变量只能在 `if {} else {}` 中使用 + +```golang +if v:=method(); v == 0 { + statment() +} +// cannot use v from here +``` + +# switch + +switch 是一连串 if-else 的简写,但是 Go 中 switch 与 C java 不同,Go 中只有命中的 case 会被执行。所以 break 相当于出现在每个 case 之后 + +```golang +func test_switch() { + fmt.Println("Go runs on") + switch os := runtime.GOOS; os { + case "darwin": + fmt.Println("OS X") + case "linux": + fmt.Println("Linux") + default: + fmt.Printf("%s\n", os) + } +} +``` + +swithc without the condition == 选择第一个 true case,空 switch 有利于书写很长的 if-else 语句 + +```golang +func switch3() { + t := time.Now() + // without condition equals to select true + switch { + case t.Hour() < 12: + fmt.Println("Good morning") + case t.Hour() < 17: + fmt.Println("Good afternoon") + default: + fmt.Println("Good evening") + + } +} +``` + +不想 C java 中 case 紧随一定要是常量,Go 中 case 后可以跟随任何变量,甚至函数 + +# fallthrough + +switch-case 中强制执行命中 case 的下一条 case + +```golang +func main() { + // 空 switch 则相当于寻找第一个为 true 的 case + switch { + case 1+1 == 2: + fmt.Println("1+1=2") + fallthrough + case 1 == 2: + fmt.Println("fallthrough") + fallthrough + default: + fmt.Println("default") + } +} +``` + +output: + +``` +1+1=2 +fallthrough +default +``` + +# defer + +defer 语句能够推迟某些语句的执行,直到 return 语句附近 + +```golang +func defer1() { + defer fmt.Println("world") + defer fmt.Println("AAAA") + + fmt.Println("Hello") +} +``` + +defer 将语句放到最后执行,然后将语句执行的顺序完全相反。其背后是通过栈(FILO)实现的。 + +output: +``` +Hello +AAAA +world ``` \ No newline at end of file diff --git a/src/md/2018-02-02-golang-method.md b/src/md/2018-02-02-golang-method.md index 97dc60e..8e4770e 100644 --- a/src/md/2018-02-02-golang-method.md +++ b/src/md/2018-02-02-golang-method.md @@ -1,202 +1,202 @@ ---- -layout: post -title: "Golang 方法" -date: 2018-02-02 09:00:05 +0800 -categories: golang ---- - -# method - -method 不同于普通 fuction,method 有指定接受者的参数 - -```golang -// 定义结构体 -type Vertex struct { - X, Y int -} - -// 定义结构体的方法 -func (v Vertex) Abs() float64 { - // 距离 - return math.Sqrt(v.X * v.X + v.Y * v.Y) -} - -func main() { - v := Vertex{3, 4} - fmt.Println(v.Abs()) -} -``` - -Go 中可以给每一个每一个类型声明方法,而不仅仅是 *struct* 类型 - -类型别名:`type MyFloat float64`,此后就可以使用 `MyFloat` 来代替 float64 - -在方法传递中,*struct* 发生的是值复制传递 - -```golang -type Vertex struct { - X float64 - Y float64 -} - -// 不会改变外围调用者的X Y值 -func (v Vertex) A(f float64) { - v.X = v.X * f - v.Y = v.Y * f -} - -// 传递指针,外围和内层使用的是同一个地址,修改会影响外围调用者 -func (v *Vertex) B(f float64) { - v.X = v.X * f - v.Y = v.Y * f -} - -func main() { - v := Vertex{3, 4} - fmt.Println(&v) - v.A(10) - fmt.Println(v) - v.B(10) - fmt.Println(v) -} -``` - -output: - -``` -&{3 4} -{3 4} -{30 40} -``` - -Go 中函数传递都是值传递,进行了拷贝复制。Go 中采用引用传递的一个地方是闭包中。 - -```golang -for i := 0; i < 3 i++ { - defer fmt.Println(i) -} -``` - -output: - -``` -2 -1 -0 -``` - -```golang -for i := 0; i < 3; i++ { - defer func() { - fmt.Println(i) - }() -} -``` - -output: - -``` -3 -3 -3 -``` - -所有的匿名函数都是引用了外部 i,所以输出结果都是相同的 - -为了防止上述情况,我们应该把 i 当做函数参数传递 - -```golang -for i := 0; i < 3; i++ { - defer func(i int){ - fmt.Println(i) - }(i) -} -``` - -output: - -``` -2 -1 -0 -``` - -函数使用*指针*传递参数的两个主要原因: -+ 需要修改调用者的值 -+ 避免大数据结构的复制 - - -# interface - -Go 中使得 interface 声明接口和具体 struct 实现方法分离,只要某个 struct 实现了所有 interface 接口的方法,那么该 struct 就是 interface 类型 - -如果接口 `var i I` 被赋予了 nil,那么方法中依然可进行方法调用,并不会触发空指针异常等,但是方法中传递的值是 `nil` - -```golang -interface {} -``` - -`var i interface{}` 任何类型的值都可以赋予给 `i`,空接口常用在处理类型未知的情况下,例如`fmt.Println()` 可以接收任意数量任意类型的参数 `func Print(a ...interface{})` - -interface type assertion - -```golang -var i I -i = &T{"hello world"} -t := i.(*T) -``` - -如果 i 中不是 T 类型,就发生 panic - -```golang -var i I -i = &T{"hello world"} -t := i.(F) // panic -``` - -```golang -var i I -i = &T{"hello world"} -t, ok := i.(F) // no panic -fmt.Println(t, ok) -``` - -如果 type assertion 成立,那么 `ok == true`,t 为对应的 T 值;否则 `ok == false`,t 为 类型 F 的 zero value。 - -# Stringer 接口 - -Golang 中使用 Stringer 接口来打印格式化 - -```golang -type Stringer interface { - String() string -} -``` - -只要实现了 `String() string` 方法,那么使用 `fmt.Sprintf("%v", v)` 就会自动调用 `String() string 方法`,输出格式化字符串。 - -# error 接口 - -```golang -type error interface { - Error() string -} -``` - -自定义异常只需要实现方法 `Error() string` - -# io.Reader - -Go 中定义一个 Reader 接口 - -```golang -type Reader interface{ - func Read(b []byte) (n int, err error) -} -``` - -大多数标准库都实现了该接口,比如文件、网络、压缩、密码等等,我们可以通过该接口方便地访问数据 - -+ []byte slice 用于承装读取的数据 -+ n 表示该次读取,成功读取了多少个字节 -+ err 如果已经到了流末尾,`err = io.EOF` +--- +layout: post +title: "Golang 方法" +date: 2018-02-02 09:00:05 +0800 +categories: golang +--- + +# method + +method 不同于普通 fuction,method 有指定接受者的参数 + +```golang +// 定义结构体 +type Vertex struct { + X, Y int +} + +// 定义结构体的方法 +func (v Vertex) Abs() float64 { + // 距离 + return math.Sqrt(v.X * v.X + v.Y * v.Y) +} + +func main() { + v := Vertex{3, 4} + fmt.Println(v.Abs()) +} +``` + +Go 中可以给每一个每一个类型声明方法,而不仅仅是 *struct* 类型 + +类型别名:`type MyFloat float64`,此后就可以使用 `MyFloat` 来代替 float64 + +在方法传递中,*struct* 发生的是值复制传递 + +```golang +type Vertex struct { + X float64 + Y float64 +} + +// 不会改变外围调用者的X Y值 +func (v Vertex) A(f float64) { + v.X = v.X * f + v.Y = v.Y * f +} + +// 传递指针,外围和内层使用的是同一个地址,修改会影响外围调用者 +func (v *Vertex) B(f float64) { + v.X = v.X * f + v.Y = v.Y * f +} + +func main() { + v := Vertex{3, 4} + fmt.Println(&v) + v.A(10) + fmt.Println(v) + v.B(10) + fmt.Println(v) +} +``` + +output: + +``` +&{3 4} +{3 4} +{30 40} +``` + +Go 中函数传递都是值传递,进行了拷贝复制。Go 中采用引用传递的一个地方是闭包中。 + +```golang +for i := 0; i < 3 i++ { + defer fmt.Println(i) +} +``` + +output: + +``` +2 +1 +0 +``` + +```golang +for i := 0; i < 3; i++ { + defer func() { + fmt.Println(i) + }() +} +``` + +output: + +``` +3 +3 +3 +``` + +所有的匿名函数都是引用了外部 i,所以输出结果都是相同的 + +为了防止上述情况,我们应该把 i 当做函数参数传递 + +```golang +for i := 0; i < 3; i++ { + defer func(i int){ + fmt.Println(i) + }(i) +} +``` + +output: + +``` +2 +1 +0 +``` + +函数使用*指针*传递参数的两个主要原因: ++ 需要修改调用者的值 ++ 避免大数据结构的复制 + + +# interface + +Go 中使得 interface 声明接口和具体 struct 实现方法分离,只要某个 struct 实现了所有 interface 接口的方法,那么该 struct 就是 interface 类型 + +如果接口 `var i I` 被赋予了 nil,那么方法中依然可进行方法调用,并不会触发空指针异常等,但是方法中传递的值是 `nil` + +```golang +interface {} +``` + +`var i interface{}` 任何类型的值都可以赋予给 `i`,空接口常用在处理类型未知的情况下,例如`fmt.Println()` 可以接收任意数量任意类型的参数 `func Print(a ...interface{})` + +interface type assertion + +```golang +var i I +i = &T{"hello world"} +t := i.(*T) +``` + +如果 i 中不是 T 类型,就发生 panic + +```golang +var i I +i = &T{"hello world"} +t := i.(F) // panic +``` + +```golang +var i I +i = &T{"hello world"} +t, ok := i.(F) // no panic +fmt.Println(t, ok) +``` + +如果 type assertion 成立,那么 `ok == true`,t 为对应的 T 值;否则 `ok == false`,t 为 类型 F 的 zero value。 + +# Stringer 接口 + +Golang 中使用 Stringer 接口来打印格式化 + +```golang +type Stringer interface { + String() string +} +``` + +只要实现了 `String() string` 方法,那么使用 `fmt.Sprintf("%v", v)` 就会自动调用 `String() string 方法`,输出格式化字符串。 + +# error 接口 + +```golang +type error interface { + Error() string +} +``` + +自定义异常只需要实现方法 `Error() string` + +# io.Reader + +Go 中定义一个 Reader 接口 + +```golang +type Reader interface{ + func Read(b []byte) (n int, err error) +} +``` + +大多数标准库都实现了该接口,比如文件、网络、压缩、密码等等,我们可以通过该接口方便地访问数据 + ++ []byte slice 用于承装读取的数据 ++ n 表示该次读取,成功读取了多少个字节 ++ err 如果已经到了流末尾,`err = io.EOF` diff --git a/src/md/2018-02-02-golang-moretype.md b/src/md/2018-02-02-golang-moretype.md index 9b7652a..13e61a3 100644 --- a/src/md/2018-02-02-golang-moretype.md +++ b/src/md/2018-02-02-golang-moretype.md @@ -1,265 +1,265 @@ ---- -layout: post -title: "Golang 更多类型" -date: 2018-02-02 09:00:05 +0800 -categories: golang ---- - -# Pointer - -与 C 不一样,Go 中不支持关于指针的运算,比如 ptr ++ 等操作。 - -# struct - -```golang -type Vertex struct { - X int - Y int -} - -// create instance -v := Vertex{1, 2} -``` - -匿名结构体 struct - -```golang -s := struct { - i int - b bool -}{1, false} -``` - -# array - -创建数组会给予默认值 - -```golang -var a [2]string // a[0] a[1] is both "" - -a := [...]int {1, 2, 3} // ... 要求编译器根据 {} 中数据计算数组长度,如果没有 ... a就变成了 slice -``` - -数组是基本类型,值类型,函数/赋值都会发生整个数组的复制,在真实应用中很容易产生灾难,传递大数组我们可以使用数组指针 - -```golang -var a = [2]string{"John", "Susan"} -p := &a -fmt.Println(p[0], p[1]) -``` - -Go 中设计哲学,让使用指针像使用引用一样简单 - -# slice - -> A slice does not store any data, it just describes a section of an underlying array. - -> Changing the elements of a slice modifies the corresponding elements of its underlying array. - -> Other slices that share the same underlying array will see those changes. - -```golang -func slice1() { - names := [4]string{ - "John", - "Paul", - "George", - "Ringo", - } - fmt.Printf("%T %v\n", names, names) - // 即使 names 是 [4]string array,但是 names 也能像 slice 一样使用 - // 创建 slice 底层的 array 复用原来的 names array,极度容易产生 bug - a := names[0:2] - b := names[1:3] - fmt.Printf("%T %v\n", a, a) - fmt.Printf("%T %v\n", b, b) - - b[0] = "Guang" - fmt.Printf("%T %v\n", names, names) - fmt.Printf("%T %v\n", a, a) - fmt.Printf("%T %v\n", b, b) -} -``` - -output: -``` -[4]string [John Paul George Ringo] -[]string [John Paul] -[]string [Paul George] -[4]string [John Guang George Ringo] -[]string [John Guang] -[]string [Guang George] -``` - -slice 可以通过以下方式生成: - -```golang -var s1 []string - -var arr [2]string{"John", "Susan"} - -s2 := arr[0:] -``` - -`[3]bool{true, false, true}` 创建一个长度为 3 的数组 - -`[]bool{true, false, true}` 底层会创建一个数组,然后创建一个 slice 引用底层的数组 - -slice 是引用类型,赋值/函数传递发生的是引用地址的复制,对于大数组的传递比较有帮助 - -对于 `a := [10]int` - -以下操作等级: - -```golang -a[:] -a[0:10] -a[:10] -a[0:] -``` - -*length* *capacity* - -+ length: slice 中元素的个数 `len(s)` -+ capacity: 底层数组的长度 `cap(s)` - -slice 能在原有数组基础上扩张 extend,也就是直接把数组中后面的值内容读取进入 slice,length 因此变长 - -```golang -func test(){ - s := []int {2, 3, 5, 7, 11, 13} - printSlice(s) - - s = s[:0] // 清空 length,但是底层的数组和capacity不会改变 - printSlice(s) - - s = s[:4] // 在原有数组上扩张 extend,length = 4,capacity和底层数组没哟改变 - printSlice(s) - - s = s[2:] // 使数组可用长度较短两位,length-=2 capacity-=2,但是数组还是原来的数组 - printSlice(s) -} - -func printSlice(s []int) { - fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s) -} -``` - -slice 操作就是修改 length capacity 数组起始指针,在没有涉及到插入元素过多,或者删除了大部分元素后,底层数组的扩张和收缩前,一切都不会涉及到底层数组操作 - -`var s []int` len(s)=0 cap(s)=0 还没有底层数组,此时 `s == nil` == true - -slice 只能够和 nil 进行比较 - -使用 make 函数创建指定 length capacity 的 slice,其中创建的默认数组长度 == capacity - -```golang -b := make([]int, 0, 5) - -c := b[:2] - -d := c[2: 5] // 这里即使 len(c) == 2,但是由于它指向的底层数组长度为5,所以这里 d 切片取得是底层数组 arr[2:5] -``` - -> Slicing does not copy the slice's data. It creates a new slice value that points to the original array. - -Slice grow,在 cap(slice) < 1024 会成倍增加 slice capacity,但是 cap(slice) > 1024 每次增长 1.25 倍 - -```golang -t := make([]byte, len(s), (cap(s)+1)*2) // +1 in case cap(s) == 0 -for i := range s { - t[i] = s[i] -} -s = t -``` - -将一个 slice 拼接到另一个 slice 后面 - -```golang -a := []int{1, 2, 3} -b := []int{4, 5, 6} -a = append(a, b...) // ... 以此取出元素,相当于 python *list **dict -``` - -# range - -range 用于遍历 array / slice / map - -在 Go 中 `_` 是一个特殊变量,不会被真正赋值,所以使用 `_` 可以去掉 range 遍历中不感兴趣的变量 - -# Maps - -The zero value of Map is nil. A nil map has no keys, nor can keys be added. 需要先 make 创建 map - -```golang -var m map[string]string -m = make(map[string]string) -m["Hello"] = "World" -``` - -or 在声明时初始化 - -```golang -m := map[string]string { - "Hello": "world", - "Family": "Father and mother I love you" -} -``` - -Insert or update: `m[key] = value` - -Delete: `delete(m, key)` if key not in map, no error - -Retieve: `elem = m[key]` - -Test a key is in map or not: `elem, ok = m[key]` if key is in map, ok = true; else key = false and elem is the zero value. - -WordCounter: - -```golang -func WordCount(s string) map[string]int { - m := make(map[string]int) - t := strings.Fields(s) - for _, v := range t { - if counter, ok := m[v]; ok { - m[v] = counter + 1 - } else { - m[v] = 1 - } - } - return m -} -``` - -# function - -函数也是 value,函数可以当做值在函数或者变量之间传递 - -函数闭包: - -```golang - -package main - -import "fmt" - -func adder() func(int) int { - sum := 0 - return func(x int) int { - sum += x - return sum - } -} - -func main() { - pos, neg := adder(), adder() - for i := 0; i < 10; i++ { - fmt.Println( - pos(i), - neg(-2*i), - ) - } -} -``` - -内层函数引用外层函数的变量,这样会促使外层变量被保存在内存中。[闭包维基百科](https://zh.wikipedia.org/wiki/%E9%97%AD%E5%8C%85_(%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6)) +--- +layout: post +title: "Golang 更多类型" +date: 2018-02-02 09:00:05 +0800 +categories: golang +--- + +# Pointer + +与 C 不一样,Go 中不支持关于指针的运算,比如 ptr ++ 等操作。 + +# struct + +```golang +type Vertex struct { + X int + Y int +} + +// create instance +v := Vertex{1, 2} +``` + +匿名结构体 struct + +```golang +s := struct { + i int + b bool +}{1, false} +``` + +# array + +创建数组会给予默认值 + +```golang +var a [2]string // a[0] a[1] is both "" + +a := [...]int {1, 2, 3} // ... 要求编译器根据 {} 中数据计算数组长度,如果没有 ... a就变成了 slice +``` + +数组是基本类型,值类型,函数/赋值都会发生整个数组的复制,在真实应用中很容易产生灾难,传递大数组我们可以使用数组指针 + +```golang +var a = [2]string{"John", "Susan"} +p := &a +fmt.Println(p[0], p[1]) +``` + +Go 中设计哲学,让使用指针像使用引用一样简单 + +# slice + +> A slice does not store any data, it just describes a section of an underlying array. + +> Changing the elements of a slice modifies the corresponding elements of its underlying array. + +> Other slices that share the same underlying array will see those changes. + +```golang +func slice1() { + names := [4]string{ + "John", + "Paul", + "George", + "Ringo", + } + fmt.Printf("%T %v\n", names, names) + // 即使 names 是 [4]string array,但是 names 也能像 slice 一样使用 + // 创建 slice 底层的 array 复用原来的 names array,极度容易产生 bug + a := names[0:2] + b := names[1:3] + fmt.Printf("%T %v\n", a, a) + fmt.Printf("%T %v\n", b, b) + + b[0] = "Guang" + fmt.Printf("%T %v\n", names, names) + fmt.Printf("%T %v\n", a, a) + fmt.Printf("%T %v\n", b, b) +} +``` + +output: +``` +[4]string [John Paul George Ringo] +[]string [John Paul] +[]string [Paul George] +[4]string [John Guang George Ringo] +[]string [John Guang] +[]string [Guang George] +``` + +slice 可以通过以下方式生成: + +```golang +var s1 []string + +var arr [2]string{"John", "Susan"} + +s2 := arr[0:] +``` + +`[3]bool{true, false, true}` 创建一个长度为 3 的数组 + +`[]bool{true, false, true}` 底层会创建一个数组,然后创建一个 slice 引用底层的数组 + +slice 是引用类型,赋值/函数传递发生的是引用地址的复制,对于大数组的传递比较有帮助 + +对于 `a := [10]int` + +以下操作等级: + +```golang +a[:] +a[0:10] +a[:10] +a[0:] +``` + +*length* *capacity* + ++ length: slice 中元素的个数 `len(s)` ++ capacity: 底层数组的长度 `cap(s)` + +slice 能在原有数组基础上扩张 extend,也就是直接把数组中后面的值内容读取进入 slice,length 因此变长 + +```golang +func test(){ + s := []int {2, 3, 5, 7, 11, 13} + printSlice(s) + + s = s[:0] // 清空 length,但是底层的数组和capacity不会改变 + printSlice(s) + + s = s[:4] // 在原有数组上扩张 extend,length = 4,capacity和底层数组没哟改变 + printSlice(s) + + s = s[2:] // 使数组可用长度较短两位,length-=2 capacity-=2,但是数组还是原来的数组 + printSlice(s) +} + +func printSlice(s []int) { + fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s) +} +``` + +slice 操作就是修改 length capacity 数组起始指针,在没有涉及到插入元素过多,或者删除了大部分元素后,底层数组的扩张和收缩前,一切都不会涉及到底层数组操作 + +`var s []int` len(s)=0 cap(s)=0 还没有底层数组,此时 `s == nil` == true + +slice 只能够和 nil 进行比较 + +使用 make 函数创建指定 length capacity 的 slice,其中创建的默认数组长度 == capacity + +```golang +b := make([]int, 0, 5) + +c := b[:2] + +d := c[2: 5] // 这里即使 len(c) == 2,但是由于它指向的底层数组长度为5,所以这里 d 切片取得是底层数组 arr[2:5] +``` + +> Slicing does not copy the slice's data. It creates a new slice value that points to the original array. + +Slice grow,在 cap(slice) < 1024 会成倍增加 slice capacity,但是 cap(slice) > 1024 每次增长 1.25 倍 + +```golang +t := make([]byte, len(s), (cap(s)+1)*2) // +1 in case cap(s) == 0 +for i := range s { + t[i] = s[i] +} +s = t +``` + +将一个 slice 拼接到另一个 slice 后面 + +```golang +a := []int{1, 2, 3} +b := []int{4, 5, 6} +a = append(a, b...) // ... 以此取出元素,相当于 python *list **dict +``` + +# range + +range 用于遍历 array / slice / map + +在 Go 中 `_` 是一个特殊变量,不会被真正赋值,所以使用 `_` 可以去掉 range 遍历中不感兴趣的变量 + +# Maps + +The zero value of Map is nil. A nil map has no keys, nor can keys be added. 需要先 make 创建 map + +```golang +var m map[string]string +m = make(map[string]string) +m["Hello"] = "World" +``` + +or 在声明时初始化 + +```golang +m := map[string]string { + "Hello": "world", + "Family": "Father and mother I love you" +} +``` + +Insert or update: `m[key] = value` + +Delete: `delete(m, key)` if key not in map, no error + +Retieve: `elem = m[key]` + +Test a key is in map or not: `elem, ok = m[key]` if key is in map, ok = true; else key = false and elem is the zero value. + +WordCounter: + +```golang +func WordCount(s string) map[string]int { + m := make(map[string]int) + t := strings.Fields(s) + for _, v := range t { + if counter, ok := m[v]; ok { + m[v] = counter + 1 + } else { + m[v] = 1 + } + } + return m +} +``` + +# function + +函数也是 value,函数可以当做值在函数或者变量之间传递 + +函数闭包: + +```golang + +package main + +import "fmt" + +func adder() func(int) int { + sum := 0 + return func(x int) int { + sum += x + return sum + } +} + +func main() { + pos, neg := adder(), adder() + for i := 0; i < 10; i++ { + fmt.Println( + pos(i), + neg(-2*i), + ) + } +} +``` + +内层函数引用外层函数的变量,这样会促使外层变量被保存在内存中。[闭包维基百科](https://zh.wikipedia.org/wiki/%E9%97%AD%E5%8C%85_(%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6)) diff --git a/src/md/2018-02-04-golang-reflect.md b/src/md/2018-02-04-golang-reflect.md index 289e784..6b8d3d1 100644 --- a/src/md/2018-02-04-golang-reflect.md +++ b/src/md/2018-02-04-golang-reflect.md @@ -1,253 +1,253 @@ ---- -layout: post -title: "Golang 反射" -date: 2018-03-06 09:00:05 +0800 -categories: golang ---- - -# 反射 - -Go 语言提供一种机制,在编译时不知道类型的情况下,可更新变量、在运行时查看值、调用方法以及直接对它们的布局进行操作,这种机制成为反射。通过反射我们甚至可以访问结构体中的非导出成员,但是不能够修改它们。 - -**为什么使用反射?** - -原因:有时需要写一个函数有能力统一处理各种值类型的函数,而这些类型可能无法共享同一个接口,也可能内存布局未知,也有可能这个类型在我们设计函数时还不存在,甚至这个类型会同时存在上面的三种问题。 - -**注意事项** - -1. 基于反射的代码非常脆弱,如果使用明确的类型进行操作,在编译期间编译器能够识别出其中的错误,而使用反射错误只有在运行时才会发现 -2. 反射降低代码可读性 -3. 使用反射比使用特定类型优化的函数慢一两个数量级 - -> 尽可能避免反射 - -曾阅读[今日头条中关于 Golang 优化](https://mp.weixin.qq.com/s?__biz=MjM5MDE0Mjc4MA==&mid=2650996069&idx=1&sn=63e7f5d5f91f9d84f1c3278426f6edf6&chksm=bdbf05368ac88c20c273f325acc257811d6ee7534df30ace674f5c7eeb0c7986984dca209131&scene=21#wechat_redirect)的一篇文章,说尽可能不要用反射 - -go 中 interface 实现了 duck-typing,那么一个问题就是通过 `interface` 变量来重新寻找原始的值 - -+ 编译时:静态 -+ 运行时:动态 - -[Data structure: interface](https://research.swtch.com/interfaces) - -介绍了 interface 变量中既存储了相应的 `type` 和 `value` - -反射三大法则: -+ Reflection goes from interface value to reflection object. -+ Reflection goes from reflection object to interface value. -+ To modify the object, the value must be settable. - -```golang -var i Interface -i = obj -``` - -上述发生的是 copy 复制,也就是说 interface 变量底层真实存储的是一个跟 `obj` 完全不同的变量,所以 interface 中的 `data` 与 `obj` 是独立变化的 - -reflect 包下定义了两个重要的类型:Type 和 Value。Type 表示 Go 语言的一个类型,它是一个有很多方法的接口,这些方法可以用来识别类型以及透视类型的组成部分,比如一个结构的各个字段或者一个函数的各个参数。 - -应该尽可能避免在包的 API 里面暴露反射相关内容。 - -```golang -var x float64 = 3.4 -fmt.Println(reflect.TypeOf(x)) // 获取 x 类型 -fmt.Println(reflect.ValueOf(x)) // 获取 x 值 -fmt.Println(reflect.ValueOf(x).Type()) // 我们可以从类型中获得值 -fmt.Println(reflect.ValueOf(x).Kind()) // 获取存储对应的基本类型,如 reflect.Float64 reflect.Slice reflect.Struct -``` - -```golang -var x float64 = 3.4 -v := reflect.ValueOf(x) -switch v.Kind() { - case reflect.Int: - fmt.Println(v.Int(())) - case reflect.Float64: - fmt.Println(v.Float()) -} -``` - -对应 reflect 中对于类型的操作 `setter getter`,总是采用最大的类型,比如 uint64 int64 float64 等 - -```golang -var x uint8 = 'x' -v := reflct.ValueOf(x) -fmt.Printf("%T", v.Uint()) // uint64 -x = uint8(v.Uint()) // 需要把类型转换到需要的类型 -``` - -`Type()` 显示的是静态类型 - -`Kind()` 显示的是底层的类型,而不是静态类型 - -```golang -type MyInt int -x := MyInt(100) -v := reflect.ValueOf(x) -fmt.Println(x.Kind()) // int,而不是 MyInt -fmt.Println(x.Type()) // main.MyInt,而不是 int -``` - -`interface <==> reflection` 这两者之间是可以转化的 - -```golang -var x float64 = 1.0 -v := reflect.ValueOf(x) -y := v.Interface() -fmt.Println(y) // 1.0 -// 提取处 float64 -f := y.(float64) -``` - -不是所有 reflect.Value 都可以修改的 - -```golang -var x float64 = 1.0 -v := reflect.ValueOf(x) -v.SetFloat(2.5) -``` - -会被抛出: - -``` -panic: reflect: reflect.Value.SetFloat using unaddressable value -``` - -原因是 v 不能够被寻址,从而不能够被修改。 - -**什么样的变量在反射后能够被寻址?** - -简单地,可以使用 `Value.CanAddr()` 判断。 - -像是一个整数 123、浮点数 3.14,就是典型的不可寻址的,为什么?因为只有变量才能够被寻址。但为什么上例中 x 是变量但是错误显示 x 无法寻址呢?因为 `reflect.ValueOf(v interface{})` 参数接收的是一个 interface,而我们知道 interface 中的值存储动态类型以及动态值,在参数传递中,会进行参数的复制,也就是动态值只是一个 copy,改一个 copy 的值有意义吗?显然没有意义。 - -那到底什么才能够在反射后能够被寻址啊!!! - -当传递给 `reflect.ValueOf(pointer)` 函数是一个指针的时候可以被寻址。因为此时 interface 里面存储的是指针,可以通过指针修改实参的值。 - -```go -x := 2 -a := reflect.ValueOf(x) -b := reflect.ValueOf(2) -c := reflect.ValueOf(&x) -d := c.Elem() // 取得 &x 对应的值,相当于 *(&x)==x -fmt.Println(a.CanAddr()) // false -fmt.Println(b.CanAddr()) // false -fmt.Println(c.CanAddr()) // false -fmt.Println(d.CanAddr()) // true -``` - -`c.Elem()` 就是取相同 `&x` 指向内存的变量,也就是 x 的值。 - -```go -d.Set(reflect.ValueOf(1)) -fmt.Println(x) // 1 -``` - -**什么样的变量在反射后能够被修改?** - -1. 首先该变量应该是能够被寻址的,连寻址都不能够做到,也就是连变量到底存储的位置都不知道,谈何修改。 -2. 在结构体中,该成员是导出的。因为执行修改的函数不在该变量声明的包内,别的包是不能访问另一个包的非导出成员的。 - -```go -var x struct{ - a int - A int - } -a := reflect.ValueOf(&x) -b := a.Elem() -b.FieldByName("A").Set(reflect.ValueOf(10)) -fmt.Println(x) // {0 10} -``` - -如果以上代码改为 - -```go -b.FieldByName("a").Set(reflect.ValueOf(10)) -``` - -会引发宕机:`panic: reflect: reflect.Value.Set using value obtained using unexported field`。最恐怖的是这是运行时的宕机,而不是编译失败,如果真实项目中运行出现 panic 宕机真实灾难。 - -通过 `CanSet` 方法查看某个 reflection object 是否可以修改 - -```golang -var x float64 = 1.0 -v := reflect.ValueOf(x) -fmt.Println(v.CanSet()) // false -``` - -在函数传递参数时,go 是进行值传递,完全 coppy 了一个新的值,所以即使我们在方法中修改了值,外围调用函数也不会收到影响。 -所以我们 `reflect.ValueOf()` 传递的是 copy,也就不能够 set。 - -所以应该传递指针 - -```golang -var x float64 = 1.0 -v := reflect.ValueOf(&x) -fmt.Println(v.CanSet()) // false -p := v.Elem() -fmt.Println(p.CanSet()) // true -p.SetFloat(2.5) -fmt.Println(x) // 2.5 -fmt.Println(*(v.Interface().(*float64))) // 2.5 -``` - -通过反射修改 struct 的值 - -```golang -type Human struct { - name string - age int -} - - -h := Human{"hello", 100} -v := reflect.ValueOf(&h).Elem() -typeOf := v.Type() -for i := 0; i < v.NumField(); i++ { - f := v.Field(i) - fmt.Printf("%d: %s %s = %v\n", i, typeOf.Field(i).Name, f.Type(), f) -} -``` - -output: - -``` -0: name string = hello -1: age int = 100 -``` - -struct 中只有大写开头的属性才能够被修改: - -```golang -type Human struct { - Name string - age int -} - -h := Human{"hello", 100} -v := reflect.ValueOf(&h).Elem() -v.Field(0).SetString("world") -typeOf := v.Type() -for i := 0; i < v.NumField(); i++ { - f := v.Field(i) - fmt.Printf("%d: %s %s = %v\n", i, typeOf.Field(i).Name, f.Type(), f) -} -fmt.Println(h) -``` - -output: - -``` -0: Name string = world -1: age int = 100 -{world 100} -``` - -如果 Human 中的名字是小写 name,那么就会引发错误:`panic: reflect: reflect.Value.SetString using value obtained using unexported field` - -除了直接使用 == 进行比较两个值之外,Go 为我们提供了一个强大的函数: - -```go -func reflect.DeepEqual(x, y interface{}) bool -``` +--- +layout: post +title: "Golang 反射" +date: 2018-03-06 09:00:05 +0800 +categories: golang +--- + +# 反射 + +Go 语言提供一种机制,在编译时不知道类型的情况下,可更新变量、在运行时查看值、调用方法以及直接对它们的布局进行操作,这种机制成为反射。通过反射我们甚至可以访问结构体中的非导出成员,但是不能够修改它们。 + +**为什么使用反射?** + +原因:有时需要写一个函数有能力统一处理各种值类型的函数,而这些类型可能无法共享同一个接口,也可能内存布局未知,也有可能这个类型在我们设计函数时还不存在,甚至这个类型会同时存在上面的三种问题。 + +**注意事项** + +1. 基于反射的代码非常脆弱,如果使用明确的类型进行操作,在编译期间编译器能够识别出其中的错误,而使用反射错误只有在运行时才会发现 +2. 反射降低代码可读性 +3. 使用反射比使用特定类型优化的函数慢一两个数量级 + +> 尽可能避免反射 + +曾阅读[今日头条中关于 Golang 优化](https://mp.weixin.qq.com/s?__biz=MjM5MDE0Mjc4MA==&mid=2650996069&idx=1&sn=63e7f5d5f91f9d84f1c3278426f6edf6&chksm=bdbf05368ac88c20c273f325acc257811d6ee7534df30ace674f5c7eeb0c7986984dca209131&scene=21#wechat_redirect)的一篇文章,说尽可能不要用反射 + +go 中 interface 实现了 duck-typing,那么一个问题就是通过 `interface` 变量来重新寻找原始的值 + ++ 编译时:静态 ++ 运行时:动态 + +[Data structure: interface](https://research.swtch.com/interfaces) + +介绍了 interface 变量中既存储了相应的 `type` 和 `value` + +反射三大法则: ++ Reflection goes from interface value to reflection object. ++ Reflection goes from reflection object to interface value. ++ To modify the object, the value must be settable. + +```golang +var i Interface +i = obj +``` + +上述发生的是 copy 复制,也就是说 interface 变量底层真实存储的是一个跟 `obj` 完全不同的变量,所以 interface 中的 `data` 与 `obj` 是独立变化的 + +reflect 包下定义了两个重要的类型:Type 和 Value。Type 表示 Go 语言的一个类型,它是一个有很多方法的接口,这些方法可以用来识别类型以及透视类型的组成部分,比如一个结构的各个字段或者一个函数的各个参数。 + +应该尽可能避免在包的 API 里面暴露反射相关内容。 + +```golang +var x float64 = 3.4 +fmt.Println(reflect.TypeOf(x)) // 获取 x 类型 +fmt.Println(reflect.ValueOf(x)) // 获取 x 值 +fmt.Println(reflect.ValueOf(x).Type()) // 我们可以从类型中获得值 +fmt.Println(reflect.ValueOf(x).Kind()) // 获取存储对应的基本类型,如 reflect.Float64 reflect.Slice reflect.Struct +``` + +```golang +var x float64 = 3.4 +v := reflect.ValueOf(x) +switch v.Kind() { + case reflect.Int: + fmt.Println(v.Int(())) + case reflect.Float64: + fmt.Println(v.Float()) +} +``` + +对应 reflect 中对于类型的操作 `setter getter`,总是采用最大的类型,比如 uint64 int64 float64 等 + +```golang +var x uint8 = 'x' +v := reflct.ValueOf(x) +fmt.Printf("%T", v.Uint()) // uint64 +x = uint8(v.Uint()) // 需要把类型转换到需要的类型 +``` + +`Type()` 显示的是静态类型 + +`Kind()` 显示的是底层的类型,而不是静态类型 + +```golang +type MyInt int +x := MyInt(100) +v := reflect.ValueOf(x) +fmt.Println(x.Kind()) // int,而不是 MyInt +fmt.Println(x.Type()) // main.MyInt,而不是 int +``` + +`interface <==> reflection` 这两者之间是可以转化的 + +```golang +var x float64 = 1.0 +v := reflect.ValueOf(x) +y := v.Interface() +fmt.Println(y) // 1.0 +// 提取处 float64 +f := y.(float64) +``` + +不是所有 reflect.Value 都可以修改的 + +```golang +var x float64 = 1.0 +v := reflect.ValueOf(x) +v.SetFloat(2.5) +``` + +会被抛出: + +``` +panic: reflect: reflect.Value.SetFloat using unaddressable value +``` + +原因是 v 不能够被寻址,从而不能够被修改。 + +**什么样的变量在反射后能够被寻址?** + +简单地,可以使用 `Value.CanAddr()` 判断。 + +像是一个整数 123、浮点数 3.14,就是典型的不可寻址的,为什么?因为只有变量才能够被寻址。但为什么上例中 x 是变量但是错误显示 x 无法寻址呢?因为 `reflect.ValueOf(v interface{})` 参数接收的是一个 interface,而我们知道 interface 中的值存储动态类型以及动态值,在参数传递中,会进行参数的复制,也就是动态值只是一个 copy,改一个 copy 的值有意义吗?显然没有意义。 + +那到底什么才能够在反射后能够被寻址啊!!! + +当传递给 `reflect.ValueOf(pointer)` 函数是一个指针的时候可以被寻址。因为此时 interface 里面存储的是指针,可以通过指针修改实参的值。 + +```go +x := 2 +a := reflect.ValueOf(x) +b := reflect.ValueOf(2) +c := reflect.ValueOf(&x) +d := c.Elem() // 取得 &x 对应的值,相当于 *(&x)==x +fmt.Println(a.CanAddr()) // false +fmt.Println(b.CanAddr()) // false +fmt.Println(c.CanAddr()) // false +fmt.Println(d.CanAddr()) // true +``` + +`c.Elem()` 就是取相同 `&x` 指向内存的变量,也就是 x 的值。 + +```go +d.Set(reflect.ValueOf(1)) +fmt.Println(x) // 1 +``` + +**什么样的变量在反射后能够被修改?** + +1. 首先该变量应该是能够被寻址的,连寻址都不能够做到,也就是连变量到底存储的位置都不知道,谈何修改。 +2. 在结构体中,该成员是导出的。因为执行修改的函数不在该变量声明的包内,别的包是不能访问另一个包的非导出成员的。 + +```go +var x struct{ + a int + A int + } +a := reflect.ValueOf(&x) +b := a.Elem() +b.FieldByName("A").Set(reflect.ValueOf(10)) +fmt.Println(x) // {0 10} +``` + +如果以上代码改为 + +```go +b.FieldByName("a").Set(reflect.ValueOf(10)) +``` + +会引发宕机:`panic: reflect: reflect.Value.Set using value obtained using unexported field`。最恐怖的是这是运行时的宕机,而不是编译失败,如果真实项目中运行出现 panic 宕机真实灾难。 + +通过 `CanSet` 方法查看某个 reflection object 是否可以修改 + +```golang +var x float64 = 1.0 +v := reflect.ValueOf(x) +fmt.Println(v.CanSet()) // false +``` + +在函数传递参数时,go 是进行值传递,完全 coppy 了一个新的值,所以即使我们在方法中修改了值,外围调用函数也不会收到影响。 +所以我们 `reflect.ValueOf()` 传递的是 copy,也就不能够 set。 + +所以应该传递指针 + +```golang +var x float64 = 1.0 +v := reflect.ValueOf(&x) +fmt.Println(v.CanSet()) // false +p := v.Elem() +fmt.Println(p.CanSet()) // true +p.SetFloat(2.5) +fmt.Println(x) // 2.5 +fmt.Println(*(v.Interface().(*float64))) // 2.5 +``` + +通过反射修改 struct 的值 + +```golang +type Human struct { + name string + age int +} + + +h := Human{"hello", 100} +v := reflect.ValueOf(&h).Elem() +typeOf := v.Type() +for i := 0; i < v.NumField(); i++ { + f := v.Field(i) + fmt.Printf("%d: %s %s = %v\n", i, typeOf.Field(i).Name, f.Type(), f) +} +``` + +output: + +``` +0: name string = hello +1: age int = 100 +``` + +struct 中只有大写开头的属性才能够被修改: + +```golang +type Human struct { + Name string + age int +} + +h := Human{"hello", 100} +v := reflect.ValueOf(&h).Elem() +v.Field(0).SetString("world") +typeOf := v.Type() +for i := 0; i < v.NumField(); i++ { + f := v.Field(i) + fmt.Printf("%d: %s %s = %v\n", i, typeOf.Field(i).Name, f.Type(), f) +} +fmt.Println(h) +``` + +output: + +``` +0: Name string = world +1: age int = 100 +{world 100} +``` + +如果 Human 中的名字是小写 name,那么就会引发错误:`panic: reflect: reflect.Value.SetString using value obtained using unexported field` + +除了直接使用 == 进行比较两个值之外,Go 为我们提供了一个强大的函数: + +```go +func reflect.DeepEqual(x, y interface{}) bool +``` diff --git a/src/md/2018-02-05-golang-flequenct-command.md b/src/md/2018-02-05-golang-flequenct-command.md index 74a4a70..4a264ff 100644 --- a/src/md/2018-02-05-golang-flequenct-command.md +++ b/src/md/2018-02-05-golang-flequenct-command.md @@ -1,205 +1,205 @@ ---- -layout: post -title: "Golang 常用命令" -date: 2018-02-05 09:00:05 +0800 -categories: golang ---- - -# go 基本命令给 - -## go install - -先编译生成对应的 `.a` 文件,然后再移动到 `$GOPATH/pkg` 或者 `$GOPATH/bin` 目录下 - -```bash -go install -``` - -`-v` 参数显示底层执行信息 - -## go build - -将该包打包为一个 `xxx.a` 文件,然后可以被其他包引用 - -```bash -go build -``` - -如果当前包为`package main`,则将此包打包为一个可运行程序;如果需要在 `$GOPATH/bin` 下生成可执行文件,那么需要执行 `go install` - -`go build` 默认情况下会忽略,`_` 和 `.` 开头的go文件 - -**go build 参数说明**: - -+ `-o-` 指定输出的文件名,可以带上路径 -+ `-i` 安装相应的包,编译+`go install` -+ `-n` 把需要执行的命令打印出来,但是不执行,这样可以理解相应命令底层到底做了什么 -+ `-p n` 指定并行可运行的编译数量,默认是 CPU 数量 -+ `-a` 更新包,对标准包不起作用 -+ `-race` 开启编译时自动显示数据竞争情况 -+ `-v` 打印我们正在编译的包名 -+ `-work` 打印编译时候临时文件夹的名称,如果已经存在就不删除 -+ `-x` 打印出编译需要执行的命令,与`-n`不同的是 `-x` 执行打印输出的命令 -+ `-ccflags 'args list'` 传递参数给 5c,6c,8c 调用 -+ `-compile name` 指定编译器,`gccgo` or `gc` -+ `-gcflags 'args list'` 传递参数给 5g,6g,8g -+ `-installsuffix suffix` 为了和安装包区分开来 -+ `-ldflags 'args list'` 传递参数给 5l,6l,8l -+ `-tags 'tags list'` 设置在编译时候可以适配的那些 tag - -如果代码需要执行某些跨平台的操作,那么可以使用 `xxxx_linux.go` `xxxx_darwin.go` `xxxx_windows.go` 等命名,执行 go 命令只会采用以 `_platform` 当前系统名称结尾的文件 - -## go get - -```bash -go get -u github.com/g10guang/xxxxx -``` - --u 参数可以自动更新包,而且 go get 的时候会自动获取第三方依赖 - -目录结构: - -``` -$GOPATH - src - |-github.com - |-g10guang - |-xxxx - pkg - |-github.com - |-g10guang - |xxxx.a -``` - -go get 本质上可以理解为先把远程代码 `fetch` 下来,然后执行 `go install` - -**参数说明:** - -+ `-d` 只下载不安装 -+ `-v` 显示执行的命令 -+ `-u` 强制使用网络去更新包和它的依赖包 -+ `-t` 下载测试所需要的包 -+ `-fix` 在获取源码之后,先运行 fix,然后再去做其他事 -+ `-f` 只有包含了 `-u` 参数时才生效,不去验证每一个 import 是否已经获取了,对于本地 fork 的包特别有效 - -## go clean - -删除当前源码包和关联源码包里面编译生成的文件 - -**参数说明:** - -+ `-i` 清除关联的安装的包和可运行文件,也就是通过 `go install` 生成的文件 -+ `-n` 打印执行的相应命令,但是不执行 -+ `-r` 循环清除 import 中引入的包 -+ `-x` 输出执行的命令,并且执行 - -## go fmt - -格式化 go 代码,也就是 vscode 失去焦点后自动执行的命令,这样可以保证一个团队中代码风格统一 - -**参数说明:** - -+ `-l` 显示需要格式化的文件 -+ `-w` 将格式化的结果输出到文件,而不是打印输出,如果不带该参数最终结果将不会输出到文件 -+ `-r` 添加重写规则,方便做批量替换 -+ `-s` 简化文件中的代码 -+ `-d` 显示格式户前后的 diff,而不写入文件 -+ `-e` 打印所有的语法错误到控制台 -+ `-cpuprofile` 支持调试模式,写入相应的 `cpufile` 到指定文件 - -## go test - -读取源码目录下的 `*_test.go` 文件,执行测试用例 - -**参数说明:** - -+ `-bench regexp` 执行相应的 benchmark,例如 `-bench=.` -+ `-cover` 开启测试覆盖率 -+ `-run regexp` 只运行匹配正则 regexp 的函数,例如 `-run=Array` 只运行所有以 Array 开头的函数 -+ `-v` 显示测试的详细命令 - -## go tool - -go tool 下聚集了很多命令,比如 `fix` `vet` - -+ `go tool fix` 用来修复老版本的代码到新版本,自动修改变化的 API -+ `go tool vet directories | files` 分析代码的语法是否正确,比如检查 `fmt.Printf()` 中的参数是否正确,`return` 之后是否还有多余的代码 - -## go generate - -用来在编译前生成某些代码,是通过分析源码中特殊的注释,判断需要生成某些特殊代码 - -`go generate` 是给当前包的开发人员使用的,而不是给使用该包的人使用的 - -比如我们经常使用 `yacc` 生成代码 - -```bash -go tool yacc -o gopher.go -p parser gopher.y -``` - -如果我们想让 `go generate` 替我们执行该命令,那么可以在代码中任意一个位置插入注释: - -```golang -//go:generate go tool yacc -o gopher.go -p parser gopher.y -``` - -在编译时候执行: - -```bash -go generate -go build -``` - -## godoc - -查看文档的命令,安装 godoc: - -```bash -go get golang.org/x/tools/cmd/godoc -``` - -比如查看 `net/http` 包的使用文档可以执行: - -```bash -godoc net/http -``` - -查看某个函数文档: - -```bash -godoc fmt Printf -``` - -查看函数源码: - -```bash -godoc -src fmt Printf -``` - -本地运行 golang.org 站点中的文档,运行在特定端口上: - -```bash -godoc -http=:8080 -``` - -我们可以在浏览器查看 `127.0.0.1:8080` 查看文档 - -## go version - -查看当前 go 版本 - -## go env - -查看当前 go 的环境变量 - -## go list - -列出当前包正在使用的包 - -## go run - -编译并运行 go 程序 - -## go help cmd - +--- +layout: post +title: "Golang 常用命令" +date: 2018-02-05 09:00:05 +0800 +categories: golang +--- + +# go 基本命令给 + +## go install + +先编译生成对应的 `.a` 文件,然后再移动到 `$GOPATH/pkg` 或者 `$GOPATH/bin` 目录下 + +```bash +go install +``` + +`-v` 参数显示底层执行信息 + +## go build + +将该包打包为一个 `xxx.a` 文件,然后可以被其他包引用 + +```bash +go build +``` + +如果当前包为`package main`,则将此包打包为一个可运行程序;如果需要在 `$GOPATH/bin` 下生成可执行文件,那么需要执行 `go install` + +`go build` 默认情况下会忽略,`_` 和 `.` 开头的go文件 + +**go build 参数说明**: + ++ `-o-` 指定输出的文件名,可以带上路径 ++ `-i` 安装相应的包,编译+`go install` ++ `-n` 把需要执行的命令打印出来,但是不执行,这样可以理解相应命令底层到底做了什么 ++ `-p n` 指定并行可运行的编译数量,默认是 CPU 数量 ++ `-a` 更新包,对标准包不起作用 ++ `-race` 开启编译时自动显示数据竞争情况 ++ `-v` 打印我们正在编译的包名 ++ `-work` 打印编译时候临时文件夹的名称,如果已经存在就不删除 ++ `-x` 打印出编译需要执行的命令,与`-n`不同的是 `-x` 执行打印输出的命令 ++ `-ccflags 'args list'` 传递参数给 5c,6c,8c 调用 ++ `-compile name` 指定编译器,`gccgo` or `gc` ++ `-gcflags 'args list'` 传递参数给 5g,6g,8g ++ `-installsuffix suffix` 为了和安装包区分开来 ++ `-ldflags 'args list'` 传递参数给 5l,6l,8l ++ `-tags 'tags list'` 设置在编译时候可以适配的那些 tag + +如果代码需要执行某些跨平台的操作,那么可以使用 `xxxx_linux.go` `xxxx_darwin.go` `xxxx_windows.go` 等命名,执行 go 命令只会采用以 `_platform` 当前系统名称结尾的文件 + +## go get + +```bash +go get -u github.com/g10guang/xxxxx +``` + +-u 参数可以自动更新包,而且 go get 的时候会自动获取第三方依赖 + +目录结构: + +``` +$GOPATH + src + |-github.com + |-g10guang + |-xxxx + pkg + |-github.com + |-g10guang + |xxxx.a +``` + +go get 本质上可以理解为先把远程代码 `fetch` 下来,然后执行 `go install` + +**参数说明:** + ++ `-d` 只下载不安装 ++ `-v` 显示执行的命令 ++ `-u` 强制使用网络去更新包和它的依赖包 ++ `-t` 下载测试所需要的包 ++ `-fix` 在获取源码之后,先运行 fix,然后再去做其他事 ++ `-f` 只有包含了 `-u` 参数时才生效,不去验证每一个 import 是否已经获取了,对于本地 fork 的包特别有效 + +## go clean + +删除当前源码包和关联源码包里面编译生成的文件 + +**参数说明:** + ++ `-i` 清除关联的安装的包和可运行文件,也就是通过 `go install` 生成的文件 ++ `-n` 打印执行的相应命令,但是不执行 ++ `-r` 循环清除 import 中引入的包 ++ `-x` 输出执行的命令,并且执行 + +## go fmt + +格式化 go 代码,也就是 vscode 失去焦点后自动执行的命令,这样可以保证一个团队中代码风格统一 + +**参数说明:** + ++ `-l` 显示需要格式化的文件 ++ `-w` 将格式化的结果输出到文件,而不是打印输出,如果不带该参数最终结果将不会输出到文件 ++ `-r` 添加重写规则,方便做批量替换 ++ `-s` 简化文件中的代码 ++ `-d` 显示格式户前后的 diff,而不写入文件 ++ `-e` 打印所有的语法错误到控制台 ++ `-cpuprofile` 支持调试模式,写入相应的 `cpufile` 到指定文件 + +## go test + +读取源码目录下的 `*_test.go` 文件,执行测试用例 + +**参数说明:** + ++ `-bench regexp` 执行相应的 benchmark,例如 `-bench=.` ++ `-cover` 开启测试覆盖率 ++ `-run regexp` 只运行匹配正则 regexp 的函数,例如 `-run=Array` 只运行所有以 Array 开头的函数 ++ `-v` 显示测试的详细命令 + +## go tool + +go tool 下聚集了很多命令,比如 `fix` `vet` + ++ `go tool fix` 用来修复老版本的代码到新版本,自动修改变化的 API ++ `go tool vet directories | files` 分析代码的语法是否正确,比如检查 `fmt.Printf()` 中的参数是否正确,`return` 之后是否还有多余的代码 + +## go generate + +用来在编译前生成某些代码,是通过分析源码中特殊的注释,判断需要生成某些特殊代码 + +`go generate` 是给当前包的开发人员使用的,而不是给使用该包的人使用的 + +比如我们经常使用 `yacc` 生成代码 + +```bash +go tool yacc -o gopher.go -p parser gopher.y +``` + +如果我们想让 `go generate` 替我们执行该命令,那么可以在代码中任意一个位置插入注释: + +```golang +//go:generate go tool yacc -o gopher.go -p parser gopher.y +``` + +在编译时候执行: + +```bash +go generate +go build +``` + +## godoc + +查看文档的命令,安装 godoc: + +```bash +go get golang.org/x/tools/cmd/godoc +``` + +比如查看 `net/http` 包的使用文档可以执行: + +```bash +godoc net/http +``` + +查看某个函数文档: + +```bash +godoc fmt Printf +``` + +查看函数源码: + +```bash +godoc -src fmt Printf +``` + +本地运行 golang.org 站点中的文档,运行在特定端口上: + +```bash +godoc -http=:8080 +``` + +我们可以在浏览器查看 `127.0.0.1:8080` 查看文档 + +## go version + +查看当前 go 版本 + +## go env + +查看当前 go 的环境变量 + +## go list + +列出当前包正在使用的包 + +## go run + +编译并运行 go 程序 + +## go help cmd + 查看某条命令的帮助文档 \ No newline at end of file diff --git a/src/md/2018-02-08-golang-make-dir-problem.md b/src/md/2018-02-08-golang-make-dir-problem.md index cabf942..9b04cd2 100644 --- a/src/md/2018-02-08-golang-make-dir-problem.md +++ b/src/md/2018-02-08-golang-make-dir-problem.md @@ -1,172 +1,172 @@ ---- -layout: post -title: "Golang 创建文件权限问题" -date: 2018-02-08 09:00:05 +0800 -categories: golang ---- - -# 问题描述 - -今天学习 Golang 文件操作实践时,当我创建一个文件(夹)出现文件权限与我代码设置不一致的问题 - -以下为我创建文件夹的代码: - -```golang -func main() { - err := os.MkdirAll("g10guang/t1/t2", 0666) - if err != nil { - fmt.Println(err) - } -} -``` - -output: - -``` -mkdir g10guang/t1: permission denied -``` - -> g10guang 文件夹创建成功,可是下面的 t1 文件夹创建失败 - -通过 `ls -l` 命令查看 g10guang 文件夹信息 - -``` -drw-rw-r-- 2 g10guang g10guang 4096 Feb 8 13:23 g10guang -``` - -# Linux 权限 - -比如: - -``` -drw-rw-r-- 2 g10guang g10guang 4096 Feb 8 13:23 g10guang -``` - -开头有 10 个字符,我们来一一分析: - -第一个 `d` 代表这是一个文件夹,`-` 为文件,`l` 为链接 - -当前用户与文件关系分为三个等级: -+ 所有者 -+ 同组用户 -+ 其他用户 - -而后面的 9 个字符分别代表文件所有者、同组用户、其他用户对文件的读写权限 - -+ `r` 读权限 -+ `w` 写权限 -+ `x` 执行权限 -+ `-` 不具有该权限 - -`rwx` ==> 111 - -`rw-` ==> 110 - -`r--` ==> 100 - -`---` ==> 000 - -如权限 `-rwxrw-r--` 表示这是一个文件,文件所有者拥有读写执行权限、同组用户有读写权限、其他用户只有读权限 - -可以通过 `chmod` 改变一个文件的权限,或者 `chown` 改变文件的所有者 - -我们创建文件夹时候指定的权限是 `0666`,这是一个八进制数字,拆分成二进制为 `110110110`,也就是 `rw-rw-rw-`,但是为什么我们创建的文件夹的权限是 `rw-rw-r--` 也就是 `0664` 呢? - -其实我们创建文件夹、文件时候都是经过 Linux 系统调用完成的,所以这是 Linux 创建文件时候会把用户指定的权限 `-` 减去 `umask`,而我本地 umask 为 `002`。通过 `umask` 命令查看 umask - - -# 分析 - -通过 `g10guang` 用户运行代码,文件夹当然所属于 `g10guang` - -尝试 `cd g10guang`,出现错误,错误信息:`cd: permission denied: g10guang` - -文件夹是属于我的,我怎么不能够 `cd` 呢? - -原因是:需要 `cd` 到一个文件夹,那么必须要有当前文件夹的执行权限 `x`,Linux 下设计一切皆文件,文件夹也是一个特殊的文件 - -如果使用 `os.MkdirAll` 方法创建文件夹时,必须基于文件夹所有者 `x` 执行权限以及 `w` 写权限,为什么需要写权限,可以通过 `vim` 去打开一个文件夹,可以看到文件夹里的信息(记得吗,文件夹也是文件),比如我一个文件夹通过 vim 打开的信息: - -```vim -" ============================================================================ -" Netrw Directory Listing (netrw v155) -" /home/g10guang/Templates/blog -" Sorted by name -" Sort sequence: [\/]$,\,\.h$,\.c$,\.cpp$,\~\=\*$,*,\.o$,\.obj$,\.info$,\.swp$,\.ba -" Quick Help: :help -:go up dir D:delete R:rename s:sort-by x:special -" ============================================================================== -../ -./ -.git/ -.sass-cache/ -_posts/ -_site/ -g10guang.github.io/ -.gitignore -.gitmodules -404.html -Gemfile -Gemfile.lock -_config.yml -about.md -index.md -``` - -文件夹中记录着里面有哪些文件以及文件夹,其中 `xxx/` 有 `/` 结尾的是文件夹,其他的是文件,所以在文件夹中创建一个文件夹需要改变文件夹信息,需要有写文件夹的权限 - -# 创建指定权限文件方法 - -两种方法: - -1. 改变 `umask` 后再创建文件,其后再把 `umask` 改为原来的 umask -2. 先创建文件,然后再改变文件的权限 - -## 方法一 - -改变 `umask` 后再创建文件,其后再把 `umask` 改为原来的 umask - -```golang -import ( - "os" - "fmt" - "syscall" -) - -func main() { - mask := syscall.Umask(0) // 改为 0000 八进制 - defer syscall.Umask(mask) // 改为原来的 umask - err := os.MkdirAll("g10guang/t1/t2", 0766) - if err != nil { - fmt.Println(err) - } -} -``` - -## 方法二 - -先创建文件,然后再改变文件的权限 - -```golang -import ( - "os" - "fmt" -) - -func main() { - err := os.MkdirAll("g10guang/t1/t2", 0777) - if err != nil { - fmt.Println(err) - } - os.Chmod("g10guang", 0777) - os.Chmod("g10guang/t1", 0777) - os.Chmod("g10guang/t1/t2", 0777) -} -``` - -golang 还不支持递归更改多个文件夹的权限,所有需要一个一个调用。 - -# 总结 - -Linux 文件操作都是调用 Linux 的系统调用完成的,虽然 `Python` 、`java` 等创建一个文件不会让显示让编程人员指定所创建的文件的权限,但是 Golang 需要,所以我们编程还是需要了解一点内核知识,比如网络编程涉及到 `I/O`,而 Linux 下 I/O 有I/O 阻塞、I/O非阻塞、I/O复用、SIGIO 、异步I/O,而I/O复用中有 select / poll / epoll 等,上次面试被问到 epoll 时,闻所未闻,[更多I/O讲解](https://tech.youzan.com/yi-bu-wang-luo-mo-xing/)。 - -现在找实习、找工作,发现公司会看重面试者是否有阅读源码等经验,因为我们不仅仅需要懂得调用别人的 API 完成某个功能,而且需要某个 API 底层到底完成了哪些操作,出了问题如何定位问题,以及如何解决问题。 +--- +layout: post +title: "Golang 创建文件权限问题" +date: 2018-02-08 09:00:05 +0800 +categories: golang +--- + +# 问题描述 + +今天学习 Golang 文件操作实践时,当我创建一个文件(夹)出现文件权限与我代码设置不一致的问题 + +以下为我创建文件夹的代码: + +```golang +func main() { + err := os.MkdirAll("g10guang/t1/t2", 0666) + if err != nil { + fmt.Println(err) + } +} +``` + +output: + +``` +mkdir g10guang/t1: permission denied +``` + +> g10guang 文件夹创建成功,可是下面的 t1 文件夹创建失败 + +通过 `ls -l` 命令查看 g10guang 文件夹信息 + +``` +drw-rw-r-- 2 g10guang g10guang 4096 Feb 8 13:23 g10guang +``` + +# Linux 权限 + +比如: + +``` +drw-rw-r-- 2 g10guang g10guang 4096 Feb 8 13:23 g10guang +``` + +开头有 10 个字符,我们来一一分析: + +第一个 `d` 代表这是一个文件夹,`-` 为文件,`l` 为链接 + +当前用户与文件关系分为三个等级: ++ 所有者 ++ 同组用户 ++ 其他用户 + +而后面的 9 个字符分别代表文件所有者、同组用户、其他用户对文件的读写权限 + ++ `r` 读权限 ++ `w` 写权限 ++ `x` 执行权限 ++ `-` 不具有该权限 + +`rwx` ==> 111 + +`rw-` ==> 110 + +`r--` ==> 100 + +`---` ==> 000 + +如权限 `-rwxrw-r--` 表示这是一个文件,文件所有者拥有读写执行权限、同组用户有读写权限、其他用户只有读权限 + +可以通过 `chmod` 改变一个文件的权限,或者 `chown` 改变文件的所有者 + +我们创建文件夹时候指定的权限是 `0666`,这是一个八进制数字,拆分成二进制为 `110110110`,也就是 `rw-rw-rw-`,但是为什么我们创建的文件夹的权限是 `rw-rw-r--` 也就是 `0664` 呢? + +其实我们创建文件夹、文件时候都是经过 Linux 系统调用完成的,所以这是 Linux 创建文件时候会把用户指定的权限 `-` 减去 `umask`,而我本地 umask 为 `002`。通过 `umask` 命令查看 umask + + +# 分析 + +通过 `g10guang` 用户运行代码,文件夹当然所属于 `g10guang` + +尝试 `cd g10guang`,出现错误,错误信息:`cd: permission denied: g10guang` + +文件夹是属于我的,我怎么不能够 `cd` 呢? + +原因是:需要 `cd` 到一个文件夹,那么必须要有当前文件夹的执行权限 `x`,Linux 下设计一切皆文件,文件夹也是一个特殊的文件 + +如果使用 `os.MkdirAll` 方法创建文件夹时,必须基于文件夹所有者 `x` 执行权限以及 `w` 写权限,为什么需要写权限,可以通过 `vim` 去打开一个文件夹,可以看到文件夹里的信息(记得吗,文件夹也是文件),比如我一个文件夹通过 vim 打开的信息: + +```vim +" ============================================================================ +" Netrw Directory Listing (netrw v155) +" /home/g10guang/Templates/blog +" Sorted by name +" Sort sequence: [\/]$,\,\.h$,\.c$,\.cpp$,\~\=\*$,*,\.o$,\.obj$,\.info$,\.swp$,\.ba +" Quick Help: :help -:go up dir D:delete R:rename s:sort-by x:special +" ============================================================================== +../ +./ +.git/ +.sass-cache/ +_posts/ +_site/ +g10guang.github.io/ +.gitignore +.gitmodules +404.html +Gemfile +Gemfile.lock +_config.yml +about.md +index.md +``` + +文件夹中记录着里面有哪些文件以及文件夹,其中 `xxx/` 有 `/` 结尾的是文件夹,其他的是文件,所以在文件夹中创建一个文件夹需要改变文件夹信息,需要有写文件夹的权限 + +# 创建指定权限文件方法 + +两种方法: + +1. 改变 `umask` 后再创建文件,其后再把 `umask` 改为原来的 umask +2. 先创建文件,然后再改变文件的权限 + +## 方法一 + +改变 `umask` 后再创建文件,其后再把 `umask` 改为原来的 umask + +```golang +import ( + "os" + "fmt" + "syscall" +) + +func main() { + mask := syscall.Umask(0) // 改为 0000 八进制 + defer syscall.Umask(mask) // 改为原来的 umask + err := os.MkdirAll("g10guang/t1/t2", 0766) + if err != nil { + fmt.Println(err) + } +} +``` + +## 方法二 + +先创建文件,然后再改变文件的权限 + +```golang +import ( + "os" + "fmt" +) + +func main() { + err := os.MkdirAll("g10guang/t1/t2", 0777) + if err != nil { + fmt.Println(err) + } + os.Chmod("g10guang", 0777) + os.Chmod("g10guang/t1", 0777) + os.Chmod("g10guang/t1/t2", 0777) +} +``` + +golang 还不支持递归更改多个文件夹的权限,所有需要一个一个调用。 + +# 总结 + +Linux 文件操作都是调用 Linux 的系统调用完成的,虽然 `Python` 、`java` 等创建一个文件不会让显示让编程人员指定所创建的文件的权限,但是 Golang 需要,所以我们编程还是需要了解一点内核知识,比如网络编程涉及到 `I/O`,而 Linux 下 I/O 有I/O 阻塞、I/O非阻塞、I/O复用、SIGIO 、异步I/O,而I/O复用中有 select / poll / epoll 等,上次面试被问到 epoll 时,闻所未闻,[更多I/O讲解](https://tech.youzan.com/yi-bu-wang-luo-mo-xing/)。 + +现在找实习、找工作,发现公司会看重面试者是否有阅读源码等经验,因为我们不仅仅需要懂得调用别人的 API 完成某个功能,而且需要某个 API 底层到底完成了哪些操作,出了问题如何定位问题,以及如何解决问题。 diff --git "a/src/md/2018-02-08-\351\230\277\351\207\214\345\267\264\345\267\264\345\256\236\344\271\240\347\224\237\351\235\242\350\257\225\345\220\216\346\204\237.md" "b/src/md/2018-02-08-\351\230\277\351\207\214\345\267\264\345\267\264\345\256\236\344\271\240\347\224\237\351\235\242\350\257\225\345\220\216\346\204\237.md" index 1c2ea4d..06629e7 100644 --- "a/src/md/2018-02-08-\351\230\277\351\207\214\345\267\264\345\267\264\345\256\236\344\271\240\347\224\237\351\235\242\350\257\225\345\220\216\346\204\237.md" +++ "b/src/md/2018-02-08-\351\230\277\351\207\214\345\267\264\345\267\264\345\256\236\344\271\240\347\224\237\351\235\242\350\257\225\345\220\216\346\204\237.md" @@ -1,64 +1,64 @@ ---- -layout: post -title: "阿里巴巴实习生面试后感" -date: 2018-02-08 21:00:05 +0800 -categories: 面经 ---- - -# 机会 - -得益在阿里工作的师姐内推机会,不用笔试,直接进入面试,面试阿里-淘宝技术的工程师。美中不足的是内推的岗位是测试工程师,但是我还是想从事 web 后端的研发的,但是对方是阿里-淘宝,测试就测试吧,在阿里测试肯定也学到很多东西。 - -今天下午接到电话约今晚 8 点电话面试…… - -捉紧时间补补,对着简历猜测: -+ 我应该怎么介绍我自己和做过的项目 -+ 面试官会问什么问题 - -# 过程 - -这个古老又神圣的问题,之前面试过几次,说是能说点什么东西,但是说实话自身没有什么亮点,没法打动人。 - -然后找一个最乐意的项目出来分享,很后悔找了一个自己第一次做外包的项目,这个项目确实让我学到了很多东西,但是含金量确实是很低。 - -当他问到我遇到了哪些技术问题,以及解决方案的时候我就知道部应该说这个项目,这个项目虽然遇到了很多问题,但是都是因为是 Green hand 的原因,说出来会显得我太菜了。但是做了一个错误的选择,得硬着头皮继续下去。 - -就其中一个问题,我说第一次做外包的时候没有了解到很多架构的知识,没有合适地使用到缓存,所有的请求都下放到 DB,那么他问我有没有想到其他方案可以解决这个问题? - -我当然不傻,知道有 redis、memcache 做缓存。 - -后来他问我是否知道 Hibernate 可以做二级缓存,单机情况下 Hibernate 做缓存已经足够了。(PS:单机意味着不会有太大的访问量) - -他一直顺着问到,问除了 Hibernate 我还有没有用到其他的 ORM,而他们之间有什么区别? - -除了 Hibernate 我就了解过 MyBatis 了,而两者对比我感觉就是 Hibernate 有自己的规范,JPA 接口等,对于开发来说更加规范,MyBastis 需要管理大量的 SQL 语句,多了以后不好管理,但是 SQL 能够解决的问题,MyBatis 实现起来都很方便,但是有可能会使得项目更加凌乱。我确实对 Hibernate 不熟悉,也不能够说出什么来,平时开发时候也就是照葫芦画瓢,具体如何优化,配置文件为什么这么写之类的一概不了解。 - -此刻,我深刻感受到如果找工作,最好深入了解某一技术,比如经典的 java 的内存模型等,这样虽然在一般开发中用不到这么高深的知识,即使用到也可以通过临时 Google 解决问题,但是如果在公司做大项目,技术方面遇到了不是随便 Google 就能解决的问题的时候,就很考技术深度。而且如果深入了解某一个领域,和面试官能够一步一步深入,聊得很 high,因为我感觉面试一般也就根据一两个方向深入问。 - -当我提到我其实有大半年不写 java,现在主要搞 Python 和 Golang 的时候(当然这可能会显得自己不务正业,亦或者在面试官看来这小伙子还挺有活力),他就问我如果学习这两个有没有想过找工作难问题,可能公司里面主流还是 java 、C系列吧。 - -我当然回答说,我秋招的时候留意过就业市场,ofo 和斗鱼都有招募 Golang 的,而且 Python 是目前很热门的脚本语言,很多公司招聘要求都是要求应聘者熟悉一门脚本语言。而且现在公司不是流行 DevOps 吗?Python 在运维方面也很有帮助啊。 - -一开始的时候,我提到其实我更想应聘研发岗位,我面试的是测试岗位。这时,其实我已经感觉凉了,因为面试我的是测试工程师,他怎么可能能够决定我能够去研发部门实习呢?或者有奇迹呢,Who knows. - -他最后问了我熟悉 Python 的内存模型不? - -从实,我不了解,java 内存模型是一个很经典的问题,但是 Python 我确实不够了解它的底层,虽然我用它已经很溜了,但对 Python 底层不是很了解,读的书还是少啊!!! - -最后给了我一个 Python 题目:如何有序去重输出 list 中的元素? - -我就应该想到这是一个考语言的题目,但我太天真,给出了两个答案都跟语言无关: -1. 排序 -2. Hash Set - -但是最后他提示 Python 有一个内置函数可以实现这个供能,可惜我实在没想到,因为平时写算法太少,即使最近写算法的时候也自己造轮子,没有使用内置函数。至今没有找到 list 中有函数可以实现这个功能,遗憾。 - -# 总结 - -我以为会问道算法题的,我最近在刷算法题目,可是没有,可能对实习生要求没那么高吧。 - -Anyway,面试官很 nice,最后我问他问题的时候,我请求他给我一点学习发展上的建议。他说,最好找和自己方向匹配的,比如我想学习 Python 和 Golang 就最好找一个公司是使用 Python 和 Golang的实习,这样我成长也比较快。(我知道已经凉了) - -其实我更加想知道对于框架、语言底层(我很不熟悉),他会给我什么建议。 - -我认为在面试前一定要好好地审视自己的项目经历,找出其中一个能够拿得出手,自己也最熟悉的项目出来和面试官分享,在多次磨炼之后,这个项目一定会越来越熟练,回答得越来越得心应手。 +--- +layout: post +title: "阿里巴巴实习生面试后感" +date: 2018-02-08 21:00:05 +0800 +categories: 面经 +--- + +# 机会 + +得益在阿里工作的师姐内推机会,不用笔试,直接进入面试,面试阿里-淘宝技术的工程师。美中不足的是内推的岗位是测试工程师,但是我还是想从事 web 后端的研发的,但是对方是阿里-淘宝,测试就测试吧,在阿里测试肯定也学到很多东西。 + +今天下午接到电话约今晚 8 点电话面试…… + +捉紧时间补补,对着简历猜测: ++ 我应该怎么介绍我自己和做过的项目 ++ 面试官会问什么问题 + +# 过程 + +这个古老又神圣的问题,之前面试过几次,说是能说点什么东西,但是说实话自身没有什么亮点,没法打动人。 + +然后找一个最乐意的项目出来分享,很后悔找了一个自己第一次做外包的项目,这个项目确实让我学到了很多东西,但是含金量确实是很低。 + +当他问到我遇到了哪些技术问题,以及解决方案的时候我就知道部应该说这个项目,这个项目虽然遇到了很多问题,但是都是因为是 Green hand 的原因,说出来会显得我太菜了。但是做了一个错误的选择,得硬着头皮继续下去。 + +就其中一个问题,我说第一次做外包的时候没有了解到很多架构的知识,没有合适地使用到缓存,所有的请求都下放到 DB,那么他问我有没有想到其他方案可以解决这个问题? + +我当然不傻,知道有 redis、memcache 做缓存。 + +后来他问我是否知道 Hibernate 可以做二级缓存,单机情况下 Hibernate 做缓存已经足够了。(PS:单机意味着不会有太大的访问量) + +他一直顺着问到,问除了 Hibernate 我还有没有用到其他的 ORM,而他们之间有什么区别? + +除了 Hibernate 我就了解过 MyBatis 了,而两者对比我感觉就是 Hibernate 有自己的规范,JPA 接口等,对于开发来说更加规范,MyBastis 需要管理大量的 SQL 语句,多了以后不好管理,但是 SQL 能够解决的问题,MyBatis 实现起来都很方便,但是有可能会使得项目更加凌乱。我确实对 Hibernate 不熟悉,也不能够说出什么来,平时开发时候也就是照葫芦画瓢,具体如何优化,配置文件为什么这么写之类的一概不了解。 + +此刻,我深刻感受到如果找工作,最好深入了解某一技术,比如经典的 java 的内存模型等,这样虽然在一般开发中用不到这么高深的知识,即使用到也可以通过临时 Google 解决问题,但是如果在公司做大项目,技术方面遇到了不是随便 Google 就能解决的问题的时候,就很考技术深度。而且如果深入了解某一个领域,和面试官能够一步一步深入,聊得很 high,因为我感觉面试一般也就根据一两个方向深入问。 + +当我提到我其实有大半年不写 java,现在主要搞 Python 和 Golang 的时候(当然这可能会显得自己不务正业,亦或者在面试官看来这小伙子还挺有活力),他就问我如果学习这两个有没有想过找工作难问题,可能公司里面主流还是 java 、C系列吧。 + +我当然回答说,我秋招的时候留意过就业市场,ofo 和斗鱼都有招募 Golang 的,而且 Python 是目前很热门的脚本语言,很多公司招聘要求都是要求应聘者熟悉一门脚本语言。而且现在公司不是流行 DevOps 吗?Python 在运维方面也很有帮助啊。 + +一开始的时候,我提到其实我更想应聘研发岗位,我面试的是测试岗位。这时,其实我已经感觉凉了,因为面试我的是测试工程师,他怎么可能能够决定我能够去研发部门实习呢?或者有奇迹呢,Who knows. + +他最后问了我熟悉 Python 的内存模型不? + +从实,我不了解,java 内存模型是一个很经典的问题,但是 Python 我确实不够了解它的底层,虽然我用它已经很溜了,但对 Python 底层不是很了解,读的书还是少啊!!! + +最后给了我一个 Python 题目:如何有序去重输出 list 中的元素? + +我就应该想到这是一个考语言的题目,但我太天真,给出了两个答案都跟语言无关: +1. 排序 +2. Hash Set + +但是最后他提示 Python 有一个内置函数可以实现这个供能,可惜我实在没想到,因为平时写算法太少,即使最近写算法的时候也自己造轮子,没有使用内置函数。至今没有找到 list 中有函数可以实现这个功能,遗憾。 + +# 总结 + +我以为会问道算法题的,我最近在刷算法题目,可是没有,可能对实习生要求没那么高吧。 + +Anyway,面试官很 nice,最后我问他问题的时候,我请求他给我一点学习发展上的建议。他说,最好找和自己方向匹配的,比如我想学习 Python 和 Golang 就最好找一个公司是使用 Python 和 Golang的实习,这样我成长也比较快。(我知道已经凉了) + +其实我更加想知道对于框架、语言底层(我很不熟悉),他会给我什么建议。 + +我认为在面试前一定要好好地审视自己的项目经历,找出其中一个能够拿得出手,自己也最熟悉的项目出来和面试官分享,在多次磨炼之后,这个项目一定会越来越熟练,回答得越来越得心应手。 diff --git a/src/md/2018-02-10-Golang-defer-panic-recover.md b/src/md/2018-02-10-Golang-defer-panic-recover.md index f45a394..1ab340b 100644 --- a/src/md/2018-02-10-Golang-defer-panic-recover.md +++ b/src/md/2018-02-10-Golang-defer-panic-recover.md @@ -1,301 +1,301 @@ ---- -layout: post -title: "Golang 错误异常处理 defer panic recover" -date: 2018-02-08 21:00:05 +0800 -categories: golang ---- - -# Golang错误、异常 - -别的语言都有异常处理语法,以 python 的异常处理语法为例: - -```python -try: - # do something - raise Exception -except KeyError as e: - pass -except Exception as e: - pass -else: - pass -finally: - pass -``` - -Golang 中并没有如此语法 - -**Golang 中错误和异常并不是一码事** - -```golang -type error interface { - Error() string -} -``` - -错误是实现了 `error` 接口的对象。 - -异常是由内置函数 `func panic(v interface{})` 发起的,可以被 `func recover() interface{}` 函数捕获 - -第三方或内置库函数最后一个函数返回 `error` 对象,调用者可以查看 `err != nil` 来判断本次调用是否成功执行,这会导致大量判断是否发生错误的冗余代码,开发中应该把处理错误逻辑相同的部分抽取出来。如果对是否发生错误不关心可以使用 `_` 来接收返回错误。(PS:`_` 在编译时会处理掉) - -而第三方或内置库函数不会 `panic` 一个异常,让调用者 `recover`。 - -如果 `panic` 没有被 `recover`,那么有可能会挂起整个程序。 -`panic` 应该在包内 `recover`,`panic` 只能由当前 goroutine `recover`。`panic` 目的就是为了解决: - -> 遇到了在本包中无法处理的异常,一个跳出多个调用栈,再被外围的 recover 捕获,该包再返回调用者 error - -# 语法 - -`recover` 只能够在 `defer` 中使用,`func recover() interface{}` 返回的是 `func panic(v interface{})` 的参数 **v**。如果没有 `panic` 但是调用了 `recover`,`recover` 返回的是 **nil**,多次调用 `recover` 不会有副作用。 - -`defer` 是一个先出后出的调用栈,参数的赋值在 `defer` 语句处已经完成,无论该函数是正常的执行,或返回 `error`,或 `panic`,`defer` 中的函数都能够保证得到执行。常用于 `recover` 捕获 `panic`、关闭文件资源、释放锁等等。 - -```golang -func main() { - -} - -func f(i int) { - defer func() { - if r := recover(); r != nil { - fmt.Println("Recovered in f", r) - } - }() - fmt.Println("Calling g.") - g(i) - fmt.Println("Returned normally from g.") -} - -func g(i int) { - if i > 3 { - fmt.Println("Panicking") - panic(fmt.Sprintf("%v", i)) - } - defer fmt.Println("Defer in g", i) - fmt.Println("Printing in g", i) - g(i + 1) -} - -``` - -output: - -``` -main start -Calling g. -Printing in g 2 -Printing in g 3 -Panicking -Defer in g 3 -Defer in g 2 -Recovered in f 4 -main end -``` - -# panic 只能在本 goroutine 处理 - -尝试一下在别的 goroutine 中处理 panic,改造一下上面的 demo,`defer recover` 放到 main,然后使用 goroutine 执行 `f(i)` 函数。 - -```golang -func main() { - defer func() { - if r := recover(); r != nil { - fmt.Println("Recovered in f", r) - } - }() - fmt.Println("main start") - go f(2) - // 当前 goroutine 睡眠,等待 panic - time.Sleep(time.Second) - fmt.Println("main end") -} - -func f(i int) { - fmt.Println("Calling g.") - g(i) - fmt.Println("Returned normally from g.") -} - -func g(i int) { - if i > 3 { - fmt.Println("Panicking") - panic(fmt.Sprintf("%v", i)) - } - defer fmt.Println("Defer in g", i) - fmt.Println("Printing in g", i) - g(i + 1) -} -``` - -output: - -``` -main start -Calling g. -Printing in g 2 -Printing in g 3 -Panicking -Defer in g 3 -Defer in g 2 -panic: 4 - -goroutine 5 [running]: -exit status 2 -``` - -panic 并没有被解决,一直抛到最上层,使整个程序崩溃,`fmt.Println("main end")` 并没有被执行,panic 只能由**当前** goroutine 解决。 - -`func g(i int)` 函数使用了递归调用本身,也就是 `panic` 发生在 `defer` 之前,但是 `defer fmt.Println("Defer in g", i)` 还是成功执行了。 - -# recover 只能在 defer 中有效 - -尝试把 `recover` 移除 `defer`,看看发生什么 - -```golang -func main() { - fmt.Println("main start") - f(2) - fmt.Println("main end") -} - -func f(i int) { - fmt.Println("Calling g.") - g(i) - fmt.Println("Returned normally from g.") - r := recover() - fmt.Println("Recovered in f", r) -} - -func g(i int) { - if i > 3 { - fmt.Println("Panicking") - panic(fmt.Sprintf("%v", i)) - } - defer fmt.Println("Defer in g", i) - fmt.Println("Printing in g", i) - g(i + 1) -} -``` - -output: - -``` -main start -Calling g. -Printing in g 2 -Printing in g 3 -Panicking -Defer in g 3 -Defer in g 2 -panic: 4 - -goroutine 1 [running]: -exit status 2 -``` - -**Ooops** - -# (err != nil) Always? - -函数返回 error 中有一个坑,看以下代码,我们自定义了一个 `MyError` 实现了错误 `error` 接口: - -```golang -func main() { - if err := try(); err != nil { - fmt.Println("err != nil") - } -} - -type MyError struct {} - -func (e *MyError) Error() string { - return "Oooops" -} - -func try() error { - var err *MyError = nil - if false { - err = new(MyError) - } - return err -} -``` - -无论运行多少次,运行结果都是 `err != nil`。 - -原因: - -`error` 是一个接口类型,其中接口类型的存储有:`data` 和 `type`,函数返回会执行: - -```golang -var err *MyError = nil -var returnError error = err -``` - -经过这个赋值后,`returnError` 中存储的 `data==nil`,但是 `type==*MyError`,`returnError` 已不再是 **nil**。 - -那该如何办? - -1. 如果没有特殊要求,就直接使用内置函数 `errors.New()` 创建一个错误,其在 `errors/errors.go` 中的剪短源码如下: - -```golang -package errors - -// New returns an error that formats as the given text. -func New(text string) error { - return &errorString{text} -} - -// errorString is a trivial implementation of error. -type errorString struct { - s string -} - -func (e *errorString) Error() string { - return e.s -} -``` - -2. 采用自定义错误,再对函数做一层封装,如下: - -```golang -type MyError struct {} - -func (e *MyError) Error() string { - return "Ooops" -} - -func main() { - err := CallMe() - if err != nil { - fmt.Println(err.Error()) - } -} - -// 大写 C 开头,被外部调用 -func CallMe() error { - err := deal() - if err != nil { - errors.New("Some msg") - } - return nil -} - -// 小写 d 开头,只能被内部使用 -func deal() (e *MyError) { - if false { - e = new(MyError) - return - } - return -} -``` - -# 总结 - -+ panic 不应跨域 package -+ recover 只能在 defer 中起作用 -+ panic 只能由当前 goroutine recover -+ 自定义异常不应该作为返回值返回给外围调用者 +--- +layout: post +title: "Golang 错误异常处理 defer panic recover" +date: 2018-02-08 21:00:05 +0800 +categories: golang +--- + +# Golang错误、异常 + +别的语言都有异常处理语法,以 python 的异常处理语法为例: + +```python +try: + # do something + raise Exception +except KeyError as e: + pass +except Exception as e: + pass +else: + pass +finally: + pass +``` + +Golang 中并没有如此语法 + +**Golang 中错误和异常并不是一码事** + +```golang +type error interface { + Error() string +} +``` + +错误是实现了 `error` 接口的对象。 + +异常是由内置函数 `func panic(v interface{})` 发起的,可以被 `func recover() interface{}` 函数捕获 + +第三方或内置库函数最后一个函数返回 `error` 对象,调用者可以查看 `err != nil` 来判断本次调用是否成功执行,这会导致大量判断是否发生错误的冗余代码,开发中应该把处理错误逻辑相同的部分抽取出来。如果对是否发生错误不关心可以使用 `_` 来接收返回错误。(PS:`_` 在编译时会处理掉) + +而第三方或内置库函数不会 `panic` 一个异常,让调用者 `recover`。 + +如果 `panic` 没有被 `recover`,那么有可能会挂起整个程序。 +`panic` 应该在包内 `recover`,`panic` 只能由当前 goroutine `recover`。`panic` 目的就是为了解决: + +> 遇到了在本包中无法处理的异常,一个跳出多个调用栈,再被外围的 recover 捕获,该包再返回调用者 error + +# 语法 + +`recover` 只能够在 `defer` 中使用,`func recover() interface{}` 返回的是 `func panic(v interface{})` 的参数 **v**。如果没有 `panic` 但是调用了 `recover`,`recover` 返回的是 **nil**,多次调用 `recover` 不会有副作用。 + +`defer` 是一个先出后出的调用栈,参数的赋值在 `defer` 语句处已经完成,无论该函数是正常的执行,或返回 `error`,或 `panic`,`defer` 中的函数都能够保证得到执行。常用于 `recover` 捕获 `panic`、关闭文件资源、释放锁等等。 + +```golang +func main() { + +} + +func f(i int) { + defer func() { + if r := recover(); r != nil { + fmt.Println("Recovered in f", r) + } + }() + fmt.Println("Calling g.") + g(i) + fmt.Println("Returned normally from g.") +} + +func g(i int) { + if i > 3 { + fmt.Println("Panicking") + panic(fmt.Sprintf("%v", i)) + } + defer fmt.Println("Defer in g", i) + fmt.Println("Printing in g", i) + g(i + 1) +} + +``` + +output: + +``` +main start +Calling g. +Printing in g 2 +Printing in g 3 +Panicking +Defer in g 3 +Defer in g 2 +Recovered in f 4 +main end +``` + +# panic 只能在本 goroutine 处理 + +尝试一下在别的 goroutine 中处理 panic,改造一下上面的 demo,`defer recover` 放到 main,然后使用 goroutine 执行 `f(i)` 函数。 + +```golang +func main() { + defer func() { + if r := recover(); r != nil { + fmt.Println("Recovered in f", r) + } + }() + fmt.Println("main start") + go f(2) + // 当前 goroutine 睡眠,等待 panic + time.Sleep(time.Second) + fmt.Println("main end") +} + +func f(i int) { + fmt.Println("Calling g.") + g(i) + fmt.Println("Returned normally from g.") +} + +func g(i int) { + if i > 3 { + fmt.Println("Panicking") + panic(fmt.Sprintf("%v", i)) + } + defer fmt.Println("Defer in g", i) + fmt.Println("Printing in g", i) + g(i + 1) +} +``` + +output: + +``` +main start +Calling g. +Printing in g 2 +Printing in g 3 +Panicking +Defer in g 3 +Defer in g 2 +panic: 4 + +goroutine 5 [running]: +exit status 2 +``` + +panic 并没有被解决,一直抛到最上层,使整个程序崩溃,`fmt.Println("main end")` 并没有被执行,panic 只能由**当前** goroutine 解决。 + +`func g(i int)` 函数使用了递归调用本身,也就是 `panic` 发生在 `defer` 之前,但是 `defer fmt.Println("Defer in g", i)` 还是成功执行了。 + +# recover 只能在 defer 中有效 + +尝试把 `recover` 移除 `defer`,看看发生什么 + +```golang +func main() { + fmt.Println("main start") + f(2) + fmt.Println("main end") +} + +func f(i int) { + fmt.Println("Calling g.") + g(i) + fmt.Println("Returned normally from g.") + r := recover() + fmt.Println("Recovered in f", r) +} + +func g(i int) { + if i > 3 { + fmt.Println("Panicking") + panic(fmt.Sprintf("%v", i)) + } + defer fmt.Println("Defer in g", i) + fmt.Println("Printing in g", i) + g(i + 1) +} +``` + +output: + +``` +main start +Calling g. +Printing in g 2 +Printing in g 3 +Panicking +Defer in g 3 +Defer in g 2 +panic: 4 + +goroutine 1 [running]: +exit status 2 +``` + +**Ooops** + +# (err != nil) Always? + +函数返回 error 中有一个坑,看以下代码,我们自定义了一个 `MyError` 实现了错误 `error` 接口: + +```golang +func main() { + if err := try(); err != nil { + fmt.Println("err != nil") + } +} + +type MyError struct {} + +func (e *MyError) Error() string { + return "Oooops" +} + +func try() error { + var err *MyError = nil + if false { + err = new(MyError) + } + return err +} +``` + +无论运行多少次,运行结果都是 `err != nil`。 + +原因: + +`error` 是一个接口类型,其中接口类型的存储有:`data` 和 `type`,函数返回会执行: + +```golang +var err *MyError = nil +var returnError error = err +``` + +经过这个赋值后,`returnError` 中存储的 `data==nil`,但是 `type==*MyError`,`returnError` 已不再是 **nil**。 + +那该如何办? + +1. 如果没有特殊要求,就直接使用内置函数 `errors.New()` 创建一个错误,其在 `errors/errors.go` 中的剪短源码如下: + +```golang +package errors + +// New returns an error that formats as the given text. +func New(text string) error { + return &errorString{text} +} + +// errorString is a trivial implementation of error. +type errorString struct { + s string +} + +func (e *errorString) Error() string { + return e.s +} +``` + +2. 采用自定义错误,再对函数做一层封装,如下: + +```golang +type MyError struct {} + +func (e *MyError) Error() string { + return "Ooops" +} + +func main() { + err := CallMe() + if err != nil { + fmt.Println(err.Error()) + } +} + +// 大写 C 开头,被外部调用 +func CallMe() error { + err := deal() + if err != nil { + errors.New("Some msg") + } + return nil +} + +// 小写 d 开头,只能被内部使用 +func deal() (e *MyError) { + if false { + e = new(MyError) + return + } + return +} +``` + +# 总结 + ++ panic 不应跨域 package ++ recover 只能在 defer 中起作用 ++ panic 只能由当前 goroutine recover ++ 自定义异常不应该作为返回值返回给外围调用者 diff --git "a/src/md/2018-02-13-8\344\272\272\350\201\232\344\274\232\345\220\216\346\204\237.md" "b/src/md/2018-02-13-8\344\272\272\350\201\232\344\274\232\345\220\216\346\204\237.md" index bc298df..d2ac786 100644 --- "a/src/md/2018-02-13-8\344\272\272\350\201\232\344\274\232\345\220\216\346\204\237.md" +++ "b/src/md/2018-02-13-8\344\272\272\350\201\232\344\274\232\345\220\216\346\204\237.md" @@ -1,30 +1,30 @@ ---- -layout: post -title: "2018-2-13高中好友聚会后感" -date: 2018-02-13 21:00:05 +0800 -categories: 感想 ---- - -高中还有联系的好友就只剩下十来个了,有那么几个是每个假期我都会去跟他们见一面的,或许是吃个饭、看个电影、喝个奶茶、打球等。我想即使我们各自在别的城市上大学,但是还是能够每个寒暑假搞一次聚会,这份友谊实在难得。 - -这次聚会是我`吹鸡`的,因为下一个暑假找工作的就忙于实习,保研的就忙于参加夏令营,考研的就忙于在图书馆复习。这次聚会格外珍惜,因为下一次不知道是什么时候了。 - -对于 8 人聚会,想要找到一个 8 个人都满意的地方是很难的。 - -其中一个朋友说,只要打一次火锅,就知道对方是什么人。因为这句话我们就到了某饭馆打牛肉火锅,一个曾经不吃牛肉的伙伴开戒了。 - -饭桌上我们没有谈太多关于之前高中的旧事,大部分话题都集中在了桌上的牛肉上,期间还不时有黄段子,聚会总有黄段子调节一下的气氛。或许是陈年旧事已经被我们谈烂了,或许是我们不再像高中一样天天聚在一起,那么熟悉对方,共同度过那么多美好的事情。 - -很快吃完午饭,下一场去哪里?每个人都不想难得见一面却要匆匆道别,所以我们在街上、广场优哉游哉地逗了一会儿,最后有人提议到篮球场去打篮球。今天天气非常好,虽然是临近春节,但是室外运动穿短袖非常舒适。 - -我们高中的友谊有大部分都是通过篮球来建立和维系的,傍晚下课后一群人拿着篮球往篮球场跑,因为场地有限,我们需要霸占场地;我们平时讨论的话题是NBA,上课也看着NBA。 - -起初我们都推脱说不想打,以出汗、鞋子衣服不合适为理由。最后大家玩 high 以后,每个人都上了场,回味了高中的欢乐,出了一把汗。这令我回想起了高中时候我对篮球的热爱,`无兄弟,不篮球`。 - -很久没有打球了,每个人的体能都下降了不少,没跑几步就气喘吁吁,身体很明显已经跟不上节奏了,典型的缺乏锻炼亚健康状态。 - -这次聚会人们问得最多的问题就是下一站去哪里? - -可是没人知道答案,我们就走着走着,不知道目的地,也没有方向。 - +--- +layout: post +title: "2018-2-13高中好友聚会后感" +date: 2018-02-13 21:00:05 +0800 +categories: 感想 +--- + +高中还有联系的好友就只剩下十来个了,有那么几个是每个假期我都会去跟他们见一面的,或许是吃个饭、看个电影、喝个奶茶、打球等。我想即使我们各自在别的城市上大学,但是还是能够每个寒暑假搞一次聚会,这份友谊实在难得。 + +这次聚会是我`吹鸡`的,因为下一个暑假找工作的就忙于实习,保研的就忙于参加夏令营,考研的就忙于在图书馆复习。这次聚会格外珍惜,因为下一次不知道是什么时候了。 + +对于 8 人聚会,想要找到一个 8 个人都满意的地方是很难的。 + +其中一个朋友说,只要打一次火锅,就知道对方是什么人。因为这句话我们就到了某饭馆打牛肉火锅,一个曾经不吃牛肉的伙伴开戒了。 + +饭桌上我们没有谈太多关于之前高中的旧事,大部分话题都集中在了桌上的牛肉上,期间还不时有黄段子,聚会总有黄段子调节一下的气氛。或许是陈年旧事已经被我们谈烂了,或许是我们不再像高中一样天天聚在一起,那么熟悉对方,共同度过那么多美好的事情。 + +很快吃完午饭,下一场去哪里?每个人都不想难得见一面却要匆匆道别,所以我们在街上、广场优哉游哉地逗了一会儿,最后有人提议到篮球场去打篮球。今天天气非常好,虽然是临近春节,但是室外运动穿短袖非常舒适。 + +我们高中的友谊有大部分都是通过篮球来建立和维系的,傍晚下课后一群人拿着篮球往篮球场跑,因为场地有限,我们需要霸占场地;我们平时讨论的话题是NBA,上课也看着NBA。 + +起初我们都推脱说不想打,以出汗、鞋子衣服不合适为理由。最后大家玩 high 以后,每个人都上了场,回味了高中的欢乐,出了一把汗。这令我回想起了高中时候我对篮球的热爱,`无兄弟,不篮球`。 + +很久没有打球了,每个人的体能都下降了不少,没跑几步就气喘吁吁,身体很明显已经跟不上节奏了,典型的缺乏锻炼亚健康状态。 + +这次聚会人们问得最多的问题就是下一站去哪里? + +可是没人知道答案,我们就走着走着,不知道目的地,也没有方向。 + 要是谁知道应该做什么,那这个人是多么的重要。无论是在聚会、组织,异或公司,他们就是这个组织的领头人。 \ No newline at end of file diff --git a/src/md/2018-02-13-golang-channel.md b/src/md/2018-02-13-golang-channel.md index fe0cdbd..0b75778 100644 --- a/src/md/2018-02-13-golang-channel.md +++ b/src/md/2018-02-13-golang-channel.md @@ -1,291 +1,291 @@ ---- -layout: post -title: "Golang happens before" -date: 2018-02-13 21:00:05 +0800 -categories: golang ---- - -# happens before - -**什么是 happens before?** - -现代编译器会对指令进行重排,已达到更优的运行效果,如以下的程序: - -```c -int a = 0 -int b = 0 - -int main() { - a = b + 1 // (1) - b = 1 // (2) -} -``` - -(1) 操作一定是先于 (2) 操作完成的吗? - -不一定。 - -可能编译器将上述代码翻译成: - -``` -加载 b 到寄存器X -进行 b = 1 赋值 -将寄存器X中值加上1后赋值给 a -``` - -(1) 操作不会对 (2) 操作进行任何影响,即使 (2) 操作可能对 (1) 操作有影响,也不需要保证 (1) 操作一定在 (2) 操作之前完成,因为可以使用寄存器作为临时存储。也就是想说的,(1) dose not happen before (2) - -happens before 指代事件 *e1* *e2* 存在某些影响,需要保证 *e1* *e2* 之间的开始和完成需要有一定的逻辑关系。 - -happens before 指的不是时序上的关系,描述的是一个写操作*w*对变量*v*的修改能够被读操作*r*感知到,那么*w* happens befoer *r* - -怎么样的写操作*w*能够被读操作*r*检测到呢? - -条件: - -1. *w* happens before *r* -2. Any other write to the shared variable v either happens before w or after r - -## Happens before 在 golang 中的体现 - -+ If a package *p* imports package *q*, the completion f *q*'s init functions happens before the start of any of *p*'s. - -+ The start of the function main.main happens after all init function have finished. - -+ The go statement that starts a new goroutine happens before the goroutines's execution begins. - -+ The exit of a program is not guaranteed to happen before any event in the program. - -+ A send on a channel happens before the corresponding receive from that channel completes. - -+ The closing of a channel happens before a receive that returns a zero value because the channel is closed. - -+ A receive from an unbuffered channel happens before the send on that channel completes. - -+ The kth receive on a channel with capacity C happens before the k+Cth send from that channel completes. - -+ For any `sync.Mutex` or `sync.RWMutex` variable l and n < m, call n of `l.Unlock()` hannpens before call m of `l.Lock()` returns - -+ For any call to `l.RLock` on a `sync.RWMutex` variable l, there is an n such that the `l.RLock` happens afrer call n to `l.Unlock` and the matching `l.RUnlock` happens before call n+1 to `l.Lock` - -# channel - -Golang 中除了锁机制外,还可以通过 channel 来达到同步控制,使用通信进行同步。 - -1. 对未初始化 channel 操作会 block,甚至会导致 deadlock,因为 goroutine scheduler 检测到一直没有 goroutine ready-to-run over worker threads -2. 连续关闭 channel 导致 panic -3. 向已经关闭的 channel 发送数据触发 panic -4. 读取已经关闭的 channel 会返回 zero-value 不会阻塞 - -**如何检测 channel 是否关闭了?** - -use comma-ok to check the channel is close or not. - -```golang -v, ok := <- ch -``` - -or use for-range to read from channel till the channel is close. - -```golang -for v := range ch { - // do something -} -``` - -**如何实现非阻塞 channel 操作?** - -使用 select-case 实现非阻塞 channel 通信 - -```golang -select { - case c1 <- 1: - // c1 可以写入 - case v <- c2: - // c2 可以读取 - ... - default: - // 没有任何 case channel 可以读或写 -} -``` - -**那如何控制 select 的最大等待时间呢?** - -time 包可以帮助我们实现这一功能,time 包下有不少功能也是通过 channel 实现的,比如以下的一个炸弹: - -```golang -func main() { - tick := time.Tick(1 * time.Second) - boom := time.After(5 * time.Second) - for { - select { - case <-tick: - fmt.Println("tick.") - case <-boom: - fmt.Println("BOOM!") - return - default: - fmt.Println(".") - time.Sleep(500 * time.Millisecond) - } - } -} -``` - -output: - -``` - . - . -tick. - . - . -tick. - . - . -tick. - . - . -tick. - . - . -tick. -BOOM! -``` - -`time.Tick` 可以控制该 channel 在指定时间后可读(永远不可写,因为函数返回的是一个只读 `<-chan Time`) - -**如何通知goroutine退出** - -通过从 channel 中读取数据实现 - -```golang -func main() { - quitSignal := make(chan int) - go work(quitSignal) - time.Sleep(5 * time.Second) - quitSignal <- 1 -} - -func work(quitSignal <-chan int) { - for{ - select { - case <-quitSignal: - fmt.Println("收到结束信号") - return - default: - fmt.Println("work") - time.Sleep(time.Second) - } - } -} -``` - -**如何控制一个函数的最大并发数?** - -通过 buffered channel 实现,每个需要执行该函数的 goroutine 向 buffered channel 发送一个数据,然后执行完成后从 channel 中取出一个数据 - -```golang -var concurrent = 3 - -var limit = make(chan uint8, concurrent) - -// 控制并发包装器 exported -func Controller(args ...interface{}) { - limit <- 1 - function(args) - <-limit -} - -// 需要被控制并发数量的函数,只有本包内可访问 -func function(args ...interface{}) { - -} -``` - -**如何实现定时操作?** - -我们可以通过 `time.After` 自己实现一个,或使用现成的 `time.AfterFunc` - -```golang -func main() { - time.AfterFunc(2 * time.Second, func() { - fmt.Println("After func") - }) - fmt.Println("haha") - time.Sleep(100 * time.Second) -} -``` - -`time.AfterFunc` 并不会阻塞当前 goroutine,而是使用了新的 goroutine - -**如何保证一个函数只被执行一次?** - -同样可以使用 channel buffer,只不过 buffer 大小为 1,执行前先向该 channel 发送数据 - -```golang -const limit = 1 -var c = make(chan int, limit) - -func main() { - for i := 0; i < 100; i++ { - go Controller() - } - time.Sleep(2 * time.Second) -} - -// exported -func Controller() { - select { - case c <- 1: - once() - default: - fmt.Println("once 已经被执行过") - } -} - -// inner -func once() { - fmt.Println("once") -} -``` - -同样的,需要修改执行的次数,只需要修改 limit - -或者使用锁机制,控制控制只有一个变量进入,而且采用一个标识符记录该函数是否已经执行过,相应的实现在 `sync.Once`。`sync.Once` 提供一个安全机制,确保通过 `sync.Once.Do(f func())` 调用的 f() 最多只被执行一次。 - -以下是其内部代码,实现非常简单: - -```golang -type Once struct { - m Mutex - done uint32 -} - -func (o *Once) Do(f func()) { - if atomic.LoadUint32(&o.done) == 1 { - return - } - // Slow-path. - o.m.Lock() - defer o.m.Unlock() - if o.done == 0 { - defer atomic.StoreUint32(&o.done, 1) - f() - } -} -``` - -# select{} 与 for{} 区别 - -`for{}` 相信大家都不陌生,但 `select{}` 很让人疑惑。 - -+ `for{}` 会把 cpu 使用率到 100% -+ `select{}` 会阻塞当前 goroutine,cpu 使用率将近 0% - - -# 参考: - -[Golang 内存模型](https://tiancaiamao.gitbooks.io/go-internals/content/zh/01.3.html) - +--- +layout: post +title: "Golang happens before" +date: 2018-02-13 21:00:05 +0800 +categories: golang +--- + +# happens before + +**什么是 happens before?** + +现代编译器会对指令进行重排,已达到更优的运行效果,如以下的程序: + +```c +int a = 0 +int b = 0 + +int main() { + a = b + 1 // (1) + b = 1 // (2) +} +``` + +(1) 操作一定是先于 (2) 操作完成的吗? + +不一定。 + +可能编译器将上述代码翻译成: + +``` +加载 b 到寄存器X +进行 b = 1 赋值 +将寄存器X中值加上1后赋值给 a +``` + +(1) 操作不会对 (2) 操作进行任何影响,即使 (2) 操作可能对 (1) 操作有影响,也不需要保证 (1) 操作一定在 (2) 操作之前完成,因为可以使用寄存器作为临时存储。也就是想说的,(1) dose not happen before (2) + +happens before 指代事件 *e1* *e2* 存在某些影响,需要保证 *e1* *e2* 之间的开始和完成需要有一定的逻辑关系。 + +happens before 指的不是时序上的关系,描述的是一个写操作*w*对变量*v*的修改能够被读操作*r*感知到,那么*w* happens befoer *r* + +怎么样的写操作*w*能够被读操作*r*检测到呢? + +条件: + +1. *w* happens before *r* +2. Any other write to the shared variable v either happens before w or after r + +## Happens before 在 golang 中的体现 + ++ If a package *p* imports package *q*, the completion f *q*'s init functions happens before the start of any of *p*'s. + ++ The start of the function main.main happens after all init function have finished. + ++ The go statement that starts a new goroutine happens before the goroutines's execution begins. + ++ The exit of a program is not guaranteed to happen before any event in the program. + ++ A send on a channel happens before the corresponding receive from that channel completes. + ++ The closing of a channel happens before a receive that returns a zero value because the channel is closed. + ++ A receive from an unbuffered channel happens before the send on that channel completes. + ++ The kth receive on a channel with capacity C happens before the k+Cth send from that channel completes. + ++ For any `sync.Mutex` or `sync.RWMutex` variable l and n < m, call n of `l.Unlock()` hannpens before call m of `l.Lock()` returns + ++ For any call to `l.RLock` on a `sync.RWMutex` variable l, there is an n such that the `l.RLock` happens afrer call n to `l.Unlock` and the matching `l.RUnlock` happens before call n+1 to `l.Lock` + +# channel + +Golang 中除了锁机制外,还可以通过 channel 来达到同步控制,使用通信进行同步。 + +1. 对未初始化 channel 操作会 block,甚至会导致 deadlock,因为 goroutine scheduler 检测到一直没有 goroutine ready-to-run over worker threads +2. 连续关闭 channel 导致 panic +3. 向已经关闭的 channel 发送数据触发 panic +4. 读取已经关闭的 channel 会返回 zero-value 不会阻塞 + +**如何检测 channel 是否关闭了?** + +use comma-ok to check the channel is close or not. + +```golang +v, ok := <- ch +``` + +or use for-range to read from channel till the channel is close. + +```golang +for v := range ch { + // do something +} +``` + +**如何实现非阻塞 channel 操作?** + +使用 select-case 实现非阻塞 channel 通信 + +```golang +select { + case c1 <- 1: + // c1 可以写入 + case v <- c2: + // c2 可以读取 + ... + default: + // 没有任何 case channel 可以读或写 +} +``` + +**那如何控制 select 的最大等待时间呢?** + +time 包可以帮助我们实现这一功能,time 包下有不少功能也是通过 channel 实现的,比如以下的一个炸弹: + +```golang +func main() { + tick := time.Tick(1 * time.Second) + boom := time.After(5 * time.Second) + for { + select { + case <-tick: + fmt.Println("tick.") + case <-boom: + fmt.Println("BOOM!") + return + default: + fmt.Println(".") + time.Sleep(500 * time.Millisecond) + } + } +} +``` + +output: + +``` + . + . +tick. + . + . +tick. + . + . +tick. + . + . +tick. + . + . +tick. +BOOM! +``` + +`time.Tick` 可以控制该 channel 在指定时间后可读(永远不可写,因为函数返回的是一个只读 `<-chan Time`) + +**如何通知goroutine退出** + +通过从 channel 中读取数据实现 + +```golang +func main() { + quitSignal := make(chan int) + go work(quitSignal) + time.Sleep(5 * time.Second) + quitSignal <- 1 +} + +func work(quitSignal <-chan int) { + for{ + select { + case <-quitSignal: + fmt.Println("收到结束信号") + return + default: + fmt.Println("work") + time.Sleep(time.Second) + } + } +} +``` + +**如何控制一个函数的最大并发数?** + +通过 buffered channel 实现,每个需要执行该函数的 goroutine 向 buffered channel 发送一个数据,然后执行完成后从 channel 中取出一个数据 + +```golang +var concurrent = 3 + +var limit = make(chan uint8, concurrent) + +// 控制并发包装器 exported +func Controller(args ...interface{}) { + limit <- 1 + function(args) + <-limit +} + +// 需要被控制并发数量的函数,只有本包内可访问 +func function(args ...interface{}) { + +} +``` + +**如何实现定时操作?** + +我们可以通过 `time.After` 自己实现一个,或使用现成的 `time.AfterFunc` + +```golang +func main() { + time.AfterFunc(2 * time.Second, func() { + fmt.Println("After func") + }) + fmt.Println("haha") + time.Sleep(100 * time.Second) +} +``` + +`time.AfterFunc` 并不会阻塞当前 goroutine,而是使用了新的 goroutine + +**如何保证一个函数只被执行一次?** + +同样可以使用 channel buffer,只不过 buffer 大小为 1,执行前先向该 channel 发送数据 + +```golang +const limit = 1 +var c = make(chan int, limit) + +func main() { + for i := 0; i < 100; i++ { + go Controller() + } + time.Sleep(2 * time.Second) +} + +// exported +func Controller() { + select { + case c <- 1: + once() + default: + fmt.Println("once 已经被执行过") + } +} + +// inner +func once() { + fmt.Println("once") +} +``` + +同样的,需要修改执行的次数,只需要修改 limit + +或者使用锁机制,控制控制只有一个变量进入,而且采用一个标识符记录该函数是否已经执行过,相应的实现在 `sync.Once`。`sync.Once` 提供一个安全机制,确保通过 `sync.Once.Do(f func())` 调用的 f() 最多只被执行一次。 + +以下是其内部代码,实现非常简单: + +```golang +type Once struct { + m Mutex + done uint32 +} + +func (o *Once) Do(f func()) { + if atomic.LoadUint32(&o.done) == 1 { + return + } + // Slow-path. + o.m.Lock() + defer o.m.Unlock() + if o.done == 0 { + defer atomic.StoreUint32(&o.done, 1) + f() + } +} +``` + +# select{} 与 for{} 区别 + +`for{}` 相信大家都不陌生,但 `select{}` 很让人疑惑。 + ++ `for{}` 会把 cpu 使用率到 100% ++ `select{}` 会阻塞当前 goroutine,cpu 使用率将近 0% + + +# 参考: + +[Golang 内存模型](https://tiancaiamao.gitbooks.io/go-internals/content/zh/01.3.html) + [The Go Memory Model](https://golang.org/ref/mem) \ No newline at end of file diff --git a/src/md/2018-02-17-golang-variable-memory-order.md b/src/md/2018-02-17-golang-variable-memory-order.md index d72d679..a666184 100644 --- a/src/md/2018-02-17-golang-variable-memory-order.md +++ b/src/md/2018-02-17-golang-variable-memory-order.md @@ -1,237 +1,237 @@ ---- -layout: post -title: "Golang 变量内存模型" -date: 2018-02-17 12:00:05 +0800 -categories: golang ---- - -# Golang 变量在内存的形式 - -int uint 在不同系统不同编译器有不同表现,`gc` `gccgo` 的实现是在 64 位系统下,int uint 为 64 位,而 32 位系统为 32 位。 - -类似的,指针长度在 64 位系统为 8 字节,32 位系统为 4 字节。 - -数组、结构体中数据在内存中的紧密相连的。 - -# 字符串 - -```golang -type stringStruct struct { - str unsafe.Pointer - len int -} -``` - -字符串使用 16 字节长的数据结构表示,包含一个指向字符串存储数据的指针和一个长度数据。采用字符串切片生成新的字符串的时候不会涉及到内存的分配和复制操作,因为多个字符串重用了底层的存储数据,因为字符串是不可变的(改变字符串会生成新的字符串),不会有内存共享问题。 - -Go 使用 utf-8 编码字符串,(utf-8编码作者是 Go 作者之一),Go 的字符串每一个字符是 rune,rune 是 uint32 的别名,unicode 字符的长度可能是1,2,3,4个字节。如果统计字数算的是 rune。 - -```golang -s := "刘曦光" -len(s) // 9,字节数 -len([]rune(s)) // 3,rune 数 -``` - -使用下标访问字符串,得到的不是第 n 个字符,而是底层存储的第 n 个 byte。 - -```golang -s := "刘曦光" -s[0] // 229 -``` - -一个一直没弄清楚的问题:**Unicode UTF-8 string 之间的关系** - -上古时代的程序员可能会出现字符集、编码方式等问题,但是现在我们开发中编码方式等问题一般都有编辑器或者IDE提供好了完美的支持。 - -通过阅读以下两篇文章我弄清楚了二者的关系: - -+ [字符编码笔记:ASCII,Unicode 和 UTF-8](http://www.ruanyifeng.com/blog/2007/10/ascii_unicode_and_utf-8.html) -+ [The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Excuses!)](https://www.joelonsoftware.com/2003/10/08/the-absolute-minimum-every-software-developer-absolutely-positively-must-know-about-unicode-and-character-sets-no-excuses/) - -简而言之: - -+ Unicode 是 charset 字符集,*for see* 对应的是码位/码点/code point -+ UTF-8 是 encoding 编码方式,*for store* 存储在存储设备上,内存外存 - -Unicode 字符集可以表示所有的字符,但是其有不同的实现方式,比如 UTF-18,UTF-16 等等。没错 UTF-8 只是 Unicode 的一种实现方式。UTF-8 采用特殊的编码方式,使用频率高的字符对应的存储字节数越短。 - -*错误例子* -之前学习 Java 的过程中阅读过某些**错误**的资料说 Java 使用 Unicode 编码,每一个字符采用两个字节存储,可以表达所有的字符。 - -事实上,两个字节 16 位一共可表达的字符数量为 `2 ** 32 = 65536`,根本不足以表达所有字符,且 Unicode 只是字符集而不是编码方式。Unicode 编码数量没有实际上限,事实上他们拥有远超 65536 个,所以不是所有 Unicode 编码都能够被压缩到 2 字节。 - -即使 Times New Roman 等使用了不同样式显示 A,但是 A 还是同一个字符。只是使用了不同字体样式显示,存储还是使用了相同的编码方式。 - -在不同的系统中可能会有大端存储 big-endian 或者小端存储 small-endian,更多关于该方面可以阅读阮一峰的一篇文章:[理解字节序](http://www.ruanyifeng.com/blog/2016/11/byte-order.html) - -In Go a string is in effect a **read-only slice of bytes**. - -```golang -unsafe.Sizeof(variable) -``` - -十六进制代表字符串 - -```golang -s := "\x68\x65\x6c\x6c\x6f" // "hello" -``` - -数字中使用 0xFF 代表十六进制,0111 代表八进制 - -```golang -fmt.Printf("%q", string(100)) -``` - -输出的是 "d",而不是 "100" - -Go 源码 source code 只允许使用 UTF-8 编码 - -使用 raw string 里面不对 `\n` `\xff` 等进行转义 - -```golang -s := `hello - world` -``` - -Go 中的 string 只能够包含 UTF-8 编码吗? - -不是的,string 还能够通过 "\xff" 等形式控制每一个 byte。 - -**for range** 遍历字符串中的每一个字符,而不是字节 - -```golang -s := "hello world" -for _, v := range s { - // v 的类型是 int32,也就是 rune - fmt.Println("%v ", v) -} -fmt.Println() -for _, v := range s { - fmt.Printf("%v ", string(v)) -} -``` - -output: - -``` -104 101 108 108 111 32 119 111 114 108 100 -h e l l o w o r l d -``` - -官方库 **unicode/utf8** 中有很多 UTF-8 方面的支持 - -字符串引用同一个源字符串的坏处:对于一个很大的源字符串,即使只有一小部分还被引用,源字符串就无法被回收。 - -字符串引用同一个源字符串的好处:字符串的切割、复制操作非常昂贵,需要分为分配-复制两步。 - -[Golang 官方对 strings 说明](https://blog.golang.org/strings) - -# slice - -```golang -type slice struct { - array unsafe.Pointer - len int - cap int -} -``` - -数组、slice 并不会真正复制一份数据,而是复用了底层的数组存储 - -即使是 slice 的赋值,底层的数组都是使用同一个,其中一个的变化会引发另外一个的同步变化 - -```golang -func main() { - x := []int{1, 2, 3, 4, 5} - y := x - y[0] = 10 - fmt.Println("x:", x) - fmt.Println("y:", y) -} -``` - -output: - -``` -x: [10 2 3 4 5] -y: [10 2 3 4 5] -``` - -## 扩容 - -在对 slice 进行 append 等操作可能会触发 slice 的扩容 - -扩容规则: - -+ 如果当前 cap < 1024,按每次 2 倍增长,否则每次按当前 cap 的 1/4 增长 - -## 创建 slice - -可以通过 **new** 或 **make** 创建 slice,new 返回的是一个已经清零的指针,而 make 返回的是一个复杂的结构。 - -创建 slice 最好使用 make 创建 - -更多请参考:[深入解析 Go 中 Slice 底层实现](https://halfrost.com/go_slice/) - -# map - -```golang -type hmap struct { - count int // # live cells == size of map. Must be first (used by len() builtin) - flags uint8 - B uint8 // log_2 of # of buckets (can hold up to loadFactor * 2^B items) - noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details - hash0 uint32 // hash seed - - buckets unsafe.Pointer // array of 2^B Buckets. may be nil if count==0. - oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing - nevacuate uintptr // progress counter for evacuation (buckets less than this have been evacuated) - - extra *mapextra // optional fields -} -``` - -```golang -func main() { - m1 := make(map[int]int) - m2 := m1 - for i := 0; i < 1000; i++ { - m1[i] = i - } - for k, _ := range m1 { - _, ok := m2[k] - fmt.Println(ok) - } -} -``` - -总是输出 **true**。 - -map 进行的复制并不会重新分配空间,而是复用了底层的存储存储结构,即使是 m1 插入了很多数据,已经触发了扩展,buckets 的重新分配,m1 和 m2 还是会同步变化的。 - -map 使用链表解决哈希冲突问题,而不是开放地址,因为开放地址法在真实扩容的时候性能下降得很快。链表的位置不需要重新计算哈希值,因为扩容是成倍增长。 - -map 的扩容采用了两个 bucket 的方法,不是一次性完成扩容操作,而不一次次地把 oldbuckets 中的元素移到 buckets 中,虽然这样不能够消除总扩展时间,但是扩展时间分摊到每一次插入,这样防止程序发生长时间的阻塞。 - -更多请参考: -+ [如何设计并实现一个线程安全的 Map ?(上篇)](https://halfrost.com/go_map_chapter_one/) -+ [如何设计并实现一个线程安全的 Map ?(下篇)](https://halfrost.com/go_map_chapter_two/) - -# nil - -空值 zero value: - -| type | value | -| ---- | ------ -| bool | false | -| int | 0 | -| string | "" | -| pointer | nil | -| func | nil | -| interface | nil | -| slice | nil | -| channel | nil | -| map | nil | - -读、写一个 nil 的 channel 会阻塞 - +--- +layout: post +title: "Golang 变量内存模型" +date: 2018-02-17 12:00:05 +0800 +categories: golang +--- + +# Golang 变量在内存的形式 + +int uint 在不同系统不同编译器有不同表现,`gc` `gccgo` 的实现是在 64 位系统下,int uint 为 64 位,而 32 位系统为 32 位。 + +类似的,指针长度在 64 位系统为 8 字节,32 位系统为 4 字节。 + +数组、结构体中数据在内存中的紧密相连的。 + +# 字符串 + +```golang +type stringStruct struct { + str unsafe.Pointer + len int +} +``` + +字符串使用 16 字节长的数据结构表示,包含一个指向字符串存储数据的指针和一个长度数据。采用字符串切片生成新的字符串的时候不会涉及到内存的分配和复制操作,因为多个字符串重用了底层的存储数据,因为字符串是不可变的(改变字符串会生成新的字符串),不会有内存共享问题。 + +Go 使用 utf-8 编码字符串,(utf-8编码作者是 Go 作者之一),Go 的字符串每一个字符是 rune,rune 是 uint32 的别名,unicode 字符的长度可能是1,2,3,4个字节。如果统计字数算的是 rune。 + +```golang +s := "刘曦光" +len(s) // 9,字节数 +len([]rune(s)) // 3,rune 数 +``` + +使用下标访问字符串,得到的不是第 n 个字符,而是底层存储的第 n 个 byte。 + +```golang +s := "刘曦光" +s[0] // 229 +``` + +一个一直没弄清楚的问题:**Unicode UTF-8 string 之间的关系** + +上古时代的程序员可能会出现字符集、编码方式等问题,但是现在我们开发中编码方式等问题一般都有编辑器或者IDE提供好了完美的支持。 + +通过阅读以下两篇文章我弄清楚了二者的关系: + ++ [字符编码笔记:ASCII,Unicode 和 UTF-8](http://www.ruanyifeng.com/blog/2007/10/ascii_unicode_and_utf-8.html) ++ [The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Excuses!)](https://www.joelonsoftware.com/2003/10/08/the-absolute-minimum-every-software-developer-absolutely-positively-must-know-about-unicode-and-character-sets-no-excuses/) + +简而言之: + ++ Unicode 是 charset 字符集,*for see* 对应的是码位/码点/code point ++ UTF-8 是 encoding 编码方式,*for store* 存储在存储设备上,内存外存 + +Unicode 字符集可以表示所有的字符,但是其有不同的实现方式,比如 UTF-18,UTF-16 等等。没错 UTF-8 只是 Unicode 的一种实现方式。UTF-8 采用特殊的编码方式,使用频率高的字符对应的存储字节数越短。 + +*错误例子* +之前学习 Java 的过程中阅读过某些**错误**的资料说 Java 使用 Unicode 编码,每一个字符采用两个字节存储,可以表达所有的字符。 + +事实上,两个字节 16 位一共可表达的字符数量为 `2 ** 32 = 65536`,根本不足以表达所有字符,且 Unicode 只是字符集而不是编码方式。Unicode 编码数量没有实际上限,事实上他们拥有远超 65536 个,所以不是所有 Unicode 编码都能够被压缩到 2 字节。 + +即使 Times New Roman 等使用了不同样式显示 A,但是 A 还是同一个字符。只是使用了不同字体样式显示,存储还是使用了相同的编码方式。 + +在不同的系统中可能会有大端存储 big-endian 或者小端存储 small-endian,更多关于该方面可以阅读阮一峰的一篇文章:[理解字节序](http://www.ruanyifeng.com/blog/2016/11/byte-order.html) + +In Go a string is in effect a **read-only slice of bytes**. + +```golang +unsafe.Sizeof(variable) +``` + +十六进制代表字符串 + +```golang +s := "\x68\x65\x6c\x6c\x6f" // "hello" +``` + +数字中使用 0xFF 代表十六进制,0111 代表八进制 + +```golang +fmt.Printf("%q", string(100)) +``` + +输出的是 "d",而不是 "100" + +Go 源码 source code 只允许使用 UTF-8 编码 + +使用 raw string 里面不对 `\n` `\xff` 等进行转义 + +```golang +s := `hello + world` +``` + +Go 中的 string 只能够包含 UTF-8 编码吗? + +不是的,string 还能够通过 "\xff" 等形式控制每一个 byte。 + +**for range** 遍历字符串中的每一个字符,而不是字节 + +```golang +s := "hello world" +for _, v := range s { + // v 的类型是 int32,也就是 rune + fmt.Println("%v ", v) +} +fmt.Println() +for _, v := range s { + fmt.Printf("%v ", string(v)) +} +``` + +output: + +``` +104 101 108 108 111 32 119 111 114 108 100 +h e l l o w o r l d +``` + +官方库 **unicode/utf8** 中有很多 UTF-8 方面的支持 + +字符串引用同一个源字符串的坏处:对于一个很大的源字符串,即使只有一小部分还被引用,源字符串就无法被回收。 + +字符串引用同一个源字符串的好处:字符串的切割、复制操作非常昂贵,需要分为分配-复制两步。 + +[Golang 官方对 strings 说明](https://blog.golang.org/strings) + +# slice + +```golang +type slice struct { + array unsafe.Pointer + len int + cap int +} +``` + +数组、slice 并不会真正复制一份数据,而是复用了底层的数组存储 + +即使是 slice 的赋值,底层的数组都是使用同一个,其中一个的变化会引发另外一个的同步变化 + +```golang +func main() { + x := []int{1, 2, 3, 4, 5} + y := x + y[0] = 10 + fmt.Println("x:", x) + fmt.Println("y:", y) +} +``` + +output: + +``` +x: [10 2 3 4 5] +y: [10 2 3 4 5] +``` + +## 扩容 + +在对 slice 进行 append 等操作可能会触发 slice 的扩容 + +扩容规则: + ++ 如果当前 cap < 1024,按每次 2 倍增长,否则每次按当前 cap 的 1/4 增长 + +## 创建 slice + +可以通过 **new** 或 **make** 创建 slice,new 返回的是一个已经清零的指针,而 make 返回的是一个复杂的结构。 + +创建 slice 最好使用 make 创建 + +更多请参考:[深入解析 Go 中 Slice 底层实现](https://halfrost.com/go_slice/) + +# map + +```golang +type hmap struct { + count int // # live cells == size of map. Must be first (used by len() builtin) + flags uint8 + B uint8 // log_2 of # of buckets (can hold up to loadFactor * 2^B items) + noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details + hash0 uint32 // hash seed + + buckets unsafe.Pointer // array of 2^B Buckets. may be nil if count==0. + oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing + nevacuate uintptr // progress counter for evacuation (buckets less than this have been evacuated) + + extra *mapextra // optional fields +} +``` + +```golang +func main() { + m1 := make(map[int]int) + m2 := m1 + for i := 0; i < 1000; i++ { + m1[i] = i + } + for k, _ := range m1 { + _, ok := m2[k] + fmt.Println(ok) + } +} +``` + +总是输出 **true**。 + +map 进行的复制并不会重新分配空间,而是复用了底层的存储存储结构,即使是 m1 插入了很多数据,已经触发了扩展,buckets 的重新分配,m1 和 m2 还是会同步变化的。 + +map 使用链表解决哈希冲突问题,而不是开放地址,因为开放地址法在真实扩容的时候性能下降得很快。链表的位置不需要重新计算哈希值,因为扩容是成倍增长。 + +map 的扩容采用了两个 bucket 的方法,不是一次性完成扩容操作,而不一次次地把 oldbuckets 中的元素移到 buckets 中,虽然这样不能够消除总扩展时间,但是扩展时间分摊到每一次插入,这样防止程序发生长时间的阻塞。 + +更多请参考: ++ [如何设计并实现一个线程安全的 Map ?(上篇)](https://halfrost.com/go_map_chapter_one/) ++ [如何设计并实现一个线程安全的 Map ?(下篇)](https://halfrost.com/go_map_chapter_two/) + +# nil + +空值 zero value: + +| type | value | +| ---- | ------ +| bool | false | +| int | 0 | +| string | "" | +| pointer | nil | +| func | nil | +| interface | nil | +| slice | nil | +| channel | nil | +| map | nil | + +读、写一个 nil 的 channel 会阻塞 + diff --git a/src/md/2018-02-26-golang-slice-operation.md b/src/md/2018-02-26-golang-slice-operation.md index 3b6f496..e94afb0 100644 --- a/src/md/2018-02-26-golang-slice-operation.md +++ b/src/md/2018-02-26-golang-slice-operation.md @@ -1,248 +1,248 @@ ---- -layout: post -title: "Golang slice操作" -date: 2018-02-26 12:00:05 +0800 -categories: [golang, slice] ---- - -## 创建 - -```golang -// 1.只声明不赋值 -var s []T - -//2. 创建 nil slice -s := []T(nil) // 也就是将 nil 转化为 slice,slice 和 nil 是可以做 == or != 比较的 - -//3.直接创建类型为 T 的 slice -s := []int{1, 2, 3} // [1 2 3] -``` - -**可以向 nil slice 进行 append 操作** - -```go -var s []string -s = append(s, "hello", "world") // 会触发 slice 的扩展 -``` - -## 判断 slice 是否已经 make - -```golang -var s []string -fmt.Println(s == nil) // true -s = make([]string, 0) -fmt.Println(s == nil) // false -``` - -## append - -通过内置函数 `append` 向 slice 中追加元素 - -函数声明: - -```golang -func append(slice []Type, elems ...Type) []Type -``` - -该函数可以接收多个参数,并且把参数依次添加到 slice。 - -在向 slice 中不断添加元素肯定会触发 slice 底层数组的扩容,那么 slice 的扩容 - -## copy - -`copy` 将 src 中的内容复制到 dst 的头部中,复制元素长度规则: - -```go -if src == nil || dst == nil { - // do nothing - return -} - -if len(src) > len(dst) { - // copy src[0:len(dst)] to dst -} else { - // copy src to dst[:len(src)] -} -``` - -## cut - -将 slice 的某部分删除 - -```go -s := make([]string, 10) -for i := 0; i < 10; i++ { - s[i] = string(i + '0') -} -// 此时 slice 中的内容是 [0 1 2 3 4 5 6 7 8 9] -// 剪去下标 [5,7) -s = append(s[0:5], s[7:]...) -``` - -## delete - -Go 中有提供 map 的 delete 函数,但是却没有提供对于 slice 中删除某个函数的操作,这样如果需要删除函数,所以我们需要自定义一个删除函数。 - -想想:slice 的 delete 和 cut 供能是不是很相像,delete 是删除一个元素,而 cut 是删除某个范围。 - -```go -s := make([]string, 10) -for i := 0; i < 10; i++ { - s[i] = string(i + '0') -} -// 此时 slice 中的内容是 [0 1 2 3 4 5 6 7 8 9] -// 删除下标为 5 的元素 -s = append(s[0:5], s[6:]...) -``` - -## pop - -Python 的 list 提供了 `pop()` 方法移除末尾元素,那么 Go 中应该如何实现呢? - -```go -s := make([]string, 10) -for i := 0; i < 10; i++ { - s[i] = string(i + '0') -} -// 此时 slice 中的内容是 [0 1 2 3 4 5 6 7 8 9] -s = s[0:len(s) - 1] // 如果需要 pop 多个元素可以是当扩大 1 的值 -``` - -### 问题 - -如果 slice 中的内容是指针或者带有指针项的结构体,slice 中的底层数组始终引用着堆中的值,这样导致了结果就是需要被垃圾回收的数据没有被及时回收,造成内存空间资源的浪费。 - -值得一提的是 Go 会对在编译时变量做逃逸分析,如果该变量有可能逃逸则在堆中分配内存空间,否则在当前 goroutine 的栈中分配内存空间。 - -**应该在 for-loop 中声明变量吗?** - -Go 处理机制是会做编译器优化复用一个内存地址还是在内存中创建多个变量呢? - -```go -func main() { - for i := 0; i < 3; i ++ { - var a int - a = i - fmt.Println(&a) - } -} -``` - -``` -0xc4200160e8 -0xc420016120 -0xc420016128 -``` - -显然通过打印变量地址知道:每次都创建不同的变量。所以写代码的时候应该注意避免在 for-loop 中创建变量 - -所以在 cut 和 delete 操作中应该把指针重定向到 nil,防止内存泄露。 - -## cut - -```go -s := make([]*int, 10) -for i := 0; i < 10; i++ { - var a int - s[i] = &a -} -// cut [5,7) -i, j := 5, 7 -copy(s[i:], s[j:]) // 将后面元素往前移动 -// 消除指针引用 -for k := len(s) - 1; k >= len(s) - j + i; k-- { - s[k] = nil -} -s = s[:len(s) - j + i] -``` - -delete 类似 - -## expand - -在特定位置扩容 slice,在下标 i 后插入 j 个元素 - -```go -s = append(a[:i], append(make([]T, j), a[i:]...)...) -``` - -但是根据 slice 的操作,该操作的内存利用率并不是很高,或者可以通过判断 `cap(s) > len(s) + j`,可以直接将 `a[i+1:]` 元素往后移动 j 位 - -## extend - -扩展 slice 的容量,向末尾添加 j 个空位,扩展容量 - -```go -s = append(s, make([]T, j)...) -``` - -## insert - -向特定下标插入特定值,比如向下标为 i 的位置插入元素 x - -```go -s = append(s[:i], append([]T{x}, s[i:]...)...) -``` - -### 问题 - -上述对 slice 的 expand extend insert 等操作会创建一个新的 slice 然后再将新 slice 中的元素复制到原来的 slice 特定位置中,然后需要用额外的 GC 去回收新创建的临时 slice。这样的操作对 cpu 和内存来说都是低效的实现方法。 - -## 更高效的 insert - -```go -s = append(s, zero_value) -copy(s[i+1:], s[i:]) -s[i] = x -``` - -## 插入 vector - -```go -a = append(a[:i], append(slice, a[i:]...)...) -``` - -## 向 slice 头部添加新元素 - -```go -s = append([]T{x}, s...) -``` - -## 倒转 slice - -```go -func reverse() { - s := make([]int, 10) - for i := 0; i < 10; i++ { - s[i] = i - } - fmt.Println(s) - for i := 0; i < len(s) / 2; i++ { - minor := len(s) - i - 1 - s[i], s[minor] = s[minor], s[i] - } - fmt.Println(s) -} -``` - -## 洗牌 - -```go -func shuffling() { - s := make([]int, 10) - for i := 0; i < 10; i++ { - s[i] = i - } - fmt.Println(s) - // 通过随机交换两个元素达到洗牌目的 - for i := len(s) - 1; i > 0; i-- { - j := rand.Intn(len(s)) - s[j], s[i] = s[i], s[j] - } - fmt.Println(s) -} -``` - -### 部分翻译自 - -[slice Trick](https://github.com/golang/go/wiki/SliceTricks#delete) +--- +layout: post +title: "Golang slice操作" +date: 2018-02-26 12:00:05 +0800 +categories: [golang, slice] +--- + +## 创建 + +```golang +// 1.只声明不赋值 +var s []T + +//2. 创建 nil slice +s := []T(nil) // 也就是将 nil 转化为 slice,slice 和 nil 是可以做 == or != 比较的 + +//3.直接创建类型为 T 的 slice +s := []int{1, 2, 3} // [1 2 3] +``` + +**可以向 nil slice 进行 append 操作** + +```go +var s []string +s = append(s, "hello", "world") // 会触发 slice 的扩展 +``` + +## 判断 slice 是否已经 make + +```golang +var s []string +fmt.Println(s == nil) // true +s = make([]string, 0) +fmt.Println(s == nil) // false +``` + +## append + +通过内置函数 `append` 向 slice 中追加元素 + +函数声明: + +```golang +func append(slice []Type, elems ...Type) []Type +``` + +该函数可以接收多个参数,并且把参数依次添加到 slice。 + +在向 slice 中不断添加元素肯定会触发 slice 底层数组的扩容,那么 slice 的扩容 + +## copy + +`copy` 将 src 中的内容复制到 dst 的头部中,复制元素长度规则: + +```go +if src == nil || dst == nil { + // do nothing + return +} + +if len(src) > len(dst) { + // copy src[0:len(dst)] to dst +} else { + // copy src to dst[:len(src)] +} +``` + +## cut + +将 slice 的某部分删除 + +```go +s := make([]string, 10) +for i := 0; i < 10; i++ { + s[i] = string(i + '0') +} +// 此时 slice 中的内容是 [0 1 2 3 4 5 6 7 8 9] +// 剪去下标 [5,7) +s = append(s[0:5], s[7:]...) +``` + +## delete + +Go 中有提供 map 的 delete 函数,但是却没有提供对于 slice 中删除某个函数的操作,这样如果需要删除函数,所以我们需要自定义一个删除函数。 + +想想:slice 的 delete 和 cut 供能是不是很相像,delete 是删除一个元素,而 cut 是删除某个范围。 + +```go +s := make([]string, 10) +for i := 0; i < 10; i++ { + s[i] = string(i + '0') +} +// 此时 slice 中的内容是 [0 1 2 3 4 5 6 7 8 9] +// 删除下标为 5 的元素 +s = append(s[0:5], s[6:]...) +``` + +## pop + +Python 的 list 提供了 `pop()` 方法移除末尾元素,那么 Go 中应该如何实现呢? + +```go +s := make([]string, 10) +for i := 0; i < 10; i++ { + s[i] = string(i + '0') +} +// 此时 slice 中的内容是 [0 1 2 3 4 5 6 7 8 9] +s = s[0:len(s) - 1] // 如果需要 pop 多个元素可以是当扩大 1 的值 +``` + +### 问题 + +如果 slice 中的内容是指针或者带有指针项的结构体,slice 中的底层数组始终引用着堆中的值,这样导致了结果就是需要被垃圾回收的数据没有被及时回收,造成内存空间资源的浪费。 + +值得一提的是 Go 会对在编译时变量做逃逸分析,如果该变量有可能逃逸则在堆中分配内存空间,否则在当前 goroutine 的栈中分配内存空间。 + +**应该在 for-loop 中声明变量吗?** + +Go 处理机制是会做编译器优化复用一个内存地址还是在内存中创建多个变量呢? + +```go +func main() { + for i := 0; i < 3; i ++ { + var a int + a = i + fmt.Println(&a) + } +} +``` + +``` +0xc4200160e8 +0xc420016120 +0xc420016128 +``` + +显然通过打印变量地址知道:每次都创建不同的变量。所以写代码的时候应该注意避免在 for-loop 中创建变量 + +所以在 cut 和 delete 操作中应该把指针重定向到 nil,防止内存泄露。 + +## cut + +```go +s := make([]*int, 10) +for i := 0; i < 10; i++ { + var a int + s[i] = &a +} +// cut [5,7) +i, j := 5, 7 +copy(s[i:], s[j:]) // 将后面元素往前移动 +// 消除指针引用 +for k := len(s) - 1; k >= len(s) - j + i; k-- { + s[k] = nil +} +s = s[:len(s) - j + i] +``` + +delete 类似 + +## expand + +在特定位置扩容 slice,在下标 i 后插入 j 个元素 + +```go +s = append(a[:i], append(make([]T, j), a[i:]...)...) +``` + +但是根据 slice 的操作,该操作的内存利用率并不是很高,或者可以通过判断 `cap(s) > len(s) + j`,可以直接将 `a[i+1:]` 元素往后移动 j 位 + +## extend + +扩展 slice 的容量,向末尾添加 j 个空位,扩展容量 + +```go +s = append(s, make([]T, j)...) +``` + +## insert + +向特定下标插入特定值,比如向下标为 i 的位置插入元素 x + +```go +s = append(s[:i], append([]T{x}, s[i:]...)...) +``` + +### 问题 + +上述对 slice 的 expand extend insert 等操作会创建一个新的 slice 然后再将新 slice 中的元素复制到原来的 slice 特定位置中,然后需要用额外的 GC 去回收新创建的临时 slice。这样的操作对 cpu 和内存来说都是低效的实现方法。 + +## 更高效的 insert + +```go +s = append(s, zero_value) +copy(s[i+1:], s[i:]) +s[i] = x +``` + +## 插入 vector + +```go +a = append(a[:i], append(slice, a[i:]...)...) +``` + +## 向 slice 头部添加新元素 + +```go +s = append([]T{x}, s...) +``` + +## 倒转 slice + +```go +func reverse() { + s := make([]int, 10) + for i := 0; i < 10; i++ { + s[i] = i + } + fmt.Println(s) + for i := 0; i < len(s) / 2; i++ { + minor := len(s) - i - 1 + s[i], s[minor] = s[minor], s[i] + } + fmt.Println(s) +} +``` + +## 洗牌 + +```go +func shuffling() { + s := make([]int, 10) + for i := 0; i < 10; i++ { + s[i] = i + } + fmt.Println(s) + // 通过随机交换两个元素达到洗牌目的 + for i := len(s) - 1; i > 0; i-- { + j := rand.Intn(len(s)) + s[j], s[i] = s[i], s[j] + } + fmt.Println(s) +} +``` + +### 部分翻译自 + +[slice Trick](https://github.com/golang/go/wiki/SliceTricks#delete) diff --git a/src/md/2018-02-27-the-go-programing-languange-note1.md b/src/md/2018-02-27-the-go-programing-languange-note1.md index a4ef5cc..b05df53 100644 --- a/src/md/2018-02-27-the-go-programing-languange-note1.md +++ b/src/md/2018-02-27-the-go-programing-languange-note1.md @@ -1,509 +1,509 @@ ---- -layout: post -title: "《Go程序设计语言》笔记一" -date: 2018-03-2 12:00:05 +0800 -categories: golang ---- - -# 标准输入输出 - -编程语言中的标准流如,**stdin** / **stdout** / **stderr** 是指向操作系统中的文件,以 Linux 为例,以下是 `os.Stdin` `os.Stdout` `os.Stderr` 的定义: - -```go -var ( - Stdin = NewFile(uintptr(syscall.Stdin), "/dev/stdin") - Stdout = NewFile(uintptr(syscall.Stdout), "/dev/stdout") - Stderr = NewFile(uintptr(syscall.Stderr), "/dev/stderr") -) -``` - -从控制台输入 `os.Stdin`, - -```go -func readFromStdin() { - reader := bufio.NewReader(os.Stdin) - fmt.Println("enter text:") - text, _ := reader.ReadString('\n') - fmt.Println(text) -} -``` - -fmt 包中提供了强大的格式化功能,可以使用 `fmt.Sprintf` 方法格式化字符串,该方法类似 `fmt.Printf` 区别是前者是将格式化的字符串作为返回结果,后者直接将格式化后的字符串输出到控制台。如果需要将格式化的字符串输出到流(实现了 `io.Writer`接口)中,可以调用 `fmt.Fprintf`。 - -# 获取运行命令的参数 - -运行命令的参数保存在 `os.Args` []string 中 - -```go -func main(){ - args := os.Args - fmt.Println(args) -} -``` - -假如我们使用 `go run main.go hello world` 命令来运行,得到不是 `["go run main.go", "hello", "world"]`,在我本机是:`["/tmp/go-build610217828/command-line-arguments/_obj/exe/readStdin", "hello", "world"]`。我们可以通过 `go run -x main.go hello world` 查看命令执行的过程。 - -# const - -Go 中常量只能是 字符串、数值、布尔值,复杂类型如slice、map、数组、结构体、指针、接口、函数等都无法声明为常量。 - -# var - -只有 chan、slice、map、函数、指针、接口变量 可以与 nil 做 `==` `!=` 比较,数组和结构体在声明时如果没有赋值,那么就会分配存储空间,然后将内容写为对应的默认值、 - -包级别的变量、常量在 **init** 函数开始前进行初始化,而 **init** 函数的执行在 **main** 函数之前。 - -只有变量才能够使用 `&` 操作符,也就是没有指针能够指向常量。 - -当两个指针变量指向同一个变量或者是两个指针都是零值 nil 时候,使用 `==` 比较得到 **true** - -可以使用 **flag** 包来开发命令行工具,flag 为我们解析参数提供了很大的帮助 - -任何一个包、任何一个 .go 文件可以包含任意多个 `func init(){...}` 函数,在初始化包的时候将会按照 init 函数声明的顺序来执行 init 函数。 - -包初始化顺序: -1. 按照 **import** 顺序完成引入的包的初始化 -2. 根据编译器导入 .go 文件的顺序进行包级别变量的初始化 -3. 按照 **init** 函数的声明顺序,执行所有 init 函数 - -pointer / slice / map / function / channel 都是引用类型,共同特点是全部都间接地指向程序变量或者状态,于是操作所引用数据的效果就会遍历该数据的全部引用。 - -# 类型 - -在不同编程语言中,去模 mod 运算 **%** 有不同表现,在 Go 中,取模余数的正负号总是与*被除数*一致,`-5 % 3 == -2` `-5 % -3 == -2`。题外话,Python 中去模余数总是和*除数*的正负号一致,`-5 % 3 == 1` `-5 % -3 == -2` - -无论是有符号数还是无符号数,若表示的运算结果所需要的位超过了该类型的范围,就会产生**溢出**。 - -对于 uint8 - -```go -var u uint = 255 -fmt.Println(u, u+1, u*u) // 255 0 1 -``` - -其中 `255 * 255` 结果的二进制形式为 `1111111000000001`,所以采取截断,最后结果为 1; -`u+1` 的结果的类型依然是 `uint8` - -类似地,对于 int8 - -```go -var i int8 = 127 -fmt.Println(u, u+1, u*u) // 127 -128 1 -``` - -## ^ &^ - -相对与 C 语言的不同,在 Go 中 `^` 运算符既可以作为一元运算符,也可以作为二元运算符: - -```go -var u uint8 = 255 -fmt.Println(^u) // 0 -fmt.Println(u^1) //254 -``` - -+ `^` 作为一元运算符时,是按位取反,相当于 C 中的 `~` -+ `^` 作为二元运算符时,是按位异或 - -Go 中还有一个 C 中没有的运算符 `&^` 按位清空。 - -```go -var u uint8 = 11 -fmt.Println(u&^1) // 254 -``` - -`x&^y` 的位运算中,将 y 中对应位为 1 的位置,将 x 中对应位置置为 0,否则保持 x 中对应位置不变,对于 uint8 `11 &^ 3 == 8` - -## fmt.Printf - -```go -fmt.Printf("%d %[1]o %[1]x %[1]b\n", 100) -``` - -其中 `[1]` 表示使用第一个参数 - -## 字符串 - -```go -s := "hello, 世界" -b := []byte(s) -s = string(b) -``` - -上述字符串转化为 []byte,[]byte 转化为 string 时候都会发生重新分配内存空间,然后再进行内存的复制。因为 string 底层是不可以改变的,如果底层数组进行复用,则会造成改变 []byte 的值会间接改变 string 的内容,进行很多这样的操作会使得程序执行非常低效。但是**某些编译器**能够识别到如果后续 []byte 不会再改变的话,string 会复用 []byte 底层的数组。 - -若要进行很多关于 string 的操作,尽可能使用 `bytes.Buffer`,其底层使用 []byte 进行数据的存储,动态扩展,效率相对高。 - -## 标准输入 - -类似于 C,Go 提供 `fmt.Scanf` 函数帮助我们从命令行中输入信息 - -## 常量 - -```go -const ( - a = 1 - b - c = 2 - d -) -// a == b == 1 -// c == d == 2 -``` - -常量只能指向基本类型或者是经过 `type Time int` 等经过重命名的基本类型,但是许多常量并不从属某一具体类型。编译器将这些从属类型待定的常量表示为某些值,可以认为它们的精度至少达到 256 位。 - -## 数组 - -声明数组时,使用 `...` 表示让编译器计算数组长度,不需要我们显式声明,如: - -```go -r := [...]int{1, 2, 3, 4} -s := []int{1, 2, 3, 4} -fmt.Printf("%T\n", r) // [4]int 数组 -fmt.Printf("%T\n", s) // []int slice -``` - -同类型的数组是可以通过 `==` 或 `!=` 进行比较的,这里需要注意的是 `[3]int [4]int` 不是同类型。 - -在函数的参数传递中,数组发生的是值传递,也就是每一次传递都在内存分配一样大小的空间,再进行数组内容的复制,在函数内改变数组元素不会影响到原数组。如果传递大数组将会十分低效。所以需要进行函数传递的建议使用 slice,slice 是一个结构体,不会发生底层数组的复制,只是将复制指针值;或者使用数组的指针进行数组的传递。 - -Go 内置函数很多都是直接对 slice 进行传递和操作,感觉 Go 还是建议人们尽可能使用 slice 而不是数组。 - -## slice - -声明数组或 slice 的时候,可以指定特定位置元素的值,如果之前的值没有显示声明,就会被声明为默认值。 - -```go -a := [...]int{99:1} // 声明一个 [100]int 数组,0~98 为 int 默认值 0,99 位元素为 1 -s := []int{99:1} // 声明一个 []int slice,0~98 为 int 默认值 0,99 位元素为 1 -``` - -```go -days := []string{1:"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"} -p := days[1:3] -t := p[0:5] -fmt.Println(p, len(p), cap(p)) -fmt.Println(t, len(t), cap(t)) -``` - -注意:`len(p) == 2` 为什么可以取 `t := p[0:5]` 长度为 5 的 slice 呢? - -因为 `cap(p) == 7`,p 底层数组复用了 days,也就是说只要不超越 **cap** 限制,就可以取任意长度的切片。 - -slice 取切片都使用指针复用了底层的数组,所以 slice 的取切片操作是一个高效的操作,并不会占用很多资源,问题是 slice 底层复用数组,有可能一个改变会影响其他的运行结果,尤其是在 goroutine 众多的情况下。 - -如果库支持的函数中只支持对应类型的 slice,而不支持数组类型参数怎么办呢? -比如:`func test([]byte)` 函数只支持 []byte,而不支持 [N]byte - -可以通过对数组取切片,得到的 slice 复用了底层的数组存储空间。 -如: - -```go -a := [3]byte{1, 2, 3} -test(a[:]) -``` - -一个等于 nil 的 slice 与`len==cap==0` 的 slice 有的唯一区别就是,与 nil 进行 `==` `!=` 比较时的表现相反。 - -**下面将会变得非常绕** - -[stackoverflow 上一个关于 copy make 等内置函数的讨论](https://stackoverflow.com/questions/18512781/built-in-source-code-location) - -一个例子: - -```go -s := []int{1, 2, 3, 4} -copy(s[1:], s[0:2]) -fmt.Println(s) // [1 1 2 4] -``` - -这里得出的结论是 copy 函数应该是从后往前复制的,如果是从前往后复制的话得到的结果是 `[1 1 1 4]` - -另一个例子: - -```go -s := []int{1, 2, 3, 4} -copy(s[0:], s[1:]) -fmt.Println(s) // [2 3 4 4] -``` - -这里得出的结论是 copy 函数应该是从前往后复制的,如果是从后往前复制的话得到的结果是 `[4 4 4 4]` - -这让我产生疑问,`copy` 函数的复制算法到第是怎么样的呢? - -**copy** 函数的实现 - -```go -func slicecopy(to, fm slice, width uintptr) int { - if fm.len == 0 || to.len == 0 { - return 0 - } - - n := fm.len - if to.len < n { - n = to.len - } - - if width == 0 { - return n - } - // 忽略中间某些关于 race 的细节 - size := uintptr(n) * width - if size == 1 { // common case worth about 2x to do here - // TODO: is this still worth it with new memmove impl? - *(*byte)(to.array) = *(*byte)(fm.array) // known to be a byte pointer - } else { - memmove(to.array, fm.array, size) - } - return n -} -``` - -`memmove` 的实现在 [https://github.com/golang/go/blob/master/src/runtime/memmove_amd64.s](https://github.com/golang/go/blob/master/src/runtime/memmove_amd64.s) 由于全是汇编没有看懂。 - -如果需要实现 slice 的循环移动的话,我们可以通过三次翻手实现: - -比如需要把 `[1 2 3 4 5]` 循环移动为 `[3 4 5 1 2]` - -1. 把 `[1 2]` 旋转为 `[2 1]` -2. 把 `[3 4 5]` 旋转为 `[5 4 3]` -3. 此时 slice 已经变成 `[2 1 5 4 3]`,再进行一次翻手得到 `[3 4 5 1 2]` - -## map - -键 K 必须能够通过操作符 `==` 进行比较,选 K 的时候尽可能不适用浮点型,虽然浮点型可以通过 `==` 进行比较,但是比较存在不精确,通常我们比较字符串是通过一个阀值做到的,比如 `1 - 0.001 <= f <= 1 + 0.001`。 - -`delete` 函数从 map 中删除指定的 K-V,即使 K 不存在于 map 中也不会发生异常,如果从 map 中取出一个 K,如果该 K 不存在 map 中会返回一个类型 K 的零值,所以通常取元素操作会附带一个 comma-ok如:`v, ok := m[k]`。 - -map 元素不是一个变量,无法获取它们的地址,如下操作是不能够通过编译的: - -```go -fmt.Printf("%p", &m["hello"]) -``` - -原因是 map 是有可能动态增长的,当发生动态增长的时候,K-V 所在的地址发生了迁移,通过获取 `&m["hello"]` 没有意义,地址无效。 - -可以通过 `len` 函数获知 map 中 K-V 的数量。 - -map 的零值是 nil,向 nil map 中查找元素、`len(m)`、`delete(m, k)`、for range 循环,都不会发生错误,其行为像对已经初始化但是依然是空的 map 操作一样,但是如果向 nil map 中设置 K-V 将会导致错误。 - -## struct - -```go -package p -type T struct {a, b int} - -package q -import "p" -var a = p.T{a:1, b: 2} // 编译错误 -var b = p.T{1, 2} // 编译错误 -``` - -因为 a b 是以小写开头的,都是不可导出的,在别的包下无法显示 `p.T{a:1, b: 2}` 或隐式 `p.T{1, 2}` 引用。 - -如果一个结构体中所有的成员都是可以比较的,那么这个结构体就是可以比较的;否则该结构体不能够通过 `==` `!=` 进行比较,可以比较的结构体可以作为 map 的 K。 - -```go -// 可比较的结构体 -type Point struct { - X, Y int -} - -// 不可比较的结构体 -type array struct { - S []int -} -``` - -结构体可以组合是 Go 实现面相对象的重要部分。 - -```go -type Point struct { - X, Y int -} - -type Circle struct { - Point - Radius int -} -``` - -这样 Circle 就能够调用 Point 的方法,Circle 中 Point 的名字就是 Point,也就是 Cicle 中不能够再有名为 Point 的成员了。 - -# 函数 - -许多编程语言都为线程分配一个固定长度的函数调用栈,大小在 64KB 到 2MB 之间,递归的深度受限于固定长度大小栈,Go 语言实现了可变长的栈,栈的使用会随着使用的增大而增大,可达到 1GB 左右的上限,这样我们可以安全地使用递归而不用担心栈溢出。当然在栈编程的时候像是 slice、map 的扩展,会出现内存的复制,频繁增长会使效率降低。 - -Go 垃圾回收机制可回收未使用的内存,但不能指望它会释放未使用的操作系统资源,比如打开文件以及网络连接等,**必须显示关闭操作系统资源**。 - -可变参数传递的是一个对应类型的 slice,如: - -```go -func sum(vals ...int) -``` - -vals 是一个 `[]int` 的 slice - -## defer - -使用 defer 语句正确的地方是在成功获得资源后。 - -值得一提是先生成 **return** 返回的值,然后再以先入后出的顺序执行 **defer** 语句。 - -```go -func main() { - a := test() - fmt.Println(a) -} - -func test() int { - a := 0 - defer func() { - fmt.Println(a) - }() - defer func() { - a++ - }() - return a -} -``` - -输出: - -``` -1 -0 -``` - -但是如果返回值有命名,那么 defer 修改命名返回值可以修改外围调用者接收到的返回值,如: - -```go -func main() { - a := test2(1) - fmt.Println(a) -} - -func test2(x int) (result int) { - defer func() {result += x}() - return x + x -} -``` - -输出: `3` - -## 闭包 - -Go 中闭包的实现是通过 **逃逸分析** 做到的,通过分析变量是否有逃逸的可能,如果有则把变量创建在堆中,否则变量分配在当前 goroutine 的栈中。 - -变量在同一个包下都是可见的,无论该属性是否是可导出的*exported* - -Go 语法糖(由编译器实现的便利功能)之一是:对于方法的接收者是**值**或者**指针**,Go 编译器会帮助我们实现**取值`*`**和**取地址`&`**操作。如: - -```go -type S struct { - s string -} - -func (s *S) hello() { - fmt.Println("hello,", s) -} - -func (s S) world() { - fmt.Println("world wide web", s) -} - -func main() { - s := S{s: "world"} - s.hello() // OK - p := &s - p.world() // OK -} -``` - -但是自动 `*` 和 `&` 操作只能针对变量有效,对于类型的字面量不起作用,如: - -```go -S{s: "world"}.hello() // 编译错误 -``` - -## nil 是合法的方法接收者 - -因为很多类型中 nil 是有意义的,比如 slice、map、chan,只要将 nil 转化为其他可以为 nil 的类型,那么 nil 也可以作为方法的接收者。比如一个链表的例子: - -```go -type IntList struct { - Val int - Next *IntList -} - -func (node *IntList) Sum() int { - if node == nil { - return 0 - } - return node.Val + node.Next.Sum() -} -``` - -如果不是环形链表的话,那么最后一个结点的 `node.Next == nil`,使用 `node.Next.Sum()` 可以正常运行。对于方法的接收者可以是 nil 的情况下要多加小心,比如修改 nil map 可能会触发宕机 panic。 - -## 方法函数变量 - -```go -type S struct { - s string -} - -func (s *S) hello() { - fmt.Println("hello,", s) -} - -func (s S) world() { - fmt.Println("world wide web", s) -} - -func test1() { - s := S{s: "world"} - f := s.hello - s.s = "hi" - fmt.Printf("%T\n", f) - f() -} - -func test2() { - p := &S{s: "world"} - f := p.world - p.s = "ja" - f() -} -``` - -test1 函数输出: - -``` -func() -hello, &{hi} -``` - -也就是修改了 `s.s` 的值后,`f()` 方法的调用收到了影响。 - -test2 函数输出: - -``` -world wide web {world} -``` - -也就是修改了 `p.s` 的值后,`f()` 方法的调用不受影响。 - -分析:因为 test1 中 f 方法保存的是 (s *S) 指针,他们始终引用同一个地址,所以修改了引用的地址的值,`f()` 也会受到影响。test2 中 f 方法保存的是 (s S) 值,与 p 指针指向的地址不一样,因为 s 已经经过了值复制,修改 p 并不会影响到 s,所以 `f()` 调用不受影响。 - -> 如果使用方法函数变量,请注意如果类型中对应有引用,可能会发生难以排查的错误 - -## 集和的实现 - -Go 并没有 build-in 集和 set(Python 有,Java 也有),而 map 与 set 非常相似,只是 map 存储的是 K-V,set 存储的是 K,可以通过把 V 插入一个 bool 值来达到把 map 改装为 set 的功能。 - -PS:Java 中的 HashSet 就是把 HashMap 中的 V 架空实现的,其他算法和 HashpMap 几乎一样。 - -```go -type set map[T]bool -``` +--- +layout: post +title: "《Go程序设计语言》笔记一" +date: 2018-03-2 12:00:05 +0800 +categories: golang +--- + +# 标准输入输出 + +编程语言中的标准流如,**stdin** / **stdout** / **stderr** 是指向操作系统中的文件,以 Linux 为例,以下是 `os.Stdin` `os.Stdout` `os.Stderr` 的定义: + +```go +var ( + Stdin = NewFile(uintptr(syscall.Stdin), "/dev/stdin") + Stdout = NewFile(uintptr(syscall.Stdout), "/dev/stdout") + Stderr = NewFile(uintptr(syscall.Stderr), "/dev/stderr") +) +``` + +从控制台输入 `os.Stdin`, + +```go +func readFromStdin() { + reader := bufio.NewReader(os.Stdin) + fmt.Println("enter text:") + text, _ := reader.ReadString('\n') + fmt.Println(text) +} +``` + +fmt 包中提供了强大的格式化功能,可以使用 `fmt.Sprintf` 方法格式化字符串,该方法类似 `fmt.Printf` 区别是前者是将格式化的字符串作为返回结果,后者直接将格式化后的字符串输出到控制台。如果需要将格式化的字符串输出到流(实现了 `io.Writer`接口)中,可以调用 `fmt.Fprintf`。 + +# 获取运行命令的参数 + +运行命令的参数保存在 `os.Args` []string 中 + +```go +func main(){ + args := os.Args + fmt.Println(args) +} +``` + +假如我们使用 `go run main.go hello world` 命令来运行,得到不是 `["go run main.go", "hello", "world"]`,在我本机是:`["/tmp/go-build610217828/command-line-arguments/_obj/exe/readStdin", "hello", "world"]`。我们可以通过 `go run -x main.go hello world` 查看命令执行的过程。 + +# const + +Go 中常量只能是 字符串、数值、布尔值,复杂类型如slice、map、数组、结构体、指针、接口、函数等都无法声明为常量。 + +# var + +只有 chan、slice、map、函数、指针、接口变量 可以与 nil 做 `==` `!=` 比较,数组和结构体在声明时如果没有赋值,那么就会分配存储空间,然后将内容写为对应的默认值、 + +包级别的变量、常量在 **init** 函数开始前进行初始化,而 **init** 函数的执行在 **main** 函数之前。 + +只有变量才能够使用 `&` 操作符,也就是没有指针能够指向常量。 + +当两个指针变量指向同一个变量或者是两个指针都是零值 nil 时候,使用 `==` 比较得到 **true** + +可以使用 **flag** 包来开发命令行工具,flag 为我们解析参数提供了很大的帮助 + +任何一个包、任何一个 .go 文件可以包含任意多个 `func init(){...}` 函数,在初始化包的时候将会按照 init 函数声明的顺序来执行 init 函数。 + +包初始化顺序: +1. 按照 **import** 顺序完成引入的包的初始化 +2. 根据编译器导入 .go 文件的顺序进行包级别变量的初始化 +3. 按照 **init** 函数的声明顺序,执行所有 init 函数 + +pointer / slice / map / function / channel 都是引用类型,共同特点是全部都间接地指向程序变量或者状态,于是操作所引用数据的效果就会遍历该数据的全部引用。 + +# 类型 + +在不同编程语言中,去模 mod 运算 **%** 有不同表现,在 Go 中,取模余数的正负号总是与*被除数*一致,`-5 % 3 == -2` `-5 % -3 == -2`。题外话,Python 中去模余数总是和*除数*的正负号一致,`-5 % 3 == 1` `-5 % -3 == -2` + +无论是有符号数还是无符号数,若表示的运算结果所需要的位超过了该类型的范围,就会产生**溢出**。 + +对于 uint8 + +```go +var u uint = 255 +fmt.Println(u, u+1, u*u) // 255 0 1 +``` + +其中 `255 * 255` 结果的二进制形式为 `1111111000000001`,所以采取截断,最后结果为 1; +`u+1` 的结果的类型依然是 `uint8` + +类似地,对于 int8 + +```go +var i int8 = 127 +fmt.Println(u, u+1, u*u) // 127 -128 1 +``` + +## ^ &^ + +相对与 C 语言的不同,在 Go 中 `^` 运算符既可以作为一元运算符,也可以作为二元运算符: + +```go +var u uint8 = 255 +fmt.Println(^u) // 0 +fmt.Println(u^1) //254 +``` + ++ `^` 作为一元运算符时,是按位取反,相当于 C 中的 `~` ++ `^` 作为二元运算符时,是按位异或 + +Go 中还有一个 C 中没有的运算符 `&^` 按位清空。 + +```go +var u uint8 = 11 +fmt.Println(u&^1) // 254 +``` + +`x&^y` 的位运算中,将 y 中对应位为 1 的位置,将 x 中对应位置置为 0,否则保持 x 中对应位置不变,对于 uint8 `11 &^ 3 == 8` + +## fmt.Printf + +```go +fmt.Printf("%d %[1]o %[1]x %[1]b\n", 100) +``` + +其中 `[1]` 表示使用第一个参数 + +## 字符串 + +```go +s := "hello, 世界" +b := []byte(s) +s = string(b) +``` + +上述字符串转化为 []byte,[]byte 转化为 string 时候都会发生重新分配内存空间,然后再进行内存的复制。因为 string 底层是不可以改变的,如果底层数组进行复用,则会造成改变 []byte 的值会间接改变 string 的内容,进行很多这样的操作会使得程序执行非常低效。但是**某些编译器**能够识别到如果后续 []byte 不会再改变的话,string 会复用 []byte 底层的数组。 + +若要进行很多关于 string 的操作,尽可能使用 `bytes.Buffer`,其底层使用 []byte 进行数据的存储,动态扩展,效率相对高。 + +## 标准输入 + +类似于 C,Go 提供 `fmt.Scanf` 函数帮助我们从命令行中输入信息 + +## 常量 + +```go +const ( + a = 1 + b + c = 2 + d +) +// a == b == 1 +// c == d == 2 +``` + +常量只能指向基本类型或者是经过 `type Time int` 等经过重命名的基本类型,但是许多常量并不从属某一具体类型。编译器将这些从属类型待定的常量表示为某些值,可以认为它们的精度至少达到 256 位。 + +## 数组 + +声明数组时,使用 `...` 表示让编译器计算数组长度,不需要我们显式声明,如: + +```go +r := [...]int{1, 2, 3, 4} +s := []int{1, 2, 3, 4} +fmt.Printf("%T\n", r) // [4]int 数组 +fmt.Printf("%T\n", s) // []int slice +``` + +同类型的数组是可以通过 `==` 或 `!=` 进行比较的,这里需要注意的是 `[3]int [4]int` 不是同类型。 + +在函数的参数传递中,数组发生的是值传递,也就是每一次传递都在内存分配一样大小的空间,再进行数组内容的复制,在函数内改变数组元素不会影响到原数组。如果传递大数组将会十分低效。所以需要进行函数传递的建议使用 slice,slice 是一个结构体,不会发生底层数组的复制,只是将复制指针值;或者使用数组的指针进行数组的传递。 + +Go 内置函数很多都是直接对 slice 进行传递和操作,感觉 Go 还是建议人们尽可能使用 slice 而不是数组。 + +## slice + +声明数组或 slice 的时候,可以指定特定位置元素的值,如果之前的值没有显示声明,就会被声明为默认值。 + +```go +a := [...]int{99:1} // 声明一个 [100]int 数组,0~98 为 int 默认值 0,99 位元素为 1 +s := []int{99:1} // 声明一个 []int slice,0~98 为 int 默认值 0,99 位元素为 1 +``` + +```go +days := []string{1:"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"} +p := days[1:3] +t := p[0:5] +fmt.Println(p, len(p), cap(p)) +fmt.Println(t, len(t), cap(t)) +``` + +注意:`len(p) == 2` 为什么可以取 `t := p[0:5]` 长度为 5 的 slice 呢? + +因为 `cap(p) == 7`,p 底层数组复用了 days,也就是说只要不超越 **cap** 限制,就可以取任意长度的切片。 + +slice 取切片都使用指针复用了底层的数组,所以 slice 的取切片操作是一个高效的操作,并不会占用很多资源,问题是 slice 底层复用数组,有可能一个改变会影响其他的运行结果,尤其是在 goroutine 众多的情况下。 + +如果库支持的函数中只支持对应类型的 slice,而不支持数组类型参数怎么办呢? +比如:`func test([]byte)` 函数只支持 []byte,而不支持 [N]byte + +可以通过对数组取切片,得到的 slice 复用了底层的数组存储空间。 +如: + +```go +a := [3]byte{1, 2, 3} +test(a[:]) +``` + +一个等于 nil 的 slice 与`len==cap==0` 的 slice 有的唯一区别就是,与 nil 进行 `==` `!=` 比较时的表现相反。 + +**下面将会变得非常绕** + +[stackoverflow 上一个关于 copy make 等内置函数的讨论](https://stackoverflow.com/questions/18512781/built-in-source-code-location) + +一个例子: + +```go +s := []int{1, 2, 3, 4} +copy(s[1:], s[0:2]) +fmt.Println(s) // [1 1 2 4] +``` + +这里得出的结论是 copy 函数应该是从后往前复制的,如果是从前往后复制的话得到的结果是 `[1 1 1 4]` + +另一个例子: + +```go +s := []int{1, 2, 3, 4} +copy(s[0:], s[1:]) +fmt.Println(s) // [2 3 4 4] +``` + +这里得出的结论是 copy 函数应该是从前往后复制的,如果是从后往前复制的话得到的结果是 `[4 4 4 4]` + +这让我产生疑问,`copy` 函数的复制算法到第是怎么样的呢? + +**copy** 函数的实现 + +```go +func slicecopy(to, fm slice, width uintptr) int { + if fm.len == 0 || to.len == 0 { + return 0 + } + + n := fm.len + if to.len < n { + n = to.len + } + + if width == 0 { + return n + } + // 忽略中间某些关于 race 的细节 + size := uintptr(n) * width + if size == 1 { // common case worth about 2x to do here + // TODO: is this still worth it with new memmove impl? + *(*byte)(to.array) = *(*byte)(fm.array) // known to be a byte pointer + } else { + memmove(to.array, fm.array, size) + } + return n +} +``` + +`memmove` 的实现在 [https://github.com/golang/go/blob/master/src/runtime/memmove_amd64.s](https://github.com/golang/go/blob/master/src/runtime/memmove_amd64.s) 由于全是汇编没有看懂。 + +如果需要实现 slice 的循环移动的话,我们可以通过三次翻手实现: + +比如需要把 `[1 2 3 4 5]` 循环移动为 `[3 4 5 1 2]` + +1. 把 `[1 2]` 旋转为 `[2 1]` +2. 把 `[3 4 5]` 旋转为 `[5 4 3]` +3. 此时 slice 已经变成 `[2 1 5 4 3]`,再进行一次翻手得到 `[3 4 5 1 2]` + +## map + +键 K 必须能够通过操作符 `==` 进行比较,选 K 的时候尽可能不适用浮点型,虽然浮点型可以通过 `==` 进行比较,但是比较存在不精确,通常我们比较字符串是通过一个阀值做到的,比如 `1 - 0.001 <= f <= 1 + 0.001`。 + +`delete` 函数从 map 中删除指定的 K-V,即使 K 不存在于 map 中也不会发生异常,如果从 map 中取出一个 K,如果该 K 不存在 map 中会返回一个类型 K 的零值,所以通常取元素操作会附带一个 comma-ok如:`v, ok := m[k]`。 + +map 元素不是一个变量,无法获取它们的地址,如下操作是不能够通过编译的: + +```go +fmt.Printf("%p", &m["hello"]) +``` + +原因是 map 是有可能动态增长的,当发生动态增长的时候,K-V 所在的地址发生了迁移,通过获取 `&m["hello"]` 没有意义,地址无效。 + +可以通过 `len` 函数获知 map 中 K-V 的数量。 + +map 的零值是 nil,向 nil map 中查找元素、`len(m)`、`delete(m, k)`、for range 循环,都不会发生错误,其行为像对已经初始化但是依然是空的 map 操作一样,但是如果向 nil map 中设置 K-V 将会导致错误。 + +## struct + +```go +package p +type T struct {a, b int} + +package q +import "p" +var a = p.T{a:1, b: 2} // 编译错误 +var b = p.T{1, 2} // 编译错误 +``` + +因为 a b 是以小写开头的,都是不可导出的,在别的包下无法显示 `p.T{a:1, b: 2}` 或隐式 `p.T{1, 2}` 引用。 + +如果一个结构体中所有的成员都是可以比较的,那么这个结构体就是可以比较的;否则该结构体不能够通过 `==` `!=` 进行比较,可以比较的结构体可以作为 map 的 K。 + +```go +// 可比较的结构体 +type Point struct { + X, Y int +} + +// 不可比较的结构体 +type array struct { + S []int +} +``` + +结构体可以组合是 Go 实现面相对象的重要部分。 + +```go +type Point struct { + X, Y int +} + +type Circle struct { + Point + Radius int +} +``` + +这样 Circle 就能够调用 Point 的方法,Circle 中 Point 的名字就是 Point,也就是 Cicle 中不能够再有名为 Point 的成员了。 + +# 函数 + +许多编程语言都为线程分配一个固定长度的函数调用栈,大小在 64KB 到 2MB 之间,递归的深度受限于固定长度大小栈,Go 语言实现了可变长的栈,栈的使用会随着使用的增大而增大,可达到 1GB 左右的上限,这样我们可以安全地使用递归而不用担心栈溢出。当然在栈编程的时候像是 slice、map 的扩展,会出现内存的复制,频繁增长会使效率降低。 + +Go 垃圾回收机制可回收未使用的内存,但不能指望它会释放未使用的操作系统资源,比如打开文件以及网络连接等,**必须显示关闭操作系统资源**。 + +可变参数传递的是一个对应类型的 slice,如: + +```go +func sum(vals ...int) +``` + +vals 是一个 `[]int` 的 slice + +## defer + +使用 defer 语句正确的地方是在成功获得资源后。 + +值得一提是先生成 **return** 返回的值,然后再以先入后出的顺序执行 **defer** 语句。 + +```go +func main() { + a := test() + fmt.Println(a) +} + +func test() int { + a := 0 + defer func() { + fmt.Println(a) + }() + defer func() { + a++ + }() + return a +} +``` + +输出: + +``` +1 +0 +``` + +但是如果返回值有命名,那么 defer 修改命名返回值可以修改外围调用者接收到的返回值,如: + +```go +func main() { + a := test2(1) + fmt.Println(a) +} + +func test2(x int) (result int) { + defer func() {result += x}() + return x + x +} +``` + +输出: `3` + +## 闭包 + +Go 中闭包的实现是通过 **逃逸分析** 做到的,通过分析变量是否有逃逸的可能,如果有则把变量创建在堆中,否则变量分配在当前 goroutine 的栈中。 + +变量在同一个包下都是可见的,无论该属性是否是可导出的*exported* + +Go 语法糖(由编译器实现的便利功能)之一是:对于方法的接收者是**值**或者**指针**,Go 编译器会帮助我们实现**取值`*`**和**取地址`&`**操作。如: + +```go +type S struct { + s string +} + +func (s *S) hello() { + fmt.Println("hello,", s) +} + +func (s S) world() { + fmt.Println("world wide web", s) +} + +func main() { + s := S{s: "world"} + s.hello() // OK + p := &s + p.world() // OK +} +``` + +但是自动 `*` 和 `&` 操作只能针对变量有效,对于类型的字面量不起作用,如: + +```go +S{s: "world"}.hello() // 编译错误 +``` + +## nil 是合法的方法接收者 + +因为很多类型中 nil 是有意义的,比如 slice、map、chan,只要将 nil 转化为其他可以为 nil 的类型,那么 nil 也可以作为方法的接收者。比如一个链表的例子: + +```go +type IntList struct { + Val int + Next *IntList +} + +func (node *IntList) Sum() int { + if node == nil { + return 0 + } + return node.Val + node.Next.Sum() +} +``` + +如果不是环形链表的话,那么最后一个结点的 `node.Next == nil`,使用 `node.Next.Sum()` 可以正常运行。对于方法的接收者可以是 nil 的情况下要多加小心,比如修改 nil map 可能会触发宕机 panic。 + +## 方法函数变量 + +```go +type S struct { + s string +} + +func (s *S) hello() { + fmt.Println("hello,", s) +} + +func (s S) world() { + fmt.Println("world wide web", s) +} + +func test1() { + s := S{s: "world"} + f := s.hello + s.s = "hi" + fmt.Printf("%T\n", f) + f() +} + +func test2() { + p := &S{s: "world"} + f := p.world + p.s = "ja" + f() +} +``` + +test1 函数输出: + +``` +func() +hello, &{hi} +``` + +也就是修改了 `s.s` 的值后,`f()` 方法的调用收到了影响。 + +test2 函数输出: + +``` +world wide web {world} +``` + +也就是修改了 `p.s` 的值后,`f()` 方法的调用不受影响。 + +分析:因为 test1 中 f 方法保存的是 (s *S) 指针,他们始终引用同一个地址,所以修改了引用的地址的值,`f()` 也会受到影响。test2 中 f 方法保存的是 (s S) 值,与 p 指针指向的地址不一样,因为 s 已经经过了值复制,修改 p 并不会影响到 s,所以 `f()` 调用不受影响。 + +> 如果使用方法函数变量,请注意如果类型中对应有引用,可能会发生难以排查的错误 + +## 集和的实现 + +Go 并没有 build-in 集和 set(Python 有,Java 也有),而 map 与 set 非常相似,只是 map 存储的是 K-V,set 存储的是 K,可以通过把 V 插入一个 bool 值来达到把 map 改装为 set 的功能。 + +PS:Java 中的 HashSet 就是把 HashMap 中的 V 架空实现的,其他算法和 HashpMap 几乎一样。 + +```go +type set map[T]bool +``` diff --git a/src/md/2018-03-02-bitmap-realize-in-golang.md b/src/md/2018-03-02-bitmap-realize-in-golang.md index f5a201c..3f2afa3 100644 --- a/src/md/2018-03-02-bitmap-realize-in-golang.md +++ b/src/md/2018-03-02-bitmap-realize-in-golang.md @@ -1,111 +1,111 @@ ---- -layout: post -title: "bitmap 位图的 Go 实现" -date: 2018-03-2 12:00:05 +0800 -categories: 算法 ---- - -# bitmap 实现 - -完成《Go程序设计语言》的一个练习,感觉位图在算法中是一个挺重要的,特别是在《编程珠玑》中提到位图在当初内存昂贵,计算资源匮乏的时代的使用。 - -```go -import ( - "bytes" - "fmt" -) - -// 用于存储数字的集和 -type IntSet struct { - words []uint64 - length int -} - -func (s *IntSet) Has(x int) bool { - word, bit := x/64, uint(x%64) - return word < len(s.words) && (s.words[word]&(1<= len(s.words) { - s.words = append(s.words, 0) - } - // 判断 x 是否已经存在 s 中 - if s.words[word]&(1< len("{") { - buf.WriteByte(' ') - } - fmt.Fprintf(&buf, "%d", 64*uint(i)+j) - } - } - } - buf.WriteByte('}') - fmt.Fprintf(&buf, "\nLength: %d", s.length) - return buf.String() -} - -func (s *IntSet) Remove(x int) { - word, bit := x/64, uint(x%64) - if word < len(s.words) { - if s.words[word] & (1 << bit) != 0 { - s.words[word] &= ^(1 << bit) - s.length-- - } - } -} - -func (s *IntSet) Len() int { - return s.length -} - -func (s *IntSet) Clear() { - s.words = nil - s.length = 0 -} - -func (s *IntSet) Copy() (cp *IntSet) { - cp = new(IntSet) - cp.words = make([]uint64, len(s.words)) - cp.length = s.length - copy(cp.words, s.words) - return cp -} +--- +layout: post +title: "bitmap 位图的 Go 实现" +date: 2018-03-2 12:00:05 +0800 +categories: 算法 +--- + +# bitmap 实现 + +完成《Go程序设计语言》的一个练习,感觉位图在算法中是一个挺重要的,特别是在《编程珠玑》中提到位图在当初内存昂贵,计算资源匮乏的时代的使用。 + +```go +import ( + "bytes" + "fmt" +) + +// 用于存储数字的集和 +type IntSet struct { + words []uint64 + length int +} + +func (s *IntSet) Has(x int) bool { + word, bit := x/64, uint(x%64) + return word < len(s.words) && (s.words[word]&(1<= len(s.words) { + s.words = append(s.words, 0) + } + // 判断 x 是否已经存在 s 中 + if s.words[word]&(1< len("{") { + buf.WriteByte(' ') + } + fmt.Fprintf(&buf, "%d", 64*uint(i)+j) + } + } + } + buf.WriteByte('}') + fmt.Fprintf(&buf, "\nLength: %d", s.length) + return buf.String() +} + +func (s *IntSet) Remove(x int) { + word, bit := x/64, uint(x%64) + if word < len(s.words) { + if s.words[word] & (1 << bit) != 0 { + s.words[word] &= ^(1 << bit) + s.length-- + } + } +} + +func (s *IntSet) Len() int { + return s.length +} + +func (s *IntSet) Clear() { + s.words = nil + s.length = 0 +} + +func (s *IntSet) Copy() (cp *IntSet) { + cp = new(IntSet) + cp.words = make([]uint64, len(s.words)) + cp.length = s.length + copy(cp.words, s.words) + return cp +} ``` \ No newline at end of file diff --git a/src/md/2018-03-03-the-go-programming-language-note2.md b/src/md/2018-03-03-the-go-programming-language-note2.md index deed2cb..278819c 100644 --- a/src/md/2018-03-03-the-go-programming-language-note2.md +++ b/src/md/2018-03-03-the-go-programming-language-note2.md @@ -1,128 +1,128 @@ ---- -layout: post -title: "《Go程序设计语言》笔记二" -date: 2018-03-03 12:00:05 +0800 -categories: golang ---- - -# 接口 interface - -从概念上讲,一个接口类型的值(简称接口值)有两部分构成:具体类型和该类型的值。二者成为接口的动态类型和动态值。接口的零值就是动态类型和动态值都是 nil。 - -将一个变量赋予接口值,会发生变量值的复制。 - -接口值可以用 `==` `!=` 操作符来比较,如果两个接口值都是 nil 或者二者的动态类型完全一致且动态值相等(使用动态类型的 `==` 操作符来做比较),那么两个接口值相等。因为接口值是可以比较的,所以他们可以作为 map 的键,也可以作为 switch 语句的操作数。 - -> 注意:如果接口的动态类型是不可以比较的,而使用了 `==` `!=` 比较操作符,则会可能触发宕机 - -```go -var x interface{} = []int{} -fmt.Println(x == x) // 宕机 -``` - -**含有空指针的非空接口** - -```go -func handle(v interface{}) { - if v == nil { - fmt.Println("v is nil") - } else { - fmt.Println("v is not nil") - } -} - -func main() { - var x *int = nil - handle(x) -} -``` - -虽然 `x == nil`,但是调用 handle 方法时,实际上 v 接收到的是一个类型为 `*int`,值为 nil 的值。所以 `v == nil` 并不成立,因为它确实装载了一个变量。 - -## sort - -sort 包提供了针对任意序列根据任意排序函数原地排序的功能。sort.Sort 函数(使用快速排序算法)对序列和其中的元素布局无任何要求,它使用 sort.Interface 接口来指定通用排序算法和每个具体的序列类型之间的协议。 - -对于排序我们需要知道的是需要排序的元素的长度、如何比较两个元素的大小以及如何交换两个元素。而 sort.Interface 接口就定义了三个对应的方法。 - -```go -package sort - -type Interface interface { - Len() int - Less(i, j int) bool - Swap(i, j int) -} -``` - -```go -type stringSlice []string - -func (s stringSlice) Len() int { - return len(s) -} - -func (s stringSlice) Less(i, j int) bool { - return s[i] < s[j] -} - -func (s stringSlice) Swap(i, j int) { - s[i], s[j] = s[j], s[i] -} - -func main() { - s := stringSlice{"hello", "world", "哈哈", "abc"} - sort.Sort(s) - fmt.Println(s) // [abc hello world 哈哈] -} -``` - -+ `r.URL` 带有 query 参数 -+ `r.URL.Path` 不带 query 参数 - -## 接口类型断言 - -判断该接口变量中存储的是否是某类型 - -假设有: - -```go -var w io.Writer -w = os.Stdout -``` - -有三种用法: -1. `f := w.(*os.File)` 只有一个结果,如果 w 中的变量类型不是 `*os.File` 就会触发 panic -2. `f, ok := w.(*os.File)` 有两个结果,如果 w 中的变量类型不是 `*os.File` 不会触发 panic,但是 `ok == false`,f 中存储的是类型 `*os.File` 的零值,也就是 nil,常配合 if 使用 - -```go -if f, ok := w.(*os.File); ok { - //... -} -``` - -3. 配合 switch 使用,tyoe-switch 语法 - -```go -switch w.(type) { - case T1: - //... - case T2: - //... - case T3: - //... - default: - //... -} -``` - -> type-switch 不能配合 fallthrough 使用 - -## 关于接口的建议 - -**以下是来自《Go程序设计语言》中关于接口的建议:** - -+ 仅在两个或多个具体类型需要按统一的方式处理时才需要接口 -+ 设计新类型时越小的接口越容易满足,一个不错的接口设计经验是*仅要你所需要的* - -通过接口可以实现灵活的动态性,给予接口的方法可以提供良好的抽象,提高代码复用。 +--- +layout: post +title: "《Go程序设计语言》笔记二" +date: 2018-03-03 12:00:05 +0800 +categories: golang +--- + +# 接口 interface + +从概念上讲,一个接口类型的值(简称接口值)有两部分构成:具体类型和该类型的值。二者成为接口的动态类型和动态值。接口的零值就是动态类型和动态值都是 nil。 + +将一个变量赋予接口值,会发生变量值的复制。 + +接口值可以用 `==` `!=` 操作符来比较,如果两个接口值都是 nil 或者二者的动态类型完全一致且动态值相等(使用动态类型的 `==` 操作符来做比较),那么两个接口值相等。因为接口值是可以比较的,所以他们可以作为 map 的键,也可以作为 switch 语句的操作数。 + +> 注意:如果接口的动态类型是不可以比较的,而使用了 `==` `!=` 比较操作符,则会可能触发宕机 + +```go +var x interface{} = []int{} +fmt.Println(x == x) // 宕机 +``` + +**含有空指针的非空接口** + +```go +func handle(v interface{}) { + if v == nil { + fmt.Println("v is nil") + } else { + fmt.Println("v is not nil") + } +} + +func main() { + var x *int = nil + handle(x) +} +``` + +虽然 `x == nil`,但是调用 handle 方法时,实际上 v 接收到的是一个类型为 `*int`,值为 nil 的值。所以 `v == nil` 并不成立,因为它确实装载了一个变量。 + +## sort + +sort 包提供了针对任意序列根据任意排序函数原地排序的功能。sort.Sort 函数(使用快速排序算法)对序列和其中的元素布局无任何要求,它使用 sort.Interface 接口来指定通用排序算法和每个具体的序列类型之间的协议。 + +对于排序我们需要知道的是需要排序的元素的长度、如何比较两个元素的大小以及如何交换两个元素。而 sort.Interface 接口就定义了三个对应的方法。 + +```go +package sort + +type Interface interface { + Len() int + Less(i, j int) bool + Swap(i, j int) +} +``` + +```go +type stringSlice []string + +func (s stringSlice) Len() int { + return len(s) +} + +func (s stringSlice) Less(i, j int) bool { + return s[i] < s[j] +} + +func (s stringSlice) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +func main() { + s := stringSlice{"hello", "world", "哈哈", "abc"} + sort.Sort(s) + fmt.Println(s) // [abc hello world 哈哈] +} +``` + ++ `r.URL` 带有 query 参数 ++ `r.URL.Path` 不带 query 参数 + +## 接口类型断言 + +判断该接口变量中存储的是否是某类型 + +假设有: + +```go +var w io.Writer +w = os.Stdout +``` + +有三种用法: +1. `f := w.(*os.File)` 只有一个结果,如果 w 中的变量类型不是 `*os.File` 就会触发 panic +2. `f, ok := w.(*os.File)` 有两个结果,如果 w 中的变量类型不是 `*os.File` 不会触发 panic,但是 `ok == false`,f 中存储的是类型 `*os.File` 的零值,也就是 nil,常配合 if 使用 + +```go +if f, ok := w.(*os.File); ok { + //... +} +``` + +3. 配合 switch 使用,tyoe-switch 语法 + +```go +switch w.(type) { + case T1: + //... + case T2: + //... + case T3: + //... + default: + //... +} +``` + +> type-switch 不能配合 fallthrough 使用 + +## 关于接口的建议 + +**以下是来自《Go程序设计语言》中关于接口的建议:** + ++ 仅在两个或多个具体类型需要按统一的方式处理时才需要接口 ++ 设计新类型时越小的接口越容易满足,一个不错的接口设计经验是*仅要你所需要的* + +通过接口可以实现灵活的动态性,给予接口的方法可以提供良好的抽象,提高代码复用。 diff --git a/src/md/2018-03-04-the-go-programming-language-note3.md b/src/md/2018-03-04-the-go-programming-language-note3.md index e417380..9b89b0e 100644 --- a/src/md/2018-03-04-the-go-programming-language-note3.md +++ b/src/md/2018-03-04-the-go-programming-language-note3.md @@ -1,455 +1,455 @@ ---- -layout: post -title: "《Go程序设计语言》笔记三" -date: 2018-03-03 12:00:05 +0800 -categories: golang ---- - -# goroutine 和通道 - -执行 main 函数的 goroutine 结束后,所有的 goroutine 都会被强行终结,然后程序退出。 - -## Unix Socket vs TCP/IP Socket - -在使用 nginx 时有如下一段配置文件: - -``` -location / { - include proxy_params; - proxy_pass http://unix:/home/xxx/xxx/xxx.sock; - } -``` - -而使用 Go 网络编程时候有 `net.Listen("tcp", "localhost:8080")` 创建 Listener,tcp 还能够被 unix 替代,这就让我感到好奇 Unix、Tcp/IP 两者到底有什么区别? - -在 serverfault 中搜索到该答案:[https://serverfault.com/a/124518/458952](https://serverfault.com/a/124518/458952) - -总结一下: -+ Unix 面向的是在同一操作系统内的进程间通信,TCP/IP 主要面向可以通过网络连接的两个操作系统之间的进程通信 -+ Unix socket 比 TCP/IP socket 更快 -+ Unix socket 连接到一个文件,TCP/IP 连接到 domainOrIP:port - -## 结束命令行输入 - -```go -func main() { - scaner := bufio.NewScanner(os.Stdin) - for scaner.Scan() { - s := scaner.Text() - fmt.Println("You input:", s) - } -} -``` - -可以发现输出永远无法结束。。。当初我就面对过这样的情况,Windows 下通过 `ctrl+z` 结束输入,Unix 下通过 `ctrl+d` 结束输入。 - -## channel - -像 map 一样,通道是一个使用 make 创建的数据结构的引用。当复制或作为参数传递到一个函数时,复制的是引用,这样调用者和被调用者都引用同一份数据结构。通道的默认值是 nil。 - -同类型的 chan 可以通过 `==` 进行比较,如果两者都是同一个通道数据的引用时,比较值为 true,通道也可以和 nil 进行比较。 - -接收者使用 comma-ok 判断一个通道是否已经关闭。 - -```go -x, ok := <- ch -``` - -1. chan 已经关闭,`ok == false` -2. chan 还没关闭,`ok == true` - -使用 for-range 循环从通道中读取元素,知道通道被关闭。 - -`close(channel)` 的调用通常是发送者。 -`close(channel)` 操作并不是必须的,通道的关闭操作主要目的是告诉接收者没有更多元素需要通过通道传递。chan 可以被垃圾回收器自动回收,判断一个 chan 是否可以回收的标准是否还有变量可以访问到 chan,而不是 chan 是否已经关闭了。chan 并不是系统资源,而是 Go 运行时库提供的功能。 - -### 单向 chan - -在函数参数声明时, -+ `in <-chan int` 只读通道 -+ `out chan<- int` 只写通道 - -通道的只读和只写是通过编译时检查出来的,也就是编译器在编译代码时,如果发现有向只读 chan 中写数据,或向只写 chan 中读数据时,编译器会抛出编译失败错误。现在很多成熟的 IDE 和插件都可以做到静态语法检查功能,可以让我们写代码时避免此类错误。 - -### buffered chan - -+ `cap(channel)` 判断带缓冲区通道的缓冲区长度 -+ `len(channel)` 判断带缓冲区通道中元素的个数 - -### 多个 goroutine 同时写 chan,那么如何控制 chan 的关闭呢? - -在多个 goroutine 同时写 chan 时,如果任意一个 goroutine 关闭了 chan 肯定会触发其他 goroutine 的 chan 的写操作宕机,最后引发整个程序的异常退出。 - -这个问题可以使用 `sync.WaitGroup` 来解决: - -```go -func main() { - var wg sync.WaitGroup - tasks := []string{"hello", "world", "!!!"} - ch := make(chan string) - for _, work := range tasks { - wg.Add(1) // 确保 Add 在开始 goroutine 之前执行 - go func(work string) { - defer wg.Done() // defer 确保该语句能够被执行 - ch <- work - }(work) - } - // 等待其他 goroutine 结束,然后关闭 channel,通知读者没有更多元素可以被读取 - go func() { - wg.Wait() - close(ch) - }() - for v := range ch { - fmt.Println(v) - } - fmt.Println("finish...") -} -``` - -如果 `wg.Add(1)` 放在 worker goroutine 内执行的话,有一种特殊情况是,worker goroutine 全部还没有得到执行,closer goroutine 先执行,`wg.Wait()` 判断 **counter == 0**,然后就关闭了 chan,导致读者 for-range 没有读到内容且 worker goroutine 向 chan 中写数据时宕机。 - -closer goroutine 必须在 for-range 循环之前启动,不然进入 for-range 循环后,没有任何操作关闭通道,closer goroutine 则永远没有机会启动。 - -### 并行度控制 - -程序的并行度太高、无限制的并行通常不是一个好主意,因为系统中总有限制因素,例如,对于计算性应用 CPU 的核数,对于磁盘 I/O 操作磁头和磁盘的个数,下载流所使用的网络带宽,或 Web 服务本身的容量。 - -两个方法控制并发度: - -1. 借助 buffer channel 控制最大的并发度,每次启动一个新的 goroutine 前先向 buffer channel 中写入一个元素,结束后从 buffer channel 中读取一个元素。从而控制只有一个 goroutine 结束后才能够启动新的 goroutine - -```go -func main() { - max := 10 - threshold := make(chan struct{}, max) - for { - threshold<- struct{}{} // 获取令牌 - go handle() - } -} - -func handle() { - defer func() { - <-threshold // 释放令牌 - } - // do something -} -``` - -2. 启动特定数量的 goroutine,每个 goroutine 都是从 channel 中读取任务然后执行 - -```go -type Task struct{ - task string -} - -func main() { - max := 10 - tasks := make(chan Task) - for i := 0; i < max; i++ { - go handle(tasks) - } - for i := 0; i < 100; i++{ - tasks <- Task{task: fmt.Sprintf("%d", i)} - } - close(tasks) -} - -func handle(taskList <-chan Task) { - for task := range taskList { - fmt.Println(task.task) - } -} -``` - -## select - -```go -select{} // 永远等待 -``` - -空的 select 几乎不会消耗 CPU,我做了以下实验得出该结论: - -```go -func main() { - runtime.GOMAXPROCS(2) // 设置该程序使用最多的OS线程 - go func() { - loop: - for{ - goto loop - } - }() - select { - - } -} -``` - -```go -func main() { - runtime.GOMAXPROCS(2) - go func() { - loop: - for{ - goto loop - } - }() - loop: - for { - goto loop - } -} -``` - -通过 top 命令查看当前运行的程序,查看到 CPU 占用率是 100%,如果 `select{}` 换成 `for{}` CPU 占用率是 200%。 - -## time.Tick - -`time.Tick` 函数的行为很像创建一个 goroutine 在循环里面调用 `time.Sleep`,然后在它每次醒来时发送事件。如果停止监听 tick,但是计时器 goroutine 还在运行,徒劳地向一个没有 goroutine 在接收的通道中不断发送————**goroutine 泄露** - -Tick 函数很方便使用,但是它仅仅在应用整个生命周期都需要时才适合。否则,我们需要使用以下这个模式: - -```go -ticker := time.NewTicker(time.Second) -<-ticker.C // 从 ticker 通道中获取元素 -ticker.Stop() // 释放资源,终止 ticker 的 goroutine -``` - -## 通知 goroutine 取消 - -chan 的零值是 nil,nil 通道有时非常有用,因为对 nil 通道发送和接收将永远阻塞。 - -一个 goroutine 无法直接观察到另一个 goroutine 的状态,有时候我们需要通过广播通知多个 goroutine 取消执行。这个功能可以配合关闭 channel 和 select 来做到。 - -如果一个 channel 已经关闭了,那么从该 channel 中读取数据不会阻塞当前 goroutine。 - -创建一个 channel 专门用于通知某些注册退出消息的 goroutine 退出执行,不向该 goroutine 中发送任何消息,通过关闭 channel,使得其他读操作,比如 select 中的 case 可以执行,每个注册该消息的 goroutine 都能收到退出消息,从而实现广播退出消息的功能。 - - ```go -func main() { - closeSignal := make(chan struct{}) - go handle(closeSignal) - time.AfterFunc(5 * time.Second, func() { - close(closeSignal) - }) - time.Sleep(10 * time.Second) - fmt.Println("main exit") -} - -func handle(closeSignal chan struct{}) { - loop: - for { - if cancelled(closeSignal){ - fmt.Println("Get cancel signal") - break loop - } - fmt.Println("running...") - time.Sleep(time.Second) - } - fmt.Println("goroutine exit") -} - -func cancelled(closeSignal chan struct{}) bool { - select { - case <-closeSignal: - return true - default: - return false - } -} - ``` - -## 使用共享变量实现并发 - -> 布要通过共享内存来通信,而应该通过通信来共享内存 - -可以限制只有一个 goroutine 能够访问变量,从而达到防止了并发带来的读写不安全。如以下的 balance 变量只能够在运行 teller 的 goroutine 访问。 - -```go -var deposits = make(chan int) -var balances = make(chan int) -type withdrawStruct struct{ - amount chan int - result chan bool -} -var withdraw = withdrawStruct{amount: make(chan int), result: make(chan bool)} - -func Deposit(amount int) { - deposits <- amount -} - -func Balance() int { - return <- balances -} - -func Withdraw(amount int) bool { - withdraw.amount<- amount - result := <-withdraw.result - return result -} - -func teller() { - // 将 balance 变量限制在 teller goroutine 内 - // 避免多个 goroutine 访问带来的异常 - balance := 0 - for { - select { - case balances <- balance: - case amount := <-deposits: - balance += amount - case amount := <-withdraw.amount: - if amount >= balance { - withdraw.result<- false - } else { - balance -= amount - withdraw.result<- true - } - } - } -} - -func main() { - go teller() - for i := 0; i < 200; i++ { - go Deposit(1) - } - for i := 0; i < 50; i++ { - go Withdraw(1) - } - for i := 0; i < 10; i++ { - go func() { - fmt.Println(Balance()) - }() - } - time.Sleep(time.Second) - fmt.Println(Balance()) -} -``` - -或者使用 chan 来 pipline 中传递某个类型的指针,但是如果上层流水线 goroutine 将指针传递给下一级后便不会再通过指针访问变量,从而达到任何时刻变量只对一个 goroutine 可见。 - -比如蛋糕师做蛋糕分为三道工序,由三个师傅分别负责做蛋糕、加糖衣、加奶油。只要蛋糕传递给下一个师傅后,上一个师傅就不再对蛋糕做任何修改。 - -```go -type Product struct { - state string -} - -func Cake(cake chan<- *Product) { - c := Product{state:"cake"} - cake <- &c -} - -func Sugar(cake <-chan *Product, ice chan <- *Product) { - c := <-cake - c.state = "sugar" - ice <- c -} - -func ice(ice chan *Product, product chan <- *Product) { - c := <- ice - c.state = "ice" - product <- c -} -``` - -第三种方法避免竞态是允许多个 goroutine 同时访问同一变量,但在同一时间只有一个 goroutine 可以访问。采用*互斥机制* - -无论是为了保护包级别的变量,还是结构中的字段,当使用一个互斥量 Mutex 时,都应该确保 Mutex 以及被保护的变量都没有导出。 - -### `sync.Mutex` vs `sync.RWMutex` - -**sync.RWMutex** 仅在绝大部分 goroutine 都在获取读锁且锁竞争比较激烈时(即一般 goroutine 都要等待后才能获得锁),RWMutex 才有优势。因为 RWMutex 需要更加复杂的内部簿记工作,所以在竞争不激烈时 **sync.RWMutex** 比 **sync.Mutex** 慢。 - -如果计算机中服务器有多个 CPU,每个 CPU 中都拥有自己的 cache,cache 并不会实时地与内存保持同步,那么可能会导致多个运行在不同 CPU 上的 goroutine 对同一个内存变量观察到不一样的值。 - -```go -var x, y int -go func() { - x = 1 // A1 - fmt.print("y:", y, " ") // A2 -} -go fun() { - y = 1 // B1 - fmt.Print("x:", x, " ") // B2 -} -``` - -我们期待的输出是以下四个之一: - -``` -y:0 x:1 -x:0 y:1 -x:1 y:1 -y:1 x:1 -``` - -但事实上也可能出现以下两种情况: - -``` -x:0 y:0 -y:0 x:0 -``` - -一个原因可能是如 `x:0 y:0` 情况, A 和 B 运行在不同 CPU 上,导致 B1 在更新 y 的值,但是缓存还未同步到内存中,A2 读到 y 的值已经过期。 - -另一个原因可能是编译器对指令进行了重排,因为 A1 和 A2在同一个 goroutine 中应该谁先执行都不会影响对方的正确性,也就是经过编译器优化后执行顺序变为:A2 --> A1 - -**所以尽可能将变量的访问限制在单个 goroutine 中,对于其他变量使用互斥锁。** - -### sync.Once 保证函数只被执行一次 - -有些场景下需要控制某个函数或者变量的方法只能被执行一次,那么我们可以通过 sync.Once 提供的 Do 方法做到。如下: - -```go -func main() { - var once sync.Once - fn := func() { - fmt.Println("hello world") - } - once.Do(fn) - once.Do(fn) -} -``` - -hello world 只被输出一次。 -当然如果自行调用 `fn()` 就可以逃避 sync.Once 的控制,看自觉。 - -## 竞态检测器 - -Go 语言运行时和工具链装备了一个精致并易于使用的动态分析工具:竞态检测器(race detector) - -简单地把 **-race** 命令行参数加入到 go build、go run、go test 命令里边即可以使用该功能。它会让编译器为应用或测试构建一个修改后的版本,这个版本有额外的手法用于高效记录在执行时对共享变量的所有访问,以及读写这些变量的 goroutine 的标示。除此之外,修改后的版本还会记录所有的同步事件,包括 go 语言、通道操作、(*sync.Mutex).Lock 调用、(*sync.WaitGroup).Wait 调用等。 - -这个工具会输出一份报告,包含变量的标示以及读写 goroutine 当时的调用栈,通常情况下这些信息足以定位问题。 - -由于额外的簿记工作,带竞态检查的程序在执行时需要更长的时间和更多的内存,但即使很多生产环境的任务,这种额外开支也是可以接收的。对于那些不常发生的竞态,使用竞态检测器可以帮助节省数小时甚至数天的调式时间。 - -## 一秒内 channel 的读写次数 - -```go -func main() { - ch := make(chan string) - go func(ch <-chan string) { - for { - <-ch - } - }(ch) - counter := 0 - deadline := time.After(1 * time.Second) - flag := true - for flag { - select { - case <-deadline: - flag = false - close(ch) - default: - ch <- "hello" - counter++ - } - } - fmt.Println(counter) -} -``` - +--- +layout: post +title: "《Go程序设计语言》笔记三" +date: 2018-03-03 12:00:05 +0800 +categories: golang +--- + +# goroutine 和通道 + +执行 main 函数的 goroutine 结束后,所有的 goroutine 都会被强行终结,然后程序退出。 + +## Unix Socket vs TCP/IP Socket + +在使用 nginx 时有如下一段配置文件: + +``` +location / { + include proxy_params; + proxy_pass http://unix:/home/xxx/xxx/xxx.sock; + } +``` + +而使用 Go 网络编程时候有 `net.Listen("tcp", "localhost:8080")` 创建 Listener,tcp 还能够被 unix 替代,这就让我感到好奇 Unix、Tcp/IP 两者到底有什么区别? + +在 serverfault 中搜索到该答案:[https://serverfault.com/a/124518/458952](https://serverfault.com/a/124518/458952) + +总结一下: ++ Unix 面向的是在同一操作系统内的进程间通信,TCP/IP 主要面向可以通过网络连接的两个操作系统之间的进程通信 ++ Unix socket 比 TCP/IP socket 更快 ++ Unix socket 连接到一个文件,TCP/IP 连接到 domainOrIP:port + +## 结束命令行输入 + +```go +func main() { + scaner := bufio.NewScanner(os.Stdin) + for scaner.Scan() { + s := scaner.Text() + fmt.Println("You input:", s) + } +} +``` + +可以发现输出永远无法结束。。。当初我就面对过这样的情况,Windows 下通过 `ctrl+z` 结束输入,Unix 下通过 `ctrl+d` 结束输入。 + +## channel + +像 map 一样,通道是一个使用 make 创建的数据结构的引用。当复制或作为参数传递到一个函数时,复制的是引用,这样调用者和被调用者都引用同一份数据结构。通道的默认值是 nil。 + +同类型的 chan 可以通过 `==` 进行比较,如果两者都是同一个通道数据的引用时,比较值为 true,通道也可以和 nil 进行比较。 + +接收者使用 comma-ok 判断一个通道是否已经关闭。 + +```go +x, ok := <- ch +``` + +1. chan 已经关闭,`ok == false` +2. chan 还没关闭,`ok == true` + +使用 for-range 循环从通道中读取元素,知道通道被关闭。 + +`close(channel)` 的调用通常是发送者。 +`close(channel)` 操作并不是必须的,通道的关闭操作主要目的是告诉接收者没有更多元素需要通过通道传递。chan 可以被垃圾回收器自动回收,判断一个 chan 是否可以回收的标准是否还有变量可以访问到 chan,而不是 chan 是否已经关闭了。chan 并不是系统资源,而是 Go 运行时库提供的功能。 + +### 单向 chan + +在函数参数声明时, ++ `in <-chan int` 只读通道 ++ `out chan<- int` 只写通道 + +通道的只读和只写是通过编译时检查出来的,也就是编译器在编译代码时,如果发现有向只读 chan 中写数据,或向只写 chan 中读数据时,编译器会抛出编译失败错误。现在很多成熟的 IDE 和插件都可以做到静态语法检查功能,可以让我们写代码时避免此类错误。 + +### buffered chan + ++ `cap(channel)` 判断带缓冲区通道的缓冲区长度 ++ `len(channel)` 判断带缓冲区通道中元素的个数 + +### 多个 goroutine 同时写 chan,那么如何控制 chan 的关闭呢? + +在多个 goroutine 同时写 chan 时,如果任意一个 goroutine 关闭了 chan 肯定会触发其他 goroutine 的 chan 的写操作宕机,最后引发整个程序的异常退出。 + +这个问题可以使用 `sync.WaitGroup` 来解决: + +```go +func main() { + var wg sync.WaitGroup + tasks := []string{"hello", "world", "!!!"} + ch := make(chan string) + for _, work := range tasks { + wg.Add(1) // 确保 Add 在开始 goroutine 之前执行 + go func(work string) { + defer wg.Done() // defer 确保该语句能够被执行 + ch <- work + }(work) + } + // 等待其他 goroutine 结束,然后关闭 channel,通知读者没有更多元素可以被读取 + go func() { + wg.Wait() + close(ch) + }() + for v := range ch { + fmt.Println(v) + } + fmt.Println("finish...") +} +``` + +如果 `wg.Add(1)` 放在 worker goroutine 内执行的话,有一种特殊情况是,worker goroutine 全部还没有得到执行,closer goroutine 先执行,`wg.Wait()` 判断 **counter == 0**,然后就关闭了 chan,导致读者 for-range 没有读到内容且 worker goroutine 向 chan 中写数据时宕机。 + +closer goroutine 必须在 for-range 循环之前启动,不然进入 for-range 循环后,没有任何操作关闭通道,closer goroutine 则永远没有机会启动。 + +### 并行度控制 + +程序的并行度太高、无限制的并行通常不是一个好主意,因为系统中总有限制因素,例如,对于计算性应用 CPU 的核数,对于磁盘 I/O 操作磁头和磁盘的个数,下载流所使用的网络带宽,或 Web 服务本身的容量。 + +两个方法控制并发度: + +1. 借助 buffer channel 控制最大的并发度,每次启动一个新的 goroutine 前先向 buffer channel 中写入一个元素,结束后从 buffer channel 中读取一个元素。从而控制只有一个 goroutine 结束后才能够启动新的 goroutine + +```go +func main() { + max := 10 + threshold := make(chan struct{}, max) + for { + threshold<- struct{}{} // 获取令牌 + go handle() + } +} + +func handle() { + defer func() { + <-threshold // 释放令牌 + } + // do something +} +``` + +2. 启动特定数量的 goroutine,每个 goroutine 都是从 channel 中读取任务然后执行 + +```go +type Task struct{ + task string +} + +func main() { + max := 10 + tasks := make(chan Task) + for i := 0; i < max; i++ { + go handle(tasks) + } + for i := 0; i < 100; i++{ + tasks <- Task{task: fmt.Sprintf("%d", i)} + } + close(tasks) +} + +func handle(taskList <-chan Task) { + for task := range taskList { + fmt.Println(task.task) + } +} +``` + +## select + +```go +select{} // 永远等待 +``` + +空的 select 几乎不会消耗 CPU,我做了以下实验得出该结论: + +```go +func main() { + runtime.GOMAXPROCS(2) // 设置该程序使用最多的OS线程 + go func() { + loop: + for{ + goto loop + } + }() + select { + + } +} +``` + +```go +func main() { + runtime.GOMAXPROCS(2) + go func() { + loop: + for{ + goto loop + } + }() + loop: + for { + goto loop + } +} +``` + +通过 top 命令查看当前运行的程序,查看到 CPU 占用率是 100%,如果 `select{}` 换成 `for{}` CPU 占用率是 200%。 + +## time.Tick + +`time.Tick` 函数的行为很像创建一个 goroutine 在循环里面调用 `time.Sleep`,然后在它每次醒来时发送事件。如果停止监听 tick,但是计时器 goroutine 还在运行,徒劳地向一个没有 goroutine 在接收的通道中不断发送————**goroutine 泄露** + +Tick 函数很方便使用,但是它仅仅在应用整个生命周期都需要时才适合。否则,我们需要使用以下这个模式: + +```go +ticker := time.NewTicker(time.Second) +<-ticker.C // 从 ticker 通道中获取元素 +ticker.Stop() // 释放资源,终止 ticker 的 goroutine +``` + +## 通知 goroutine 取消 + +chan 的零值是 nil,nil 通道有时非常有用,因为对 nil 通道发送和接收将永远阻塞。 + +一个 goroutine 无法直接观察到另一个 goroutine 的状态,有时候我们需要通过广播通知多个 goroutine 取消执行。这个功能可以配合关闭 channel 和 select 来做到。 + +如果一个 channel 已经关闭了,那么从该 channel 中读取数据不会阻塞当前 goroutine。 + +创建一个 channel 专门用于通知某些注册退出消息的 goroutine 退出执行,不向该 goroutine 中发送任何消息,通过关闭 channel,使得其他读操作,比如 select 中的 case 可以执行,每个注册该消息的 goroutine 都能收到退出消息,从而实现广播退出消息的功能。 + + ```go +func main() { + closeSignal := make(chan struct{}) + go handle(closeSignal) + time.AfterFunc(5 * time.Second, func() { + close(closeSignal) + }) + time.Sleep(10 * time.Second) + fmt.Println("main exit") +} + +func handle(closeSignal chan struct{}) { + loop: + for { + if cancelled(closeSignal){ + fmt.Println("Get cancel signal") + break loop + } + fmt.Println("running...") + time.Sleep(time.Second) + } + fmt.Println("goroutine exit") +} + +func cancelled(closeSignal chan struct{}) bool { + select { + case <-closeSignal: + return true + default: + return false + } +} + ``` + +## 使用共享变量实现并发 + +> 布要通过共享内存来通信,而应该通过通信来共享内存 + +可以限制只有一个 goroutine 能够访问变量,从而达到防止了并发带来的读写不安全。如以下的 balance 变量只能够在运行 teller 的 goroutine 访问。 + +```go +var deposits = make(chan int) +var balances = make(chan int) +type withdrawStruct struct{ + amount chan int + result chan bool +} +var withdraw = withdrawStruct{amount: make(chan int), result: make(chan bool)} + +func Deposit(amount int) { + deposits <- amount +} + +func Balance() int { + return <- balances +} + +func Withdraw(amount int) bool { + withdraw.amount<- amount + result := <-withdraw.result + return result +} + +func teller() { + // 将 balance 变量限制在 teller goroutine 内 + // 避免多个 goroutine 访问带来的异常 + balance := 0 + for { + select { + case balances <- balance: + case amount := <-deposits: + balance += amount + case amount := <-withdraw.amount: + if amount >= balance { + withdraw.result<- false + } else { + balance -= amount + withdraw.result<- true + } + } + } +} + +func main() { + go teller() + for i := 0; i < 200; i++ { + go Deposit(1) + } + for i := 0; i < 50; i++ { + go Withdraw(1) + } + for i := 0; i < 10; i++ { + go func() { + fmt.Println(Balance()) + }() + } + time.Sleep(time.Second) + fmt.Println(Balance()) +} +``` + +或者使用 chan 来 pipline 中传递某个类型的指针,但是如果上层流水线 goroutine 将指针传递给下一级后便不会再通过指针访问变量,从而达到任何时刻变量只对一个 goroutine 可见。 + +比如蛋糕师做蛋糕分为三道工序,由三个师傅分别负责做蛋糕、加糖衣、加奶油。只要蛋糕传递给下一个师傅后,上一个师傅就不再对蛋糕做任何修改。 + +```go +type Product struct { + state string +} + +func Cake(cake chan<- *Product) { + c := Product{state:"cake"} + cake <- &c +} + +func Sugar(cake <-chan *Product, ice chan <- *Product) { + c := <-cake + c.state = "sugar" + ice <- c +} + +func ice(ice chan *Product, product chan <- *Product) { + c := <- ice + c.state = "ice" + product <- c +} +``` + +第三种方法避免竞态是允许多个 goroutine 同时访问同一变量,但在同一时间只有一个 goroutine 可以访问。采用*互斥机制* + +无论是为了保护包级别的变量,还是结构中的字段,当使用一个互斥量 Mutex 时,都应该确保 Mutex 以及被保护的变量都没有导出。 + +### `sync.Mutex` vs `sync.RWMutex` + +**sync.RWMutex** 仅在绝大部分 goroutine 都在获取读锁且锁竞争比较激烈时(即一般 goroutine 都要等待后才能获得锁),RWMutex 才有优势。因为 RWMutex 需要更加复杂的内部簿记工作,所以在竞争不激烈时 **sync.RWMutex** 比 **sync.Mutex** 慢。 + +如果计算机中服务器有多个 CPU,每个 CPU 中都拥有自己的 cache,cache 并不会实时地与内存保持同步,那么可能会导致多个运行在不同 CPU 上的 goroutine 对同一个内存变量观察到不一样的值。 + +```go +var x, y int +go func() { + x = 1 // A1 + fmt.print("y:", y, " ") // A2 +} +go fun() { + y = 1 // B1 + fmt.Print("x:", x, " ") // B2 +} +``` + +我们期待的输出是以下四个之一: + +``` +y:0 x:1 +x:0 y:1 +x:1 y:1 +y:1 x:1 +``` + +但事实上也可能出现以下两种情况: + +``` +x:0 y:0 +y:0 x:0 +``` + +一个原因可能是如 `x:0 y:0` 情况, A 和 B 运行在不同 CPU 上,导致 B1 在更新 y 的值,但是缓存还未同步到内存中,A2 读到 y 的值已经过期。 + +另一个原因可能是编译器对指令进行了重排,因为 A1 和 A2在同一个 goroutine 中应该谁先执行都不会影响对方的正确性,也就是经过编译器优化后执行顺序变为:A2 --> A1 + +**所以尽可能将变量的访问限制在单个 goroutine 中,对于其他变量使用互斥锁。** + +### sync.Once 保证函数只被执行一次 + +有些场景下需要控制某个函数或者变量的方法只能被执行一次,那么我们可以通过 sync.Once 提供的 Do 方法做到。如下: + +```go +func main() { + var once sync.Once + fn := func() { + fmt.Println("hello world") + } + once.Do(fn) + once.Do(fn) +} +``` + +hello world 只被输出一次。 +当然如果自行调用 `fn()` 就可以逃避 sync.Once 的控制,看自觉。 + +## 竞态检测器 + +Go 语言运行时和工具链装备了一个精致并易于使用的动态分析工具:竞态检测器(race detector) + +简单地把 **-race** 命令行参数加入到 go build、go run、go test 命令里边即可以使用该功能。它会让编译器为应用或测试构建一个修改后的版本,这个版本有额外的手法用于高效记录在执行时对共享变量的所有访问,以及读写这些变量的 goroutine 的标示。除此之外,修改后的版本还会记录所有的同步事件,包括 go 语言、通道操作、(*sync.Mutex).Lock 调用、(*sync.WaitGroup).Wait 调用等。 + +这个工具会输出一份报告,包含变量的标示以及读写 goroutine 当时的调用栈,通常情况下这些信息足以定位问题。 + +由于额外的簿记工作,带竞态检查的程序在执行时需要更长的时间和更多的内存,但即使很多生产环境的任务,这种额外开支也是可以接收的。对于那些不常发生的竞态,使用竞态检测器可以帮助节省数小时甚至数天的调式时间。 + +## 一秒内 channel 的读写次数 + +```go +func main() { + ch := make(chan string) + go func(ch <-chan string) { + for { + <-ch + } + }(ch) + counter := 0 + deadline := time.After(1 * time.Second) + flag := true + for flag { + select { + case <-deadline: + flag = false + close(ch) + default: + ch <- "hello" + counter++ + } + } + fmt.Println(counter) +} +``` + 在我电脑上输出结果为 **3353895**,也就是一秒内两个 goroutine 通过 channel 进行了三百多万次通信。 \ No newline at end of file diff --git a/src/md/2018-03-05-the-go-programming-language-note4.md b/src/md/2018-03-05-the-go-programming-language-note4.md index 8fb0a92..404bc4d 100644 --- a/src/md/2018-03-05-the-go-programming-language-note4.md +++ b/src/md/2018-03-05-the-go-programming-language-note4.md @@ -1,184 +1,184 @@ ---- -layout: post -title: "《Go程序设计语言》笔记四" -date: 2018-03-05 12:00:05 +0800 -categories: golang ---- - -# 包和 go 工具 - -## 包 - -Go 通过 import 语句导入的包一般在 **$GOROOT**、**$GOPATH** 下,**$GOROOT** 下导入的是 Go build-in 的包,比如 `math/rand`;而 **$GOPATH** 下导入的是通过 `go get` 命令从互联网上拉下的包,或者是自己之前写的程序。 - -1. 不管包的导入路径是什么,如果该包定义了一条命令(可执行 Go 程序),那么它总是使用名称 main。告诉 go build 必须调用连接器生成可执行文件 -2. 目录中可能有一些名字以 _test.go 结尾,包名中会出现以 _test 结尾。这样的目录包含两个包一个普通的,一个外部测试包。`_test` 后缀告诉 go test 两个包都要构建,并且指明文件属于哪个包。外部测试包避免所依赖的导入图中的循环依赖 -3. 有一些依赖管理工具会在包导入路径的尾部追加版本号后缀,如 `"gokpg.in/yaml.v2"`。包名不包含后缀,因此这种情况下包名为 **yaml** - -### 重命名导入 - -```go -import ( - "crypto/rand" - mrand "math/rand" -) -``` - -### 空导入 - -```go -import ( - _ "github.com/lib/pq" -) -``` - -空导入将包的名字重命名为 **_**,也就是无法在代码中引用该包中的变量和函数。这会产生什么影响呢? - -虽然无法在代码只能够使用该包中的变量和函数,但该包下的包级别变量还是会进行声明和初始化,`init()` 函数还是会执行。如上空导入的作用就是加载 postgres 的数据库驱动,完成数据库驱动的注册。 - -## go 工具 - -### 工作空间组织 - -$GOPATH 下有三个文件夹,分别是:src、pkg、bin - -1. src 存储源代码 -2. pkg 存储构建工具编译后的包,go install 命令将会在 pkg 产生相应的 .a 文件 -3. bin 存储可执行文件 - -go build 命令构建所有需要的包以及它们所有的依赖性,然后丢弃除了最终可执行程序之外的所有编译后的代码。显然如果当项目变得复杂,引用的包多了以后,重新编译依赖性的时间明显变慢。 - -go install 命令和 go build 命令相似,区别是它会*保存*每一个包的编译代码和命令,而不是把他们丢弃,结果保存在 $GOPATH/pkg 目录下。 - -go build 命令会特殊对待导入路径中包含路径片段 internal 的情况,这些包叫内部包。内部包只能够被位于以 internal 目录的父目录为根目录的树中。例如,给定下面的包,net/http/internal/chunked 可以从 net/http/httputil 或 net/http 导入,但是不能从 net/url 进行导入。 - -# 测试 - -在 *_test.go 文件中,是那种函数需要特殊对待,即功能测试函数、基准测试函数和示例函数。 - -1. *功能测试函数* 是以 Test 前缀命名的函数,用来检测一些程序逻辑的正确性,go test 运行测试函数,并且报告结果是 PASS 还是 FAIL。 -2. *基准测试函数* 的名称以 Benchmark 开头,用来测试某些操作的性能,go test 汇报操作的平均执行时间。 -3. *示例函数* 以 Example 开头,用来提供机器检查过的文档。 - -每一个测试文件必须导入 testing 包,这些函数签名如下: - -```go -func TestXxx(t *testing.T) { - -} -``` - -1. go test -v 输出包中每个测试用力的名称和执行的时间 -2. go test -run="regexp" -run 参数是一个正则表达式,它可以使得 go test 只运行那些测试函数名称匹配给定模式的函数,而不用重新运行所有的测试用例 - -## 外部测试包 - -在编写测试用例的时候,为了避免包之间的循环引用。比如 net/http 依赖于 net/url,那么当我们在 net/url 保证写测试时候引用到了 net/http,这就出现了循环引用。解决方法是将测试写在 net/url_test 包中。 - -查看某个包内的 go 文件: - -``` -go list -f={{.GoFiles}} fmt -``` - -查看某个包内的测试文件: - -``` -go list -f={{.TestGoFiles}} fmt -``` - -查看某个包的外部测试包: - -``` -go list -f={{.XTestGoFiles}} fmt -``` - -如果一个包内的某些函数需要进行白盒测试,而这些函数又正巧是不可导出的呢? - -这问题可以通过写像 fmt/export_test.go 类似的文件,使用一个可导出的别名指向该函数: - -```go -package fmt - -var IsSpace = isSpace -var Parsenum = parsenum -``` - -## cover 覆盖率 - -从本质上看,测试从来不会结束,因为可能的输入有无限多种情况。无论多少测试都无法证明一个包是没 bug 的,最好的情况下,这些包是可以在很多重要场景下使用的。 - -```bash -go tool cover -``` - -工具为我们查看测试代码的覆盖率起到了很好的帮助,它可以输出一个 html 文档,在浏览器中打开大大增强了可阅读性。 - -## benchmark 基准测试 - -基准测试就是在一定的工作负载之下检测程序性能的一种方法。在 Go 里面,基准测试函数看上去像一个测试函数,但是前缀是 Benchmark 并且拥有 *testing.B 参数用来提供大多数和 *testing.T 相同的方法,额外增加了一些与性能测试相关方法,其中提供了一个整形成员 N,用来指定被检测操作的执行次数。 - -```go -import "testing" - -func BenchmarkXxx(b *testing.B) { - for i := 0; i < b.N; i++ { - // test code - } -} -``` - -默认情况下不会执行任何基准测试,标记 -bench 参数指定了要运行的基准测试,它是一个匹配 Benchmark 函数名称的**正则表达式**,它有默认值不匹配任何函数。模式 "." 使它匹配包 word 中所有的基准测试函数。 - -```go -go test -bench="." - -go test -bench="Xxx" -``` - -## profile 性能剖析 - -性能剖析是通过自动化手段在程序执行过程中给予一些性能事件的采样来进行性能评价,然后再从这些采样中推断分析,得到统计报告。 - -Go 支持很多性能剖析方式,每一个都和一个不同方面的性能指标相关,但是它们都需要记录一些相关事件,每一个都有一个相关的栈信息————在事件发生时活跃的函数调用栈。工具 go test 内置支持一些类别的性能剖析。 - -**CPU 性能剖析**识别执行过程中需要 CPU 最多的函数。 - -```bash -go test -cpuprofile=cpu.out -``` - -**堆性能剖析**识别出负责分配最多内存的语句。 - -```bash -go test -blockprofile=block.out -``` - -**阻塞性能剖析**识别出那些阻塞协程最久的操作。 - -```bash -go test -memprofile=mem.out -``` - -性能剖析并非只能够通过命令行来启动,对于长时间运行的程序也可以开启性能剖析,Go 运行时的性能剖析特性可以让程序员通过 runtime API 来启用。 - -在获取性能分析结果后,需要使用 pprof 工具来分析它,基本的参数有两个,产生性能剖析结果的可执行文件和性能剖析日志。 - -```bash -go tool pprof -``` - -## Example 函数 - -函数以 Example 开头,既没有参数也没有返回结果,如: - -```go -func ExampleXxx() { - fmt.Println(Xxx()) -} -``` - -示例函数有三个目的: -1. 作为文档,比起乏味的描述,举例子是描述函数功能最简洁直观的方法。和普通文档不一样,ExampleXxx 函数是 Go 代码必须要通过编译检查,它会随着代码的改变而改变。 -2. 可以通过 go test 运行的可执行测试。 -3. 提供手动实验代码。 +--- +layout: post +title: "《Go程序设计语言》笔记四" +date: 2018-03-05 12:00:05 +0800 +categories: golang +--- + +# 包和 go 工具 + +## 包 + +Go 通过 import 语句导入的包一般在 **$GOROOT**、**$GOPATH** 下,**$GOROOT** 下导入的是 Go build-in 的包,比如 `math/rand`;而 **$GOPATH** 下导入的是通过 `go get` 命令从互联网上拉下的包,或者是自己之前写的程序。 + +1. 不管包的导入路径是什么,如果该包定义了一条命令(可执行 Go 程序),那么它总是使用名称 main。告诉 go build 必须调用连接器生成可执行文件 +2. 目录中可能有一些名字以 _test.go 结尾,包名中会出现以 _test 结尾。这样的目录包含两个包一个普通的,一个外部测试包。`_test` 后缀告诉 go test 两个包都要构建,并且指明文件属于哪个包。外部测试包避免所依赖的导入图中的循环依赖 +3. 有一些依赖管理工具会在包导入路径的尾部追加版本号后缀,如 `"gokpg.in/yaml.v2"`。包名不包含后缀,因此这种情况下包名为 **yaml** + +### 重命名导入 + +```go +import ( + "crypto/rand" + mrand "math/rand" +) +``` + +### 空导入 + +```go +import ( + _ "github.com/lib/pq" +) +``` + +空导入将包的名字重命名为 **_**,也就是无法在代码中引用该包中的变量和函数。这会产生什么影响呢? + +虽然无法在代码只能够使用该包中的变量和函数,但该包下的包级别变量还是会进行声明和初始化,`init()` 函数还是会执行。如上空导入的作用就是加载 postgres 的数据库驱动,完成数据库驱动的注册。 + +## go 工具 + +### 工作空间组织 + +$GOPATH 下有三个文件夹,分别是:src、pkg、bin + +1. src 存储源代码 +2. pkg 存储构建工具编译后的包,go install 命令将会在 pkg 产生相应的 .a 文件 +3. bin 存储可执行文件 + +go build 命令构建所有需要的包以及它们所有的依赖性,然后丢弃除了最终可执行程序之外的所有编译后的代码。显然如果当项目变得复杂,引用的包多了以后,重新编译依赖性的时间明显变慢。 + +go install 命令和 go build 命令相似,区别是它会*保存*每一个包的编译代码和命令,而不是把他们丢弃,结果保存在 $GOPATH/pkg 目录下。 + +go build 命令会特殊对待导入路径中包含路径片段 internal 的情况,这些包叫内部包。内部包只能够被位于以 internal 目录的父目录为根目录的树中。例如,给定下面的包,net/http/internal/chunked 可以从 net/http/httputil 或 net/http 导入,但是不能从 net/url 进行导入。 + +# 测试 + +在 *_test.go 文件中,是那种函数需要特殊对待,即功能测试函数、基准测试函数和示例函数。 + +1. *功能测试函数* 是以 Test 前缀命名的函数,用来检测一些程序逻辑的正确性,go test 运行测试函数,并且报告结果是 PASS 还是 FAIL。 +2. *基准测试函数* 的名称以 Benchmark 开头,用来测试某些操作的性能,go test 汇报操作的平均执行时间。 +3. *示例函数* 以 Example 开头,用来提供机器检查过的文档。 + +每一个测试文件必须导入 testing 包,这些函数签名如下: + +```go +func TestXxx(t *testing.T) { + +} +``` + +1. go test -v 输出包中每个测试用力的名称和执行的时间 +2. go test -run="regexp" -run 参数是一个正则表达式,它可以使得 go test 只运行那些测试函数名称匹配给定模式的函数,而不用重新运行所有的测试用例 + +## 外部测试包 + +在编写测试用例的时候,为了避免包之间的循环引用。比如 net/http 依赖于 net/url,那么当我们在 net/url 保证写测试时候引用到了 net/http,这就出现了循环引用。解决方法是将测试写在 net/url_test 包中。 + +查看某个包内的 go 文件: + +``` +go list -f={{.GoFiles}} fmt +``` + +查看某个包内的测试文件: + +``` +go list -f={{.TestGoFiles}} fmt +``` + +查看某个包的外部测试包: + +``` +go list -f={{.XTestGoFiles}} fmt +``` + +如果一个包内的某些函数需要进行白盒测试,而这些函数又正巧是不可导出的呢? + +这问题可以通过写像 fmt/export_test.go 类似的文件,使用一个可导出的别名指向该函数: + +```go +package fmt + +var IsSpace = isSpace +var Parsenum = parsenum +``` + +## cover 覆盖率 + +从本质上看,测试从来不会结束,因为可能的输入有无限多种情况。无论多少测试都无法证明一个包是没 bug 的,最好的情况下,这些包是可以在很多重要场景下使用的。 + +```bash +go tool cover +``` + +工具为我们查看测试代码的覆盖率起到了很好的帮助,它可以输出一个 html 文档,在浏览器中打开大大增强了可阅读性。 + +## benchmark 基准测试 + +基准测试就是在一定的工作负载之下检测程序性能的一种方法。在 Go 里面,基准测试函数看上去像一个测试函数,但是前缀是 Benchmark 并且拥有 *testing.B 参数用来提供大多数和 *testing.T 相同的方法,额外增加了一些与性能测试相关方法,其中提供了一个整形成员 N,用来指定被检测操作的执行次数。 + +```go +import "testing" + +func BenchmarkXxx(b *testing.B) { + for i := 0; i < b.N; i++ { + // test code + } +} +``` + +默认情况下不会执行任何基准测试,标记 -bench 参数指定了要运行的基准测试,它是一个匹配 Benchmark 函数名称的**正则表达式**,它有默认值不匹配任何函数。模式 "." 使它匹配包 word 中所有的基准测试函数。 + +```go +go test -bench="." + +go test -bench="Xxx" +``` + +## profile 性能剖析 + +性能剖析是通过自动化手段在程序执行过程中给予一些性能事件的采样来进行性能评价,然后再从这些采样中推断分析,得到统计报告。 + +Go 支持很多性能剖析方式,每一个都和一个不同方面的性能指标相关,但是它们都需要记录一些相关事件,每一个都有一个相关的栈信息————在事件发生时活跃的函数调用栈。工具 go test 内置支持一些类别的性能剖析。 + +**CPU 性能剖析**识别执行过程中需要 CPU 最多的函数。 + +```bash +go test -cpuprofile=cpu.out +``` + +**堆性能剖析**识别出负责分配最多内存的语句。 + +```bash +go test -blockprofile=block.out +``` + +**阻塞性能剖析**识别出那些阻塞协程最久的操作。 + +```bash +go test -memprofile=mem.out +``` + +性能剖析并非只能够通过命令行来启动,对于长时间运行的程序也可以开启性能剖析,Go 运行时的性能剖析特性可以让程序员通过 runtime API 来启用。 + +在获取性能分析结果后,需要使用 pprof 工具来分析它,基本的参数有两个,产生性能剖析结果的可执行文件和性能剖析日志。 + +```bash +go tool pprof +``` + +## Example 函数 + +函数以 Example 开头,既没有参数也没有返回结果,如: + +```go +func ExampleXxx() { + fmt.Println(Xxx()) +} +``` + +示例函数有三个目的: +1. 作为文档,比起乏味的描述,举例子是描述函数功能最简洁直观的方法。和普通文档不一样,ExampleXxx 函数是 Go 代码必须要通过编译检查,它会随着代码的改变而改变。 +2. 可以通过 go test 运行的可执行测试。 +3. 提供手动实验代码。 diff --git a/src/md/2018-03-07-golang-unsafe.md b/src/md/2018-03-07-golang-unsafe.md index 2ddbeb7..0001320 100644 --- a/src/md/2018-03-07-golang-unsafe.md +++ b/src/md/2018-03-07-golang-unsafe.md @@ -1,169 +1,169 @@ ---- -layout: post -title: "Golang unsafe 低级编程" -date: 2018-03-06 09:00:05 +0800 -categories: golang ---- - -# unsafe - -虽然我们程序中引入 unsafe `import "unsafe"` 像是引入其他使用 go 实现的包一样,unsafe 包下的功能是不是通过 go 代码实现的,而是通过**编译器**实现的。 - -unsafe 中的功能暴露了 Go 底层的实现细节,虽然 Go 是跨平台的,但是每个平台上 Go 底层实现都不一样,这样就造成在不同平台上 unsafe 的表现可能有所不同,而且 unsafe 不保证向后兼容。unsafe 包广泛被和操作系统交互的低级包中,如 runtime、os、syscall 和 net。 - -> 普通程序不需要使用 unsafe - -举一个例子体现 unsafe 的奇怪: - -```go -type ArbitraryType int - -func Sizeof(x ArbitraryType) uintptr -``` - -如果站在 Go 语法上说,unsafe.Sizeof() 不可能接收任意类型的参数,但是事实上 unsafe.Sizeof() 可以接收任何类型的参数,所以说这个非常奇怪。**编译器做了手脚** - -slice 中结构中不是真的存储了一个数组的指针,而是一个 unsafe.Pointer: - -```go -type slice struct { - array unsafe.Pointer - len int - cap int -} -``` - -## 字 - -int 类型的长度都是跟当前操作系统的位数相关的,比如在 32 位系统上为 32 位,在 64 系统上为 64 位。 - -操作处理数据最小的单位不是 bit,也不是 byte 而是一个字(不是字节)。那么一个字的长度是多少呢?32 位操作系统为 32 位 4 字节,64 位操作系统为 64 位 8 字节。 - -我电脑为 64 位操作系统: - -```go -fmt.Println(unsafe.Sizeof(int(0))) // 8 -``` - -## 结构体中变量编排 - -如果结构体成员的类型是不同的,那么**将相同类型的成员定义在一起可以节省内存空间**。以下三个结构体拥有相同的成员,但是第一个定义比其他两个定义要多占内存。 - -```go -fmt.Println(unsafe.Sizeof(struct { - bool; float64; int16 - }{})) // 24 - fmt.Println(unsafe.Sizeof(struct { - float64; int16; bool - }{})) // 16 - fmt.Println(unsafe.Sizeof(struct { - bool; int16; float64 - }{})) // 16 -``` - -```go -var x struct { - a bool - b int16 - c []int -} -``` - -其内存布局如下: - -![内存对齐示例图](../../src/img/2018-03-05-golang-struct-mem-model.jpg) - -```go -Sizeof(x) = 32 Alignof(x) = 8 -Sizeof(x.a) = 1 Alignof(x.a) = 1 Offsetof(x.a) = 0 -Sizeof(x.b) = 2 Alignof(x.b) = 2 Offsetof(x.b) = 2 -Sizeof(x.c) = 24 Alignof(x.c) = 8 Offsetof(x.c) = 8 -``` - -Alignof 查看的对齐方式。x.a 是一个字节一个字节地对齐,x.b 是两个字节两个字节地对齐,x.c 是八个字节八个字节对齐。 - -Offsetof 查看变量在从结构体开头的偏移量。 - -Sizeof、Alignof、Offsetof 三个方法是安全的,我们可以通过他们来查看某个结构体中变量的大小、对齐和排列等信息。 - -## unsafe.Pointer - -unsafe.Pointer 是一个特殊的指针,能够指向任何类型的变量地址。但是无法直接使用 unsafe.Pointer 指针对变量进行操作或访问,因为还不知道指向地址的具体类型,因为只有知道了具体类型后才知道如何解析里面的数据。如,`1101000` 一个字节可以解析为 'h' 或者是 104。 - -我们也可以将指针的内容强制解析为某些类型,或者直接对其数据进行更改(这是不安全的)。下面例子中,我们将浮点数的内容当做 int64 来解析,然后又将整形数目写入到本来是浮点数的内存中,最后当做浮点数来解析: - -```go -f := 1.0 -pf := &f -pi := (*int64)(unsafe.Pointer(pf)) -fmt.Printf("%d\n", *pi) // 4607182418800017408 -*pi = 0 -fmt.Printf("%g\n", f) // 0 -``` - -**这样的代码可读性必然差。** - -还可以访问任意本程序中的任意内存地址? - -是的,uintptr 和 unsafe.pointer 之间进行转换,而 unsafe.Pointer 可以转化为任意类型的指针。但是 uintptr 传递的不都是合法的内存地址,这样做会破坏类型系统。但是还是来个 demo 看看: - -```go -var x struct { - a bool - b int16 - c []int - } -pb := (*int16)(unsafe.Pointer(uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b))) -fmt.Printf("%v %v\n", pb, &x.b) // 0xc42000a062 0xc42000a062 -*pb = 1 -fmt.Println(x.b) // 1 -``` - -上面我们中规中矩采用的是 x 的地址再加偏移量得到的 b 的地址,要是我们随便读取一个地址呢? - -```go -ptr := (*int16)(unsafe.Pointer(uintptr(0xc42000a0))) -fmt.Printf("%v %b\n", ptr, *ptr) -``` - -输出: - -``` -unexpected fault address 0xc42000a0 -fatal error: fault -[signal SIGSEGV: segmentation violation code=0x1 addr=0xc42000a0 pc=0x48988b] -``` - -Ooops,说了不是所有地址都是合法的内存地址。 - -还有一种很隐晦的错误: - -```go -tmp := uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b) -pb := (*int16)(unsafe.Pointer(tmp)) -*pb = 1 -``` - -如果进行 GC 后,变量的位置可能已经发生了移动,这时候地址可能已经不是之前那个地址了。当进行垃圾回收时变量移动,指针(unsafe.Pointer、*T)的值也跟随这变量的地址改变而改变,但是上面例子 tmp 是一个 uintptr 类型变量,GC 不会进行指针同步,缓存失效。goroutine 的连续栈增长时也会导致类似错误。 - -类似的错误: - -```go -ptr := uintptr(unsafe.Pointer(new(T))) -``` - -变量 new(T) 可能在创建后马上被 GC 回收,因为 GC 检测不到有任何指针指向该内存。**uintptr 不是指针** - -两个地址相同的变量一定相等吗? - -如果你还记得上面那个图片,那么你就知道不是的。不信的话运行下面的程序: - -```go -var x struct { - a bool - b int16 - c []int -} -fmt.Printf("%p %p\n", &x, &x.a) -``` - +--- +layout: post +title: "Golang unsafe 低级编程" +date: 2018-03-06 09:00:05 +0800 +categories: golang +--- + +# unsafe + +虽然我们程序中引入 unsafe `import "unsafe"` 像是引入其他使用 go 实现的包一样,unsafe 包下的功能是不是通过 go 代码实现的,而是通过**编译器**实现的。 + +unsafe 中的功能暴露了 Go 底层的实现细节,虽然 Go 是跨平台的,但是每个平台上 Go 底层实现都不一样,这样就造成在不同平台上 unsafe 的表现可能有所不同,而且 unsafe 不保证向后兼容。unsafe 包广泛被和操作系统交互的低级包中,如 runtime、os、syscall 和 net。 + +> 普通程序不需要使用 unsafe + +举一个例子体现 unsafe 的奇怪: + +```go +type ArbitraryType int + +func Sizeof(x ArbitraryType) uintptr +``` + +如果站在 Go 语法上说,unsafe.Sizeof() 不可能接收任意类型的参数,但是事实上 unsafe.Sizeof() 可以接收任何类型的参数,所以说这个非常奇怪。**编译器做了手脚** + +slice 中结构中不是真的存储了一个数组的指针,而是一个 unsafe.Pointer: + +```go +type slice struct { + array unsafe.Pointer + len int + cap int +} +``` + +## 字 + +int 类型的长度都是跟当前操作系统的位数相关的,比如在 32 位系统上为 32 位,在 64 系统上为 64 位。 + +操作处理数据最小的单位不是 bit,也不是 byte 而是一个字(不是字节)。那么一个字的长度是多少呢?32 位操作系统为 32 位 4 字节,64 位操作系统为 64 位 8 字节。 + +我电脑为 64 位操作系统: + +```go +fmt.Println(unsafe.Sizeof(int(0))) // 8 +``` + +## 结构体中变量编排 + +如果结构体成员的类型是不同的,那么**将相同类型的成员定义在一起可以节省内存空间**。以下三个结构体拥有相同的成员,但是第一个定义比其他两个定义要多占内存。 + +```go +fmt.Println(unsafe.Sizeof(struct { + bool; float64; int16 + }{})) // 24 + fmt.Println(unsafe.Sizeof(struct { + float64; int16; bool + }{})) // 16 + fmt.Println(unsafe.Sizeof(struct { + bool; int16; float64 + }{})) // 16 +``` + +```go +var x struct { + a bool + b int16 + c []int +} +``` + +其内存布局如下: + +![内存对齐示例图](../../src/img/2018-03-05-golang-struct-mem-model.jpg) + +```go +Sizeof(x) = 32 Alignof(x) = 8 +Sizeof(x.a) = 1 Alignof(x.a) = 1 Offsetof(x.a) = 0 +Sizeof(x.b) = 2 Alignof(x.b) = 2 Offsetof(x.b) = 2 +Sizeof(x.c) = 24 Alignof(x.c) = 8 Offsetof(x.c) = 8 +``` + +Alignof 查看的对齐方式。x.a 是一个字节一个字节地对齐,x.b 是两个字节两个字节地对齐,x.c 是八个字节八个字节对齐。 + +Offsetof 查看变量在从结构体开头的偏移量。 + +Sizeof、Alignof、Offsetof 三个方法是安全的,我们可以通过他们来查看某个结构体中变量的大小、对齐和排列等信息。 + +## unsafe.Pointer + +unsafe.Pointer 是一个特殊的指针,能够指向任何类型的变量地址。但是无法直接使用 unsafe.Pointer 指针对变量进行操作或访问,因为还不知道指向地址的具体类型,因为只有知道了具体类型后才知道如何解析里面的数据。如,`1101000` 一个字节可以解析为 'h' 或者是 104。 + +我们也可以将指针的内容强制解析为某些类型,或者直接对其数据进行更改(这是不安全的)。下面例子中,我们将浮点数的内容当做 int64 来解析,然后又将整形数目写入到本来是浮点数的内存中,最后当做浮点数来解析: + +```go +f := 1.0 +pf := &f +pi := (*int64)(unsafe.Pointer(pf)) +fmt.Printf("%d\n", *pi) // 4607182418800017408 +*pi = 0 +fmt.Printf("%g\n", f) // 0 +``` + +**这样的代码可读性必然差。** + +还可以访问任意本程序中的任意内存地址? + +是的,uintptr 和 unsafe.pointer 之间进行转换,而 unsafe.Pointer 可以转化为任意类型的指针。但是 uintptr 传递的不都是合法的内存地址,这样做会破坏类型系统。但是还是来个 demo 看看: + +```go +var x struct { + a bool + b int16 + c []int + } +pb := (*int16)(unsafe.Pointer(uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b))) +fmt.Printf("%v %v\n", pb, &x.b) // 0xc42000a062 0xc42000a062 +*pb = 1 +fmt.Println(x.b) // 1 +``` + +上面我们中规中矩采用的是 x 的地址再加偏移量得到的 b 的地址,要是我们随便读取一个地址呢? + +```go +ptr := (*int16)(unsafe.Pointer(uintptr(0xc42000a0))) +fmt.Printf("%v %b\n", ptr, *ptr) +``` + +输出: + +``` +unexpected fault address 0xc42000a0 +fatal error: fault +[signal SIGSEGV: segmentation violation code=0x1 addr=0xc42000a0 pc=0x48988b] +``` + +Ooops,说了不是所有地址都是合法的内存地址。 + +还有一种很隐晦的错误: + +```go +tmp := uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b) +pb := (*int16)(unsafe.Pointer(tmp)) +*pb = 1 +``` + +如果进行 GC 后,变量的位置可能已经发生了移动,这时候地址可能已经不是之前那个地址了。当进行垃圾回收时变量移动,指针(unsafe.Pointer、*T)的值也跟随这变量的地址改变而改变,但是上面例子 tmp 是一个 uintptr 类型变量,GC 不会进行指针同步,缓存失效。goroutine 的连续栈增长时也会导致类似错误。 + +类似的错误: + +```go +ptr := uintptr(unsafe.Pointer(new(T))) +``` + +变量 new(T) 可能在创建后马上被 GC 回收,因为 GC 检测不到有任何指针指向该内存。**uintptr 不是指针** + +两个地址相同的变量一定相等吗? + +如果你还记得上面那个图片,那么你就知道不是的。不信的话运行下面的程序: + +```go +var x struct { + a bool + b int16 + c []int +} +fmt.Printf("%p %p\n", &x, &x.a) +``` + diff --git a/src/md/2018-03-08-why-golang.md b/src/md/2018-03-08-why-golang.md index 08c29d9..dec902f 100644 --- a/src/md/2018-03-08-why-golang.md +++ b/src/md/2018-03-08-why-golang.md @@ -1,43 +1,43 @@ ---- -layout: post -title: "Golang unsafe 低级编程" -date: 2018-02-05 09:00:05 +0800 -categories: golang ---- - -我为什么要学 golang? -golang 相对其他语言的优势: -1. goroutine 适合用于高并发,但是 goroutine 之间没有优先级 -2. channel 的设计 - -channel 能够实现很多同步,需要使用一定的技巧。比如控制最多执行的 goroutine,select 通知一个 goroutine 的退出。 - -1 和 2 都是基于 go 运行时的强大 - -golang 的 GC 显然不算强大,应该比不上 JVM - -结构体、接口设计与 C 系语言不一样。接口的实现是隐式的 - -defer 语句确保某些操作能够被执行 - -不一样的错误异常处理,将错误以返回值的形式传递。 - -听说类型别名使得代码更加易于维护 - -指针,类似于 java 等语言的引用 - -可变长的 goroutine 栈,最多能够扩展至 1GB,可以执行很多层递归 - -go 的工具链,但是我还没使用过高级功能 - -内置函数都是通过调用 C 来完成的,cgo协议。所以每个语言内置的东西都差不多 - -背后有 Google 维护,强大的创新力,很多大公司使用 Go,使得 Go 有创新力 - -语法糖上没有 Python 强大,当然做小事情的时候写起来没有 Python 方便,也 Python 的第三方包丰富。but golang 打包后只是一个执行文件,而且非常小。而 java 编译后就是多个 class 文件然后在真实运行时在一个一个地连接在一起。Python GIL 啊。。。 - -Python 有 GIL 锁,在多核高并发并不适合,Python 的协程书写不方便 - -当然 Go 只是一门编程语言,在功能上 Go 能够实现的其他语言同样能够实现。就像英文、中文一样,使用人数越多,该语言就越有价值。编程语言也类似,使用的人越多,文档也就越丰富,比如翻译过来的中文文档,stackoverflow 等社区上关于相关问题的答案也就越多,成员也更加活跃,网上相关博客更多,学习书籍也更多,第三方库也更多,轮子啊,再也不用重新造轮子……这就是价值。 - +--- +layout: post +title: "Golang unsafe 低级编程" +date: 2018-02-05 09:00:05 +0800 +categories: golang +--- + +我为什么要学 golang? +golang 相对其他语言的优势: +1. goroutine 适合用于高并发,但是 goroutine 之间没有优先级 +2. channel 的设计 + +channel 能够实现很多同步,需要使用一定的技巧。比如控制最多执行的 goroutine,select 通知一个 goroutine 的退出。 + +1 和 2 都是基于 go 运行时的强大 + +golang 的 GC 显然不算强大,应该比不上 JVM + +结构体、接口设计与 C 系语言不一样。接口的实现是隐式的 + +defer 语句确保某些操作能够被执行 + +不一样的错误异常处理,将错误以返回值的形式传递。 + +听说类型别名使得代码更加易于维护 + +指针,类似于 java 等语言的引用 + +可变长的 goroutine 栈,最多能够扩展至 1GB,可以执行很多层递归 + +go 的工具链,但是我还没使用过高级功能 + +内置函数都是通过调用 C 来完成的,cgo协议。所以每个语言内置的东西都差不多 + +背后有 Google 维护,强大的创新力,很多大公司使用 Go,使得 Go 有创新力 + +语法糖上没有 Python 强大,当然做小事情的时候写起来没有 Python 方便,也 Python 的第三方包丰富。but golang 打包后只是一个执行文件,而且非常小。而 java 编译后就是多个 class 文件然后在真实运行时在一个一个地连接在一起。Python GIL 啊。。。 + +Python 有 GIL 锁,在多核高并发并不适合,Python 的协程书写不方便 + +当然 Go 只是一门编程语言,在功能上 Go 能够实现的其他语言同样能够实现。就像英文、中文一样,使用人数越多,该语言就越有价值。编程语言也类似,使用的人越多,文档也就越丰富,比如翻译过来的中文文档,stackoverflow 等社区上关于相关问题的答案也就越多,成员也更加活跃,网上相关博客更多,学习书籍也更多,第三方库也更多,轮子啊,再也不用重新造轮子……这就是价值。 + 语言层面一些差别,我指的是一般开发者即使努力也不会改变的一些事实。语言的语法,运行速度,底层内存模型,有无 GC,GC 的质量,编译型或解释性……这些对于开发者来说就是天大的难题 \ No newline at end of file diff --git a/src/md/2018-03-21-mysql-protocol.md b/src/md/2018-03-21-mysql-protocol.md index 1a94bc3..c0c7af0 100644 --- a/src/md/2018-03-21-mysql-protocol.md +++ b/src/md/2018-03-21-mysql-protocol.md @@ -1,222 +1,222 @@ ---- -layout: post -title: "MySQL 协议学习笔记" -date: 2018-03-21 09:00:05 +0800 -categories: MySQL ---- - -采用 tcpdump 可以获得本机网络通过 TCP 协议通信的数据详情,但是无法监听 unix socket,所以本地不能够采用 **localhost**,而应该采用 **127.0.0.1**。 - -## 编码方式 - -MySQL 协议中有两种编码方式: -+ 固定长度的编码 -+ 第一个字节说明长度,其后跟着指定长度的字节 - -字符串有两种编码方式: -1. 字符串长度+内容 -2. 字符串以(0x00)结束 - -## 通用请求数据包 Client-->Server - -| Type | Name | Description | -| ---- | ---- | ----- | -| int<3> | payload_length | 负载的长度,数据包中除了初始4字节的长度 | -| int<1> | sequence_id | 序列号 | -| string | payload | [len=payload_length] 数据包中的内容 | - -Example: `01 00 00 00 01` 代表 `payload_length = 1, sequence_id = 0x00, payload=0x01` - -payload_length 由3位无符号整形表示,所以一次最多传输 `2^24-1` 字节。如果需要传输的数据大于等于 `2^24-1` 字节,那么会有额外的数据包发送,直到有一个数据包的长度小于 `2^24-1` 字节。sequence_id 由 0 开始,然后依次自增。当客户端向服务端发送新的命令时,sequence_id 会被重置为 0。 - -payload 的第一个字节表明客户端携带的命令类型 command-type: - -| Hex | Constant Name | -| ---- | ------------ | -| 00 | [COM_SLEEP](https://dev.mysql.com/doc/internals/en/com-sleep.html) | -| 01 | [COM_QUIT](https://dev.mysql.com/doc/internals/en/com-quit.html) | -| 02 | [COM_INIT_DB](https://dev.mysql.com/doc/internals/en/com-init-db.html) | -| 03 | [COM_QUERY](https://dev.mysql.com/doc/internals/en/com-query.html) | -| 04 | [COM_FIELF_LIST](https://dev.mysql.com/doc/internals/en/com-field-list.html) | -| 05 | [COM_CREATE_DB](https://dev.mysql.com/doc/internals/en/com-create-db.html) | -| 06 | [COM_DROP_DB](https://dev.mysql.com/doc/internals/en/com-drop-db.html) | -| 07 | [COM_REFRESH](https://dev.mysql.com/doc/internals/en/com-refresh.html) | -| 08 | [COM_SHUTDOWN](https://dev.mysql.com/doc/internals/en/com-shutdown.html) | -| 09 | [COM_STATISTICS](https://dev.mysql.com/doc/internals/en/com-statistics.html) | -| 0a | [COM_PROCESS_INFO](https://dev.mysql.com/doc/internals/en/com-process-info.html) | -| 0b | [COM_CONNECT](https://dev.mysql.com/doc/internals/en/com-connect.html) | -| 0c | [COM_PROCESS_KILL](https://dev.mysql.com/doc/internals/en/com-process-kill.html) | -| 0d | [COM_DEBUG](https://dev.mysql.com/doc/internals/en/com-debug.html) | -| 0e | [COM_PING](https://dev.mysql.com/doc/internals/en/com-ping.html) | -| 0f | [COM_TIME](https://dev.mysql.com/doc/internals/en/com-time.html) | -| 10 | [COM_DELAYED_INSERT](https://dev.mysql.com/doc/internals/en/com-delayed-insert.html) | -| 11 | [COM_CHANGE_USER](https://dev.mysql.com/doc/internals/en/com-change-user.html) | -| 12 | COM_BINLOG_DUMP | -| 13 | COM_TABLE_DUMP | -| 14 | COM_CONNECT_OUT | -| 15 | COM_REGISTER_SLAVE | -| 16 | [COM_STMT_PREPARE](https://dev.mysql.com/doc/internals/en/com-stmt-prepare.html) | -| 17 | [COM_STMT_EXECUTE](https://dev.mysql.com/doc/internals/en/com-stmt-execute.html) | -| 18 | [COM_STMT_SEND_LONG_DATA](https://dev.mysql.com/doc/internals/en/com-stmt-send-long-data.html) | -| 19 | [COM_STMT_CLOSE](https://dev.mysql.com/doc/internals/en/com-stmt-close.html) | -| 1a | [COM_STMT_RESET](https://dev.mysql.com/doc/internals/en/com-stmt-reset.html) | -| 1b | COM_OPTION | -| 1c | [COM_STMT_FETCH](https://dev.mysql.com/doc/internals/en/com-stmt-fetch.html) | -| 1d | [COM_DAEMON](https://dev.mysql.com/doc/internals/en/com-daemon.html) | -| 1e | COM_BINLOG_DUMP_GTID | -| 1f | [COM_RESET_CONNECTION](https://dev.mysql.com/doc/internals/en/com-reset-connection.html) | - -## 通用回复数据包 Server-->Client - -+ OK_Packet -+ ERR_Packet -+ EOF_Packet -+ Status Flags - -从 MySQL 5.7.5 开始,EOF_Packet 被 OK_Packet 替代。代表成功和结束都是返回 OK_Packet。 - -| Type | Name | Description | -| ---- | ---- | ----------- | -| int<1> | header | [00] for OK; [fe] for EOF | -| int | affected_rows | affected rows | -| int | last_inserted_id | last inserted id | - -还有部分没有加入表中,更多参考 [https://dev.mysql.com/doc/internals/en/packet-OK_Packet.html](https://dev.mysql.com/doc/internals/en/packet-OK_Packet.html) - -+ OK: header = 0 and length_of_packet > 7 -+ EOF: header = 0xfe and length_of_packet < 9 - -ERR_Packet - -| Type | Name | Description | -| ---- | ----- | ---------- | -| int<1> | header | [ff] for ERR_Packet | -| int<2> | error_code | error code | -| string | error_message | human readable error message | - -Status Flags - -| Flag | Value | -| ---- | ----- | -| SERVER_STATUS_IN_TRANS | 0x0001 | -| SERVER_STATUS_AUTOCOMMIT | 0x0002 | -| SERVER_MORE_RESULTS_EXISTS | 0x0008 | -| SERVER_STATUS_NO_GOOD_INDEX_USED | 0x0010 | -| SERVER_STATUS_NO_INDEX_USED | 0x0020 | -| SERVER_STATUS_CURSOR_EXISTS | 0x0040 | -| SERVER_STATUS_LAST_ROW_SENT | 0x0080 | -| SERVER_STATUS_DB_DROPPED | 0x0100 | -| SERVER_STATUS_NO_BLACKSLASH_ESCAPES | 0x0200 | -| SERVER_STATUS_METADATA_CHANGED | 0x0400 | -| SERVER_QUERY_WAS_SLOW | 0x0800 | -| SERVER_PS_OUT_PARAMS | 0x1000 | -| SERVER_STATUS_IN_TRANS_READONLY | 0x2000 | -| SERVER_SESSION_STATE_CHANGED | 0x4000 | - -字符集 character set - -查看MySQL服务支持的 collations - -character set 字符集是符号和编码的集合,collation 是如何**比较**编码中的字符的规则的集合。 - -例子:有 `A=0, B=1, a=2, b=3`,那么 A, B, a, b 是符号 symbol 的集合;0, 1, 2, 3 是编码方式 encoding;而 0<1 所以 A | length of compressed payload: 数据包长度-数据包头长度(7bytes) | -| int<1> | compressed sequence id: 压缩的数据太大,无法一次传输,所以需要序列号 | -| int<3> | length of payload before compression: 数据压缩前的大小 | - -压缩算法是 defalte。 - -通常 payload 小于50Bytes不会进行压缩传输。 -[发送没有压缩的数据](https://dev.mysql.com/doc/internals/en/uncompressed-payload.html): -1. 将 length of payload before compression 设置为 0 -2. 将没有压缩的数据直接存放在 compressed data 中 - -### Text Protocol - -[https://dev.mysql.com/doc/internals/en/text-protocol.html](https://dev.mysql.com/doc/internals/en/text-protocol.html) - - - -MySQL 协议采用小端方式,比如传输 int<3> 1 对应的是:`01 00 00` - -同一个客户端不能够同时向服务端发送两个请求,必须完成一个请求后再进行另一个请求。 - -对于客户端的 COM_QUERY 的查询语句,服务端返回 [Response 格式](https://dev.mysql.com/doc/internals/en/com-query-response.html) - -COM_STMT_PREPARE 会返回一个 prepared statement 对应的 ID。 - -在 COM_QUERY 中,允许客户端同时向服务端发送多个 sql 语句,他们之间通过 ; 隔开。 - -问题: -1. 如何处理不同字符集 -2. 如何处理经过压缩 -3. 捕获命令是否是可执行的 SQL - -## binlog - -以二进制的形式记录对数据库有修改的事件(insert / update / delete / create / drop),但是高版本的 binlog 会对事件进行修改,binlog 同时包含每个 statement 的执行时间。binlog 不会对 SELECT 等对数据不会产生副作用的事件进行记录。 - -binlog 目的: -- 增量备份数据库 -- 恢复丢失数据 -- 主从备份之间的同步 - -对于时间的 NOW() 等函数,MySQL 会自动跳转到日志生成的时间。 - -[Binlog Event](https://dev.mysql.com/doc/internals/en/binlog-event.html) - - -需要处理请求的类型: -- COM_QUERY -- COM_INIT_DB -- COM_STMT_PREPARE -- COM_STMT_EXECUTE -- COM_DROP_DB -- COM_CREATE_DB - - -## TCP - -options for tcp: [mss 1412,sackOK,TS val 845532788 ecr 0,nop,wscale 7] - +--- +layout: post +title: "MySQL 协议学习笔记" +date: 2018-03-21 09:00:05 +0800 +categories: MySQL +--- + +采用 tcpdump 可以获得本机网络通过 TCP 协议通信的数据详情,但是无法监听 unix socket,所以本地不能够采用 **localhost**,而应该采用 **127.0.0.1**。 + +## 编码方式 + +MySQL 协议中有两种编码方式: ++ 固定长度的编码 ++ 第一个字节说明长度,其后跟着指定长度的字节 + +字符串有两种编码方式: +1. 字符串长度+内容 +2. 字符串以(0x00)结束 + +## 通用请求数据包 Client-->Server + +| Type | Name | Description | +| ---- | ---- | ----- | +| int<3> | payload_length | 负载的长度,数据包中除了初始4字节的长度 | +| int<1> | sequence_id | 序列号 | +| string | payload | [len=payload_length] 数据包中的内容 | + +Example: `01 00 00 00 01` 代表 `payload_length = 1, sequence_id = 0x00, payload=0x01` + +payload_length 由3位无符号整形表示,所以一次最多传输 `2^24-1` 字节。如果需要传输的数据大于等于 `2^24-1` 字节,那么会有额外的数据包发送,直到有一个数据包的长度小于 `2^24-1` 字节。sequence_id 由 0 开始,然后依次自增。当客户端向服务端发送新的命令时,sequence_id 会被重置为 0。 + +payload 的第一个字节表明客户端携带的命令类型 command-type: + +| Hex | Constant Name | +| ---- | ------------ | +| 00 | [COM_SLEEP](https://dev.mysql.com/doc/internals/en/com-sleep.html) | +| 01 | [COM_QUIT](https://dev.mysql.com/doc/internals/en/com-quit.html) | +| 02 | [COM_INIT_DB](https://dev.mysql.com/doc/internals/en/com-init-db.html) | +| 03 | [COM_QUERY](https://dev.mysql.com/doc/internals/en/com-query.html) | +| 04 | [COM_FIELF_LIST](https://dev.mysql.com/doc/internals/en/com-field-list.html) | +| 05 | [COM_CREATE_DB](https://dev.mysql.com/doc/internals/en/com-create-db.html) | +| 06 | [COM_DROP_DB](https://dev.mysql.com/doc/internals/en/com-drop-db.html) | +| 07 | [COM_REFRESH](https://dev.mysql.com/doc/internals/en/com-refresh.html) | +| 08 | [COM_SHUTDOWN](https://dev.mysql.com/doc/internals/en/com-shutdown.html) | +| 09 | [COM_STATISTICS](https://dev.mysql.com/doc/internals/en/com-statistics.html) | +| 0a | [COM_PROCESS_INFO](https://dev.mysql.com/doc/internals/en/com-process-info.html) | +| 0b | [COM_CONNECT](https://dev.mysql.com/doc/internals/en/com-connect.html) | +| 0c | [COM_PROCESS_KILL](https://dev.mysql.com/doc/internals/en/com-process-kill.html) | +| 0d | [COM_DEBUG](https://dev.mysql.com/doc/internals/en/com-debug.html) | +| 0e | [COM_PING](https://dev.mysql.com/doc/internals/en/com-ping.html) | +| 0f | [COM_TIME](https://dev.mysql.com/doc/internals/en/com-time.html) | +| 10 | [COM_DELAYED_INSERT](https://dev.mysql.com/doc/internals/en/com-delayed-insert.html) | +| 11 | [COM_CHANGE_USER](https://dev.mysql.com/doc/internals/en/com-change-user.html) | +| 12 | COM_BINLOG_DUMP | +| 13 | COM_TABLE_DUMP | +| 14 | COM_CONNECT_OUT | +| 15 | COM_REGISTER_SLAVE | +| 16 | [COM_STMT_PREPARE](https://dev.mysql.com/doc/internals/en/com-stmt-prepare.html) | +| 17 | [COM_STMT_EXECUTE](https://dev.mysql.com/doc/internals/en/com-stmt-execute.html) | +| 18 | [COM_STMT_SEND_LONG_DATA](https://dev.mysql.com/doc/internals/en/com-stmt-send-long-data.html) | +| 19 | [COM_STMT_CLOSE](https://dev.mysql.com/doc/internals/en/com-stmt-close.html) | +| 1a | [COM_STMT_RESET](https://dev.mysql.com/doc/internals/en/com-stmt-reset.html) | +| 1b | COM_OPTION | +| 1c | [COM_STMT_FETCH](https://dev.mysql.com/doc/internals/en/com-stmt-fetch.html) | +| 1d | [COM_DAEMON](https://dev.mysql.com/doc/internals/en/com-daemon.html) | +| 1e | COM_BINLOG_DUMP_GTID | +| 1f | [COM_RESET_CONNECTION](https://dev.mysql.com/doc/internals/en/com-reset-connection.html) | + +## 通用回复数据包 Server-->Client + ++ OK_Packet ++ ERR_Packet ++ EOF_Packet ++ Status Flags + +从 MySQL 5.7.5 开始,EOF_Packet 被 OK_Packet 替代。代表成功和结束都是返回 OK_Packet。 + +| Type | Name | Description | +| ---- | ---- | ----------- | +| int<1> | header | [00] for OK; [fe] for EOF | +| int | affected_rows | affected rows | +| int | last_inserted_id | last inserted id | + +还有部分没有加入表中,更多参考 [https://dev.mysql.com/doc/internals/en/packet-OK_Packet.html](https://dev.mysql.com/doc/internals/en/packet-OK_Packet.html) + ++ OK: header = 0 and length_of_packet > 7 ++ EOF: header = 0xfe and length_of_packet < 9 + +ERR_Packet + +| Type | Name | Description | +| ---- | ----- | ---------- | +| int<1> | header | [ff] for ERR_Packet | +| int<2> | error_code | error code | +| string | error_message | human readable error message | + +Status Flags + +| Flag | Value | +| ---- | ----- | +| SERVER_STATUS_IN_TRANS | 0x0001 | +| SERVER_STATUS_AUTOCOMMIT | 0x0002 | +| SERVER_MORE_RESULTS_EXISTS | 0x0008 | +| SERVER_STATUS_NO_GOOD_INDEX_USED | 0x0010 | +| SERVER_STATUS_NO_INDEX_USED | 0x0020 | +| SERVER_STATUS_CURSOR_EXISTS | 0x0040 | +| SERVER_STATUS_LAST_ROW_SENT | 0x0080 | +| SERVER_STATUS_DB_DROPPED | 0x0100 | +| SERVER_STATUS_NO_BLACKSLASH_ESCAPES | 0x0200 | +| SERVER_STATUS_METADATA_CHANGED | 0x0400 | +| SERVER_QUERY_WAS_SLOW | 0x0800 | +| SERVER_PS_OUT_PARAMS | 0x1000 | +| SERVER_STATUS_IN_TRANS_READONLY | 0x2000 | +| SERVER_SESSION_STATE_CHANGED | 0x4000 | + +字符集 character set + +查看MySQL服务支持的 collations + +character set 字符集是符号和编码的集合,collation 是如何**比较**编码中的字符的规则的集合。 + +例子:有 `A=0, B=1, a=2, b=3`,那么 A, B, a, b 是符号 symbol 的集合;0, 1, 2, 3 是编码方式 encoding;而 0<1 所以 A | length of compressed payload: 数据包长度-数据包头长度(7bytes) | +| int<1> | compressed sequence id: 压缩的数据太大,无法一次传输,所以需要序列号 | +| int<3> | length of payload before compression: 数据压缩前的大小 | + +压缩算法是 defalte。 + +通常 payload 小于50Bytes不会进行压缩传输。 +[发送没有压缩的数据](https://dev.mysql.com/doc/internals/en/uncompressed-payload.html): +1. 将 length of payload before compression 设置为 0 +2. 将没有压缩的数据直接存放在 compressed data 中 + +### Text Protocol + +[https://dev.mysql.com/doc/internals/en/text-protocol.html](https://dev.mysql.com/doc/internals/en/text-protocol.html) + + + +MySQL 协议采用小端方式,比如传输 int<3> 1 对应的是:`01 00 00` + +同一个客户端不能够同时向服务端发送两个请求,必须完成一个请求后再进行另一个请求。 + +对于客户端的 COM_QUERY 的查询语句,服务端返回 [Response 格式](https://dev.mysql.com/doc/internals/en/com-query-response.html) + +COM_STMT_PREPARE 会返回一个 prepared statement 对应的 ID。 + +在 COM_QUERY 中,允许客户端同时向服务端发送多个 sql 语句,他们之间通过 ; 隔开。 + +问题: +1. 如何处理不同字符集 +2. 如何处理经过压缩 +3. 捕获命令是否是可执行的 SQL + +## binlog + +以二进制的形式记录对数据库有修改的事件(insert / update / delete / create / drop),但是高版本的 binlog 会对事件进行修改,binlog 同时包含每个 statement 的执行时间。binlog 不会对 SELECT 等对数据不会产生副作用的事件进行记录。 + +binlog 目的: +- 增量备份数据库 +- 恢复丢失数据 +- 主从备份之间的同步 + +对于时间的 NOW() 等函数,MySQL 会自动跳转到日志生成的时间。 + +[Binlog Event](https://dev.mysql.com/doc/internals/en/binlog-event.html) + + +需要处理请求的类型: +- COM_QUERY +- COM_INIT_DB +- COM_STMT_PREPARE +- COM_STMT_EXECUTE +- COM_DROP_DB +- COM_CREATE_DB + + +## TCP + +options for tcp: [mss 1412,sackOK,TS val 845532788 ecr 0,nop,wscale 7] + diff --git a/src/md/2018-04-01-utf8-in-golang.md b/src/md/2018-04-01-utf8-in-golang.md index ab963ce..6dc3b6d 100644 --- a/src/md/2018-04-01-utf8-in-golang.md +++ b/src/md/2018-04-01-utf8-in-golang.md @@ -1,442 +1,442 @@ ---- -layout: post -title: "utf8 in Golang" -date: 2018-04-01 20:00:05 +0800 -categories: golang ---- - -# 为什么记录下这文章? - -刷算法题的时候很多时候遇到字符串操作问题,我比较喜欢使用 Python 或者 Golang 进行算法的实现,但是比较坑的一点就是 Golang 的字符串操作比较麻烦。 - -怎么麻烦?我举几个例子: - -```go -for _, x := range "hello" { - // x 的类型是 rune,其实就是对应字符的 utf8 编码 -} -``` - -```go -s = "刘曦光" -s[0] == uint8(229) -``` - -上面我标明是 uint8 的原因是 Go 是强类型语言,甚至没有像 C 一样的隐式转化。不同类型之间是不能够直接进行互操作的。比如 int 与 uint8 就无法比较大小。 - -```go -len("刘曦光") == 9 -``` - -返回的是底层字符串字节所占用的长度,而不是字符串中对应 utf8 字符的个数。 - -如果我想取字符串中的第 k 个字符该咋搞呢? - -如果字符串只包含 ASCII 符号(ASCII 所有字符编码长度为 1Byte),那么我们完全可以使用下标解决: - -```go -s = "hello" -s[0] == byte('h') -s[1] == byte('e') -s[2] == byte('l') -s[3] == byte('l') -s[5] == byte('o') -``` - -但是如果遇到中文等 utf8 编码长度不止 1Byte 的呢?怎么办? - -比较简单的解决方案: - -```go -func find(s string, index int) (rune, error) { - for i, v := range s { - if i == index { - return v, nil - } - } - return rune(0), errors.New("Out of range.") -} -``` - -# unicode/utf8 - -求助 unicode/utf8 官方库的帮助,遗憾的是该库中也没有特别丰富的函数支持,但是总比我们自己写轮子好。 - -以下例子隐含: - -```go -import ( - "unicode/utf8" - "fmt" -) -``` - -## 遍历 utf8 字符串 - -```go -func main() { - s := "今天愚人节" - for i := 0; i < len(s); { - r, size := utf8.DecodeRuneInString(s[i:]) - fmt.Print(string(r)) - i += size - } -} -``` - -每次取字符串的一个切片,对字符串取切片的一个开销很小的操作,新的切片会引用原字符串。 - -## 逆序遍历 utf8 - -```go -func main() { - s := "今天愚人节" - for i := len(s); i > 0; { - r, size := utf8.DecodeLastRuneInString(s[:i]) - fmt.Print(string(r)) - i -= size - } -} -``` - -## 取 utf8 字符串长度 - -```go -func main() { - s := "今天愚人节" - fmt.Println(utf8.RuneCountInString(s)) -} -``` - -## 取特定下标的 utf8 字符 - -```go -int main() { - s := "今天愚人节" - fmt.Printf("%q", string([]rune(s)[3])) // "人" -} -``` - -这里我们先把字符串 s 转化为 []rune 类型,也就是相当于把字符串中的每一个 utf8 字符解码出来。如果字符串长度不是非常大的话,建议先将 string 转化为 []rune,但如果字符串很大的场景下,这个方法并不适用 - -**改良版** - -```go -func stringIndex(s string, index int) (rune, error) { - c := 0 - for i := 0; i < len(s); c++ { - r, size := utf8.DecodeLastRuneInString(s[i:]) - if c == index { - return r, nil - } - i += size - } - return 0, errors.New(fmt.Sprintf("index=%d len(s)=%d index out of range", index, c)) -} -``` - -unicode/utf8 下的方法并不是很丰富,在刷算法题中可能用到的就是上面几个,上面对应的 string 方法都有一个面相 []byte 的版本。 - -# strings - -每个语言都会有非常丰富的对应字符串的操作方法集和,Go 的字符串操作在 strings 包。 - -以下简单介绍以下 strings 的基本操作 - -## 比较两个字符串 - -最简单的方法是使用 `< > == >= <=` 等操作符实现,非常灵活。 - -也可以使用 `strings.Compare(a, b string)` 方法实现,直接使用操作符返回的是 bool 值,而 `strings.Compare` 返回 -1 0 1 三者之一。想到什么没有?可以使用 switch 语句,减少大量的 if-else 语句。 - -```go -func main() { - a, b := "hello", "world" - switch strings.Compare(a, b) { - case 0: - fmt.Printf("%q==%q", a, b) - case -1: - fmt.Printf("%q<%q", a, b) - case 1: - fmt.Printf("%q>%q", a, b) - } -} -``` - -## 判断是否字串 - -这个问题是不是与搜索字串及其下标很相似? - -```go -strings.Contains(s, substr string) bool -``` - -判断字符串是否包含某个字符集和 - -```go -strings.ContainsAny(s, chars string) bool -``` - -没错,判断是否包含子串就是搜索子串下标,然后判断搜索下标是否为 -1。 - -## 判断两个字符串是否相等 case-insensitive - -```go -fmt.Println(strings.EqualFold("hello你好", "HELLO你好")) // true -fmt.Println(strings.EqualFold("hello", "world")) // false -``` - -## 字符串切割 - -通过 unicode.IsSpace 判断是否是空白字符,然后通过空白字符进行切割 - -```go -fmt.Println(strings.Fields("hello\tworld 你好\n世界")) -// [hello world 你好 世界] -``` - -如果需要进行切割的不是空白字符,需要自定义方法怎么办? - -```go -fmt.Println(strings.FieldsFunc("hello world", func(r rune) bool { - return r == ' ' -})) -// [hello world] -``` - -## 前后缀 - -判断是否包含前缀 - -```go -fmt.Println(strings.HasPrefix("hello world", "hello")) // true -``` - -判断是否包含后缀 - -```go -fmt.Println(strings.HasSuffix("hello world", "world")) // true -``` - -## 子串所在位置 - -```go -fmt.Println(strings.Index("hello world", "world")) // 6 -``` - -上面例子是第一个出现的位置,下面这个例子我们求最后一次出现的位置 - -```go -fmt.Println(strings.LastIndex("hello world", "l")) // 9 -``` - -如果我们要求符合某个特别条件的字符的位置呢? - -```go -fmt.Println(strings.IndexFunc("hello world", func(r rune) bool { - return r + 1 == 'e' -})) -``` - -对应的也有 `strings.LastIndexFunc(s string, f func(r rune) bool)` - -## 拼接字符串 - -如果我们有一个字符串slice []string,将他们拼装起来,并且指定分隔符 - -```go -fmt.Println(strings.Join([]string{"hello", "world"}, "/")) // hello/world -``` - -`fmt.Sprintf()` 为我们提供了非常强大的拼装字符串的能力,而且能够指定输出的格式,与 `fmt.Printf` 类似,不同的是前者返回格式化后的字符串,后者将该字符串写入到标准输出流。比如将十进制数转化为对应的二进制字符串: - -```go -s := fmt.Sprintf("%b\n", 100) -fmt.Println(s) // 1100100 -``` - -## Map 操作 - -如果使用过 Python 的 map filter reduce 函数的会觉得这样用起来写代码非常舒服,不用写很多循环,数据像是管道一样流动。 - -Go 对字符串也有对应的支持,下面演示一个对简单的[凯撒密码](https://en.wikipedia.org/wiki/Caesar_cipher)实现: - -```go -func Caesar(s string) string { - return strings.Map(func(r rune) rune { - if 'a' <= r && r <= 'z' { - r -= 3 - if r < 'a' { - r += 26 - } - } - return r - }, s) -} -``` - -## 字符串切割 - -```go -fmt.Println(strings.Split("hello world", "l")) // [he o wor d] -``` - -上面例子会把所有含有子串的都切割开,但是如果我们只是想切割指定数量的字符串呢? - -```go -fmt.Println(strings.SplitN("hello world", "l", 2)) // [he lo world] -``` - -要求高一点想要保留子串怎么办? - -```go -fmt.Println(strings.SplitAfter("hello world", "l")) // [hel l o worl d] -``` - -类似的,指定相应切割数量对应的方法: - -```go -fmt.Println(strings.SplitAfterN("hello world", "l", 2)) // [hel lo world] -``` - -## 字符串复制增长 N 倍 - -增长字符串除了 `+` 运算符,`strings.Join` 方法还可以使用以下方法: - -```go -fmt.Println(strings.Repeat("i", 10)) // iiiiiiiiii -``` - -## 大小写转换 - -这个功能手写也很简单,但是有轮子就别重复造了 - -```go -func ToLower(s string) string -func ToUpper(s string) string -``` - -Go 官方文档给出了一个例子,还可以自定义大小写转化的映射,有特殊需求的同学可以实现 unicode.SpecialCase。 - -```go -func ToLowerSpecial(c unicode.SpecialCase, s string) string -func ToUpperSpecial(c unicode.SpecialCase, s string) string -``` - -对应的还有 `strings.ToTitle` - -## 统计子串出现的次数 - -```go -fmt.Println(strings.Count("hello world", "l")) // 3 -``` - -## 去除特定前后缀 - -```go -func Trim(s string, cutset string) string -``` - -Trim 还有很多变种,比如 TrimPrefix、TrimSuffix 等。 - -## 子串替换 - -```go -fmt.Println(strings.Replace("hello world", "l", "L", 2)) // heLLo world -``` - -# strconv - -strconv 下是一些字符串与其他基本类型的转换,如整数、浮点数与字符串之间的转换。 - -## 字符串与整形之间转换 - -Atoi / Itoa 与 C 语言中的字符串与整形之间的转换非常像: - -```go -s := strconv.Itoa(-100) // "-100" -i, _ := strconv.Atoi("-100") // -100 -``` - -因为字符串转化为整形有可能输入值不合法,所以 Atoi 方法会返回一个错误值。 - -## strconv.ParseXxx - -strconv.ParseXxx 将字符串转化为相应类型的数据: - -```go -b, _ := strconv.ParseBool("true") -f, _ := strconv.ParseFloat("3.1415926", 64) // 最后一个参数指明 float 的 bitSize -``` - -```go -i64, _ := strconv.ParseInt("-100", 10, 32) -``` - -在字符串转化为整数可以指定进制,比如十进制、二进制、十六进制等等....虽然可以 bitSize 参数,但是返回的值还是 int64 类型。 - -二进制: - -```go -bin, _ := strconv.ParseInt("1001", 2, 64) // 9 -``` - -十六进制: - -```go -hex, _ := strconv.ParseInt("1a", 16, 64) // 26 -``` - -注意这里不能改写为 `0x1a`,如果加上 `0x` 或者 `0X` 前缀则无法解析。但可以加 `0` 前缀,比如改写为 `01a`。 - -八进制: - -```go -oct, _ := strconv.ParseInt("-017", 8, 64) // -15 -``` - -类似十六进制、二进制,八进制也可以加 `0` 前缀,负数只需要再追前面加上 `-` 即可。 - -字符串转化为整数不能够出现小数点,也就是不能够输入一个浮点数格式的字符串。 - -无符号数的转换: - -```go -u64, _ := strconv.ParseUint("100", 10, 64) // 100 -u64, err := strconv.ParseUint("-100", 10, 64) // 0 -``` - -如果无符号数转化中,字符串中形式是有符号的,则返回错误值,并且返回无符号数的零值也就是 0。 - -## strconv.FormatXxx - -`strconv.FormatXxx` 与 `strconv.ParseXxx` 相反。其功能也可以通过 `fmt.Sprintf` 实现,`strconv.FormatXxx` 的相对优点是指定参数更加灵活,不需要写死到字符串中。 - -```go -b := strconv.FormatBool(true) // "true" -``` - -```go -f := strconv.FormatFloat(3.1415926, 'f', 10, 32) // "3.1415925026" -``` - -字符串的转化函数的参数比较多,具体可以参考[文档](https://golang.org/pkg/strconv/#FormatFloat) - -```go -i := strconv.FormatInt(100, 2) // "1100100" -i = strconv.FormatInt(-100, 16) // "-64" -``` - -```go -u := strconv.FormatUint(10, 2) // "1010" -``` - -# 总结 - -**工欲善其事,必先利其器** - -学习中、工作中需要掌握某些有利于提升我们效率的工具,比如对 IDE 快捷键、语言的标准库、第三方库等,要好好利用前人载的树来乘凉。Python 为什么在数据科学等领域如此火热呢?我想除了其自身特性外,更中要的是丰富的第三方库支持。很多不是 Python 开发的工具都有 Python 的接口实现,比如著名了 Tensorflow。 - -学习不仅需要花大量时间,更重要的是提升效率。 - -*使用 Golang 做算法题让我不禁怀念 Python 的语法糖* +--- +layout: post +title: "utf8 in Golang" +date: 2018-04-01 20:00:05 +0800 +categories: golang +--- + +# 为什么记录下这文章? + +刷算法题的时候很多时候遇到字符串操作问题,我比较喜欢使用 Python 或者 Golang 进行算法的实现,但是比较坑的一点就是 Golang 的字符串操作比较麻烦。 + +怎么麻烦?我举几个例子: + +```go +for _, x := range "hello" { + // x 的类型是 rune,其实就是对应字符的 utf8 编码 +} +``` + +```go +s = "刘曦光" +s[0] == uint8(229) +``` + +上面我标明是 uint8 的原因是 Go 是强类型语言,甚至没有像 C 一样的隐式转化。不同类型之间是不能够直接进行互操作的。比如 int 与 uint8 就无法比较大小。 + +```go +len("刘曦光") == 9 +``` + +返回的是底层字符串字节所占用的长度,而不是字符串中对应 utf8 字符的个数。 + +如果我想取字符串中的第 k 个字符该咋搞呢? + +如果字符串只包含 ASCII 符号(ASCII 所有字符编码长度为 1Byte),那么我们完全可以使用下标解决: + +```go +s = "hello" +s[0] == byte('h') +s[1] == byte('e') +s[2] == byte('l') +s[3] == byte('l') +s[5] == byte('o') +``` + +但是如果遇到中文等 utf8 编码长度不止 1Byte 的呢?怎么办? + +比较简单的解决方案: + +```go +func find(s string, index int) (rune, error) { + for i, v := range s { + if i == index { + return v, nil + } + } + return rune(0), errors.New("Out of range.") +} +``` + +# unicode/utf8 + +求助 unicode/utf8 官方库的帮助,遗憾的是该库中也没有特别丰富的函数支持,但是总比我们自己写轮子好。 + +以下例子隐含: + +```go +import ( + "unicode/utf8" + "fmt" +) +``` + +## 遍历 utf8 字符串 + +```go +func main() { + s := "今天愚人节" + for i := 0; i < len(s); { + r, size := utf8.DecodeRuneInString(s[i:]) + fmt.Print(string(r)) + i += size + } +} +``` + +每次取字符串的一个切片,对字符串取切片的一个开销很小的操作,新的切片会引用原字符串。 + +## 逆序遍历 utf8 + +```go +func main() { + s := "今天愚人节" + for i := len(s); i > 0; { + r, size := utf8.DecodeLastRuneInString(s[:i]) + fmt.Print(string(r)) + i -= size + } +} +``` + +## 取 utf8 字符串长度 + +```go +func main() { + s := "今天愚人节" + fmt.Println(utf8.RuneCountInString(s)) +} +``` + +## 取特定下标的 utf8 字符 + +```go +int main() { + s := "今天愚人节" + fmt.Printf("%q", string([]rune(s)[3])) // "人" +} +``` + +这里我们先把字符串 s 转化为 []rune 类型,也就是相当于把字符串中的每一个 utf8 字符解码出来。如果字符串长度不是非常大的话,建议先将 string 转化为 []rune,但如果字符串很大的场景下,这个方法并不适用 + +**改良版** + +```go +func stringIndex(s string, index int) (rune, error) { + c := 0 + for i := 0; i < len(s); c++ { + r, size := utf8.DecodeLastRuneInString(s[i:]) + if c == index { + return r, nil + } + i += size + } + return 0, errors.New(fmt.Sprintf("index=%d len(s)=%d index out of range", index, c)) +} +``` + +unicode/utf8 下的方法并不是很丰富,在刷算法题中可能用到的就是上面几个,上面对应的 string 方法都有一个面相 []byte 的版本。 + +# strings + +每个语言都会有非常丰富的对应字符串的操作方法集和,Go 的字符串操作在 strings 包。 + +以下简单介绍以下 strings 的基本操作 + +## 比较两个字符串 + +最简单的方法是使用 `< > == >= <=` 等操作符实现,非常灵活。 + +也可以使用 `strings.Compare(a, b string)` 方法实现,直接使用操作符返回的是 bool 值,而 `strings.Compare` 返回 -1 0 1 三者之一。想到什么没有?可以使用 switch 语句,减少大量的 if-else 语句。 + +```go +func main() { + a, b := "hello", "world" + switch strings.Compare(a, b) { + case 0: + fmt.Printf("%q==%q", a, b) + case -1: + fmt.Printf("%q<%q", a, b) + case 1: + fmt.Printf("%q>%q", a, b) + } +} +``` + +## 判断是否字串 + +这个问题是不是与搜索字串及其下标很相似? + +```go +strings.Contains(s, substr string) bool +``` + +判断字符串是否包含某个字符集和 + +```go +strings.ContainsAny(s, chars string) bool +``` + +没错,判断是否包含子串就是搜索子串下标,然后判断搜索下标是否为 -1。 + +## 判断两个字符串是否相等 case-insensitive + +```go +fmt.Println(strings.EqualFold("hello你好", "HELLO你好")) // true +fmt.Println(strings.EqualFold("hello", "world")) // false +``` + +## 字符串切割 + +通过 unicode.IsSpace 判断是否是空白字符,然后通过空白字符进行切割 + +```go +fmt.Println(strings.Fields("hello\tworld 你好\n世界")) +// [hello world 你好 世界] +``` + +如果需要进行切割的不是空白字符,需要自定义方法怎么办? + +```go +fmt.Println(strings.FieldsFunc("hello world", func(r rune) bool { + return r == ' ' +})) +// [hello world] +``` + +## 前后缀 + +判断是否包含前缀 + +```go +fmt.Println(strings.HasPrefix("hello world", "hello")) // true +``` + +判断是否包含后缀 + +```go +fmt.Println(strings.HasSuffix("hello world", "world")) // true +``` + +## 子串所在位置 + +```go +fmt.Println(strings.Index("hello world", "world")) // 6 +``` + +上面例子是第一个出现的位置,下面这个例子我们求最后一次出现的位置 + +```go +fmt.Println(strings.LastIndex("hello world", "l")) // 9 +``` + +如果我们要求符合某个特别条件的字符的位置呢? + +```go +fmt.Println(strings.IndexFunc("hello world", func(r rune) bool { + return r + 1 == 'e' +})) +``` + +对应的也有 `strings.LastIndexFunc(s string, f func(r rune) bool)` + +## 拼接字符串 + +如果我们有一个字符串slice []string,将他们拼装起来,并且指定分隔符 + +```go +fmt.Println(strings.Join([]string{"hello", "world"}, "/")) // hello/world +``` + +`fmt.Sprintf()` 为我们提供了非常强大的拼装字符串的能力,而且能够指定输出的格式,与 `fmt.Printf` 类似,不同的是前者返回格式化后的字符串,后者将该字符串写入到标准输出流。比如将十进制数转化为对应的二进制字符串: + +```go +s := fmt.Sprintf("%b\n", 100) +fmt.Println(s) // 1100100 +``` + +## Map 操作 + +如果使用过 Python 的 map filter reduce 函数的会觉得这样用起来写代码非常舒服,不用写很多循环,数据像是管道一样流动。 + +Go 对字符串也有对应的支持,下面演示一个对简单的[凯撒密码](https://en.wikipedia.org/wiki/Caesar_cipher)实现: + +```go +func Caesar(s string) string { + return strings.Map(func(r rune) rune { + if 'a' <= r && r <= 'z' { + r -= 3 + if r < 'a' { + r += 26 + } + } + return r + }, s) +} +``` + +## 字符串切割 + +```go +fmt.Println(strings.Split("hello world", "l")) // [he o wor d] +``` + +上面例子会把所有含有子串的都切割开,但是如果我们只是想切割指定数量的字符串呢? + +```go +fmt.Println(strings.SplitN("hello world", "l", 2)) // [he lo world] +``` + +要求高一点想要保留子串怎么办? + +```go +fmt.Println(strings.SplitAfter("hello world", "l")) // [hel l o worl d] +``` + +类似的,指定相应切割数量对应的方法: + +```go +fmt.Println(strings.SplitAfterN("hello world", "l", 2)) // [hel lo world] +``` + +## 字符串复制增长 N 倍 + +增长字符串除了 `+` 运算符,`strings.Join` 方法还可以使用以下方法: + +```go +fmt.Println(strings.Repeat("i", 10)) // iiiiiiiiii +``` + +## 大小写转换 + +这个功能手写也很简单,但是有轮子就别重复造了 + +```go +func ToLower(s string) string +func ToUpper(s string) string +``` + +Go 官方文档给出了一个例子,还可以自定义大小写转化的映射,有特殊需求的同学可以实现 unicode.SpecialCase。 + +```go +func ToLowerSpecial(c unicode.SpecialCase, s string) string +func ToUpperSpecial(c unicode.SpecialCase, s string) string +``` + +对应的还有 `strings.ToTitle` + +## 统计子串出现的次数 + +```go +fmt.Println(strings.Count("hello world", "l")) // 3 +``` + +## 去除特定前后缀 + +```go +func Trim(s string, cutset string) string +``` + +Trim 还有很多变种,比如 TrimPrefix、TrimSuffix 等。 + +## 子串替换 + +```go +fmt.Println(strings.Replace("hello world", "l", "L", 2)) // heLLo world +``` + +# strconv + +strconv 下是一些字符串与其他基本类型的转换,如整数、浮点数与字符串之间的转换。 + +## 字符串与整形之间转换 + +Atoi / Itoa 与 C 语言中的字符串与整形之间的转换非常像: + +```go +s := strconv.Itoa(-100) // "-100" +i, _ := strconv.Atoi("-100") // -100 +``` + +因为字符串转化为整形有可能输入值不合法,所以 Atoi 方法会返回一个错误值。 + +## strconv.ParseXxx + +strconv.ParseXxx 将字符串转化为相应类型的数据: + +```go +b, _ := strconv.ParseBool("true") +f, _ := strconv.ParseFloat("3.1415926", 64) // 最后一个参数指明 float 的 bitSize +``` + +```go +i64, _ := strconv.ParseInt("-100", 10, 32) +``` + +在字符串转化为整数可以指定进制,比如十进制、二进制、十六进制等等....虽然可以 bitSize 参数,但是返回的值还是 int64 类型。 + +二进制: + +```go +bin, _ := strconv.ParseInt("1001", 2, 64) // 9 +``` + +十六进制: + +```go +hex, _ := strconv.ParseInt("1a", 16, 64) // 26 +``` + +注意这里不能改写为 `0x1a`,如果加上 `0x` 或者 `0X` 前缀则无法解析。但可以加 `0` 前缀,比如改写为 `01a`。 + +八进制: + +```go +oct, _ := strconv.ParseInt("-017", 8, 64) // -15 +``` + +类似十六进制、二进制,八进制也可以加 `0` 前缀,负数只需要再追前面加上 `-` 即可。 + +字符串转化为整数不能够出现小数点,也就是不能够输入一个浮点数格式的字符串。 + +无符号数的转换: + +```go +u64, _ := strconv.ParseUint("100", 10, 64) // 100 +u64, err := strconv.ParseUint("-100", 10, 64) // 0 +``` + +如果无符号数转化中,字符串中形式是有符号的,则返回错误值,并且返回无符号数的零值也就是 0。 + +## strconv.FormatXxx + +`strconv.FormatXxx` 与 `strconv.ParseXxx` 相反。其功能也可以通过 `fmt.Sprintf` 实现,`strconv.FormatXxx` 的相对优点是指定参数更加灵活,不需要写死到字符串中。 + +```go +b := strconv.FormatBool(true) // "true" +``` + +```go +f := strconv.FormatFloat(3.1415926, 'f', 10, 32) // "3.1415925026" +``` + +字符串的转化函数的参数比较多,具体可以参考[文档](https://golang.org/pkg/strconv/#FormatFloat) + +```go +i := strconv.FormatInt(100, 2) // "1100100" +i = strconv.FormatInt(-100, 16) // "-64" +``` + +```go +u := strconv.FormatUint(10, 2) // "1010" +``` + +# 总结 + +**工欲善其事,必先利其器** + +学习中、工作中需要掌握某些有利于提升我们效率的工具,比如对 IDE 快捷键、语言的标准库、第三方库等,要好好利用前人载的树来乘凉。Python 为什么在数据科学等领域如此火热呢?我想除了其自身特性外,更中要的是丰富的第三方库支持。很多不是 Python 开发的工具都有 Python 的接口实现,比如著名了 Tensorflow。 + +学习不仅需要花大量时间,更重要的是提升效率。 + +*使用 Golang 做算法题让我不禁怀念 Python 的语法糖* diff --git "a/src/md/2018-04-01-\345\206\205\345\255\230\345\257\271\351\275\220.md" "b/src/md/2018-04-01-\345\206\205\345\255\230\345\257\271\351\275\220.md" index 9e57eb6..ccfb27f 100644 --- "a/src/md/2018-04-01-\345\206\205\345\255\230\345\257\271\351\275\220.md" +++ "b/src/md/2018-04-01-\345\206\205\345\255\230\345\257\271\351\275\220.md" @@ -1,233 +1,233 @@ ---- -layout: post -title: "内存对齐" -date: 2018-04-01 09:00:05 +0800 -categories: 内存对齐 ---- - -> 本机器 64 位操作系统 - -# 为什么有内存对齐? - -对时间和空间的双重优化,比如 64 位系统数据总线大小为 64 位,使用内存对齐方案就可以一次从内存中加载更多数据,比如 64 位中可以存放 `char[4] + int`。CPU 中的高速缓存是非常小的,笔者当前电脑 CPU 是 4 核 8 线程,每一个线程的高速缓存是 6144KB。 - -对于程序员来说数据是分为一个个字节的,而对于 CPU 来说数据是分为一块一块的,比如 64-bit 架构下,CPU 一次可加载 64-bit 数据,而且对于数据的起始地址是有一定限制的。某些平台下,严格要求特定长度的块需要从特定地址加载,比如加载一个 64-bit int 要求加载的起始位置是 8 的倍数,如果地址不符合条件,需要转化为两次加载,最后将两次读取的数据拼接,会增大 CPU 的消耗。 - -所以一方面是减少内存使用,数据尽量小,增加高速缓存的命中率,一方面是改变默认数据对齐,增加 CPU 加载数据的时间消耗。没有哪个方案一定优于另一个方案,所以 GCC 提供了 `#pragma pack(n)` 编译选项,给予程序员选择的自由,代价是程序员需知道如何去驾驭它。 - -C/Cpp 中的内存对齐方案同样适用于 Golang 和 Rust。 - -# C 中基本类型大小 - -```c - int main() { - printf("int=%ld char=%ld float=%ld double=%ld long=%ld pointer=%ld long double=%ld\n", sizeof(int), sizeof(char), sizeof(float), sizeof(double), sizeof(long), sizeof(int *), sizeof(long double)); -``` - -output: - -``` -int=4 char=1 float=4 double=8 long=8 pointer=8 long double=16 -``` - -- 数组类型大小 = 类型大小 * 数组大小 # 数组长度为 0 很奇怪 -- 指针大小 = 操作系统字长 # 本机器中为 `64 / 8 = 8` - -# 对齐方式 - -**C 的变量不是能够存放在任意地址的**,限制:变量存放的起始位置必须是类型大小的整数倍。 - -- char:`sizeof(char) == 1` char 能够存放在任何起始地址 -- short:`sizeof(short) == 2` short 的起始地址必须是 2 的整数倍 -- int:`sizeof(int) == 4` int 的起始地址必须是 4 的整数倍 -- float:与 int 一样 -- double:`sizeof(double) == 8` double 的起始地址必须是 8 的整数倍 -- long:与 doubel 一样 - -> 起始地址至于类型大小有关,与有无符号类型无关 - -填充 padding 的值不确定,这不影响程序员编程,因为我们根本不会访问到 padding。 - -```c -// A -char *p; -char c; - -// B -char c; -char *p; -``` - -使用 A 方案可能会比 B 方案浪费更多内存。因为 A 方案的指针需要进行内存对齐,p 的起始存储地址必须是 8 的整数倍,而 B 方案的字符类型 c 可以存放在任何起始地址。 - -## struct 结构体 - -结构体的大小会根据其内部最长**基本类型**对齐,这句话听起来确实比较别扭,举些例子: - -> 基本类型最长为 16 字节,long double - -有以下结构体: - -```c -struct foo { - char *p; - long x; - char c; -}; -``` - -其对应的内存布局为: - -| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | -| - | - | - | - | - | - | - | - | -| p | p | p | p | p | p | p | p | -| x | x | x | x | x | x | x | x | -| c | pad | pad | pad | pad | pad | pad | pad | - -第三行的 1~7 字节仍然属于结构体 foo。 - -```c -typedef struct A { - int a; -}A; -``` - -`sizeof(A) == 4`,因为其类型内部最长基本类型是 int,长度为 4 字节。 - -```c -typedef struct B { - char b; - A a; -}; -``` - -`sizeof(B) == 8` 其内部最长基本类型为 int,`type(A.a) == int`,所以长度需要是 4 的倍数,最后 B 中的填充应该为: - -| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | -| - | - | - | - | - | - | - | - | -| b | pad | pad | pad | A.a | A.a | A.a | A.a | - -## #pragma pack(n) - -在代码中使用 `#pragma pack(n)` 可以指定对齐系数,n=1,2,4,8,16(在不同平台下的不同编译器支持不一样),希望通过举几个例子让大家更好的理解其工作方式: - -```c -struct Test { - char a; - int b; - char c; -}; -``` - -在 64 位系统中的对齐应该如下: - -| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | -| - | - | - | - | - | - | - | - | -| a(0) | pad | pad | pad | b(0) | b(1) | b(2) | b(3) | -| c(0) | pad | pad | pad | | | | | - -显而易见,`sizeof(Test) == 12`,其中第二行的 4~7 字节已经不属于 Test 结构体的部分。 - -如果使用 `#pragma pack(1)` - -```c -#pragma pack(1) -struct Test { - char a; - int b; - char c; -}; -``` - -`sizeof(Test) == 6`,为什么?请看下面的内存对齐方式: - -| 0 | -| - | -| c(0) | -| b(0) | -| b(1) | -| b(2) | -| b(3) | -| c(0) | - -如果使用 `#pragma pack(2)` - -```c -#pragma pack(2) -struct Test { - char a; - int b; - char c; -}; -``` - -`sizeof(Test) == 8`,为什么?请看下面内存对齐方式: - -| 0 | 1 | -| - | - | -| a(0) | pad | -| b(0) | b(1) | -| b(2) | b(3) | -| c(0) | pad | - -如果使用 `#pragma pack(4)` - -```c -#pragma pack(4) -struct Test { - char a; - double b; // 注意这里改为了 double 类型 - char c; -}; -``` - -`sizeof(Test) == 12`,为什么?请看下面内存对齐方式: - -| 0 | 1 | 2 | 3 | -| - | - | - | - | -| a(0) | pad | pad | pad | -| b(0) | b(1) | b(2) | b(3) | -| b(4) | b(5) | b(6) | b(7) | -| c(0) | pad | pad | pad | - -> #pragma pack(n),如果 n * 8 >= 当前操作系统位数,则与不使用 #pragma pack 是一样的效果。 - -## 长度为 0 的数组 - -前面提到长度为 0 的数组表现得很奇怪。 - -```c -int main() { - int a[0]; - printf("%ld\n", sizeof(a)); // 0 -} -``` - -长度为 0 的数组有什么用呢? - -**可以实现动态分配数组长度**。不知道读者写 C 的时候有没有一种很不爽的感觉,就是声明数组的时候一定需要声明常数长度数组,一点都不灵活。有兴趣的读者可以参考:[Using the GNU Compiler Collection (GCC): Zero Length](https://gcc.gnu.org/onlinedocs/gcc/Zero-Length.html) - -```c - typedef struct { - int x; - char c; - int y[0]; - } data; -``` - -```c -data *dp = (data *)malloc(100); -printf("%p\t%p\n", dp, dp->y); // 0xc44010 0xc44018 -``` - -从输出结果可以看出,y 占位符也是遵循内存对齐原则的,而不是紧跟在 char c 之后。 - -64位 glic 下,malloc 函数返回的值(内存起始地址)总是 16 的倍数。 - -# 谁去完成内存对齐的工作? - -**编译器** - -编译器能够为代码做很多优化,比如使生成的目标程序最小等,但是 C 编译器**不会自动**进行结构体的变量顺序的优化,因为 C 是一门主要面相操作系统等控制硬件的软件开发,如果 C 擅自进行了结构体中变量顺序的优化有可能导致异常行为。因为很多硬件信号都是通过某一特定位来控制的。 - -对于想更加深入了解内存模型的读者,可以深入看看 C++ 的内存模型,融入了更多虚函数表、继承等特性。 +--- +layout: post +title: "内存对齐" +date: 2018-04-01 09:00:05 +0800 +categories: 内存对齐 +--- + +> 本机器 64 位操作系统 + +# 为什么有内存对齐? + +对时间和空间的双重优化,比如 64 位系统数据总线大小为 64 位,使用内存对齐方案就可以一次从内存中加载更多数据,比如 64 位中可以存放 `char[4] + int`。CPU 中的高速缓存是非常小的,笔者当前电脑 CPU 是 4 核 8 线程,每一个线程的高速缓存是 6144KB。 + +对于程序员来说数据是分为一个个字节的,而对于 CPU 来说数据是分为一块一块的,比如 64-bit 架构下,CPU 一次可加载 64-bit 数据,而且对于数据的起始地址是有一定限制的。某些平台下,严格要求特定长度的块需要从特定地址加载,比如加载一个 64-bit int 要求加载的起始位置是 8 的倍数,如果地址不符合条件,需要转化为两次加载,最后将两次读取的数据拼接,会增大 CPU 的消耗。 + +所以一方面是减少内存使用,数据尽量小,增加高速缓存的命中率,一方面是改变默认数据对齐,增加 CPU 加载数据的时间消耗。没有哪个方案一定优于另一个方案,所以 GCC 提供了 `#pragma pack(n)` 编译选项,给予程序员选择的自由,代价是程序员需知道如何去驾驭它。 + +C/Cpp 中的内存对齐方案同样适用于 Golang 和 Rust。 + +# C 中基本类型大小 + +```c + int main() { + printf("int=%ld char=%ld float=%ld double=%ld long=%ld pointer=%ld long double=%ld\n", sizeof(int), sizeof(char), sizeof(float), sizeof(double), sizeof(long), sizeof(int *), sizeof(long double)); +``` + +output: + +``` +int=4 char=1 float=4 double=8 long=8 pointer=8 long double=16 +``` + +- 数组类型大小 = 类型大小 * 数组大小 # 数组长度为 0 很奇怪 +- 指针大小 = 操作系统字长 # 本机器中为 `64 / 8 = 8` + +# 对齐方式 + +**C 的变量不是能够存放在任意地址的**,限制:变量存放的起始位置必须是类型大小的整数倍。 + +- char:`sizeof(char) == 1` char 能够存放在任何起始地址 +- short:`sizeof(short) == 2` short 的起始地址必须是 2 的整数倍 +- int:`sizeof(int) == 4` int 的起始地址必须是 4 的整数倍 +- float:与 int 一样 +- double:`sizeof(double) == 8` double 的起始地址必须是 8 的整数倍 +- long:与 doubel 一样 + +> 起始地址至于类型大小有关,与有无符号类型无关 + +填充 padding 的值不确定,这不影响程序员编程,因为我们根本不会访问到 padding。 + +```c +// A +char *p; +char c; + +// B +char c; +char *p; +``` + +使用 A 方案可能会比 B 方案浪费更多内存。因为 A 方案的指针需要进行内存对齐,p 的起始存储地址必须是 8 的整数倍,而 B 方案的字符类型 c 可以存放在任何起始地址。 + +## struct 结构体 + +结构体的大小会根据其内部最长**基本类型**对齐,这句话听起来确实比较别扭,举些例子: + +> 基本类型最长为 16 字节,long double + +有以下结构体: + +```c +struct foo { + char *p; + long x; + char c; +}; +``` + +其对应的内存布局为: + +| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | +| - | - | - | - | - | - | - | - | +| p | p | p | p | p | p | p | p | +| x | x | x | x | x | x | x | x | +| c | pad | pad | pad | pad | pad | pad | pad | + +第三行的 1~7 字节仍然属于结构体 foo。 + +```c +typedef struct A { + int a; +}A; +``` + +`sizeof(A) == 4`,因为其类型内部最长基本类型是 int,长度为 4 字节。 + +```c +typedef struct B { + char b; + A a; +}; +``` + +`sizeof(B) == 8` 其内部最长基本类型为 int,`type(A.a) == int`,所以长度需要是 4 的倍数,最后 B 中的填充应该为: + +| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | +| - | - | - | - | - | - | - | - | +| b | pad | pad | pad | A.a | A.a | A.a | A.a | + +## #pragma pack(n) + +在代码中使用 `#pragma pack(n)` 可以指定对齐系数,n=1,2,4,8,16(在不同平台下的不同编译器支持不一样),希望通过举几个例子让大家更好的理解其工作方式: + +```c +struct Test { + char a; + int b; + char c; +}; +``` + +在 64 位系统中的对齐应该如下: + +| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | +| - | - | - | - | - | - | - | - | +| a(0) | pad | pad | pad | b(0) | b(1) | b(2) | b(3) | +| c(0) | pad | pad | pad | | | | | + +显而易见,`sizeof(Test) == 12`,其中第二行的 4~7 字节已经不属于 Test 结构体的部分。 + +如果使用 `#pragma pack(1)` + +```c +#pragma pack(1) +struct Test { + char a; + int b; + char c; +}; +``` + +`sizeof(Test) == 6`,为什么?请看下面的内存对齐方式: + +| 0 | +| - | +| c(0) | +| b(0) | +| b(1) | +| b(2) | +| b(3) | +| c(0) | + +如果使用 `#pragma pack(2)` + +```c +#pragma pack(2) +struct Test { + char a; + int b; + char c; +}; +``` + +`sizeof(Test) == 8`,为什么?请看下面内存对齐方式: + +| 0 | 1 | +| - | - | +| a(0) | pad | +| b(0) | b(1) | +| b(2) | b(3) | +| c(0) | pad | + +如果使用 `#pragma pack(4)` + +```c +#pragma pack(4) +struct Test { + char a; + double b; // 注意这里改为了 double 类型 + char c; +}; +``` + +`sizeof(Test) == 12`,为什么?请看下面内存对齐方式: + +| 0 | 1 | 2 | 3 | +| - | - | - | - | +| a(0) | pad | pad | pad | +| b(0) | b(1) | b(2) | b(3) | +| b(4) | b(5) | b(6) | b(7) | +| c(0) | pad | pad | pad | + +> #pragma pack(n),如果 n * 8 >= 当前操作系统位数,则与不使用 #pragma pack 是一样的效果。 + +## 长度为 0 的数组 + +前面提到长度为 0 的数组表现得很奇怪。 + +```c +int main() { + int a[0]; + printf("%ld\n", sizeof(a)); // 0 +} +``` + +长度为 0 的数组有什么用呢? + +**可以实现动态分配数组长度**。不知道读者写 C 的时候有没有一种很不爽的感觉,就是声明数组的时候一定需要声明常数长度数组,一点都不灵活。有兴趣的读者可以参考:[Using the GNU Compiler Collection (GCC): Zero Length](https://gcc.gnu.org/onlinedocs/gcc/Zero-Length.html) + +```c + typedef struct { + int x; + char c; + int y[0]; + } data; +``` + +```c +data *dp = (data *)malloc(100); +printf("%p\t%p\n", dp, dp->y); // 0xc44010 0xc44018 +``` + +从输出结果可以看出,y 占位符也是遵循内存对齐原则的,而不是紧跟在 char c 之后。 + +64位 glic 下,malloc 函数返回的值(内存起始地址)总是 16 的倍数。 + +# 谁去完成内存对齐的工作? + +**编译器** + +编译器能够为代码做很多优化,比如使生成的目标程序最小等,但是 C 编译器**不会自动**进行结构体的变量顺序的优化,因为 C 是一门主要面相操作系统等控制硬件的软件开发,如果 C 擅自进行了结构体中变量顺序的优化有可能导致异常行为。因为很多硬件信号都是通过某一特定位来控制的。 + +对于想更加深入了解内存模型的读者,可以深入看看 C++ 的内存模型,融入了更多虚函数表、继承等特性。 diff --git a/src/md/2018-04-04-super-in-python.md b/src/md/2018-04-04-super-in-python.md index af9544b..ea3f229 100644 --- a/src/md/2018-04-04-super-in-python.md +++ b/src/md/2018-04-04-super-in-python.md @@ -1,378 +1,378 @@ ---- -layout: post -title: "super in python" -date: 2018-04-04 20:00:05 +0800 -categories: python ---- - -# 问题描述 - -Java 只允许单继承,创建类很少出现某些奇怪现象,但是 Python 支持多继承 不熟悉 MRO 有可能导致类无法被创建?不相信请尝试以下代码: - -```python -O = object - -class X(O): pass - -class Y(O): pass - -class A(X, Y): pass - -class B(Y, X): pass - -class C(A, B): pass -``` - -具体原因设计到 MRO 所使用的 C3 算法,笔者有在下面展开分析。 - -## super 在 Python2 与 Python3 之间的区别 - -以下代码在 2 和 3 都能够正常运行 - -```python -class Child(Base): - def __init__(self): - super(Child, self).__init__() -``` - -以下代码只能在 3 运行 - -```python -class Child(Base): - def __init__(self): - super().__init__() -``` - -## `super().__init__()` 与 `Base.__init__(self)` 的区别 - -思考以下以下两个代码片段可能产生的效果有什么区别: - -```python -# 1 -class Child(Base): - def __init__(self): - super(Child, self).__init__() -``` - -```python -# 2 -class Child(Base): - def __init(self): - Base.__init__(self) -``` - -**一定要使用代码片段 1,而不应该使用代码片段 2** - -- `super(Child, self)` 可以减少硬编码为 `Base` - -如果 Python 的解析器能够帮助我们做的事情,我们为什么一定要硬编码?如果未来 Child 的父类改变了,忘了改 `Base.__init__(self)` 那就可能产生灾难。Python 是脚本语言并没有经过完整的编译,上述错误只有在运行时才能够被发现。 - -- `super()` 可以实现多继承,造成可能像 C++ 一样出现基类重复的情况,C++ 的解决方案是虚基类,那么 Python 呢? - -Python 支持多继承,假设 `class Child(Base1, Base2)`,那么是不是手动一个一个地调用父类的 `__init__` 方法,如以下丑陋且易错的代码: - - -```python -class Child(Base1, Base2): - def __init__(self): - Base1.__init__(self) - Base2.__init__(self) -``` - -> 继承中一定要使用 `super` - -如果你在生产环境采用了类似的代码,那么 code review 的时候很可能被公开批评,特别是在多继承的结构变得复杂以后,尤其容易出错。具体分析看下面的 MRO 介绍。 - -# 正文 - -## MRO - -`super()` 是根据 MRO(Method Resolution Order) 计算的,而 Python 的 MRO 采用了 C3 算法。 - -### C3 算法 - -有以下的类结构: - -```python -O = object -class F(O): pass -class E(O): pass -class D(O): pass -class C(D,F): pass -class B(D,E): pass -class A(B,C): pass -``` - -1. 设 L[cls] 为类 cls 到其根父类的路径 -2. 设 merge(P1, P2, P3) 操作是从 P1...P3 寻找元素 x,其中符合 x 要么不在 P 中,要么是 P 的第一个元素,如: - -``` -merge(abc, ac, co) -= a + merge(bc, c, co) -= ab + merge(c, c, co) -= abc + merge(o) -= abco -``` - -``` -L[O] = O -L[F] = FO -L[E] = EO -L[D] = DO -``` - -上述三个我想读者都不会有异议。下面着重分析 C/B/A: - -``` -L[C] = C + merge(L[D], L[F], DF) -= C + merge(DO, FO, DF) -= CD + merge(O, FO, F) -= CDF + merge(O) -= CDFO - -L[B] = B + merge(L[D], L[E], DE) -= B + merge(DO, EO, DE) -= BD + merge(O, EO, E) -= BDEO - -L[A] = A + merge(L[B], L[C], BC) -= A + merge(BDEO, CDFO, BC) -= AB + merge(DEO, CDFO, C) -= ABC + merge(DEO, DFO) -= ABCD + merge(EO, FO) -= ABCDEFO -``` - -所以创建 A 类的 `__init__` 和 `__new__` 方法调用顺序为:`A-->B-->C-->D-->E-->F-->O`。 - -读者可以通过上述代码,通过 `A.mro()` 或 `A.__mro__` 检验是否正确。 - -### 分析 C 为什么无法被创建 - -回到之前的问题,为什么 class C 是无法被创建的。 - -```python -O = object - -class X(O): pass - -class Y(O): pass - -class A(X, Y): pass - -class B(Y, X): pass - -class C(A, B): pass -``` - -继承树的结构如下: - -``` - O - / \ - / \ - X Y - / \ / \ - /____\/____\ - A B - \ / - \ / - \ / - C -``` - -很容易产生错误的认识,如果创建类 C 不会产生问题,类似 C++ 中的虚基类初始化顺序为:`O-->X-->Y-->X-->A-->B-->C`,但事实上确实无法创建 class C。 - -按照上述 C3 算法计算 L[C]: - -``` -L[O] = O -L[X] = XO -L[Y] = YO -L[A] = AXYO -L[B] = BYX0 - -L[C] = C + merge(L[A], L[B], AB) -= C + merge(AXYO, BYXO, AB) -= CA + merge(XYO, BYXO, B) -= CAB + merge(XYO, YXO) # 无法继续计算 -``` - -`merge(XYO, YXO)` 误解,因为 X/Y/O 三个元素都不满足以下两个条件: -1. 要不不存在 P 中 -2. 要么是 P 中的第一个元素 - -所以 Python 无法确定其初始化的顺序,也就无法创建类 C。 - -## `__init__` 与 `__new__` 的区别 - -```python -class A: - def __init__(self, *args, **kwargs): - super(A, self).__init__(*args, **kwargs) - - def __new__(cls, *args, **kwargs): - return super(A, cls).__new__(cls, *args, **kwargs) -``` - -| `__init__` | `__new__` | -| :-------: | :-----: | -| 初始化实例的属性 | 创建实例 | -| 没返回值 | 有返回值 | -| 不需要传递 self | 需要传递 cls | -| 后于 `__new__` 调用 | 先于 `__init__` 调用 | -| 实例方法,第一个参数是 self | 类方法,第一个参数是 cls | - -大多数情况下,我们是不需要重写父类的 `__new__` 方法的,除非需要实现单例模式、不可变量等属性。元编程可以借助 `__new__` 实现,后面有机会写一篇关于 Python 元编程的文章。 - -`super().__init__()` 并没有携带 self 参数,说明 `super()` 调用返回的是一个实例。 - -而为什么 `super().__new__(cls)` 需要附带 cls 参数呢? - -那首先得知道 `super()` 到底返回的是什么,在不同情况下调用有什么不同的表现? - -我写了这个小 demo: - -```python -from typing import Any - - -class A: - def __new__(cls) -> Any: - print('A.__new__') - s = super() - return s.__new__(cls) - - def __init__(self): - print('A.__init__') - s = super() - s.__init__() - - -class B(A): - - def __new__(cls) -> Any: - print('B.__new__') - s = super() - return s.__new__(cls) - - def __init__(self): - print('B.__init__') - s = super() - s.__init__() - - -class C(B): - - def __new__(cls) -> Any: - print('C.__new__') - s = super() - return s.__new__(cls) - - def __init__(self): - print('C.__init__') - s = super() - s.__init__() - - -c = C() -``` - -通过打断点,逐个检查 super() 的返回值,以及每一个 `__new__` 方法中的 cls 参数 和 `__init__` 方法中的参数 self 的变化,我得出以下结论: - -- `__new__` 方法先于 `__init__` 方法执行 -- `super()` 似乎每次都返回相同的值 -- `__new__` 返回不是本类的实例,`__init__` 方法也就无法被调用 -- `__new__` 中方法的 cls 一直都是同一个 cls,也就是 cls 一直往下传递。我通过 `id(cls)` 来判断的,在 CPython 中,id 方法返回的是内存地址值,我发现 `id(cls)` 每次都返回同样的内容 -- `__init__` 中方法的 self 一直都是同一个 self,验证方法与 `__new__` 一样 - -也就是说 `super()` 是找到 MRO 中下一个父类的 `__new__` 和 `__init__` 进行调用。 - -发现了没,Python 类中如果重写了某个父类方法 `fun(self)`,但是在某个时刻我们需要调用父类的方法 `fun`,该如何处理呢? - -假设父类为 Base,子类为 Child。除了可以使用 `Base.fun(self)` 调用外。还可以 `super().fun()`,当然这种方案只能够调用在 MRO 中紧跟 Child 的类的方法,但是如果我们想多跳几级呢?设 Base 的唯一父类为 SuperBase,我们需要在 Child 中调用 SuperBase 的实例方法,我们可以 `super(Base, self).fun()`。当然在代码中应该避免这样的调用,因为下次阅读代码需要再次计算 MRO,为了代码的可读性,应该这样调用:`SuperBase.fun(self)`。 - -## new-style & old-style - -在 stackoverflow 看到这样一个问题: -[old-style class 与 new-style class 区别](https://stackoverflow.com/questions/1848474/method-resolution-order-mro-in-new-style-classes/1848647#1848647) - -```python -class A: x = 'a' - -class B(A): pass - -class C(A): x = 'c' - -class D(B, C): pass - -D.x # 'a' -``` - -```python -class A(object): x = 'a' - -class B(A): pass - -class C(A): x = 'c' - -class D(B, C): pass - -D.x # 'c' -``` - -上述的结果我在 Python3.6 中都无法复现,但是我在 Python2.7 中复现了。因为 "old-style class" 只存在于 Python2,Python3 中只有 new-style class。new-style class 是在 Python2.1 后引入的,以下声明方法决定其是 new or old style: - -```python -# new -class A(object): pass - -# old -class A: pass -``` - -## 不使用 super() 实现父类实例初始化 - -没有 super,怎么调用多级的父类 `__init__` 方法呢? - -还记得我们有 `mro()` 方法,而且 super() 的初始化顺序就是按照 MRO 进行的。 - -如果没有 `super()`,可能需要写类似以下的代码: - -```python -class Child(Base): - def __init__(self): - mro = type(self).mro() - for next_class in mro[mro.index(Child)+1:]: - if hasattr(next_class, '__init__'): - # 调用实例方法 - next_class.__init__(self) - break -``` - -在多继承中以下代码还能够正常运行吗? - -可以。 - -最后举个**错误**例子: - -```python -class A: - def __init__(self): - print('A.__init__') - super(self.__class__, self).__init__() # 1 - - -class B(A): - def __init__(self): - print('B.__init__') - super(self.__class__, self).__init__() # 2 -``` - -在 2 处调用 `super(self.__class__, self).__init__()` 时候传递的参数 self 是 B 的实例,所以传递到 `A.__init__` 1 处 self 依然是 B 的实例,`super(self.__class__, self).__init__()` 这条语句和 2 产生一样的效果,继续执行 `A.__init__` 最后导致栈溢出。 - -# 参考 - -[Things to Know About Python Super](https://www.artima.com/weblogs/viewpost.jsp?thread=236275) - -[Python MRO](https://www.python.org/download/releases/2.3/mro/) +--- +layout: post +title: "super in python" +date: 2018-04-04 20:00:05 +0800 +categories: python +--- + +# 问题描述 + +Java 只允许单继承,创建类很少出现某些奇怪现象,但是 Python 支持多继承 不熟悉 MRO 有可能导致类无法被创建?不相信请尝试以下代码: + +```python +O = object + +class X(O): pass + +class Y(O): pass + +class A(X, Y): pass + +class B(Y, X): pass + +class C(A, B): pass +``` + +具体原因设计到 MRO 所使用的 C3 算法,笔者有在下面展开分析。 + +## super 在 Python2 与 Python3 之间的区别 + +以下代码在 2 和 3 都能够正常运行 + +```python +class Child(Base): + def __init__(self): + super(Child, self).__init__() +``` + +以下代码只能在 3 运行 + +```python +class Child(Base): + def __init__(self): + super().__init__() +``` + +## `super().__init__()` 与 `Base.__init__(self)` 的区别 + +思考以下以下两个代码片段可能产生的效果有什么区别: + +```python +# 1 +class Child(Base): + def __init__(self): + super(Child, self).__init__() +``` + +```python +# 2 +class Child(Base): + def __init(self): + Base.__init__(self) +``` + +**一定要使用代码片段 1,而不应该使用代码片段 2** + +- `super(Child, self)` 可以减少硬编码为 `Base` + +如果 Python 的解析器能够帮助我们做的事情,我们为什么一定要硬编码?如果未来 Child 的父类改变了,忘了改 `Base.__init__(self)` 那就可能产生灾难。Python 是脚本语言并没有经过完整的编译,上述错误只有在运行时才能够被发现。 + +- `super()` 可以实现多继承,造成可能像 C++ 一样出现基类重复的情况,C++ 的解决方案是虚基类,那么 Python 呢? + +Python 支持多继承,假设 `class Child(Base1, Base2)`,那么是不是手动一个一个地调用父类的 `__init__` 方法,如以下丑陋且易错的代码: + + +```python +class Child(Base1, Base2): + def __init__(self): + Base1.__init__(self) + Base2.__init__(self) +``` + +> 继承中一定要使用 `super` + +如果你在生产环境采用了类似的代码,那么 code review 的时候很可能被公开批评,特别是在多继承的结构变得复杂以后,尤其容易出错。具体分析看下面的 MRO 介绍。 + +# 正文 + +## MRO + +`super()` 是根据 MRO(Method Resolution Order) 计算的,而 Python 的 MRO 采用了 C3 算法。 + +### C3 算法 + +有以下的类结构: + +```python +O = object +class F(O): pass +class E(O): pass +class D(O): pass +class C(D,F): pass +class B(D,E): pass +class A(B,C): pass +``` + +1. 设 L[cls] 为类 cls 到其根父类的路径 +2. 设 merge(P1, P2, P3) 操作是从 P1...P3 寻找元素 x,其中符合 x 要么不在 P 中,要么是 P 的第一个元素,如: + +``` +merge(abc, ac, co) += a + merge(bc, c, co) += ab + merge(c, c, co) += abc + merge(o) += abco +``` + +``` +L[O] = O +L[F] = FO +L[E] = EO +L[D] = DO +``` + +上述三个我想读者都不会有异议。下面着重分析 C/B/A: + +``` +L[C] = C + merge(L[D], L[F], DF) += C + merge(DO, FO, DF) += CD + merge(O, FO, F) += CDF + merge(O) += CDFO + +L[B] = B + merge(L[D], L[E], DE) += B + merge(DO, EO, DE) += BD + merge(O, EO, E) += BDEO + +L[A] = A + merge(L[B], L[C], BC) += A + merge(BDEO, CDFO, BC) += AB + merge(DEO, CDFO, C) += ABC + merge(DEO, DFO) += ABCD + merge(EO, FO) += ABCDEFO +``` + +所以创建 A 类的 `__init__` 和 `__new__` 方法调用顺序为:`A-->B-->C-->D-->E-->F-->O`。 + +读者可以通过上述代码,通过 `A.mro()` 或 `A.__mro__` 检验是否正确。 + +### 分析 C 为什么无法被创建 + +回到之前的问题,为什么 class C 是无法被创建的。 + +```python +O = object + +class X(O): pass + +class Y(O): pass + +class A(X, Y): pass + +class B(Y, X): pass + +class C(A, B): pass +``` + +继承树的结构如下: + +``` + O + / \ + / \ + X Y + / \ / \ + /____\/____\ + A B + \ / + \ / + \ / + C +``` + +很容易产生错误的认识,如果创建类 C 不会产生问题,类似 C++ 中的虚基类初始化顺序为:`O-->X-->Y-->X-->A-->B-->C`,但事实上确实无法创建 class C。 + +按照上述 C3 算法计算 L[C]: + +``` +L[O] = O +L[X] = XO +L[Y] = YO +L[A] = AXYO +L[B] = BYX0 + +L[C] = C + merge(L[A], L[B], AB) += C + merge(AXYO, BYXO, AB) += CA + merge(XYO, BYXO, B) += CAB + merge(XYO, YXO) # 无法继续计算 +``` + +`merge(XYO, YXO)` 误解,因为 X/Y/O 三个元素都不满足以下两个条件: +1. 要不不存在 P 中 +2. 要么是 P 中的第一个元素 + +所以 Python 无法确定其初始化的顺序,也就无法创建类 C。 + +## `__init__` 与 `__new__` 的区别 + +```python +class A: + def __init__(self, *args, **kwargs): + super(A, self).__init__(*args, **kwargs) + + def __new__(cls, *args, **kwargs): + return super(A, cls).__new__(cls, *args, **kwargs) +``` + +| `__init__` | `__new__` | +| :-------: | :-----: | +| 初始化实例的属性 | 创建实例 | +| 没返回值 | 有返回值 | +| 不需要传递 self | 需要传递 cls | +| 后于 `__new__` 调用 | 先于 `__init__` 调用 | +| 实例方法,第一个参数是 self | 类方法,第一个参数是 cls | + +大多数情况下,我们是不需要重写父类的 `__new__` 方法的,除非需要实现单例模式、不可变量等属性。元编程可以借助 `__new__` 实现,后面有机会写一篇关于 Python 元编程的文章。 + +`super().__init__()` 并没有携带 self 参数,说明 `super()` 调用返回的是一个实例。 + +而为什么 `super().__new__(cls)` 需要附带 cls 参数呢? + +那首先得知道 `super()` 到底返回的是什么,在不同情况下调用有什么不同的表现? + +我写了这个小 demo: + +```python +from typing import Any + + +class A: + def __new__(cls) -> Any: + print('A.__new__') + s = super() + return s.__new__(cls) + + def __init__(self): + print('A.__init__') + s = super() + s.__init__() + + +class B(A): + + def __new__(cls) -> Any: + print('B.__new__') + s = super() + return s.__new__(cls) + + def __init__(self): + print('B.__init__') + s = super() + s.__init__() + + +class C(B): + + def __new__(cls) -> Any: + print('C.__new__') + s = super() + return s.__new__(cls) + + def __init__(self): + print('C.__init__') + s = super() + s.__init__() + + +c = C() +``` + +通过打断点,逐个检查 super() 的返回值,以及每一个 `__new__` 方法中的 cls 参数 和 `__init__` 方法中的参数 self 的变化,我得出以下结论: + +- `__new__` 方法先于 `__init__` 方法执行 +- `super()` 似乎每次都返回相同的值 +- `__new__` 返回不是本类的实例,`__init__` 方法也就无法被调用 +- `__new__` 中方法的 cls 一直都是同一个 cls,也就是 cls 一直往下传递。我通过 `id(cls)` 来判断的,在 CPython 中,id 方法返回的是内存地址值,我发现 `id(cls)` 每次都返回同样的内容 +- `__init__` 中方法的 self 一直都是同一个 self,验证方法与 `__new__` 一样 + +也就是说 `super()` 是找到 MRO 中下一个父类的 `__new__` 和 `__init__` 进行调用。 + +发现了没,Python 类中如果重写了某个父类方法 `fun(self)`,但是在某个时刻我们需要调用父类的方法 `fun`,该如何处理呢? + +假设父类为 Base,子类为 Child。除了可以使用 `Base.fun(self)` 调用外。还可以 `super().fun()`,当然这种方案只能够调用在 MRO 中紧跟 Child 的类的方法,但是如果我们想多跳几级呢?设 Base 的唯一父类为 SuperBase,我们需要在 Child 中调用 SuperBase 的实例方法,我们可以 `super(Base, self).fun()`。当然在代码中应该避免这样的调用,因为下次阅读代码需要再次计算 MRO,为了代码的可读性,应该这样调用:`SuperBase.fun(self)`。 + +## new-style & old-style + +在 stackoverflow 看到这样一个问题: +[old-style class 与 new-style class 区别](https://stackoverflow.com/questions/1848474/method-resolution-order-mro-in-new-style-classes/1848647#1848647) + +```python +class A: x = 'a' + +class B(A): pass + +class C(A): x = 'c' + +class D(B, C): pass + +D.x # 'a' +``` + +```python +class A(object): x = 'a' + +class B(A): pass + +class C(A): x = 'c' + +class D(B, C): pass + +D.x # 'c' +``` + +上述的结果我在 Python3.6 中都无法复现,但是我在 Python2.7 中复现了。因为 "old-style class" 只存在于 Python2,Python3 中只有 new-style class。new-style class 是在 Python2.1 后引入的,以下声明方法决定其是 new or old style: + +```python +# new +class A(object): pass + +# old +class A: pass +``` + +## 不使用 super() 实现父类实例初始化 + +没有 super,怎么调用多级的父类 `__init__` 方法呢? + +还记得我们有 `mro()` 方法,而且 super() 的初始化顺序就是按照 MRO 进行的。 + +如果没有 `super()`,可能需要写类似以下的代码: + +```python +class Child(Base): + def __init__(self): + mro = type(self).mro() + for next_class in mro[mro.index(Child)+1:]: + if hasattr(next_class, '__init__'): + # 调用实例方法 + next_class.__init__(self) + break +``` + +在多继承中以下代码还能够正常运行吗? + +可以。 + +最后举个**错误**例子: + +```python +class A: + def __init__(self): + print('A.__init__') + super(self.__class__, self).__init__() # 1 + + +class B(A): + def __init__(self): + print('B.__init__') + super(self.__class__, self).__init__() # 2 +``` + +在 2 处调用 `super(self.__class__, self).__init__()` 时候传递的参数 self 是 B 的实例,所以传递到 `A.__init__` 1 处 self 依然是 B 的实例,`super(self.__class__, self).__init__()` 这条语句和 2 产生一样的效果,继续执行 `A.__init__` 最后导致栈溢出。 + +# 参考 + +[Things to Know About Python Super](https://www.artima.com/weblogs/viewpost.jsp?thread=236275) + +[Python MRO](https://www.python.org/download/releases/2.3/mro/) diff --git "a/src/md/2018-04-08-\350\277\236\347\273\255\346\225\260\347\273\204\344\270\255\347\232\204\346\234\200\345\244\247\345\255\220\346\225\260\347\273\204.md" "b/src/md/2018-04-08-\350\277\236\347\273\255\346\225\260\347\273\204\344\270\255\347\232\204\346\234\200\345\244\247\345\255\220\346\225\260\347\273\204.md" index bb40938..8883edd 100644 --- "a/src/md/2018-04-08-\350\277\236\347\273\255\346\225\260\347\273\204\344\270\255\347\232\204\346\234\200\345\244\247\345\255\220\346\225\260\347\273\204.md" +++ "b/src/md/2018-04-08-\350\277\236\347\273\255\346\225\260\347\273\204\344\270\255\347\232\204\346\234\200\345\244\247\345\255\220\346\225\260\347\273\204.md" @@ -1,100 +1,100 @@ ---- -layout: post -title: "数组中的连续元素的最大和" -date: 2018-04-08 15:00:05 +0800 -categories: 算法 ---- - -# 分治法 - -设数组左边界为 lo,右边界为 hi,即 `data[lo]...data[hi]`,可以计算出中点下标为:`mid = (hi+lo)/2`。 - -数组中的连续元素的最大和一共有三种情况: -1. 在 `data[lo:mid]` 中,也就是在左子数组中 -2. 在 `data[mid+1:]` 中,也就是在右子数组中 -3. 在 `data[x:y]` 中,其中 `x<=mid<=y` 也就是跨越左右子数组 - -可以看出在上述情况中,问题可以分解为更小的子问题来解决,在原算法复杂度高于 O(n) 时,分治法可以提高时间效率,但是需要话费更大的空间,因为需要递归栈。 - -撸起袖子写代码: - -```go -func MaxSubArrayDivideAndConquer(data []int) int { - if len(data) == 0 { - return math.MinInt32 - } - mid := len(data) / 2 - // 计算跨越中介的最大子数组 - midmax, sum := math.MinInt32, 0 - for i := mid; i >= 0; i-- { - sum += data[i] - if sum > midmax { - midmax = sum - } - } - for j := mid + 1; j < len(data); j++ { - sum += data[j] - if sum > midmax { - midmax = sum - } - } - // 计算左数组中的最大值 - l := MaxSubArrayDivideAndConquer(data[:mid]) - // 计算右数组的最大值 - r := MaxSubArrayDivideAndConquer(data[mid+1:]) - var max int - if l > r { - max = l - } else { - max = r - } - if midmax > max { - max = midmax - } - return max -} -``` - -`T(n) = T(n/2) + n` - -- 时间复杂度:`O(nlog(n))` -- 空间复杂度:`O(log(n)` - -其中 log(n) 为递归栈的深度,在递归树中,每一层都需要计算 n 个元素之和。 - -# 线性方法 - -解决该问题还有一种效率更高的方法: - -每一个下标 j 都有可能是最大子数组的结束下标,计算以 j 结尾的最大子数组之和,只需要知道以 j-1 结尾的最大子数组之和就可以,公式表达为: - -`m[j] = max(0, max[j-1]) + data[j]` - -算法实现: - -```go -func MaxSubArrayLinear(data []int) int { - max, leftSum := math.MinInt32, math.MinInt32 - for i := 0; i < len(data); i++ { - if leftSum <= 0 { - leftSum = 0 - } - leftSum += data[i] - if leftSum > max { - max = leftSum - } - } - return max -} -``` - -- 时间复杂度:`O(n)` -- 空间复杂度:`O(1)` - -**好的算法实现起来往往是简单的,在时间和空间效率上更高** - -# 总结 - -最近笔试各大公司在最后算法题总是不能 AC,也在跟着这个[仓库](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/Leetcode%20%E9%A2%98%E8%A7%A3.md) 刷 leetcode 题目。 - -最近很大的感受是,只有把思路理清楚后再写代码,即使是在笔试或者面试时候,不然逻辑上不可能有正确的代码,如果逻辑上(算法)本来就是错误的,用再多时间去写代码也是浪费时间而已。我之前就是还没理清思路就写代码,使得代码越来越丑(判断边界条件的代码越来越多),到后面调试的时候非常痛苦,当然凌乱的逻辑最后往往也是不能够 AC 的。 +--- +layout: post +title: "数组中的连续元素的最大和" +date: 2018-04-08 15:00:05 +0800 +categories: 算法 +--- + +# 分治法 + +设数组左边界为 lo,右边界为 hi,即 `data[lo]...data[hi]`,可以计算出中点下标为:`mid = (hi+lo)/2`。 + +数组中的连续元素的最大和一共有三种情况: +1. 在 `data[lo:mid]` 中,也就是在左子数组中 +2. 在 `data[mid+1:]` 中,也就是在右子数组中 +3. 在 `data[x:y]` 中,其中 `x<=mid<=y` 也就是跨越左右子数组 + +可以看出在上述情况中,问题可以分解为更小的子问题来解决,在原算法复杂度高于 O(n) 时,分治法可以提高时间效率,但是需要话费更大的空间,因为需要递归栈。 + +撸起袖子写代码: + +```go +func MaxSubArrayDivideAndConquer(data []int) int { + if len(data) == 0 { + return math.MinInt32 + } + mid := len(data) / 2 + // 计算跨越中介的最大子数组 + midmax, sum := math.MinInt32, 0 + for i := mid; i >= 0; i-- { + sum += data[i] + if sum > midmax { + midmax = sum + } + } + for j := mid + 1; j < len(data); j++ { + sum += data[j] + if sum > midmax { + midmax = sum + } + } + // 计算左数组中的最大值 + l := MaxSubArrayDivideAndConquer(data[:mid]) + // 计算右数组的最大值 + r := MaxSubArrayDivideAndConquer(data[mid+1:]) + var max int + if l > r { + max = l + } else { + max = r + } + if midmax > max { + max = midmax + } + return max +} +``` + +`T(n) = T(n/2) + n` + +- 时间复杂度:`O(nlog(n))` +- 空间复杂度:`O(log(n)` + +其中 log(n) 为递归栈的深度,在递归树中,每一层都需要计算 n 个元素之和。 + +# 线性方法 + +解决该问题还有一种效率更高的方法: + +每一个下标 j 都有可能是最大子数组的结束下标,计算以 j 结尾的最大子数组之和,只需要知道以 j-1 结尾的最大子数组之和就可以,公式表达为: + +`m[j] = max(0, max[j-1]) + data[j]` + +算法实现: + +```go +func MaxSubArrayLinear(data []int) int { + max, leftSum := math.MinInt32, math.MinInt32 + for i := 0; i < len(data); i++ { + if leftSum <= 0 { + leftSum = 0 + } + leftSum += data[i] + if leftSum > max { + max = leftSum + } + } + return max +} +``` + +- 时间复杂度:`O(n)` +- 空间复杂度:`O(1)` + +**好的算法实现起来往往是简单的,在时间和空间效率上更高** + +# 总结 + +最近笔试各大公司在最后算法题总是不能 AC,也在跟着这个[仓库](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/Leetcode%20%E9%A2%98%E8%A7%A3.md) 刷 leetcode 题目。 + +最近很大的感受是,只有把思路理清楚后再写代码,即使是在笔试或者面试时候,不然逻辑上不可能有正确的代码,如果逻辑上(算法)本来就是错误的,用再多时间去写代码也是浪费时间而已。我之前就是还没理清思路就写代码,使得代码越来越丑(判断边界条件的代码越来越多),到后面调试的时候非常痛苦,当然凌乱的逻辑最后往往也是不能够 AC 的。 diff --git "a/src/md/2018-04-10-\345\255\227\347\254\246\344\270\262\344\270\255\347\232\204\345\255\220\344\270\262\351\227\256\351\242\230.md" "b/src/md/2018-04-10-\345\255\227\347\254\246\344\270\262\344\270\255\347\232\204\345\255\220\344\270\262\351\227\256\351\242\230.md" index 54fb351..fa3b397 100644 --- "a/src/md/2018-04-10-\345\255\227\347\254\246\344\270\262\344\270\255\347\232\204\345\255\220\344\270\262\351\227\256\351\242\230.md" +++ "b/src/md/2018-04-10-\345\255\227\347\254\246\344\270\262\344\270\255\347\232\204\345\255\220\344\270\262\351\227\256\351\242\230.md" @@ -1,293 +1,293 @@ ---- -layout: post -title: "字符串中的子串问题" -date: 2018-04-11 20:00:05 +0800 -categories: 算法 ---- - -# 问题描述 - -在阅读《编程珠玑》第二版字符串一章中提到了一个解决寻找最长子串的问题的方案,之前还没见识过这方案,今天阅读后觉得有必要 mark down。 - -**如何寻找字符串中重复出现的最长子串?** - -# 算法介绍 - -> 注意:以下代码使用 Go 实现。简单起见,假设所有输入都是 ASCII 编码的字符,因为 Go 使用 UTF-8 编码,解码 utf-8 操作需要额外操作,而这里以介绍算法为主。 - -Go 中采用了类似 Java 的字符串池化的概念,取子串的消耗非常低,因为底层复用了父字符串,所以在算法实现中使用了大量的取字符串的子串操作。 - -先定义一个用于计算两个字符串相同前缀的长度的函数: - -```go -func commonLen(i, j int, s string) (cnt int) { - if i > j { - i, j = j, i - } - for j < len(s) && s[j] == s[i] { - cnt++ - i++ - j++ - } - return cnt -} -``` - -## 嵌套循环 - -最简单的解决方案是使用两个循环进行比较: - -```go -func TwoCircle(source string) string { - maxLen := 0 - maxStart := 0 - for i := 0; i < len(source)-1; i++ { - for j := i + 1; j < len(source); j++ { - if r := commonLen(i, j, source); r > maxLen { - maxLen = r - maxStart = i - } - } - } - return source[maxStart: maxStart+maxLen] -} -``` - -- 时间复杂度:`O(n^2)` -- 空间复杂度:`O(1)` - -## 后缀数组 - -**Step 1** 从第 0 个位置开始,每次切割切割字符串中头部,比如 banana,定义指针数组 `char *a[6]`: - -```c -a[0] = "banana" -a[1] = "anana" -a[2] = "nana" -a[3] = "ana" -a[4] = "na" -a[5] = "a" -``` - -**规律:每个字符串拥有相同的后缀** - -**step 2** 对 a 指针数组进行排序,其比较规则是根据指针指向数组的大小,结果为: - -```c -a[0] = "a" -a[1] = "ana" -a[2] = "anana" -a[3] = "banana" -a[4] = "na" -a[5] = "nana" -``` - -**规律:具有相同前缀的字符串互相相邻** - -**step 3** 遍历数组 a 比较相邻元素,找出最长子串长度。上面的排序操作的目的就是为了让具有相同前缀的字符串相邻,这样进行比较操作时,只需要与相邻元素进行比较,而不需要从头到尾进行比较。 - -显而易见,最长子串是:`ana` - -### 代码实现 - -```go -func SuffixArray(source string) string { - // 因为 Go 中指针不可运算,所以这里我们存储的是字符的起始位置 - sarr := make([]int, len(source)) - for i := 0; i < len(sarr); i++ { - sarr[i] = i - } - ssa := StringSuffixArray{sarr:sarr, source:source} - sort.Sort(&ssa) - // 对排序后的子字符串,每个字符串与右边字符串进行比较,寻找字符串中重复出现的最长子串 - maxStart, maxLen := 0, 0 - for i := 0; i < len(sarr) - 1; i++ { - if r := commonLen(sarr[i], sarr[i+1], source); r > maxLen { - maxLen = r - maxStart = sarr[i] - } - } - return source[maxStart:maxStart+maxLen] -} - -// 以下代码是为实现 sort.Interface 接口,使用 Go 自带的快速排序算法 -type StringSuffixArray struct { - sarr []int - source string -} - -func (s *StringSuffixArray) Less(i, j int) bool { - return strings.Compare(s.source[s.sarr[i]:], s.source[s.sarr[j]:]) < 0 -} - -func (s *StringSuffixArray) Swap(i, j int) { - s.sarr[i], s.sarr[j] = s.sarr[j], s.sarr[i] -} - -func (s *StringSuffixArray) Len() int { - return len(s.sarr) -} -``` - -- 时间复杂度:`O(nlogn)` -- 空间复杂度:`O(n)` - -基于后缀数组实现查找字符串中重复出现的最长子串算法,时间主要花费在对子串进行排序。 - -## 衍生问题 - -### 至少重复 M 次的串 - -已经找到了“查找重复出现的最长子串”(问题一)的算法,那请思考如何实现“查找重复出现至少 M 次的最长子串”(问题二)? - -可以尝试着寻找问题一与问题二有哪些共通之处: - -1. 重复出现 -2. 子串 - -在阅读下面之前希望读者先花几分钟思考一样如何使用**后缀数组**实现。 - -#### 解决算法 - -对后缀数组排序后得到的子串的特点是: - -1. 每个字符串具有相同的后缀 -2. 具有相同、相似前缀的字符串相邻 - -这里假设一个字符串:"bananana",寻找重复出现至少 3 次的最长子串。 - -**step 1** 从头到为取后缀子串: - -```c -a[0] = "bananana" -a[1] = "ananana" -a[2] = "nanana" -a[3] = "anana" -a[4] = "nana" -a[5] = "ana" -a[6] = "na" -a[7] = "a" -``` - -**step 2** 对后缀数组进行排序,得到结果: - -```c -a[0] = "a" -a[1] = "ana" -a[2] = "anana" -a[3] = "ananana" -a[4] = "bananana" -a[5] = "na" -a[6] = "nana" -a[7] = "nanana" -``` - -比较子串相同部分的长度主要是比较其前缀,和相邻元素进行比较是寻找重复出现至少 2 次的子串(这个结论很重要)。 - -那么要寻找至少重复出现 M 次的子串,则只需要 `a[i]` 与 `a[i+M-1]` 进行前缀对比,找出相同前缀的长度。 - -为什么? - -因为如果 `a[i]` 与 `a[i+M-1]` 具有相同的前缀 prefix,那么可以证明 `a[i]` 与 `a[i+1]`、`a[i+2]`....`a[i+M-1]` 都具有 prefix 前缀,`a[i]`...`a[i+M-1]` 共有 M 个子串,所以能够保证 prefix 至少重复出现了 M 次。(可以借鉴 "bananana" 的排序后的子串) - -#### 代码实现 - -```go -func SubStringDuplicateMTimes(source string, M int) string { - if M <= 1 { - return source - } - sarr := make([]int, len(source)) - for i := 0; i < len(sarr); i++ { - sarr[i] = i - } - ssa := StringSuffixArray{sarr:sarr, source:source} - sort.Sort(&ssa) - maxStart, maxLen := 0, 0 - // 从后往前遍历 - for i := len(sarr) - 1; i - M + 1 >= 0; i-- { - if r := commonLen(sarr[i], sarr[i-M+1], source); r > maxLen { - maxLen = r - maxStart = sarr[i] - } - } - return source[maxStart:maxStart+maxLen] -} -``` - -### 两个字符串中最长公共子串 - -这是面试腾讯实习时一道面试题。 - -后缀数组是针对一个字符串的,如果题目给出了两个或者多个字符串应该怎么办呢? - -**将两个字符串拼接成一个字符串。** - -最后更新公共子串时只要保证不越界即可,具体代码如下: - -```go -func LongestCommonSubstring(s, t string) string { - // merge to one string and use suffix array. - m := s + t - sl, ml := len(s), len(m) - arr := make([]int, ml) - for i := 0; i < ml; i++ { - arr[i] = i - } - maxLen, start := 0, 0 - sort.Sort(&StringSuffixArray{arr, m}) - for x := 0; x < ml-1; x++ { - ax := arr[x] - for y := x + 1; y < ml; y++ { - ay := arr[y] - if ax > ay { - ax, ay = ay, ax - } - if ax >= sl && ay < sl { - continue - } - if v := commLen(ax, ay, m); v > maxLen { - if ax+v > sl { - v = sl - ax - } - if v > maxLen { - maxLen, start = v, ax - } - } - } - } - return s[start : start+maxLen] -} -``` - -### 字符串中最长回文串 - -设串 s:`abccbaccdcpffpcdef`,求字符串中最长回文串。答案是显然的 `dcpffpcd`。 - -回文串的特点:**正序与逆序相等**。 - -解决方案: - -1. 令 t 为 s 的反转(`t = "fedcpffpcdccabccba"`) -2. 求 s 与 t 的最长公共子串 - -```go -func longestPalindrome(s string) string { - t := reverse(s) - return LongestCommonSubstring(s, t) -} - -// reverse string s -// note: only for ascii -func reverse(s string) string { - t := []rune(s) - for i, j := 0, len(s) - 1; i < j; i, j = i + 1, j - 1 { - t[i], t[j] = t[j], t[i] - } - return string(t) -} -``` - -# 写在最后 - -《编程珠玑》中多次提到不要用几分钟思考算法,用几个小时去实现;而应该用一个小时去思考,几十分钟去实现。厉害的程序员总是很“懒”的,大部分时间都不在写代码,而是思考。 +--- +layout: post +title: "字符串中的子串问题" +date: 2018-04-11 20:00:05 +0800 +categories: 算法 +--- + +# 问题描述 + +在阅读《编程珠玑》第二版字符串一章中提到了一个解决寻找最长子串的问题的方案,之前还没见识过这方案,今天阅读后觉得有必要 mark down。 + +**如何寻找字符串中重复出现的最长子串?** + +# 算法介绍 + +> 注意:以下代码使用 Go 实现。简单起见,假设所有输入都是 ASCII 编码的字符,因为 Go 使用 UTF-8 编码,解码 utf-8 操作需要额外操作,而这里以介绍算法为主。 + +Go 中采用了类似 Java 的字符串池化的概念,取子串的消耗非常低,因为底层复用了父字符串,所以在算法实现中使用了大量的取字符串的子串操作。 + +先定义一个用于计算两个字符串相同前缀的长度的函数: + +```go +func commonLen(i, j int, s string) (cnt int) { + if i > j { + i, j = j, i + } + for j < len(s) && s[j] == s[i] { + cnt++ + i++ + j++ + } + return cnt +} +``` + +## 嵌套循环 + +最简单的解决方案是使用两个循环进行比较: + +```go +func TwoCircle(source string) string { + maxLen := 0 + maxStart := 0 + for i := 0; i < len(source)-1; i++ { + for j := i + 1; j < len(source); j++ { + if r := commonLen(i, j, source); r > maxLen { + maxLen = r + maxStart = i + } + } + } + return source[maxStart: maxStart+maxLen] +} +``` + +- 时间复杂度:`O(n^2)` +- 空间复杂度:`O(1)` + +## 后缀数组 + +**Step 1** 从第 0 个位置开始,每次切割切割字符串中头部,比如 banana,定义指针数组 `char *a[6]`: + +```c +a[0] = "banana" +a[1] = "anana" +a[2] = "nana" +a[3] = "ana" +a[4] = "na" +a[5] = "a" +``` + +**规律:每个字符串拥有相同的后缀** + +**step 2** 对 a 指针数组进行排序,其比较规则是根据指针指向数组的大小,结果为: + +```c +a[0] = "a" +a[1] = "ana" +a[2] = "anana" +a[3] = "banana" +a[4] = "na" +a[5] = "nana" +``` + +**规律:具有相同前缀的字符串互相相邻** + +**step 3** 遍历数组 a 比较相邻元素,找出最长子串长度。上面的排序操作的目的就是为了让具有相同前缀的字符串相邻,这样进行比较操作时,只需要与相邻元素进行比较,而不需要从头到尾进行比较。 + +显而易见,最长子串是:`ana` + +### 代码实现 + +```go +func SuffixArray(source string) string { + // 因为 Go 中指针不可运算,所以这里我们存储的是字符的起始位置 + sarr := make([]int, len(source)) + for i := 0; i < len(sarr); i++ { + sarr[i] = i + } + ssa := StringSuffixArray{sarr:sarr, source:source} + sort.Sort(&ssa) + // 对排序后的子字符串,每个字符串与右边字符串进行比较,寻找字符串中重复出现的最长子串 + maxStart, maxLen := 0, 0 + for i := 0; i < len(sarr) - 1; i++ { + if r := commonLen(sarr[i], sarr[i+1], source); r > maxLen { + maxLen = r + maxStart = sarr[i] + } + } + return source[maxStart:maxStart+maxLen] +} + +// 以下代码是为实现 sort.Interface 接口,使用 Go 自带的快速排序算法 +type StringSuffixArray struct { + sarr []int + source string +} + +func (s *StringSuffixArray) Less(i, j int) bool { + return strings.Compare(s.source[s.sarr[i]:], s.source[s.sarr[j]:]) < 0 +} + +func (s *StringSuffixArray) Swap(i, j int) { + s.sarr[i], s.sarr[j] = s.sarr[j], s.sarr[i] +} + +func (s *StringSuffixArray) Len() int { + return len(s.sarr) +} +``` + +- 时间复杂度:`O(nlogn)` +- 空间复杂度:`O(n)` + +基于后缀数组实现查找字符串中重复出现的最长子串算法,时间主要花费在对子串进行排序。 + +## 衍生问题 + +### 至少重复 M 次的串 + +已经找到了“查找重复出现的最长子串”(问题一)的算法,那请思考如何实现“查找重复出现至少 M 次的最长子串”(问题二)? + +可以尝试着寻找问题一与问题二有哪些共通之处: + +1. 重复出现 +2. 子串 + +在阅读下面之前希望读者先花几分钟思考一样如何使用**后缀数组**实现。 + +#### 解决算法 + +对后缀数组排序后得到的子串的特点是: + +1. 每个字符串具有相同的后缀 +2. 具有相同、相似前缀的字符串相邻 + +这里假设一个字符串:"bananana",寻找重复出现至少 3 次的最长子串。 + +**step 1** 从头到为取后缀子串: + +```c +a[0] = "bananana" +a[1] = "ananana" +a[2] = "nanana" +a[3] = "anana" +a[4] = "nana" +a[5] = "ana" +a[6] = "na" +a[7] = "a" +``` + +**step 2** 对后缀数组进行排序,得到结果: + +```c +a[0] = "a" +a[1] = "ana" +a[2] = "anana" +a[3] = "ananana" +a[4] = "bananana" +a[5] = "na" +a[6] = "nana" +a[7] = "nanana" +``` + +比较子串相同部分的长度主要是比较其前缀,和相邻元素进行比较是寻找重复出现至少 2 次的子串(这个结论很重要)。 + +那么要寻找至少重复出现 M 次的子串,则只需要 `a[i]` 与 `a[i+M-1]` 进行前缀对比,找出相同前缀的长度。 + +为什么? + +因为如果 `a[i]` 与 `a[i+M-1]` 具有相同的前缀 prefix,那么可以证明 `a[i]` 与 `a[i+1]`、`a[i+2]`....`a[i+M-1]` 都具有 prefix 前缀,`a[i]`...`a[i+M-1]` 共有 M 个子串,所以能够保证 prefix 至少重复出现了 M 次。(可以借鉴 "bananana" 的排序后的子串) + +#### 代码实现 + +```go +func SubStringDuplicateMTimes(source string, M int) string { + if M <= 1 { + return source + } + sarr := make([]int, len(source)) + for i := 0; i < len(sarr); i++ { + sarr[i] = i + } + ssa := StringSuffixArray{sarr:sarr, source:source} + sort.Sort(&ssa) + maxStart, maxLen := 0, 0 + // 从后往前遍历 + for i := len(sarr) - 1; i - M + 1 >= 0; i-- { + if r := commonLen(sarr[i], sarr[i-M+1], source); r > maxLen { + maxLen = r + maxStart = sarr[i] + } + } + return source[maxStart:maxStart+maxLen] +} +``` + +### 两个字符串中最长公共子串 + +这是面试腾讯实习时一道面试题。 + +后缀数组是针对一个字符串的,如果题目给出了两个或者多个字符串应该怎么办呢? + +**将两个字符串拼接成一个字符串。** + +最后更新公共子串时只要保证不越界即可,具体代码如下: + +```go +func LongestCommonSubstring(s, t string) string { + // merge to one string and use suffix array. + m := s + t + sl, ml := len(s), len(m) + arr := make([]int, ml) + for i := 0; i < ml; i++ { + arr[i] = i + } + maxLen, start := 0, 0 + sort.Sort(&StringSuffixArray{arr, m}) + for x := 0; x < ml-1; x++ { + ax := arr[x] + for y := x + 1; y < ml; y++ { + ay := arr[y] + if ax > ay { + ax, ay = ay, ax + } + if ax >= sl && ay < sl { + continue + } + if v := commLen(ax, ay, m); v > maxLen { + if ax+v > sl { + v = sl - ax + } + if v > maxLen { + maxLen, start = v, ax + } + } + } + } + return s[start : start+maxLen] +} +``` + +### 字符串中最长回文串 + +设串 s:`abccbaccdcpffpcdef`,求字符串中最长回文串。答案是显然的 `dcpffpcd`。 + +回文串的特点:**正序与逆序相等**。 + +解决方案: + +1. 令 t 为 s 的反转(`t = "fedcpffpcdccabccba"`) +2. 求 s 与 t 的最长公共子串 + +```go +func longestPalindrome(s string) string { + t := reverse(s) + return LongestCommonSubstring(s, t) +} + +// reverse string s +// note: only for ascii +func reverse(s string) string { + t := []rune(s) + for i, j := 0, len(s) - 1; i < j; i, j = i + 1, j - 1 { + t[i], t[j] = t[j], t[i] + } + return string(t) +} +``` + +# 写在最后 + +《编程珠玑》中多次提到不要用几分钟思考算法,用几个小时去实现;而应该用一个小时去思考,几十分钟去实现。厉害的程序员总是很“懒”的,大部分时间都不在写代码,而是思考。 diff --git "a/src/md/2018-06-10-\346\200\273\347\273\223.md" "b/src/md/2018-06-10-\346\200\273\347\273\223.md" index 38f0c14..b1884ad 100644 --- "a/src/md/2018-06-10-\346\200\273\347\273\223.md" +++ "b/src/md/2018-06-10-\346\200\273\347\273\223.md" @@ -1,207 +1,207 @@ -# 总结 - -昨晚考完大学最后一门考试后,就带着行李箱离开了大学,准备接下来的去腾讯深圳实习了。 - -在等火车的图中,我无意间看了一下李笑来老师在得到上的专栏中的部分免费内容,感觉收益还是挺大的。我简单地列举一下他其中提到的几个思想: - -- 人生中的选择就是投资行为: - - 无论做什么都会存在机会成本和时间成本,所以选择做不做某件事、怎么做、期望得到什么样的结果是一种投资。 -- 周期: - - 在短期中购买等投资行为会有波动,但是将视野拉到更长远,股价是越来越高的。 - - 人们总是觉得当下的情况是多么糟糕,而从长期来看,社会总在进步,人们的生活质量总在提高。 -- 短期与长期: - - 投资某些东西在短期内可以快速见到成效,某些事则是需要经过时间的验证则能够得到回报。比如看一个笑话在短期内得到快乐,在长期来看能够帮助我们成为更加幽默的人。 -- 活在未来: - - 做决策不仅仅只满足当前受益幸福的最大化,而应该最隹整个时间维度上受益的最大化。也就是我们做投资需要更加注重长期带来的影响。 -- 做总结: - - 做总结能够帮助我们整理头脑中一个个知识小碎片,能够起到反思的作用。 - - 借助互联网上的平台还可以分享自己的文章,起到自我营销的作用。 - - 能够帮助我们提高表达能力。 -- 思考判断决策的能力: - - 不要人云亦云,要有独立的思考能力,事情到底是不是像别人所说的那样。 - - 互联网上文章等消息来源非常多,而且很多是重复的,甚至是虚假的,我们需要有甄别能力。 - - 更加好的做法应该是从几个比较优秀的平台吸收消息。 - - 很多别人认为一定是对的事情,很可能就是错的。 -- 碎片化学习: - - 碎片化学习不等于低效学习,某些知识更加适合碎片化的学习。 - - 个人经验发现,在短期内花几天的时间认真看完一本书后,即使期间经历了大量的思考,但是长时间不使用很快就会淡出我的脑海。 - - 学以致用,用则进,不用则废。学习一门新知识需要思考学习它对自己未来的生活工作等是否有帮助。 -- 搭建知识体系: - - 碎片化的知识很难被整理出来,或者说我们的大脑本来就不适合用于存储碎片化的知识。 - - 学习就像是建筑,除了看到某个小楼道外,还需要看见整个建筑的设计和结构,为什么需要这样做。记住推导过程后就不需要再记忆繁琐的最终结果。 -- 学习编程: - - 编程能够帮助我们提高逻辑能力。 - - 编程能够实现自动化某些事情,比如爬虫可以得到我们需要的信息。 - - 在我看来编程语言就是一个软件,不同于一般软件的是,编程语言提供的人机交互接口不是按钮、滑动等,而是通过代码来实现人机交互。它能够很大地提升我们使用软件的边界,学会编程就不需要每次需要一个小众功能时,花费大量的时间在网上搜索相应的软件。 - - 编程并不难,像 Python 等编程语言就很适合非依靠编程工作的人学习。它跟其他的编程语言相比就是机械时代的汽车和现代搭载很多电子设备的汽车的差别,依靠电子设备我们能够更好地操作,比如倒车时的距离感应器,后视摄像头等。 - -## 找实习过程 - -我比较有忧患意识,而且有实验室的学长指导,我算是比较早地开始找实习找工作的准备。 - -比较坑的一点是大三上参加了一个对无论是长期还是短期内对我都没有太多帮助的比赛,花费了我大量的时间,而且期间体验非常差。如果觉得自己在某件事情上的投资不快乐时,需要勇敢地提出离开。如果不快乐地工作,那么长期来看你是在伤害整个团队。这也是刘强东在自传中提到的,如果某个员工不适合公司,会通过补偿机制鼓励他主动离开。 - -各大公司的笔试环节考核的主要还是算法能力和编程能力,如果刷过大量的题目的话,会发现其实最后也就是同一个题型经过描述包装了一下而已。所以我想说的是如果做题目或者解决问题的时候需要对问题以及解决方案做一个抽象,将根本的问题抽象出来,而数学建模就是通过将现实问题抽象为数学模型,然后再使用合适的编程技术来解决。比如: - -> 题目:约瑟夫环 -> 抽象:映射问题,怎么找到元素之间的映射关系是关键 - -笔试环节可以通过内推等方式跳过,能够走内推环节还是尽量走内推环节,不认识在理想公司中的师兄师姐可以在牛客网等平台寻找内推机会。但内推又不是十分必要的,为什么这么说: - -面试官逃不掉的就是考核面试者的算法编程能力,如果算法能力优秀的一定要做笔试,这样可以向面试官展现自己能力。如果算法能力不行,即使有内推最后还是会被面试官刷掉的。 - -基础真的不是能够速成的,需要通过花费大量的时间去钻研思考,引用一句话:什么是经典,经典就是很多人想读,但是却很少人会花时间去钻研的知识。 - -面试过程中就是面试者向面试官展示实习的时刻,不排除某些人为了提高自己在面试过程中的表达能力和应对能力去刷面试的可能性。而面试者可以通过什么途径能够在这么短的时间内展现自己的能力呢?我认为以下几点非常重要: - -- 内推的人在公司或者部门是一个什么样的角色,如果他足够牛逼,可以在一定程度上证明你的实力。这个过程就像是巴菲特给你说这个股票潜力不错,就问你买不买。 -- 简历需要有能够突出重点,至少要让面试官能够找到他感兴趣的点。面试不同的公司、不同的岗位可以使用不同的简历。 -- 项目经历,一定要对项目的整体架构,开发的目的,解决问题等有一个明确的了解,介绍的时候很多人以及书都推荐 STAR 方法。 -- 现场快速找到算法,以及手写代码的能力: - - 现场却要在短时间内(10~15分钟)需要将面试官包装后的问题抽象为一个解决对应问题的算法。 - - 手写代码确实需要练习,因为我们很容易发现平时写代码太过于依赖 IDE 提供的代码提示、静态语法分析等功能了,听说像微软会让面试者在普通的记事本中写代码,我只能说手写代码确实是一门艺术。 - -根据我面试的经历,很多面试官都会问到对未来的规划。这个问题对于在校学生来说真是一个比较复杂的问题,因为大部分人确实是活在当下的。每次遇到这样的问题,我都会直接告诉对方我的真实想法(创业)。对于这个问题的回答,可能不同公司不同岗位不同面试官听到后有不一样的结果。未来规划甚至可以尝试写进简历中,如果简历还有空间。 - -对于大部分公司来说员工创业就意味着人员的流失,当初招聘进来的培养可能会被白白地浪费掉,而且内部创业可能带走的不仅仅只是一个人,而是一个团队。 - -但对于像腾讯、阿里巴巴等比较开放的公司可能更加倾向于促进员工发挥产品思维、内部创业,比如腾讯有众创空间,他们提供了一个更好的制度来鼓励员工内部创业,而且可以通过员工的内部创业可以提高母公司的实力和影响力。 - -大部分人会选择在面试官面前进行各种各样的包装修饰,但是我比较直男,倾向于发表内心真实的想法,因为招聘是一个双向选择的过程,如果要削足适履,那么可能最后只是穿小鞋,自己不能够被分配到合适的岗位。 - -我强烈建议,无论是否想去实习或者找工作的同学都最好去尝试一下招聘,其一能够增长应聘经验,了解理想的岗位需要什么的人才,无论什么学历毕业,最终还是需要面相社会的。能够和腾讯、阿里巴巴等知名企业中的技术大佬坐下来聊上几十分钟还是有很大的收获的,面试最后的提问环节还能够提出内心的疑问,可以是技术上的,也可以是职业发展上的,面试官能够坐在我们前面面试我们,他还是有两把斧子的。 - -BTW,我被腾讯录用为暑假实习生,事业群是IEG,游戏增值服务部门。比较迷的一点是,我到现在为止都没有导师联系方式,也不知道未来需要做的技术方向,但没关系,我对自己的快速学习能力持有自信。 - -大学课程已经全部结束了,剩下来的暑假和大四可能尽可能多的时间会放在在外实习中。 - -## 区块链 - -前一段时间没能耐得住好奇心,去了解了一下区块链,主要是从网上搜索各种各样的资源和开源的文档来了解区块链。我认为比较好的资料是 [https://legacy.gitbook.com/book/yeasy/blockchain_guide/details](https://legacy.gitbook.com/book/yeasy/blockchain_guide/details),我认为他比较好的原因是他除了介绍区块链的技术以外,还花了很大的篇幅介绍区块链的应用场景和区块链制度上的设计优越性。 - -> 我只是简单地了解了区块链的运作方式和应用场景。 - -区块链:一个去中心化的、不可更改的、能够快速验证的、不可抵赖的、价值自平衡存储系统。 - -**去中心化真的有这么重要吗?或者说中心有这么大的影响力吗?** - -区块链的一个比较大的应用场景是加密货币,而加密货币中规模比较大的是比特币,比特币的信徒都相信一个“事实”(政府能够通过不断地发币,稀释持币人所拥有的资产的价值,从而提高政府可以支配的资金),政府的阴谋论。所以有不少人将区块链等同于是反政府的技术。而且比特币的数量是有上限的,这就如同金本位一样安全(地球上可开发黄金数量是有上限的),所以比特币有很好的抗通货膨胀的能力。 - -在没有学习薛兆丰老师的北大经济学课之前我是相信上面的“事实”的。 - -- 政府发币该发多少:这不是由政府完全决定的,当然这不排除部分独裁国家是完全由政府控制。市场上货币总量应该大体上等于市场上货物流通所需要的货币。 -- 政府超发造成的通货膨胀对政府并没有好处,这对入口、出口并没有帮助,最终结果是大家都尽量不使用该国的货币,这样政府印发再多的钞票也没用,因为透支了信用,最后的结果是人们不再相信该政府,该政府可能就会被推翻。 -- 除了科技创新、学术理论的突破,还有制度创新,好的制度能够更好地促进人与人之间的合作、信任与利益分配。 - -### 区块链的博弈论 - -**有没有人会攻击比特币或者其他加密货币呢?** - -在没有了解到区块链的制度设计上时,我心里的答案是肯定会,因为攻击比特币能够获得大量的比特币,自身财富实现了增长。但是进一步思考,某个人或者组织发动了全网 51% 的算力去攻击比特币,那么不仅在重新计算比特币的过程中会消耗大量的电力,所需要的电力可能比所得到的比特币的价值还要大,如果该组织比较贪心,将大部分的比特币都归自己所有,那么比特币网的价值就会大大下降,甚至没有人会使用,攻击者冒着受法律惩罚的风险,攻击比特币并没有什么好处。如果已有的区块链越长,那么想要攻击区块链的难度就越大,因为只要修改了一个区块,那么后续的所有区块都需要改变,因为后一个区块引用了上一个区块的哈希值作为计算本区块哈希值的一个变量。 - -**如果挖矿能够赚取比特币,那么会不会越来越多人加入到挖矿当中呢?** - -不会,比特币网络中的矿机应该维持在一个稳定的水平。如果比特币网络中有非常多的矿机,那么要发动 51% 攻击就越难,比特币就越安全,从而越有价值。但是平均每个旷工所得到的收益就会变小,直到小于某个阀值(将矿机用于其他地方能够赚取到更大的收益),那么就会有旷工陆续地离开比特币网络。相反如果比特币网络中的矿机非常少(此时比特币非常容易受到攻击),那么平均每个旷工得到的收益非常高,就会吸引其他地方的矿机加入到比特币网络中。 - -当我了解到上述两个的博弈设计后,我深深地感受到区块链设计的美妙。 - -但是比特币同样存在一些问题: - -- 无法应对通货紧缩。 -- 初始的几个旷工拿到了太多的财富,分配不均是一个大问题。为什么人们对钟本聪这么感兴趣,除了他是区块链比特币的设计者外,还有就是他很有可能拥有大量的比特币。 -- 产生区块的速度太慢,很可能需要十几分钟、几个小时甚至几天才能够被处理,如果手续费给的不够高,甚至可能永远不会被算进新的区块中,交易无法进行。想想一下,服务员只为给小费高的客人服务,很让人抓狂,但是从另一角度看,如果某个人能够给更高的小费,那么为该客人服务能够让社会的效率更大化。 -- 需要大量的电力资源去计算。 -- 如果某个拥有大量比特币的人死了,而且他的私钥没人知道,那么他所拥有的比特币从此不再流动,随着人的死去,世界上拥有的比特币越来越少,而比特币是有一个最小切割单位(聪),而不是像外界所说的那样可以无休无止地分割比特币的(浮点数计算也会出现不精确性)。 -- 未来的量子计算机等怪物可能会轻松破解公私钥。 - -由于挖矿的存在,用于挖矿的芯片越来越先进,在一定程度上推动了这一科技的进步。 -怎么才能够让一个事务能够发展得快,或者说怎么才能够保护濒临灭绝的动物呢?答案是让人们对他们产生需要,If it pays, it stays. - -除了比特币,区块链还有两个非常大的应用,以太坊与超级账本。 - -相对于比特币,以太坊引入了智能合约的概念。智能合约是代码,可以由用户自行定义功能,在矿机中运行,运行智能合约需要燃料,燃料需要以太坊购买。如果某个智能合约需要执行的代码越多,那么需要的燃料越多。 - - -给我的感觉是,以太坊像是创造了一个将去中心化的政府,以太坊(货币)的发行速度由数学规则设定,人们可以上传智能合约,使用燃料运行某些智能合约,具体运行智能合约的机器是矿机,每个人或者组织都可以发行自己的货币。 - -通过智能合约,我们可以实现像投票、众筹、供应链的管理等去中心化操作。 - -当了解到像比特币的挖矿机制时候,我就觉得要保证完全去中心化的成本是多么的高昂。新的比特币的产生会随着新的区块计算成功而产生,而产生新的比特币的价值可能还不如计算区块所需要的电费价值高(当然这只是站在现在的角度看,长期来看新产生的比特币的价值可能会更加高)。 - -## docker - -学习了 docker 后,我突然发现应用管理与部署等可以变得非常简单,因为大部分部署的命令都封装在配置文件中,部署通常只需要敲几条命令即可。同样安装软件也只需要敲击几条命令即可,通过 docker registry,可以便利地安装经过其他程序员自定义的软件。删除那就更加方便了,一条简单的命令就可以,也不会残留很多垃圾文件。 - -docker 对监控应用的执行状况等也很好地进行检测和限制。 - -因为 docker 采用的文件系统实现了重用,也就是多个应用可以重用某些空间,比如应用A与B同时需要某些中间层,那么该中间层只需要在镜像中被存储一份即可。 - -总体来说,docker 确实非常值得学习,无论是不是需要做运维,docker 能够提供对应用管理隔离提供很好的服务。 - -## 《暗时间》 - -在网上搜索优秀博客的时候,突然有人推荐:[刘未鹏的博客](http://mindhacks.cn/),在其中无意中发现了他写的一本书《暗时间》,本来以为本书介绍的是时间管理,所以从图书馆借回来阅读,后来发现,除了时间管理,其中还说到了心理学,数学,逻辑学,计算机科学等知识点,虽然该书由很多碎片组成,但是这本书确实是一本值得被推荐的书。 - -**在课堂、讲座上,台上的老师可能问台下有什么需要疑问?** - -大部分人都没站起来,理由是不知道要问什么。而不知道不等于没有,不知道也就是心里面拥有非常多的疑问(甚至是不能够理解某事物的定义),而且台上站着的是在该领域有过很多思考、硕果累累的人,能够与这么牛的人交流,为什么不勇敢地提出内心的疑问。 - -不要总是期待自己会突发奇想得到某些好点子,而应该是花大量的时间去思考,因为思考的越多,大脑在潜意识才会产生随机走动,从而产生灵感。 - -## 土木工程与软件工程的对比 - -建筑业发展了上千年,但是真正迈向工程化我认为还是最近几十年的事情。 - -**为什么土木工程建立的建筑能够多年树立不倒的,而软件工程生产的软件会有如此多的 bug,而且后期的维护费用还是如此的高昂呢?** - -**而且建筑工程通常是由非常优秀的人去设计,建筑团队的领导者带领着建筑工人去修建,而基层真正施工的团队受教育水平可能远远没有设计师高,甚至不能够理解设计师的核心思想,或者对设计师的理念产生了误解;而软件工程由最聪明的人完成架构设计,由受过高层教育的程序员符则实施,即使这样生产的软件还是会错漏百出,每次修复一个 bug 都会有可能引入新的 bug。为什么软件工程向建筑工程学习这么困难呢?** - -我对以上问题非常感兴趣。 - -我也和别的同学讨论过这个问题,我得到过以下的答案: - -- 土木工程的复杂度没有软件工程高:土木工程更加倾向于劳动、资金密集型,而软件工程是知识密集型。 -- 土木工程采用了更多冗余:在理论上,地基挖 10米就足够安全了,但是施工团队可能为了安全将地基挖到 12米。 -- 土木工程发展历史更久:所以他们在理论和工程管理上更加先进。 -- 土木工程施工中替代品更多:建房子除了红砖,还可以用水泥砖等等。 -- 土木工程一旦出事故引发的灾难会被法律追究责任,而软件工程被攻击可能更多责任归咎到攻击者,而不是软件的开发团队。 -- 软件工程写代码的人都太过厉害,每个人都有自己的想法,而产生每个人都强烈拥抱自己的想法,导致团队想法不一致。 -- 土木工程设定了很多默认值,大多数情况每一个施工团队都会如此干,但是软件工程的自定义程度太高了,每人都有自己的想法,导致整个团队的目标不一致。 - -为了认识软件工程,我两次尝试读《人月神话》,但是我发现真的是读不懂,有些他们当初存在的问题,现在可能已经不存在了,比如 1MB 的内存。Anyway,未来我还是会尝试读一下,或者是英文原版。 - -## 与高效率的人合作 - -这几天因为要外出实习,不参加学校的实训,多次和导员等进行沟通。办事的体验极差: - -1. 不知道导员等相关工作人员不知道什么时候在办公室 -2. 问一个问题存在极大的不确定性且回复时间极慢 - -比如问老师我能否去校外自主实习,老师说如果XXX同意了就可以,XXX又说只要YYY同意就可以。。。我感觉我的事情在他们那里优先级一点都不高,只有自己干着急。 - -所以我想未来合作一定要找一些办事效率高,能够快速决策的人,如果办事过程中大部分时间都在等待,那么每个人的时间利用率都会大大地降低,无论一个人多么努力,多么优秀他一天可用的时间也就是 24 小时,所以我们一定要提高时间的利用率。 - -## 创业学院上课 - -由于一个活动的机会,我报名参加了为时一天的创业学院的课程,上课的主题是:投资谈判与风险管理、资源整合。 - -期间我还遇到了一位中年的经济学老师来旁听课,她提出了不少看法。我印象比较深刻的有: - -1. 对于一个企业发展最重要是什么,她的答案是忽悠。。。 -2. 她批评上投资谈判与风险管理的老师上课有问题,文不对题,讲的内容是鼓舞人而不是传播知识,也就是说他在说鸡汤而不是传授知识。她作为老师,评判老师上课质量的角度确实与我们学生不大一样。 - -给我最大的感受是,创业学院的老师都是有创业经历、或者是正在创业的。他们上课非常有激情,而且充满自信,与平时上我专业课的老师差距很大,感觉创业的经历给他们带来了非常大的帮助。但话说回来,可能我把因果关系给捣乱了,或许是他们拥有以上的特点,他们才会选择创业。但是这次经历增大了我对创业的热情,感觉自己收到了鼓舞。 - -## task list - -接下的一段时间的任务清单: - -- 争取实习留用 -- 《深入理解计算机系统》 -- [500lines](https://github.com/aosabook/500lines/blob/master/README.md)。You don't know what you cannot build. -- 可能需要学 C/Cpp -- Pytho、Go 的深入学习 -- [9.11 The Human Brain](https://nancysbraintalks.mit.edu/course/9-11-the-human-brain),介绍大脑的一门课,听说非常有趣,当锻炼英语来学习一下 -- 《未来简史》 -- 宁向东的清华管理学课 -- 《王立铭·生命科学50讲》已完成一半 +# 总结 + +昨晚考完大学最后一门考试后,就带着行李箱离开了大学,准备接下来的去腾讯深圳实习了。 + +在等火车的图中,我无意间看了一下李笑来老师在得到上的专栏中的部分免费内容,感觉收益还是挺大的。我简单地列举一下他其中提到的几个思想: + +- 人生中的选择就是投资行为: + - 无论做什么都会存在机会成本和时间成本,所以选择做不做某件事、怎么做、期望得到什么样的结果是一种投资。 +- 周期: + - 在短期中购买等投资行为会有波动,但是将视野拉到更长远,股价是越来越高的。 + - 人们总是觉得当下的情况是多么糟糕,而从长期来看,社会总在进步,人们的生活质量总在提高。 +- 短期与长期: + - 投资某些东西在短期内可以快速见到成效,某些事则是需要经过时间的验证则能够得到回报。比如看一个笑话在短期内得到快乐,在长期来看能够帮助我们成为更加幽默的人。 +- 活在未来: + - 做决策不仅仅只满足当前受益幸福的最大化,而应该最隹整个时间维度上受益的最大化。也就是我们做投资需要更加注重长期带来的影响。 +- 做总结: + - 做总结能够帮助我们整理头脑中一个个知识小碎片,能够起到反思的作用。 + - 借助互联网上的平台还可以分享自己的文章,起到自我营销的作用。 + - 能够帮助我们提高表达能力。 +- 思考判断决策的能力: + - 不要人云亦云,要有独立的思考能力,事情到底是不是像别人所说的那样。 + - 互联网上文章等消息来源非常多,而且很多是重复的,甚至是虚假的,我们需要有甄别能力。 + - 更加好的做法应该是从几个比较优秀的平台吸收消息。 + - 很多别人认为一定是对的事情,很可能就是错的。 +- 碎片化学习: + - 碎片化学习不等于低效学习,某些知识更加适合碎片化的学习。 + - 个人经验发现,在短期内花几天的时间认真看完一本书后,即使期间经历了大量的思考,但是长时间不使用很快就会淡出我的脑海。 + - 学以致用,用则进,不用则废。学习一门新知识需要思考学习它对自己未来的生活工作等是否有帮助。 +- 搭建知识体系: + - 碎片化的知识很难被整理出来,或者说我们的大脑本来就不适合用于存储碎片化的知识。 + - 学习就像是建筑,除了看到某个小楼道外,还需要看见整个建筑的设计和结构,为什么需要这样做。记住推导过程后就不需要再记忆繁琐的最终结果。 +- 学习编程: + - 编程能够帮助我们提高逻辑能力。 + - 编程能够实现自动化某些事情,比如爬虫可以得到我们需要的信息。 + - 在我看来编程语言就是一个软件,不同于一般软件的是,编程语言提供的人机交互接口不是按钮、滑动等,而是通过代码来实现人机交互。它能够很大地提升我们使用软件的边界,学会编程就不需要每次需要一个小众功能时,花费大量的时间在网上搜索相应的软件。 + - 编程并不难,像 Python 等编程语言就很适合非依靠编程工作的人学习。它跟其他的编程语言相比就是机械时代的汽车和现代搭载很多电子设备的汽车的差别,依靠电子设备我们能够更好地操作,比如倒车时的距离感应器,后视摄像头等。 + +## 找实习过程 + +我比较有忧患意识,而且有实验室的学长指导,我算是比较早地开始找实习找工作的准备。 + +比较坑的一点是大三上参加了一个对无论是长期还是短期内对我都没有太多帮助的比赛,花费了我大量的时间,而且期间体验非常差。如果觉得自己在某件事情上的投资不快乐时,需要勇敢地提出离开。如果不快乐地工作,那么长期来看你是在伤害整个团队。这也是刘强东在自传中提到的,如果某个员工不适合公司,会通过补偿机制鼓励他主动离开。 + +各大公司的笔试环节考核的主要还是算法能力和编程能力,如果刷过大量的题目的话,会发现其实最后也就是同一个题型经过描述包装了一下而已。所以我想说的是如果做题目或者解决问题的时候需要对问题以及解决方案做一个抽象,将根本的问题抽象出来,而数学建模就是通过将现实问题抽象为数学模型,然后再使用合适的编程技术来解决。比如: + +> 题目:约瑟夫环 +> 抽象:映射问题,怎么找到元素之间的映射关系是关键 + +笔试环节可以通过内推等方式跳过,能够走内推环节还是尽量走内推环节,不认识在理想公司中的师兄师姐可以在牛客网等平台寻找内推机会。但内推又不是十分必要的,为什么这么说: + +面试官逃不掉的就是考核面试者的算法编程能力,如果算法能力优秀的一定要做笔试,这样可以向面试官展现自己能力。如果算法能力不行,即使有内推最后还是会被面试官刷掉的。 + +基础真的不是能够速成的,需要通过花费大量的时间去钻研思考,引用一句话:什么是经典,经典就是很多人想读,但是却很少人会花时间去钻研的知识。 + +面试过程中就是面试者向面试官展示实习的时刻,不排除某些人为了提高自己在面试过程中的表达能力和应对能力去刷面试的可能性。而面试者可以通过什么途径能够在这么短的时间内展现自己的能力呢?我认为以下几点非常重要: + +- 内推的人在公司或者部门是一个什么样的角色,如果他足够牛逼,可以在一定程度上证明你的实力。这个过程就像是巴菲特给你说这个股票潜力不错,就问你买不买。 +- 简历需要有能够突出重点,至少要让面试官能够找到他感兴趣的点。面试不同的公司、不同的岗位可以使用不同的简历。 +- 项目经历,一定要对项目的整体架构,开发的目的,解决问题等有一个明确的了解,介绍的时候很多人以及书都推荐 STAR 方法。 +- 现场快速找到算法,以及手写代码的能力: + - 现场却要在短时间内(10~15分钟)需要将面试官包装后的问题抽象为一个解决对应问题的算法。 + - 手写代码确实需要练习,因为我们很容易发现平时写代码太过于依赖 IDE 提供的代码提示、静态语法分析等功能了,听说像微软会让面试者在普通的记事本中写代码,我只能说手写代码确实是一门艺术。 + +根据我面试的经历,很多面试官都会问到对未来的规划。这个问题对于在校学生来说真是一个比较复杂的问题,因为大部分人确实是活在当下的。每次遇到这样的问题,我都会直接告诉对方我的真实想法(创业)。对于这个问题的回答,可能不同公司不同岗位不同面试官听到后有不一样的结果。未来规划甚至可以尝试写进简历中,如果简历还有空间。 + +对于大部分公司来说员工创业就意味着人员的流失,当初招聘进来的培养可能会被白白地浪费掉,而且内部创业可能带走的不仅仅只是一个人,而是一个团队。 + +但对于像腾讯、阿里巴巴等比较开放的公司可能更加倾向于促进员工发挥产品思维、内部创业,比如腾讯有众创空间,他们提供了一个更好的制度来鼓励员工内部创业,而且可以通过员工的内部创业可以提高母公司的实力和影响力。 + +大部分人会选择在面试官面前进行各种各样的包装修饰,但是我比较直男,倾向于发表内心真实的想法,因为招聘是一个双向选择的过程,如果要削足适履,那么可能最后只是穿小鞋,自己不能够被分配到合适的岗位。 + +我强烈建议,无论是否想去实习或者找工作的同学都最好去尝试一下招聘,其一能够增长应聘经验,了解理想的岗位需要什么的人才,无论什么学历毕业,最终还是需要面相社会的。能够和腾讯、阿里巴巴等知名企业中的技术大佬坐下来聊上几十分钟还是有很大的收获的,面试最后的提问环节还能够提出内心的疑问,可以是技术上的,也可以是职业发展上的,面试官能够坐在我们前面面试我们,他还是有两把斧子的。 + +BTW,我被腾讯录用为暑假实习生,事业群是IEG,游戏增值服务部门。比较迷的一点是,我到现在为止都没有导师联系方式,也不知道未来需要做的技术方向,但没关系,我对自己的快速学习能力持有自信。 + +大学课程已经全部结束了,剩下来的暑假和大四可能尽可能多的时间会放在在外实习中。 + +## 区块链 + +前一段时间没能耐得住好奇心,去了解了一下区块链,主要是从网上搜索各种各样的资源和开源的文档来了解区块链。我认为比较好的资料是 [https://legacy.gitbook.com/book/yeasy/blockchain_guide/details](https://legacy.gitbook.com/book/yeasy/blockchain_guide/details),我认为他比较好的原因是他除了介绍区块链的技术以外,还花了很大的篇幅介绍区块链的应用场景和区块链制度上的设计优越性。 + +> 我只是简单地了解了区块链的运作方式和应用场景。 + +区块链:一个去中心化的、不可更改的、能够快速验证的、不可抵赖的、价值自平衡存储系统。 + +**去中心化真的有这么重要吗?或者说中心有这么大的影响力吗?** + +区块链的一个比较大的应用场景是加密货币,而加密货币中规模比较大的是比特币,比特币的信徒都相信一个“事实”(政府能够通过不断地发币,稀释持币人所拥有的资产的价值,从而提高政府可以支配的资金),政府的阴谋论。所以有不少人将区块链等同于是反政府的技术。而且比特币的数量是有上限的,这就如同金本位一样安全(地球上可开发黄金数量是有上限的),所以比特币有很好的抗通货膨胀的能力。 + +在没有学习薛兆丰老师的北大经济学课之前我是相信上面的“事实”的。 + +- 政府发币该发多少:这不是由政府完全决定的,当然这不排除部分独裁国家是完全由政府控制。市场上货币总量应该大体上等于市场上货物流通所需要的货币。 +- 政府超发造成的通货膨胀对政府并没有好处,这对入口、出口并没有帮助,最终结果是大家都尽量不使用该国的货币,这样政府印发再多的钞票也没用,因为透支了信用,最后的结果是人们不再相信该政府,该政府可能就会被推翻。 +- 除了科技创新、学术理论的突破,还有制度创新,好的制度能够更好地促进人与人之间的合作、信任与利益分配。 + +### 区块链的博弈论 + +**有没有人会攻击比特币或者其他加密货币呢?** + +在没有了解到区块链的制度设计上时,我心里的答案是肯定会,因为攻击比特币能够获得大量的比特币,自身财富实现了增长。但是进一步思考,某个人或者组织发动了全网 51% 的算力去攻击比特币,那么不仅在重新计算比特币的过程中会消耗大量的电力,所需要的电力可能比所得到的比特币的价值还要大,如果该组织比较贪心,将大部分的比特币都归自己所有,那么比特币网的价值就会大大下降,甚至没有人会使用,攻击者冒着受法律惩罚的风险,攻击比特币并没有什么好处。如果已有的区块链越长,那么想要攻击区块链的难度就越大,因为只要修改了一个区块,那么后续的所有区块都需要改变,因为后一个区块引用了上一个区块的哈希值作为计算本区块哈希值的一个变量。 + +**如果挖矿能够赚取比特币,那么会不会越来越多人加入到挖矿当中呢?** + +不会,比特币网络中的矿机应该维持在一个稳定的水平。如果比特币网络中有非常多的矿机,那么要发动 51% 攻击就越难,比特币就越安全,从而越有价值。但是平均每个旷工所得到的收益就会变小,直到小于某个阀值(将矿机用于其他地方能够赚取到更大的收益),那么就会有旷工陆续地离开比特币网络。相反如果比特币网络中的矿机非常少(此时比特币非常容易受到攻击),那么平均每个旷工得到的收益非常高,就会吸引其他地方的矿机加入到比特币网络中。 + +当我了解到上述两个的博弈设计后,我深深地感受到区块链设计的美妙。 + +但是比特币同样存在一些问题: + +- 无法应对通货紧缩。 +- 初始的几个旷工拿到了太多的财富,分配不均是一个大问题。为什么人们对钟本聪这么感兴趣,除了他是区块链比特币的设计者外,还有就是他很有可能拥有大量的比特币。 +- 产生区块的速度太慢,很可能需要十几分钟、几个小时甚至几天才能够被处理,如果手续费给的不够高,甚至可能永远不会被算进新的区块中,交易无法进行。想想一下,服务员只为给小费高的客人服务,很让人抓狂,但是从另一角度看,如果某个人能够给更高的小费,那么为该客人服务能够让社会的效率更大化。 +- 需要大量的电力资源去计算。 +- 如果某个拥有大量比特币的人死了,而且他的私钥没人知道,那么他所拥有的比特币从此不再流动,随着人的死去,世界上拥有的比特币越来越少,而比特币是有一个最小切割单位(聪),而不是像外界所说的那样可以无休无止地分割比特币的(浮点数计算也会出现不精确性)。 +- 未来的量子计算机等怪物可能会轻松破解公私钥。 + +由于挖矿的存在,用于挖矿的芯片越来越先进,在一定程度上推动了这一科技的进步。 +怎么才能够让一个事务能够发展得快,或者说怎么才能够保护濒临灭绝的动物呢?答案是让人们对他们产生需要,If it pays, it stays. + +除了比特币,区块链还有两个非常大的应用,以太坊与超级账本。 + +相对于比特币,以太坊引入了智能合约的概念。智能合约是代码,可以由用户自行定义功能,在矿机中运行,运行智能合约需要燃料,燃料需要以太坊购买。如果某个智能合约需要执行的代码越多,那么需要的燃料越多。 + + +给我的感觉是,以太坊像是创造了一个将去中心化的政府,以太坊(货币)的发行速度由数学规则设定,人们可以上传智能合约,使用燃料运行某些智能合约,具体运行智能合约的机器是矿机,每个人或者组织都可以发行自己的货币。 + +通过智能合约,我们可以实现像投票、众筹、供应链的管理等去中心化操作。 + +当了解到像比特币的挖矿机制时候,我就觉得要保证完全去中心化的成本是多么的高昂。新的比特币的产生会随着新的区块计算成功而产生,而产生新的比特币的价值可能还不如计算区块所需要的电费价值高(当然这只是站在现在的角度看,长期来看新产生的比特币的价值可能会更加高)。 + +## docker + +学习了 docker 后,我突然发现应用管理与部署等可以变得非常简单,因为大部分部署的命令都封装在配置文件中,部署通常只需要敲几条命令即可。同样安装软件也只需要敲击几条命令即可,通过 docker registry,可以便利地安装经过其他程序员自定义的软件。删除那就更加方便了,一条简单的命令就可以,也不会残留很多垃圾文件。 + +docker 对监控应用的执行状况等也很好地进行检测和限制。 + +因为 docker 采用的文件系统实现了重用,也就是多个应用可以重用某些空间,比如应用A与B同时需要某些中间层,那么该中间层只需要在镜像中被存储一份即可。 + +总体来说,docker 确实非常值得学习,无论是不是需要做运维,docker 能够提供对应用管理隔离提供很好的服务。 + +## 《暗时间》 + +在网上搜索优秀博客的时候,突然有人推荐:[刘未鹏的博客](http://mindhacks.cn/),在其中无意中发现了他写的一本书《暗时间》,本来以为本书介绍的是时间管理,所以从图书馆借回来阅读,后来发现,除了时间管理,其中还说到了心理学,数学,逻辑学,计算机科学等知识点,虽然该书由很多碎片组成,但是这本书确实是一本值得被推荐的书。 + +**在课堂、讲座上,台上的老师可能问台下有什么需要疑问?** + +大部分人都没站起来,理由是不知道要问什么。而不知道不等于没有,不知道也就是心里面拥有非常多的疑问(甚至是不能够理解某事物的定义),而且台上站着的是在该领域有过很多思考、硕果累累的人,能够与这么牛的人交流,为什么不勇敢地提出内心的疑问。 + +不要总是期待自己会突发奇想得到某些好点子,而应该是花大量的时间去思考,因为思考的越多,大脑在潜意识才会产生随机走动,从而产生灵感。 + +## 土木工程与软件工程的对比 + +建筑业发展了上千年,但是真正迈向工程化我认为还是最近几十年的事情。 + +**为什么土木工程建立的建筑能够多年树立不倒的,而软件工程生产的软件会有如此多的 bug,而且后期的维护费用还是如此的高昂呢?** + +**而且建筑工程通常是由非常优秀的人去设计,建筑团队的领导者带领着建筑工人去修建,而基层真正施工的团队受教育水平可能远远没有设计师高,甚至不能够理解设计师的核心思想,或者对设计师的理念产生了误解;而软件工程由最聪明的人完成架构设计,由受过高层教育的程序员符则实施,即使这样生产的软件还是会错漏百出,每次修复一个 bug 都会有可能引入新的 bug。为什么软件工程向建筑工程学习这么困难呢?** + +我对以上问题非常感兴趣。 + +我也和别的同学讨论过这个问题,我得到过以下的答案: + +- 土木工程的复杂度没有软件工程高:土木工程更加倾向于劳动、资金密集型,而软件工程是知识密集型。 +- 土木工程采用了更多冗余:在理论上,地基挖 10米就足够安全了,但是施工团队可能为了安全将地基挖到 12米。 +- 土木工程发展历史更久:所以他们在理论和工程管理上更加先进。 +- 土木工程施工中替代品更多:建房子除了红砖,还可以用水泥砖等等。 +- 土木工程一旦出事故引发的灾难会被法律追究责任,而软件工程被攻击可能更多责任归咎到攻击者,而不是软件的开发团队。 +- 软件工程写代码的人都太过厉害,每个人都有自己的想法,而产生每个人都强烈拥抱自己的想法,导致团队想法不一致。 +- 土木工程设定了很多默认值,大多数情况每一个施工团队都会如此干,但是软件工程的自定义程度太高了,每人都有自己的想法,导致整个团队的目标不一致。 + +为了认识软件工程,我两次尝试读《人月神话》,但是我发现真的是读不懂,有些他们当初存在的问题,现在可能已经不存在了,比如 1MB 的内存。Anyway,未来我还是会尝试读一下,或者是英文原版。 + +## 与高效率的人合作 + +这几天因为要外出实习,不参加学校的实训,多次和导员等进行沟通。办事的体验极差: + +1. 不知道导员等相关工作人员不知道什么时候在办公室 +2. 问一个问题存在极大的不确定性且回复时间极慢 + +比如问老师我能否去校外自主实习,老师说如果XXX同意了就可以,XXX又说只要YYY同意就可以。。。我感觉我的事情在他们那里优先级一点都不高,只有自己干着急。 + +所以我想未来合作一定要找一些办事效率高,能够快速决策的人,如果办事过程中大部分时间都在等待,那么每个人的时间利用率都会大大地降低,无论一个人多么努力,多么优秀他一天可用的时间也就是 24 小时,所以我们一定要提高时间的利用率。 + +## 创业学院上课 + +由于一个活动的机会,我报名参加了为时一天的创业学院的课程,上课的主题是:投资谈判与风险管理、资源整合。 + +期间我还遇到了一位中年的经济学老师来旁听课,她提出了不少看法。我印象比较深刻的有: + +1. 对于一个企业发展最重要是什么,她的答案是忽悠。。。 +2. 她批评上投资谈判与风险管理的老师上课有问题,文不对题,讲的内容是鼓舞人而不是传播知识,也就是说他在说鸡汤而不是传授知识。她作为老师,评判老师上课质量的角度确实与我们学生不大一样。 + +给我最大的感受是,创业学院的老师都是有创业经历、或者是正在创业的。他们上课非常有激情,而且充满自信,与平时上我专业课的老师差距很大,感觉创业的经历给他们带来了非常大的帮助。但话说回来,可能我把因果关系给捣乱了,或许是他们拥有以上的特点,他们才会选择创业。但是这次经历增大了我对创业的热情,感觉自己收到了鼓舞。 + +## task list + +接下的一段时间的任务清单: + +- 争取实习留用 +- 《深入理解计算机系统》 +- [500lines](https://github.com/aosabook/500lines/blob/master/README.md)。You don't know what you cannot build. +- 可能需要学 C/Cpp +- Pytho、Go 的深入学习 +- [9.11 The Human Brain](https://nancysbraintalks.mit.edu/course/9-11-the-human-brain),介绍大脑的一门课,听说非常有趣,当锻炼英语来学习一下 +- 《未来简史》 +- 宁向东的清华管理学课 +- 《王立铭·生命科学50讲》已完成一半 diff --git "a/src/md/2018-09-01-\350\205\276\350\256\257\345\256\236\344\271\240\346\200\273\347\273\223.md" "b/src/md/2018-09-01-\350\205\276\350\256\257\345\256\236\344\271\240\346\200\273\347\273\223.md" index f1178fc..0fbd7af 100644 --- "a/src/md/2018-09-01-\350\205\276\350\256\257\345\256\236\344\271\240\346\200\273\347\273\223.md" +++ "b/src/md/2018-09-01-\350\205\276\350\256\257\345\256\236\344\271\240\346\200\273\347\273\223.md" @@ -1,135 +1,135 @@ ---- -layout: post -title: "腾讯实习总结" -date: 2018-09-01 09:00:05 +0800 -categories: 总结 ---- - -# In short - -很遗憾没有拿到留用 offer,办公环境真是好,福利超级棒,人很 Nice,思想很活跃,技术栈有点老和闭源,关注理财。 - -# Main - -**我实习的时间不长,看到的东西比较片面,就像苏格兰黑山羊。** - -到腾讯实习后,才知道一面面试官是搞数据挖掘和分析的,二面面试官是客户端大佬,而我应聘的岗位是后台开发。我一直在想如果他们不从事后台开发,他们面试我的标准是什么呢?他们为什么觉得我能胜任后台开发的工作呢?或者对实习生,招聘要求没有那么高。 - -工作所在组:IEG ==> 增值服务部 ==> 营销服务与应用中心 ==> 接口开发组。 - -事业群 IEG 是负责互娱方面的,包括游戏开发、腾讯动漫等主营业务。 - -增值服务部主要负责游戏的公共部分的开发,比如数据分析、游戏公共主件的开发、游戏接口封装等,可以理解为部门是为其他游戏服务的,帮助游戏开发和运营。 - -营销服务与应用中心主要有几个核心业务,游戏公共活动主件开发、游戏接口开发、登陆功能与关系链等【游戏内比较公共常见的功能】、帮助游戏快速出海、充值平台、实名认证、防沉迷等。 - -接口开发组是提供游戏接入平台,避免其他业务重复性地对接游戏,提供游戏一致的访问协议和接口,其中组内还曾经维护着防沉迷系统和充值系统。 - -每个组的人都不多,我所在组加上我就 8 人,其中真正从事技术方面工作的也就 6 人,因为维护的系统已经相当稳定,会接其他的组的任务和部门新下发的项目。给我第一印象就是组内的后台开发工程师是通过 ssh 连接到跳板机,然后通过 vim 编辑器写代码的,日常开发工作中几乎不接触图形界面,顿时感觉自己虽然用着 ubuntu,但是平时通过 IDE 写代码的有点菜,在后来的项目中我也见识到他们快速开发的能力是多么的强,对所使用的技术、框架是多么的了解。 - -## Q&A - -我问了组里和部门同事几个关于技术的问题: - -1. 问:为什么开发用的技术有点老? - -使用什么技术实现不是最关键的,关键是程序最终要达到项目指标,比如并发量、延时等,一般项目对使用什么具体的技术栈,可以由开发者自定义,但是引入新技术的过程中,如果出了什么岔子,可是要负责任的,除非对新引入的技术非常了解和该技术已经得到外界的认可,不然不会随意引入新技术。做后台最关键的是架构、存储怎么做到可靠、服务的可靠性怎么保证、线上出了问题能不能快速定位并解决。如果线上业务出了问题,而且还有很多用户在访问,怎么通过各种技术手段、工具定位问题,日常使用的框架每一行代码要有熟练了解,并且可以通过日志快速追踪到问题。所以他们一般选择最稳定的解决方案,而这个解决方案可能是已经使用了好几年的,经过多次项目考验的。如果线上业务出了问题 5 分钟内不能定位问题、10 分钟内不能解决,那么问题就很严重了。 - -2. 问:日常在服务器终端开发,怎么进行调试? - -GDB 等调试工具效率并不高,比较好的方式是使用日志追踪执行流信息。 - -在导师给我安排的实习计划中,有一项任务就是学习 GDB,比如临时 attach 到一个进程进行调试等。 - -3. 问:有没有计划改技术栈? - -没有这个必要,现在使用的技术从外界看来可能有点断层,但是确实非常稳定,对于老员工来说这些技术比外界使用的新技术好,而且开源代码有可能面临无人维护等问题,比如发现了一个 bug 或者发现了框架的性能热点,如果直接给开源社区提一个 issue 或者 pull request,开源社区有可能没有马上响应,或这个功能开源社区认为并不那么重要,公司内部改了开源框架,导致框架产生了另一个分支,同样需要内部维护。当初腾讯起家时开源社区还没有这么活跃,现使用的框架都是根据业务特点来设计的。现在使用的很多技术框架系统都糅合在一起了,比如日志、监控上报、检测系统等等,一时半会要改不太容易。 - -技术闭源的缺点是新人学习成本相对高,离职后,即使再熟悉内部系统也带不走,现在腾讯在推开源,希望能降低新员工学习成本和让离职员工有可以带走的东西。 - -4. 问:工作后每天下班都已经比较晚而且也很累了,工作后还有保持学习吗? - -需要保持学习,但不只是技术方面,还有理财。 - -之前部门发起精英项目,鼓励员工发起新项目,解决业务痛点。虽然我们组是后台开发,但也计划发起机器学习、区块链相关的项目。 - -1. 为什么我们使用 vim 而不使用 IDE 呢? - -不熟悉 vim 觉得他效率很低,但是如果熟悉 vim 后效率就能够得到大大地提升【我熟悉 vim 后效率确实也挺高的】。 - -还没支持 Linux 入域,后台开发的应用最终是运行在特定发行版的 Linux 上,C++ 的跨平台能力并不是十分强,如果使用 IDE 在本地开发的代码,需要测试需要重新将代码同步到远端,而中间还通过了跳板机,与 CLion 的直接同步到目的机器的做法不太一致。这样开发效率会受到大大的影响,经常会在不同机器上编辑代码,C++ 在运行前需要通过编译和链接阶段,没有像 Python 等脚本语言这么方便。 - -注:部分同事也会选择使用 VSCode、Sublime 等编辑器或 Visual Studio、Sublime 等编辑器,然后同步到远端编译、链接、调试。我在刚入职时也折腾过 vim 插件安装【完全离线方式安装 YouCompleteMe 就弄了我好几天】,CLion 结合 Windows 的 Ubuntu 子系统开发,因为部门使用的 Linux 发行版是 Centos 改造的,部分东西可能不兼容,所以最后还是选择了 ssh 到服务器,直接使用 vim 编写代码。 - -6. 大部分机器是不允许访问外网的,不能直接 clone Github 的开源项目,也不能通过 pip install 安装库,那么如果怎么安装软件和库呢? - -第一,通过代理。 - -第二,自己下载开源软件的源代码,将源代码传到服务器上,然后 make install。 - -注:之前一直不知道怎么不获取 root 权限安装软件,后来才知道可以从源码编译软件。如 apt install 或 yum install 之所以需要 root 权限是它们需要将可执行文件复制或链接到 `/usr/bin` 目录下,而该目录的权限列表:`drwxr-xr-x 2 root root 64K Sep 1 13:44 bin`,头文件拷贝到 `/usr/include`,库拷贝或链接到 `/usr/lib` 下。可以通过配置程序 configure 或 config 来设置 prefix 来指定软件安装的目录,只需要这个目录有写权限和可执行权限。使用时再指定头文件所在位置、库所在位置就可以。 - -## What I see & How I feel - -发现身边的同事(实习生或正职)学历普遍是高校研究生,或者海归,自己作为一个普通本科生感觉能够进来实习非常幸运的。如果我作为面试官,在不是很了解应聘者的前提下,我会选择学历更高的,因为至少从学历侧面地证明他们的实力。腾讯现在不少业务在出海,更是海归欢迎。腾讯能够招聘这么多优秀的人才,证明外界对腾讯的认可程度还是很高的,而且福利、薪资等也很诱人。 - -正职可以申请一个台台式主机(可为 iMac)、两台显示器、以及一个笔记本(可为 MacBook Pro),在有需要的时可以申请服务器、测试使用的移动设备、内存条、显卡等等,可见公司对于工程师的支持力度非常大。 - -下午有水果盘,有时是零食、雪糕、小龙虾,早餐免费吃,晚饭宵夜券可以抵,吃不需要愁。早上有班车(覆盖面非常广),晚上 10 点后下班够免费打车回家。公司内还有很多培训,无论是技术方面的、还是产品、设计、管理等方面,很多讲座是在上班时间,可以在上班时间学习,错过了可在内部系统看回放。 - -内部有一个类似知乎的问答系统,匿名提问,实名回答,有一些比较尖锐的问题,公司高层也会回答员工的问题。内部有很有优秀的文章,具体到还有关于产品的具体设计、技术实现,从中能够学习到不少东西。因为员工的考核与内部文章有关,所以员工发布优秀文章的积极性高,显现出制度力量的强大。 - -和组里的成员吃饭时,他们会聊一些关于理财方面的话题,特别是中美贸易战开始那段时间,中国股市下跌,美国股市创新高等,房价走势如何,政府推出了什么新政策。做技术的,除了关注技术外,也该关注理财方面的知识,怎么利用之前积累的财富去创造更大的财富也是一个重要问题。 - -部门从事与游戏相关,最近腾讯内部组织了很多游戏相关的比赛,如王者荣耀、刺激战场、斗地主、麻将、LOL等,午休和晚饭后,不少人会开黑打游戏,培养大家的友谊,鼓励员工了解公司的产品。很遗憾我们组的队伍在第一场就被对方 KO 了。 - -部门每周包运动场提供给喜欢运动的员工锻炼身体,比如篮球场、足球场、羽毛球场,公司内有免费健身房,专业的教练上课和私人辅导。他们身体素质确实下降得非常快,有些可能毕业没多少年,长肚腩了,体能也大不如前。工作时间坐的时间太长了,有些员工购买站立工作台,这样对他们的腰椎和脖子会有比较好的保护。 - -外界经常吐槽互联网加班严重,特别是做游戏方面的。公司内部并不建议员工加班,每周三晚上严禁加班。公司重视员工健康,提供免费的体检服务。我们部门在我实习期间大部分人都是双休的。 - -一开始我感觉组里的人并不很忙,工作还是很轻松愉快的。但当他们接了一个新项目后,整个中心都忙起来了,从策划架构到编码实现,整个过程非常快速,我参加了某些会议,听了他们讨论,知道这是一个很重要的项目,而且技术挑战性很大的项目。我导师告诉我,真正项目中编码过程并不长,更长的是前期方案的协定和后期的测试,他们做的项目一般都面相所有腾讯游戏,如果方案选错了,要调整的难度是非常大的,特别是数据结构和架构上的更改。有专门的测试开发岗位,有一次分享中看到他们的自动化测试,感觉真的很牛逼,公司有一个开源测试服务:[https://github.com/Tencent/GAutomator](https://github.com/Tencent/GAutomator)。 - -公司提供员工很大的学习时间和空间,在一次部门新人入职会上,有一名 5 星产品经理说,入职第一年主要还是学习积累、了解周边资源。 - -部门举行 GM 面对面项目,目的是让部门的每一名员工都有机会直面总经理,反馈问题和解惑等。很荣幸,作为实习生有机会参加饭局。问了一些关于管理、人员提拔等事情,总经理管理方面确实有一套,其中他提到不会跨级去考核员工,对员工的考核尊重直接上级的意见,直接上级更了解员工,他要做的是放权。部门招聘最后一关是总经理,他主要是看人,看大家能不能聊到一块去,适不适合部门的氛围等,他提到外国招聘有些是一起去一个旅行,看双方是否真的能够相处,能否信任对方。做事情前首先要看资源。 - -公司内部职称的晋升主要是通过答辩,如技术答辩描述在公司里做了什么项目、项目采用的技术与架构、最终成效如何。我第一想到的是我对过往做的项目还熟悉吗?有什么数据能够说明我做的事情的成效吗?我过往写的代码有留下文档吗?文书工作很重要的,要学会如何管理文档。在我的一个开发中,组里的人多次要我拿出文档,我没做好,我提供了不同版本的文档,数据很多都是在企业微信上发的,没有记录备份好,很多信息丢失在聊天记录里,只能重新做压测或者从聊天记录中找回数据,工作效率难免会变低。 - -内部系统都是配有管理端的,这样做一方面方便运维人员操作,另一方面方便推广,有些管理端是提供给产品经历等使用的,比如活动管理。 - -## My work - -导师给我列了一个实习计划,大部分内容主要是学习,如公司内部的技术、组里维护的框架、搭建一个中心常用的服务模型。 - -组里主要使用 C++,所以我也花了一周时间去找回 C++ 的感觉,从服务器上使用 vim,自己写 Makefile 编译、链接,使用 GDB 调试还是很有难度的,但这个过程让我重新了解了 C++,原来不适用 IDE 进行开发是这样的,IDE 屏蔽了很多技术细节,好处是开发效率的提高,坏处是程序员了解的东西更少了,更加不熟悉当前使用的技术。 - -我主要是做了两个项目: - -1. 根据组里使用的技术,搭建一个以 Protobuf 序列化数据、持久化于 MySQL、缓存至 Redis、L5 客户端负载均衡、SPP 微线程网络框架及C++ 编程语言服务模型。 -2. 腾讯游戏接口 IDIP 改造,使用 Openssl + Curl 实现 HTTPS 接入、Curl 连接池实现、使用 AES CBC 模式实现二进制流加密、签名验证、大包 JSON 解析优化、使用 Valgrind 定位性能热点、Python搭建测试桩、tcpdump 抓包分析及证书调研。 - -写代码的时间并不长,时间更多花在调研和写测试代码,自己先写一个简单的例子,验证后再合并到业务框架代码中。一方面是由于这样做代码比较简单清晰,容易调试,还有就是部署业务代码太麻烦了。 - -游戏接口 IDIP 主要干的事情是游戏协议转换、限流、动态配置和监控上报。 - -架构是客户端应用通过 tcp 连接访问 router(支持长连接,通过 '\n' 切包),worker 向 router 发起注册,worker 定时向 router 心跳保活,router 将请求路由到具体的 worker 处理,worker 加载 libparser.a 协议转化代码实现,管理端下发配置 xml 配置文件和 sqlite DB,其中 xml 描述协议、sqlite 配置一些动态信息(xml 描述一个请求 A 应该附带什么参数、参数的类型是什么、如果没有附带参数是否应该设置为默认值等等,对应有响应描述。sqlite 配置秘钥、游戏大区信息等),libparser.a 解析 xml 提取中数据,根据请求来判断应该怎么处理请求。libparser.a 访问游戏服务可通过二进制方式或 HTTP / HTTPS 方式。 - -游戏有加密、携带签名等需求,我的工作主要是在原有框架基础上添加 HTTPS 接入等,因为 HTTPS 短连接性能问题比较严重,所以打算引入长连接和共享 sessionID 方案,减少证书发送和重新协定秘钥等操作。 - -一款游戏需要传输很大的数据量,一个 16KB 大小的 JSON 回包,这样导致了 JSON 解析成性能热点,CPU 负载非常高,导师给了一个任务调研如何定位程序的热点,网上推荐 Valgrind 工具,Valgrind 不仅仅可以检测 C/C++ 的内存泄露,还可以检测缓存命中、调用分析、堆分析、线程竞态分析,测试后能够生成报告。而我主要使用调用分析,通过 Valgrind 生成的数据,再转化为调用图,很清晰地看到程序的调用性能热点。 - -隔壁组的一个高级工程师推荐使用 perf 工具,它真的很简单直接,比如通过 perf top 抽取栈顶信息,看程序大部分时间都在运行哪个函数,从而判断程序的热点。 - -最终定位到是一个递归太多了,将递归优化后问题迎刃而解。 - -修改代码或者新增了功能之后怎么保证程序的正确性?只能通过测试去保证了,因为 IDIP 框架的特殊性,没有专门的测试岗位,由开发者去写测试。我主要通过 Python 搭建测试桩,包括客户端请求模拟、HTTP 测试桩、HTTPS 测试桩和 TCP Server 测试桩,嵌入一些日志和统计代码。 - -部署是一个复杂的过程,router 和 worker 需要分开部署,幸好有管理端可以自动化很多部署工作,我导师尝试着带着我通过 cmd 来直接部署。需要配置白名单、L5 负载均衡、XML、SqliteDB、使用 supervisor 管理 worker / router 进程、将编译好的 libparser.a 放置到特定目录上,最后再将 router 和 worker 启动起来,判断 router 和 worker 是否正常启动。 - -程序产生的日志是惊人的,一个日志文件就上 GB 大小,而且有很多个不同时间结点的日志文件。可以通过 awk / grep / sed 等文本工具很方便地实现日志的查阅、统计、替换功能。 - -# end - -腾讯是一家很 Nice 的公司。 - -工作之后感觉生活还是挺无聊的(社会上有趣的事情挺多的,不应该让自己这么无聊),除了上班其他时间不知道应该干什么,应该多发展兴趣爱好,多交朋友,业余时间应该保持学习,学习一点新东西,拓宽自己的路子,有条件也可以了解一下理财方面的知识。实习后感觉身体素质差了不少,要加强锻炼。 +--- +layout: post +title: "腾讯实习总结" +date: 2018-09-01 09:00:05 +0800 +categories: 总结 +--- + +# In short + +很遗憾没有拿到留用 offer,办公环境真是好,福利超级棒,人很 Nice,思想很活跃,技术栈有点老和闭源,关注理财。 + +# Main + +**我实习的时间不长,看到的东西比较片面,就像苏格兰黑山羊。** + +到腾讯实习后,才知道一面面试官是搞数据挖掘和分析的,二面面试官是客户端大佬,而我应聘的岗位是后台开发。我一直在想如果他们不从事后台开发,他们面试我的标准是什么呢?他们为什么觉得我能胜任后台开发的工作呢?或者对实习生,招聘要求没有那么高。 + +工作所在组:IEG ==> 增值服务部 ==> 营销服务与应用中心 ==> 接口开发组。 + +事业群 IEG 是负责互娱方面的,包括游戏开发、腾讯动漫等主营业务。 + +增值服务部主要负责游戏的公共部分的开发,比如数据分析、游戏公共主件的开发、游戏接口封装等,可以理解为部门是为其他游戏服务的,帮助游戏开发和运营。 + +营销服务与应用中心主要有几个核心业务,游戏公共活动主件开发、游戏接口开发、登陆功能与关系链等【游戏内比较公共常见的功能】、帮助游戏快速出海、充值平台、实名认证、防沉迷等。 + +接口开发组是提供游戏接入平台,避免其他业务重复性地对接游戏,提供游戏一致的访问协议和接口,其中组内还曾经维护着防沉迷系统和充值系统。 + +每个组的人都不多,我所在组加上我就 8 人,其中真正从事技术方面工作的也就 6 人,因为维护的系统已经相当稳定,会接其他的组的任务和部门新下发的项目。给我第一印象就是组内的后台开发工程师是通过 ssh 连接到跳板机,然后通过 vim 编辑器写代码的,日常开发工作中几乎不接触图形界面,顿时感觉自己虽然用着 ubuntu,但是平时通过 IDE 写代码的有点菜,在后来的项目中我也见识到他们快速开发的能力是多么的强,对所使用的技术、框架是多么的了解。 + +## Q&A + +我问了组里和部门同事几个关于技术的问题: + +1. 问:为什么开发用的技术有点老? + +使用什么技术实现不是最关键的,关键是程序最终要达到项目指标,比如并发量、延时等,一般项目对使用什么具体的技术栈,可以由开发者自定义,但是引入新技术的过程中,如果出了什么岔子,可是要负责任的,除非对新引入的技术非常了解和该技术已经得到外界的认可,不然不会随意引入新技术。做后台最关键的是架构、存储怎么做到可靠、服务的可靠性怎么保证、线上出了问题能不能快速定位并解决。如果线上业务出了问题,而且还有很多用户在访问,怎么通过各种技术手段、工具定位问题,日常使用的框架每一行代码要有熟练了解,并且可以通过日志快速追踪到问题。所以他们一般选择最稳定的解决方案,而这个解决方案可能是已经使用了好几年的,经过多次项目考验的。如果线上业务出了问题 5 分钟内不能定位问题、10 分钟内不能解决,那么问题就很严重了。 + +2. 问:日常在服务器终端开发,怎么进行调试? + +GDB 等调试工具效率并不高,比较好的方式是使用日志追踪执行流信息。 + +在导师给我安排的实习计划中,有一项任务就是学习 GDB,比如临时 attach 到一个进程进行调试等。 + +3. 问:有没有计划改技术栈? + +没有这个必要,现在使用的技术从外界看来可能有点断层,但是确实非常稳定,对于老员工来说这些技术比外界使用的新技术好,而且开源代码有可能面临无人维护等问题,比如发现了一个 bug 或者发现了框架的性能热点,如果直接给开源社区提一个 issue 或者 pull request,开源社区有可能没有马上响应,或这个功能开源社区认为并不那么重要,公司内部改了开源框架,导致框架产生了另一个分支,同样需要内部维护。当初腾讯起家时开源社区还没有这么活跃,现使用的框架都是根据业务特点来设计的。现在使用的很多技术框架系统都糅合在一起了,比如日志、监控上报、检测系统等等,一时半会要改不太容易。 + +技术闭源的缺点是新人学习成本相对高,离职后,即使再熟悉内部系统也带不走,现在腾讯在推开源,希望能降低新员工学习成本和让离职员工有可以带走的东西。 + +4. 问:工作后每天下班都已经比较晚而且也很累了,工作后还有保持学习吗? + +需要保持学习,但不只是技术方面,还有理财。 + +之前部门发起精英项目,鼓励员工发起新项目,解决业务痛点。虽然我们组是后台开发,但也计划发起机器学习、区块链相关的项目。 + +1. 为什么我们使用 vim 而不使用 IDE 呢? + +不熟悉 vim 觉得他效率很低,但是如果熟悉 vim 后效率就能够得到大大地提升【我熟悉 vim 后效率确实也挺高的】。 + +还没支持 Linux 入域,后台开发的应用最终是运行在特定发行版的 Linux 上,C++ 的跨平台能力并不是十分强,如果使用 IDE 在本地开发的代码,需要测试需要重新将代码同步到远端,而中间还通过了跳板机,与 CLion 的直接同步到目的机器的做法不太一致。这样开发效率会受到大大的影响,经常会在不同机器上编辑代码,C++ 在运行前需要通过编译和链接阶段,没有像 Python 等脚本语言这么方便。 + +注:部分同事也会选择使用 VSCode、Sublime 等编辑器或 Visual Studio、Sublime 等编辑器,然后同步到远端编译、链接、调试。我在刚入职时也折腾过 vim 插件安装【完全离线方式安装 YouCompleteMe 就弄了我好几天】,CLion 结合 Windows 的 Ubuntu 子系统开发,因为部门使用的 Linux 发行版是 Centos 改造的,部分东西可能不兼容,所以最后还是选择了 ssh 到服务器,直接使用 vim 编写代码。 + +6. 大部分机器是不允许访问外网的,不能直接 clone Github 的开源项目,也不能通过 pip install 安装库,那么如果怎么安装软件和库呢? + +第一,通过代理。 + +第二,自己下载开源软件的源代码,将源代码传到服务器上,然后 make install。 + +注:之前一直不知道怎么不获取 root 权限安装软件,后来才知道可以从源码编译软件。如 apt install 或 yum install 之所以需要 root 权限是它们需要将可执行文件复制或链接到 `/usr/bin` 目录下,而该目录的权限列表:`drwxr-xr-x 2 root root 64K Sep 1 13:44 bin`,头文件拷贝到 `/usr/include`,库拷贝或链接到 `/usr/lib` 下。可以通过配置程序 configure 或 config 来设置 prefix 来指定软件安装的目录,只需要这个目录有写权限和可执行权限。使用时再指定头文件所在位置、库所在位置就可以。 + +## What I see & How I feel + +发现身边的同事(实习生或正职)学历普遍是高校研究生,或者海归,自己作为一个普通本科生感觉能够进来实习非常幸运的。如果我作为面试官,在不是很了解应聘者的前提下,我会选择学历更高的,因为至少从学历侧面地证明他们的实力。腾讯现在不少业务在出海,更是海归欢迎。腾讯能够招聘这么多优秀的人才,证明外界对腾讯的认可程度还是很高的,而且福利、薪资等也很诱人。 + +正职可以申请一个台台式主机(可为 iMac)、两台显示器、以及一个笔记本(可为 MacBook Pro),在有需要的时可以申请服务器、测试使用的移动设备、内存条、显卡等等,可见公司对于工程师的支持力度非常大。 + +下午有水果盘,有时是零食、雪糕、小龙虾,早餐免费吃,晚饭宵夜券可以抵,吃不需要愁。早上有班车(覆盖面非常广),晚上 10 点后下班够免费打车回家。公司内还有很多培训,无论是技术方面的、还是产品、设计、管理等方面,很多讲座是在上班时间,可以在上班时间学习,错过了可在内部系统看回放。 + +内部有一个类似知乎的问答系统,匿名提问,实名回答,有一些比较尖锐的问题,公司高层也会回答员工的问题。内部有很有优秀的文章,具体到还有关于产品的具体设计、技术实现,从中能够学习到不少东西。因为员工的考核与内部文章有关,所以员工发布优秀文章的积极性高,显现出制度力量的强大。 + +和组里的成员吃饭时,他们会聊一些关于理财方面的话题,特别是中美贸易战开始那段时间,中国股市下跌,美国股市创新高等,房价走势如何,政府推出了什么新政策。做技术的,除了关注技术外,也该关注理财方面的知识,怎么利用之前积累的财富去创造更大的财富也是一个重要问题。 + +部门从事与游戏相关,最近腾讯内部组织了很多游戏相关的比赛,如王者荣耀、刺激战场、斗地主、麻将、LOL等,午休和晚饭后,不少人会开黑打游戏,培养大家的友谊,鼓励员工了解公司的产品。很遗憾我们组的队伍在第一场就被对方 KO 了。 + +部门每周包运动场提供给喜欢运动的员工锻炼身体,比如篮球场、足球场、羽毛球场,公司内有免费健身房,专业的教练上课和私人辅导。他们身体素质确实下降得非常快,有些可能毕业没多少年,长肚腩了,体能也大不如前。工作时间坐的时间太长了,有些员工购买站立工作台,这样对他们的腰椎和脖子会有比较好的保护。 + +外界经常吐槽互联网加班严重,特别是做游戏方面的。公司内部并不建议员工加班,每周三晚上严禁加班。公司重视员工健康,提供免费的体检服务。我们部门在我实习期间大部分人都是双休的。 + +一开始我感觉组里的人并不很忙,工作还是很轻松愉快的。但当他们接了一个新项目后,整个中心都忙起来了,从策划架构到编码实现,整个过程非常快速,我参加了某些会议,听了他们讨论,知道这是一个很重要的项目,而且技术挑战性很大的项目。我导师告诉我,真正项目中编码过程并不长,更长的是前期方案的协定和后期的测试,他们做的项目一般都面相所有腾讯游戏,如果方案选错了,要调整的难度是非常大的,特别是数据结构和架构上的更改。有专门的测试开发岗位,有一次分享中看到他们的自动化测试,感觉真的很牛逼,公司有一个开源测试服务:[https://github.com/Tencent/GAutomator](https://github.com/Tencent/GAutomator)。 + +公司提供员工很大的学习时间和空间,在一次部门新人入职会上,有一名 5 星产品经理说,入职第一年主要还是学习积累、了解周边资源。 + +部门举行 GM 面对面项目,目的是让部门的每一名员工都有机会直面总经理,反馈问题和解惑等。很荣幸,作为实习生有机会参加饭局。问了一些关于管理、人员提拔等事情,总经理管理方面确实有一套,其中他提到不会跨级去考核员工,对员工的考核尊重直接上级的意见,直接上级更了解员工,他要做的是放权。部门招聘最后一关是总经理,他主要是看人,看大家能不能聊到一块去,适不适合部门的氛围等,他提到外国招聘有些是一起去一个旅行,看双方是否真的能够相处,能否信任对方。做事情前首先要看资源。 + +公司内部职称的晋升主要是通过答辩,如技术答辩描述在公司里做了什么项目、项目采用的技术与架构、最终成效如何。我第一想到的是我对过往做的项目还熟悉吗?有什么数据能够说明我做的事情的成效吗?我过往写的代码有留下文档吗?文书工作很重要的,要学会如何管理文档。在我的一个开发中,组里的人多次要我拿出文档,我没做好,我提供了不同版本的文档,数据很多都是在企业微信上发的,没有记录备份好,很多信息丢失在聊天记录里,只能重新做压测或者从聊天记录中找回数据,工作效率难免会变低。 + +内部系统都是配有管理端的,这样做一方面方便运维人员操作,另一方面方便推广,有些管理端是提供给产品经历等使用的,比如活动管理。 + +## My work + +导师给我列了一个实习计划,大部分内容主要是学习,如公司内部的技术、组里维护的框架、搭建一个中心常用的服务模型。 + +组里主要使用 C++,所以我也花了一周时间去找回 C++ 的感觉,从服务器上使用 vim,自己写 Makefile 编译、链接,使用 GDB 调试还是很有难度的,但这个过程让我重新了解了 C++,原来不适用 IDE 进行开发是这样的,IDE 屏蔽了很多技术细节,好处是开发效率的提高,坏处是程序员了解的东西更少了,更加不熟悉当前使用的技术。 + +我主要是做了两个项目: + +1. 根据组里使用的技术,搭建一个以 Protobuf 序列化数据、持久化于 MySQL、缓存至 Redis、L5 客户端负载均衡、SPP 微线程网络框架及C++ 编程语言服务模型。 +2. 腾讯游戏接口 IDIP 改造,使用 Openssl + Curl 实现 HTTPS 接入、Curl 连接池实现、使用 AES CBC 模式实现二进制流加密、签名验证、大包 JSON 解析优化、使用 Valgrind 定位性能热点、Python搭建测试桩、tcpdump 抓包分析及证书调研。 + +写代码的时间并不长,时间更多花在调研和写测试代码,自己先写一个简单的例子,验证后再合并到业务框架代码中。一方面是由于这样做代码比较简单清晰,容易调试,还有就是部署业务代码太麻烦了。 + +游戏接口 IDIP 主要干的事情是游戏协议转换、限流、动态配置和监控上报。 + +架构是客户端应用通过 tcp 连接访问 router(支持长连接,通过 '\n' 切包),worker 向 router 发起注册,worker 定时向 router 心跳保活,router 将请求路由到具体的 worker 处理,worker 加载 libparser.a 协议转化代码实现,管理端下发配置 xml 配置文件和 sqlite DB,其中 xml 描述协议、sqlite 配置一些动态信息(xml 描述一个请求 A 应该附带什么参数、参数的类型是什么、如果没有附带参数是否应该设置为默认值等等,对应有响应描述。sqlite 配置秘钥、游戏大区信息等),libparser.a 解析 xml 提取中数据,根据请求来判断应该怎么处理请求。libparser.a 访问游戏服务可通过二进制方式或 HTTP / HTTPS 方式。 + +游戏有加密、携带签名等需求,我的工作主要是在原有框架基础上添加 HTTPS 接入等,因为 HTTPS 短连接性能问题比较严重,所以打算引入长连接和共享 sessionID 方案,减少证书发送和重新协定秘钥等操作。 + +一款游戏需要传输很大的数据量,一个 16KB 大小的 JSON 回包,这样导致了 JSON 解析成性能热点,CPU 负载非常高,导师给了一个任务调研如何定位程序的热点,网上推荐 Valgrind 工具,Valgrind 不仅仅可以检测 C/C++ 的内存泄露,还可以检测缓存命中、调用分析、堆分析、线程竞态分析,测试后能够生成报告。而我主要使用调用分析,通过 Valgrind 生成的数据,再转化为调用图,很清晰地看到程序的调用性能热点。 + +隔壁组的一个高级工程师推荐使用 perf 工具,它真的很简单直接,比如通过 perf top 抽取栈顶信息,看程序大部分时间都在运行哪个函数,从而判断程序的热点。 + +最终定位到是一个递归太多了,将递归优化后问题迎刃而解。 + +修改代码或者新增了功能之后怎么保证程序的正确性?只能通过测试去保证了,因为 IDIP 框架的特殊性,没有专门的测试岗位,由开发者去写测试。我主要通过 Python 搭建测试桩,包括客户端请求模拟、HTTP 测试桩、HTTPS 测试桩和 TCP Server 测试桩,嵌入一些日志和统计代码。 + +部署是一个复杂的过程,router 和 worker 需要分开部署,幸好有管理端可以自动化很多部署工作,我导师尝试着带着我通过 cmd 来直接部署。需要配置白名单、L5 负载均衡、XML、SqliteDB、使用 supervisor 管理 worker / router 进程、将编译好的 libparser.a 放置到特定目录上,最后再将 router 和 worker 启动起来,判断 router 和 worker 是否正常启动。 + +程序产生的日志是惊人的,一个日志文件就上 GB 大小,而且有很多个不同时间结点的日志文件。可以通过 awk / grep / sed 等文本工具很方便地实现日志的查阅、统计、替换功能。 + +# end + +腾讯是一家很 Nice 的公司。 + +工作之后感觉生活还是挺无聊的(社会上有趣的事情挺多的,不应该让自己这么无聊),除了上班其他时间不知道应该干什么,应该多发展兴趣爱好,多交朋友,业余时间应该保持学习,学习一点新东西,拓宽自己的路子,有条件也可以了解一下理财方面的知识。实习后感觉身体素质差了不少,要加强锻炼。 diff --git "a/src/md/2018-09-04-\344\272\224\347\247\215\345\270\270\350\247\201\345\277\253\346\216\222\346\200\235\350\267\257.md" "b/src/md/2018-09-04-\344\272\224\347\247\215\345\270\270\350\247\201\345\277\253\346\216\222\346\200\235\350\267\257.md" index 76270b7..38989ca 100644 --- "a/src/md/2018-09-04-\344\272\224\347\247\215\345\270\270\350\247\201\345\277\253\346\216\222\346\200\235\350\267\257.md" +++ "b/src/md/2018-09-04-\344\272\224\347\247\215\345\270\270\350\247\201\345\277\253\346\216\222\346\200\235\350\267\257.md" @@ -1,214 +1,214 @@ ---- -layout: post -title: "五种常见快排" -date: 2018-09-04 09:00:05 +0800 -categories: 算法 ---- - -# In short - -最近开始重新复习算法,从最基本的数据结构开始,到排序和查找,动态规划,将最常见算法问题使用代码写一遍。每次写快排或看快排代码实现时都觉得特别凌乱。[[《暗时间》](http://mindhacks.cn/2008/06/13/why-is-quicksort-so-quick/)中说道,快排是一个很简单的算法,很多人之所以觉得难是因为没有理解好它背后的思路;在《编程珠玑》里也有对快排的分析,不同质量的快排代码有好几倍的性能差距。以下我分析一下常见的四种快排算法。 - -快排思想:从数组中随机选择一个数 pivot,将所有小于等于 pivot 的数移动到数组左边,将所有大于 pivot 的数移动到右边,分别对左右两边的子数组重复进行上述操作,直到子数组的长度小于等于 1。 - -```python -def qsort(arr: list, low: int, high: int): - if low >= high: - return - pos = partition(arr, low, high) - qsort(arr, low, pos - 1) - qsort(arr, pos + 1, high) -``` - -partition 函数的功能:将所有小于等于 pivot 的数移动到数组左边,将所有大于 pivot 的数移动到右边,并且返回最终 pivot 元素的位置 pos,满足: -`arr[low : pos] <= pivot and arr[pos + 1 : high] > pivot` 。 - -以下函数声明 `def partition(arr: list, low: int, high: int)` 均为左闭右闭,当前数组为 `arr[low:high+1]`。 - -## 第一种 - -边界条件: - -1. 选择当前数组的最后一个元素作为分界值 -2. 选择 index 满足 `arr[low:index] <= pivot` - -```python -def partition(arr: list, low: int, high: int): - # choose the last elem as pivot. - pivot = arr[high] - # make sure arr[low:index] <= pivot - index = low - for i in range(low, high): - if arr[i] <= pivot: - arr[index], arr[i] = arr[i], arr[index] - index += 1 - # if pivot == max(arr[low:high+1]): - # index == high - # else: - # index < high and arr[index] > pivot - arr[index], arr[high] = pivot, arr[index] - return index -``` - -## 第二种 - -边界条件: - -1. 选择当前数组的第 0 号元素作为分界值 -2. `arr[:head] <= pivot and arr[tail:high] > pivot` -3. `scanner == head + 1` - -```python -def partition(arr: list, low: int, high: int): - # choose the first elem as pivot - pivot = arr[low] - # arr[low:head] <= pivot - # arr[tail+1:high+1] > pivot - # head point to the empty position - head = low - tail = high - # always scanner == head + 1 - scanner = head + 1 - while scanner <= tail: - if arr[scanner] > pivot: - arr[tail], arr[scanner] = arr[scanner], arr[tail] - tail -= 1 - else: - arr[head] = arr[scanner] - head += 1 - scanner += 1 - # head point to the empty position. and satisfy: - # arr[low:head] <= pivot and arr[tail+1:high+1] > pivot and head == tail - # so arr[head+1:high+1] > pivot, arr[head] is the position for pivot. - arr[head] = pivot - return head -``` - -# 第三种 - -边界条件: - -1. 选择当前数组第 0 号元素作为分界值 -2. 同时发起从左到右、从右到左扫描,将大于 pivot 的放在右边,小于等于 pivot 的放在左边 -3. `arr[low:l] <= pivot and arr[h+1:high+1] > pivot` - -```python -def partition(arr: list, low: int, high: int): - # choose the first elem as pivot - pivot = arr[low] - # arr[low:l] <= pivot - # arr[h+1:high+1] >= pivot - l = low + 1 - h = high - while l <= h: - while l <= h and arr[l] <= pivot: - l += 1 - while l <= h and arr[h] > pivot: - h -= 1 - if l < h: - # arr[l] > pivot and arr[h] <= pivot, switch them. - arr[l], arr[h] = arr[h], arr[l] - # satisfy l == h + 1 and arr[low:l] <= pivot and arr[l:high+1] > pivot - # so arr[h] <= pivot - arr[low], arr[h] = arr[h], pivot - return h -``` - -三种快排实现选择了不同的方法,但核心思想是一样的。 - -前三种快排实现的思路:选择当前数组中的一个元素作为 pivot,将大于 pivot 的元素移到右边,将小于等于 pivot 的元素移到左边。 - -更好的实现是随机地选择一个的元素作为 pivot。 - -## 第四种 - -如果当前数组中重复的元素特别多的时候,上述的快排实现性能会快速下降。快排最理想的情况是,每次都均分地切分数组,树的高度为 logN,时间复杂度为:O(NlogN),其中 N 为数组长度。快排最不理想的情况是,每次选择的 pivot 为数组的最大值,每次都将数组切割为 arr[low:high] 与 arr[high](arr[high] == pivot),树的高度为 O(N),时间复杂度:O(N^2)。 - -三路快排:将当前数组分为三部分 `low <= l <= h <= high` 满足 `arr[low:l] < pivot and arr[l:h+1] == pivot and arr[h+1:high+1] > pivot`。 - -边界条件: - -1. 选择当前数组第 0 个元素作为分界值 -2. `arr[low:l] < pivot and arr[l:scanner] == pivot and arr[h+1:high+1] > pivot and arr[scanner:h+1] to scan` - -```python -def qsort3(arr: list, low: int, high: int): - if low >= high: - return - pivot = arr[low] - # arr[low:l] < pivot - # arr[l] == pivot - # arr[h+1:high+1] > pivot - l = low - h = high - scanner = low + 1 - while scanner <= h: - if arr[scanner] < pivot: - arr[l], arr[scanner] = arr[scanner], arr[l] - l += 1 - scanner += 1 - elif arr[scanner] > pivot: - arr[h], arr[scanner] = arr[scanner], arr[h] - h -= 1 - else: - scanner += 1 - # scanner == h + 1 - # arr[l] == pivot - # arr[h] == pivot - # arr[low:l] < pivot and arr[h+1:high+1] - # so split the arr to three part: arr[low:l] + arr[l:h+1] + arr[h+1:high+1] - qsort3(arr, low, l - 1) - qsort3(arr, h + 1, high) -``` - -在我理解过程中,三路快排是最容易的。 - -## 第五种 - -单向链表的快速排序。 - -单链表能够访问一个节点的下一个节点,但是链表中节点的交换位置操作比较复杂,涉及到比较多边界条件,比如是否是头结点之类的,所以交换节点位置的操作改为交换节点中 key-value。代码如下: - -```go -type Node struct { - v interface{} - k int - next *Node -} - -// Quick sort for list [start, end) -func qsortList(start, end *Node) { - if start == end { - return - } - p, q := start, start.next - for q != end { - if q.k < p.k { - // swap the value, not change list structure - swap(p, q) - p = p.next - swap(p, q) - } - q = q.next - } - // sort [start, p) - qsortList(start, p) - // sort (p, end) - qsortList(p.next, end) -} - -func swap(p, q *Node) { - p.v, q.v = q.v, p.v - p.k, q.k = q.k, p.k -} -``` - -# Conclusion - -写代码前先确保几件事: - -1. 掌握算法的核心思想 -2. 设定几个边界条件 -3. 变量在变化过程中严格遵守边界条件 - -按照上述要求,我们在理解代码离开循环或选择语句时就不会陷入迷惑。 +--- +layout: post +title: "五种常见快排" +date: 2018-09-04 09:00:05 +0800 +categories: 算法 +--- + +# In short + +最近开始重新复习算法,从最基本的数据结构开始,到排序和查找,动态规划,将最常见算法问题使用代码写一遍。每次写快排或看快排代码实现时都觉得特别凌乱。[[《暗时间》](http://mindhacks.cn/2008/06/13/why-is-quicksort-so-quick/)中说道,快排是一个很简单的算法,很多人之所以觉得难是因为没有理解好它背后的思路;在《编程珠玑》里也有对快排的分析,不同质量的快排代码有好几倍的性能差距。以下我分析一下常见的四种快排算法。 + +快排思想:从数组中随机选择一个数 pivot,将所有小于等于 pivot 的数移动到数组左边,将所有大于 pivot 的数移动到右边,分别对左右两边的子数组重复进行上述操作,直到子数组的长度小于等于 1。 + +```python +def qsort(arr: list, low: int, high: int): + if low >= high: + return + pos = partition(arr, low, high) + qsort(arr, low, pos - 1) + qsort(arr, pos + 1, high) +``` + +partition 函数的功能:将所有小于等于 pivot 的数移动到数组左边,将所有大于 pivot 的数移动到右边,并且返回最终 pivot 元素的位置 pos,满足: +`arr[low : pos] <= pivot and arr[pos + 1 : high] > pivot` 。 + +以下函数声明 `def partition(arr: list, low: int, high: int)` 均为左闭右闭,当前数组为 `arr[low:high+1]`。 + +## 第一种 + +边界条件: + +1. 选择当前数组的最后一个元素作为分界值 +2. 选择 index 满足 `arr[low:index] <= pivot` + +```python +def partition(arr: list, low: int, high: int): + # choose the last elem as pivot. + pivot = arr[high] + # make sure arr[low:index] <= pivot + index = low + for i in range(low, high): + if arr[i] <= pivot: + arr[index], arr[i] = arr[i], arr[index] + index += 1 + # if pivot == max(arr[low:high+1]): + # index == high + # else: + # index < high and arr[index] > pivot + arr[index], arr[high] = pivot, arr[index] + return index +``` + +## 第二种 + +边界条件: + +1. 选择当前数组的第 0 号元素作为分界值 +2. `arr[:head] <= pivot and arr[tail:high] > pivot` +3. `scanner == head + 1` + +```python +def partition(arr: list, low: int, high: int): + # choose the first elem as pivot + pivot = arr[low] + # arr[low:head] <= pivot + # arr[tail+1:high+1] > pivot + # head point to the empty position + head = low + tail = high + # always scanner == head + 1 + scanner = head + 1 + while scanner <= tail: + if arr[scanner] > pivot: + arr[tail], arr[scanner] = arr[scanner], arr[tail] + tail -= 1 + else: + arr[head] = arr[scanner] + head += 1 + scanner += 1 + # head point to the empty position. and satisfy: + # arr[low:head] <= pivot and arr[tail+1:high+1] > pivot and head == tail + # so arr[head+1:high+1] > pivot, arr[head] is the position for pivot. + arr[head] = pivot + return head +``` + +# 第三种 + +边界条件: + +1. 选择当前数组第 0 号元素作为分界值 +2. 同时发起从左到右、从右到左扫描,将大于 pivot 的放在右边,小于等于 pivot 的放在左边 +3. `arr[low:l] <= pivot and arr[h+1:high+1] > pivot` + +```python +def partition(arr: list, low: int, high: int): + # choose the first elem as pivot + pivot = arr[low] + # arr[low:l] <= pivot + # arr[h+1:high+1] >= pivot + l = low + 1 + h = high + while l <= h: + while l <= h and arr[l] <= pivot: + l += 1 + while l <= h and arr[h] > pivot: + h -= 1 + if l < h: + # arr[l] > pivot and arr[h] <= pivot, switch them. + arr[l], arr[h] = arr[h], arr[l] + # satisfy l == h + 1 and arr[low:l] <= pivot and arr[l:high+1] > pivot + # so arr[h] <= pivot + arr[low], arr[h] = arr[h], pivot + return h +``` + +三种快排实现选择了不同的方法,但核心思想是一样的。 + +前三种快排实现的思路:选择当前数组中的一个元素作为 pivot,将大于 pivot 的元素移到右边,将小于等于 pivot 的元素移到左边。 + +更好的实现是随机地选择一个的元素作为 pivot。 + +## 第四种 + +如果当前数组中重复的元素特别多的时候,上述的快排实现性能会快速下降。快排最理想的情况是,每次都均分地切分数组,树的高度为 logN,时间复杂度为:O(NlogN),其中 N 为数组长度。快排最不理想的情况是,每次选择的 pivot 为数组的最大值,每次都将数组切割为 arr[low:high] 与 arr[high](arr[high] == pivot),树的高度为 O(N),时间复杂度:O(N^2)。 + +三路快排:将当前数组分为三部分 `low <= l <= h <= high` 满足 `arr[low:l] < pivot and arr[l:h+1] == pivot and arr[h+1:high+1] > pivot`。 + +边界条件: + +1. 选择当前数组第 0 个元素作为分界值 +2. `arr[low:l] < pivot and arr[l:scanner] == pivot and arr[h+1:high+1] > pivot and arr[scanner:h+1] to scan` + +```python +def qsort3(arr: list, low: int, high: int): + if low >= high: + return + pivot = arr[low] + # arr[low:l] < pivot + # arr[l] == pivot + # arr[h+1:high+1] > pivot + l = low + h = high + scanner = low + 1 + while scanner <= h: + if arr[scanner] < pivot: + arr[l], arr[scanner] = arr[scanner], arr[l] + l += 1 + scanner += 1 + elif arr[scanner] > pivot: + arr[h], arr[scanner] = arr[scanner], arr[h] + h -= 1 + else: + scanner += 1 + # scanner == h + 1 + # arr[l] == pivot + # arr[h] == pivot + # arr[low:l] < pivot and arr[h+1:high+1] + # so split the arr to three part: arr[low:l] + arr[l:h+1] + arr[h+1:high+1] + qsort3(arr, low, l - 1) + qsort3(arr, h + 1, high) +``` + +在我理解过程中,三路快排是最容易的。 + +## 第五种 + +单向链表的快速排序。 + +单链表能够访问一个节点的下一个节点,但是链表中节点的交换位置操作比较复杂,涉及到比较多边界条件,比如是否是头结点之类的,所以交换节点位置的操作改为交换节点中 key-value。代码如下: + +```go +type Node struct { + v interface{} + k int + next *Node +} + +// Quick sort for list [start, end) +func qsortList(start, end *Node) { + if start == end { + return + } + p, q := start, start.next + for q != end { + if q.k < p.k { + // swap the value, not change list structure + swap(p, q) + p = p.next + swap(p, q) + } + q = q.next + } + // sort [start, p) + qsortList(start, p) + // sort (p, end) + qsortList(p.next, end) +} + +func swap(p, q *Node) { + p.v, q.v = q.v, p.v + p.k, q.k = q.k, p.k +} +``` + +# Conclusion + +写代码前先确保几件事: + +1. 掌握算法的核心思想 +2. 设定几个边界条件 +3. 变量在变化过程中严格遵守边界条件 + +按照上述要求,我们在理解代码离开循环或选择语句时就不会陷入迷惑。 diff --git "a/src/md/2018-09-05-64\344\275\215\346\225\264\346\225\260\346\234\211\345\244\232\345\260\221\344\270\2521.md" "b/src/md/2018-09-05-64\344\275\215\346\225\264\346\225\260\346\234\211\345\244\232\345\260\221\344\270\2521.md" index ad5a29c..f3d6010 100644 --- "a/src/md/2018-09-05-64\344\275\215\346\225\264\346\225\260\346\234\211\345\244\232\345\260\221\344\270\2521.md" +++ "b/src/md/2018-09-05-64\344\275\215\346\225\264\346\225\260\346\234\211\345\244\232\345\260\221\344\270\2521.md" @@ -1,131 +1,131 @@ ---- -layout: post -title: "64位整数中有多少个1" -date: 2018-09-04 09:00:05 +0800 -categories: 算法 ---- - -# In short - -这是一个看似简单的问题,学过编程的同学不难解决这个问题,但怎么样更优雅地解决问题是这篇文章想讲述的。 - -# Main - -C 语言在不同平台上数据长度有所差异,我测试机器是 64 位: - -```c -printf("%lu %lu %lu\n", sizeof(unsigned int), sizeof(unsigned long), sizeof(unsigned long long)); -// output: 4 8 8 -``` - -理解 C 语言中的类型转换机制能够帮助理解下面代码。 - -C 语言中的强制类型转换,比如 unsigned int 与 int 之间的转换,采用二进制不变方式,也就是改变解析的方式,对于编译器来说就是拷贝一块内快。比如: - -```c -int x = -1; // 位模式:11111111 11111111 11111111 11111111 -unsigned int y = (unsigned int)x; // 位模式:11111111 11111111 11111111 11111111,也就是 y == 2 ^ 32 - 1 == 4294967295 -printf("%u", y); -// output: 4294967295 -``` - -如果将 64-bit int 转化为 32-bit,采取低位截断方式。比如: - -```c -unsigned long long y = 0xffffffffffffffff; -int x = y; // 位模式:11111111 11111111 11111111 11111111 -printf("%d\n", x); -// output: -1 -``` - -## 暴力破解 - -思路:遍历 64 位就知道 64-bit 整数有多少个 1。 - -```c -unsigned int counter(unsigned long long n) -{ - unsigned int cnt = 0; - while (n) - { - if (n & 1) cnt += 1; - n >>= 1; - } - return cnt; -} -``` - -在一般应用场景下,这个解决方案已经能够满足了,最多循环次数为 64。 - -## 借助数学 - -数学是伟大的学科,这个问题也可以借助数学进行优化。 - -数学规律:**对于任意一个非 0 整数 n,n & (n - 1) 能够去除 n 二进制中最低位的 1。** - -如 4-bit 整数 n == 5,二进制表示为 0101,n-1 == 4,二进制表示为 0100,`0101 & 0100 == 0100`。 - -借助这个规律有以下代码实现: - -```c -unsigned int counter(unsigned long long n) -{ - unsigned int cnt = 0; - while (n) - { - cnt += 1; - // 去除最低位的 1 - n &= n - 1; - } - return cnt; -} -``` - -一个 64-bit 整数 1 的平均个数为 32,所以平均循环次数为 32。 - -## 查表法 - -可以先将所有 8-bit 整数,-128 ~ 127 预先计算一遍对应有多少个 1,下次计算时就可以直接查表。 - -```c -static unsigned int table[255]; - -void init() -{ - for (unsigned int i = 0; i <= 0xff; ++i) - { - table[i] = table[i & (i - 1)] + 1; - } -} - -unsigned int counter(unsigned long long n) -{ - unsigned int cnt = 0; - while (n) - { - cnt += table[(n & 0xff)]; - n >>= 8; - } - return cnt; -} -``` - -如果采用 8-bit 作为切分需要查 8 次表,那么如果内存允许情况下,按照 32-bit 切分只需要查 2 次表是否会更快呢? - -如果单纯站在算法的角度思考问题,答案是肯定的。但是如果需要考虑到计算机运行环境,答案是否定的。 - -为什么,按照 8-bit 切分需要的内存为 `(2 ^ 8)B == 256B` 按照 32-bit 切分需要的内存为 `(2 ^ 32)B == 4GB`。 - -而普通 CPU 缓存大概为 10MB左右,Unix 系操作系统可通过 `cat /proc/cpuinfo` 查看当前电脑 CPU 信息,我电脑单核 CPU 的缓存: - -``` -cache size : 6144 KB -``` - -也就是说按照 8-bit 切分的数据能够完全存放于 CPU 告诉缓存中,高速缓存的速度是最接近 CPU 运算速度的存储介质,而按照 32-bit 切分的数据只能够存放于内存,每次查表还需要重新将数据从内存加载到高速缓存中,而且假设 4GB 数据被访问的可能性相等,那么也就是一个被加载进高速缓存的数据只用于一次查询后就失效了。 - -总而言之,按 8-bit 切分无论是时间还是空间上都是更好的方案。 - -# Conclusion - -无论多么简单的问题都会很大的优化空间,除了算法,还要从计算机系统角度思考问题。 +--- +layout: post +title: "64位整数中有多少个1" +date: 2018-09-04 09:00:05 +0800 +categories: 算法 +--- + +# In short + +这是一个看似简单的问题,学过编程的同学不难解决这个问题,但怎么样更优雅地解决问题是这篇文章想讲述的。 + +# Main + +C 语言在不同平台上数据长度有所差异,我测试机器是 64 位: + +```c +printf("%lu %lu %lu\n", sizeof(unsigned int), sizeof(unsigned long), sizeof(unsigned long long)); +// output: 4 8 8 +``` + +理解 C 语言中的类型转换机制能够帮助理解下面代码。 + +C 语言中的强制类型转换,比如 unsigned int 与 int 之间的转换,采用二进制不变方式,也就是改变解析的方式,对于编译器来说就是拷贝一块内快。比如: + +```c +int x = -1; // 位模式:11111111 11111111 11111111 11111111 +unsigned int y = (unsigned int)x; // 位模式:11111111 11111111 11111111 11111111,也就是 y == 2 ^ 32 - 1 == 4294967295 +printf("%u", y); +// output: 4294967295 +``` + +如果将 64-bit int 转化为 32-bit,采取低位截断方式。比如: + +```c +unsigned long long y = 0xffffffffffffffff; +int x = y; // 位模式:11111111 11111111 11111111 11111111 +printf("%d\n", x); +// output: -1 +``` + +## 暴力破解 + +思路:遍历 64 位就知道 64-bit 整数有多少个 1。 + +```c +unsigned int counter(unsigned long long n) +{ + unsigned int cnt = 0; + while (n) + { + if (n & 1) cnt += 1; + n >>= 1; + } + return cnt; +} +``` + +在一般应用场景下,这个解决方案已经能够满足了,最多循环次数为 64。 + +## 借助数学 + +数学是伟大的学科,这个问题也可以借助数学进行优化。 + +数学规律:**对于任意一个非 0 整数 n,n & (n - 1) 能够去除 n 二进制中最低位的 1。** + +如 4-bit 整数 n == 5,二进制表示为 0101,n-1 == 4,二进制表示为 0100,`0101 & 0100 == 0100`。 + +借助这个规律有以下代码实现: + +```c +unsigned int counter(unsigned long long n) +{ + unsigned int cnt = 0; + while (n) + { + cnt += 1; + // 去除最低位的 1 + n &= n - 1; + } + return cnt; +} +``` + +一个 64-bit 整数 1 的平均个数为 32,所以平均循环次数为 32。 + +## 查表法 + +可以先将所有 8-bit 整数,-128 ~ 127 预先计算一遍对应有多少个 1,下次计算时就可以直接查表。 + +```c +static unsigned int table[255]; + +void init() +{ + for (unsigned int i = 0; i <= 0xff; ++i) + { + table[i] = table[i & (i - 1)] + 1; + } +} + +unsigned int counter(unsigned long long n) +{ + unsigned int cnt = 0; + while (n) + { + cnt += table[(n & 0xff)]; + n >>= 8; + } + return cnt; +} +``` + +如果采用 8-bit 作为切分需要查 8 次表,那么如果内存允许情况下,按照 32-bit 切分只需要查 2 次表是否会更快呢? + +如果单纯站在算法的角度思考问题,答案是肯定的。但是如果需要考虑到计算机运行环境,答案是否定的。 + +为什么,按照 8-bit 切分需要的内存为 `(2 ^ 8)B == 256B` 按照 32-bit 切分需要的内存为 `(2 ^ 32)B == 4GB`。 + +而普通 CPU 缓存大概为 10MB左右,Unix 系操作系统可通过 `cat /proc/cpuinfo` 查看当前电脑 CPU 信息,我电脑单核 CPU 的缓存: + +``` +cache size : 6144 KB +``` + +也就是说按照 8-bit 切分的数据能够完全存放于 CPU 告诉缓存中,高速缓存的速度是最接近 CPU 运算速度的存储介质,而按照 32-bit 切分的数据只能够存放于内存,每次查表还需要重新将数据从内存加载到高速缓存中,而且假设 4GB 数据被访问的可能性相等,那么也就是一个被加载进高速缓存的数据只用于一次查询后就失效了。 + +总而言之,按 8-bit 切分无论是时间还是空间上都是更好的方案。 + +# Conclusion + +无论多么简单的问题都会很大的优化空间,除了算法,还要从计算机系统角度思考问题。 diff --git "a/src/md/2018-09-15-\345\246\202\344\275\225\345\210\244\346\226\255\345\244\247\345\260\217\347\253\257.md" "b/src/md/2018-09-15-\345\246\202\344\275\225\345\210\244\346\226\255\345\244\247\345\260\217\347\253\257.md" index 2cf05c1..378a7a7 100644 --- "a/src/md/2018-09-15-\345\246\202\344\275\225\345\210\244\346\226\255\345\244\247\345\260\217\347\253\257.md" +++ "b/src/md/2018-09-15-\345\246\202\344\275\225\345\210\244\346\226\255\345\244\247\345\260\217\347\253\257.md" @@ -1,108 +1,108 @@ ---- -layout: post -title: "字节序" -date: 2018-09-15 20:00:05 +0800 -categories: 计算机系统 ---- - -# In Short - -计算机系统中字节序有两种: - -1. 大端 big-endian -2. 小端 little-endian - -传闻这两个词是从《格列佛游记》中人们吃鸡蛋是先从哪一端吃起而引起无数的战争而延伸而来的,有强烈的讽刺意味。对于是小端或大端不同处理器制造商、操作系统开发商有不同的理解,两个派别都有各自忠实的拥趸。Intel 的字节序是小端,Sun Solaris 操作系统的字节序是大端,**网络中字节序是大端**,JVM 的字节序是大端。 - -# Main - -直接使用 TCP 进行数据传输时,每收到一个 4-byte 数字都需要去进行网络序到主机序的转换,发送数据时又要进行主机序到网络序的转换,每次写类似转换代码的时都想为什么不统一字节序呢,这样能省多少麻烦,当今社会对网络依赖程度高,浪费在字节序转换的资源(CPU、时间、能源)也是一个不小的数字。好消息是几乎每个编程语言都有对网络请求都要良好的封装,甚至不需要程序员考虑字节序方面的问题。 - -假设 32-bit `int x = 0x12345678; char *p = (char *)&x;` p = 0x10 - -## big-endian - -x 在内存的存储方式: - -| 0x10 | 0x11 | 0x12 | 0x13 | -| -- | --- | ---- | ---- | -| 0x12 | 0x34 | 0x56 | 0x78 | - -将权重更大的字节放在低地址。 - -## little-endian - -x 在内存的存储方式: - -| 0x10 | 0x11 | 0x12 | 0x13 | -| ---- | ---- | ---- | ---- | -| 0x78 | 0x56 | 0x34 | 0x12 | - -将权重更小的字节放在高地址。 - -## 判断当前主机字节序 - -在 Unix 上可以直接使用 lscpu 命令,输出关于 CPU 的相关信息: - -``` -➜ ~ lscpu -Architecture: x86_64 -CPU op-mode(s): 32-bit, 64-bit -Byte Order: Little Endian -CPU(s): 8 -On-line CPU(s) list: 0-7 -Thread(s) per core: 2 -Core(s) per socket: 4 -Socket(s): 1 -NUMA node(s): 1 -Vendor ID: GenuineIntel -CPU family: 6 -Model: 60 -Model name: Intel(R) Core(TM) i7-4710MQ CPU @ 2.50GHz -Stepping: 3 -CPU MHz: 1127.050 -CPU max MHz: 3500.0000 -CPU min MHz: 800.0000 -BogoMIPS: 4988.52 -Virtualization: VT-x -L1d cache: 32K -L1i cache: 32K -L2 cache: 256K -L3 cache: 6144K -NUMA node0 CPU(s): 0-7 -Flags: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid xsaveopt dtherm ida arat pln pts flush_l1d -``` - -或者使用编程语言自带的接口,比如 Python 可以通过查看变量 `sys.byteorder` 查看。 - -**要不写代码判断?** - -C 语言中,取一个变量的地址总是指向变量所在地址的起始。如上例中,变量 x 所在的内存:`0x10 0x11 0x12 0x13`,`char *p = &x;`,则 p = 0x10。 - -```c -#include - -int main() { - int x = 1; - char *p = (char *)&x; - if (*(p) & 0xff) { - printf("little-endian"); - } else { - printf("big-endian"); - } - return 0; -} -``` - -编译链接命令: - -```bash -gcc test.c -o test -``` - -命令输出可执行程序 test,命令行输入:`./test` 运行。 - -# End - -- 大端存储:低位地址 存储 数字高位 +--- +layout: post +title: "字节序" +date: 2018-09-15 20:00:05 +0800 +categories: 计算机系统 +--- + +# In Short + +计算机系统中字节序有两种: + +1. 大端 big-endian +2. 小端 little-endian + +传闻这两个词是从《格列佛游记》中人们吃鸡蛋是先从哪一端吃起而引起无数的战争而延伸而来的,有强烈的讽刺意味。对于是小端或大端不同处理器制造商、操作系统开发商有不同的理解,两个派别都有各自忠实的拥趸。Intel 的字节序是小端,Sun Solaris 操作系统的字节序是大端,**网络中字节序是大端**,JVM 的字节序是大端。 + +# Main + +直接使用 TCP 进行数据传输时,每收到一个 4-byte 数字都需要去进行网络序到主机序的转换,发送数据时又要进行主机序到网络序的转换,每次写类似转换代码的时都想为什么不统一字节序呢,这样能省多少麻烦,当今社会对网络依赖程度高,浪费在字节序转换的资源(CPU、时间、能源)也是一个不小的数字。好消息是几乎每个编程语言都有对网络请求都要良好的封装,甚至不需要程序员考虑字节序方面的问题。 + +假设 32-bit `int x = 0x12345678; char *p = (char *)&x;` p = 0x10 + +## big-endian + +x 在内存的存储方式: + +| 0x10 | 0x11 | 0x12 | 0x13 | +| -- | --- | ---- | ---- | +| 0x12 | 0x34 | 0x56 | 0x78 | + +将权重更大的字节放在低地址。 + +## little-endian + +x 在内存的存储方式: + +| 0x10 | 0x11 | 0x12 | 0x13 | +| ---- | ---- | ---- | ---- | +| 0x78 | 0x56 | 0x34 | 0x12 | + +将权重更小的字节放在高地址。 + +## 判断当前主机字节序 + +在 Unix 上可以直接使用 lscpu 命令,输出关于 CPU 的相关信息: + +``` +➜ ~ lscpu +Architecture: x86_64 +CPU op-mode(s): 32-bit, 64-bit +Byte Order: Little Endian +CPU(s): 8 +On-line CPU(s) list: 0-7 +Thread(s) per core: 2 +Core(s) per socket: 4 +Socket(s): 1 +NUMA node(s): 1 +Vendor ID: GenuineIntel +CPU family: 6 +Model: 60 +Model name: Intel(R) Core(TM) i7-4710MQ CPU @ 2.50GHz +Stepping: 3 +CPU MHz: 1127.050 +CPU max MHz: 3500.0000 +CPU min MHz: 800.0000 +BogoMIPS: 4988.52 +Virtualization: VT-x +L1d cache: 32K +L1i cache: 32K +L2 cache: 256K +L3 cache: 6144K +NUMA node0 CPU(s): 0-7 +Flags: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid xsaveopt dtherm ida arat pln pts flush_l1d +``` + +或者使用编程语言自带的接口,比如 Python 可以通过查看变量 `sys.byteorder` 查看。 + +**要不写代码判断?** + +C 语言中,取一个变量的地址总是指向变量所在地址的起始。如上例中,变量 x 所在的内存:`0x10 0x11 0x12 0x13`,`char *p = &x;`,则 p = 0x10。 + +```c +#include + +int main() { + int x = 1; + char *p = (char *)&x; + if (*(p) & 0xff) { + printf("little-endian"); + } else { + printf("big-endian"); + } + return 0; +} +``` + +编译链接命令: + +```bash +gcc test.c -o test +``` + +命令输出可执行程序 test,命令行输入:`./test` 运行。 + +# End + +- 大端存储:低位地址 存储 数字高位 - 小端存储:低位地址 存储 数字低位 \ No newline at end of file diff --git a/src/md/2018-09-16-50GB_URL.md b/src/md/2018-09-16-50GB_URL.md index 58cea7d..9f16c1b 100644 --- a/src/md/2018-09-16-50GB_URL.md +++ b/src/md/2018-09-16-50GB_URL.md @@ -1,135 +1,135 @@ ---- -layout: post -title: "50GB URL 统计出现次数 TopK URL" -date: 2018-09-16 20:00:05 +0800 -categories: 算法 ---- - -# In short - -以下是我在字节跳动面试中遇到的一道算法题: - -> 有 50GB 的 URL 文件,从中寻找出现次数前 10 的 URL,其中 URL 长度最大为 1024B,需要在一台内存为 2GB 的机器中完成。 - -PS: 面试官后面允许统计中可有一定的误差。 - -*面试过程中回答的并不好。* - -# Main - -## 方案一 排序 - -解决思路:通过外部排序将 50GB 的 URL 进行一次排序,排序后相同的 URL 会相邻,然后扫描 50GB 的文件,统计每一个 URL 出现的次数,使用小根堆维护 Top 10 的 URL。 - -面试官显然不满意这个答案,问以上解决方案的空间复杂度和时间复杂度。 - -时间复杂度和空间复杂度的瓶颈都在外部排序上,假设使用归并排序实现外部排序,那么时间复杂度为 `O(1024nlogn)`,空间复杂度为 `O(n)`,也就是需要额外的 50GB 的磁盘空间。`O(50GBlog(50GB))≈ 1913614957980.12`,即使 CPU 一秒能够执行 10亿 条指令,完成整个过程也至少需要 2000秒,如果加上磁盘频繁的读写,整个程序肯定会运行得很慢,这显然不是一个理想的解决方案。 - -如果有多台 2GB 内存的机器,可以借助 MapReduce 并行计算的威力。 - -## 方案二 哈希计数 - -如果 URL 大部分是重复的,那么 50GB 放入 2GB 内存中统计也是一个可以解决的方案,但是如果重复可能性不大,那么该方案也就无法在 2GB 内存中完成。 - -解决思路:每次从文件中读取一个 URL,对 URL 进行哈希,然后自增对应的哈希值,扫描一遍文件后,得出出现次数 Top 10 的哈希值,然后再对 50GB 文件做一次扫描,找出哈希值与 Top 10 哈希值一致的 URL,并对这些 URL 进行统计计数,由于哈希可能存在冲突,所以最后取出的 URL 不只有 10 个,再从这些 URL 中筛选出现次数 Top 10 的 URL。 - -假设每个 URL 都不同,且每个 URL 长度为 1024B,那么 50GB 里大概有 52428800 个 URL,大概需要 26 字节才能够标识每一个 URL(`2^25 < 52428800 < 2^26`),`(2^30 * 8) - 52428800 * 26 = 7226785792`,`7226785792 / 8 / 52428800 ≈ 17`,平均一个 URL 有 17字节做计数统计,足以做统计计数了。 - -哈希函数选择 MD5 足矣,MD5 有 128位,能够标识 340282366920938463463374607431768211456 个 URL,远超当前场景需求。 - -这个方案会在一个问题,可能不准确。 - -## 方案三 哈希切割文件 - -> 这是虎牙面试官给的思路,没错虎牙面试问了同一个问题。 - -如果能够将所有 URL 放进内存统计计数,那么这个这个问题也就迎刃而解。 - -内存不够一般做法: - -1. 使用位图等数据结构尽可能压缩数据 -2. 借助外存(磁盘) -3. 牺牲准确性,使用基于统计概率的算法 - -这个方案主要是借助磁盘空间,需要 O(n) 的磁盘空间,在这一题中是 50GB,将 50GB 的大文件,切分为每一个大小大约为 1GB 的小文件,足以放进内存进行统计计数。 - -步骤: - -1. 顺序读入 50GB URL,将每一个 URL 进行以下操作 -2. 计算 URL 的 MD5 值,将该 URL 写入文件名为 `Hash % 50` 文件中。得到名为 0 ~ 49 大小约为 1GB 的文件 -3. 分别计算每个文件 URL 出现次数 Top10 的 URL -4. 利用步骤 3 的结果,求出 50GB URL 中出现次数 Top10 - -解释: - -1. 哈希是为了将 URL 数值化,把 URL 归类 -2. `m = Hash(x)`,如果 x 一样,那么产生的 m 一定一样,也就是想同的 URL 一定被写入到同一个文件,方便统计计数 - -缺点:如果 50GB 文件中 URL 比较凌乱,随机写 50GB 数据到磁盘性能是很差的。 - -优化:扫描 50GB URL 50次,每次写入 `Hash(URL) % 50 == time` 的文件,批量读写,每次读写的大小近可能以磁盘页为单位进行,将磁盘的随机读写转化顺序读写。 - -## 方案四 Trie - -![Trie 图解](../img/400px-Trie_example.svg.png) - -[Trie](https://en.wikipedia.org/wiki/Trie) 前缀树,树中的每一个节点都存储键的一部分信息,因为一个节点存储的信息可以被多个键复用,大大地降低了数据的冗余,提升存储效率。在判断英文单词前缀是否正确,匹配最长前缀等有应用。 - -面试时候想到这个方案,但是没有说出这种方案,因为假设 URL 平均长度为 20 个字符且仅包含 ASCII 编码的字符,那么 Trie 树的高度为 20,每一个节点有 255 个子节点,也就是 Trie 树子节点的数量大概为 `255^20 == 1351461283755592687189686338827705478668212890625`,不可能存储在 2GB 的内存中。 - -## 方案五 布隆过滤器 - -![bloom filter解析](../img/649px-Bloom_filter.svg.png) - -[布隆过滤器](https://en.wikipedia.org/wiki/Bloom_filter)的优点:不存储具体的键数据,借助多个稀疏哈希函数来对数据进行快速地检索,能够保证布隆过滤器判断键不存在的情况下,该键一定不存在;如果布隆过滤器判断键存在的情况下,该键以一个可以推算的概率判断它存在。 - -后面被问将 50GB 的数据映射到 2GB 内存中,布隆过滤器的错误率是多少。之前简单看过布隆过滤器的错误率计算公式,但是没理解清楚,没答上。知其然不知其所以然。我预估冲突概率的思路: - -假设布隆过滤器占用内存为 1GB,有 10 个数组,每个数组长度约为 100MB 有 838860800位,能够标示 `2^838860800` 个元素,50GB URL 中大概有 52428800 个链接,每个 URL 通过稀疏哈希生成的 1 个数都 10 个,`52428800 * 10 / (2^838860800)` 错误率小到可以忽略。 - -解决思路:在内存中建立一个接近 2GB 大小的布隆过滤器,每取出一个 URL 都判断这个 URL 是否已经加入到布隆过滤器中,如果已经加入到就自增 URL 计数,否则将 URL 添加到布隆过滤器中。 - -上述方案最大的问题是如何以及在哪里进行统计计数。对于这个问题实在没有想到好的解决方案,能够想到的就是在磁盘上进行统计计数,说实话这个方案并不理想。 - -## 方案六 Count-Min Sketch - -![Count-Min Sketch解析图片](../img/count-min-sketch.jpg) - -[Count-Min Sketch](https://en.wikipedia.org/wiki/Count%E2%80%93min_sketch) 是布隆过滤器的变种,除了能够判断一个键是否存在外,还能够记录该键出现的次数,与本题目的要求非常符合。 - -Count-Min Sketch 适用于 Top K 都是出现次数非常多的,也就是与其他相差悬殊,但是如果每个 URL 出现次数相差不大,那么使用 Count-Mean-Min Sketch 会更好。 - -如果对于数据准确性要求更高,可通过 Count-Min Sketch 统计计数,将出现次数大于某个阈值的 URL 加入到堆中进行排序。 - -## 方案七 Lossy Count - -[Lossy Count](https://micvog.com/2015/07/18/frequency-counting-algorithms-over-data-streams/) 有损计数,有可能产生误报,但是可以将误报控制在一定的概率下,建议点开博客链接查看具体算法实现。 - -大概步骤: - -1. 将元素集合平均分为若干个窗口 -2. 合并相邻窗口,将相邻窗口相同的元素数量相加,并且每个元素数量减一 -3. 一直进行步骤 2,直到所有窗口合并完成 -4. 剩下的元素就是高频元素 - -核心思想:通过不断地筛选,最后剩下的都是高频元素。 - -如果应用在本例 URL TopK 的筛选,将 50GB 平均分为 100 个窗口处理,每个窗口大小约为 0.5GB,先加载第一个窗口进内存,并对 URL 进行统计计数,依次加载 2 ~ 100 号窗口进内存,统计计数,所有 URL 统计数量 -1,如果 -1 后变为 0 就舍弃该元素(该元素很有可能不是高频的元素),统计合并完所有窗口后,剩下的元素中,通过排序或堆得出 TopK URL。 - -## 方案八 HyperLogLog - -Redis 有一个基于概率的统计计数解决方案 HyperLogLog,该算法能够控制在 10^9 个元素下,只消耗 1.5KB 内存,且将错误率控制在 2% 以下。 - -# End - -上述很多方案都需要用到哈希运算,要注意的是不是所有哈希函数否符合条件的,对于布隆过滤器等应用应该选择 murmurhash 稀疏哈希(期待 0 远多于 1),而不是像 sha1、md5 这种每一位是 0 还是 1 的概率都是相等。 - -布隆过滤器、Count-Min Sketch以及 Lossy Count 都是基于概率的解决方案,能将保证误报率控制在某个阀值下,且相对其他绝对准确的解决方案,能够节省大量的内存以及处理时间。 - -Count-Min Sketch、Lossy Count 和 Redis HyperLogLog 在平常阅读博客中已经接触过,但是只是简单地知道它的存在,对两者没有很好的理解,关键时候没有联想到。还是书读的少,很多东西知其然不知其所以然(**You don't know what you can't build.**)。平时应该多关注其他公司比较先进的架构设计理念,成知识体系地看书。 - -在处理数据量大的情况下,应该借助位图、MapReduce和统计学方面的知识去解决问题。 - -对大数据下的统计计数感兴趣的可以参考: - -- [https://highlyscalable.wordpress.com/2012/05/01/probabilistic-structures-web-analytics-data-mining/](https://highlyscalable.wordpress.com/2012/05/01/probabilistic-structures-web-analytics-data-mining/) +--- +layout: post +title: "50GB URL 统计出现次数 TopK URL" +date: 2018-09-16 20:00:05 +0800 +categories: 算法 +--- + +# In short + +以下是我在字节跳动面试中遇到的一道算法题: + +> 有 50GB 的 URL 文件,从中寻找出现次数前 10 的 URL,其中 URL 长度最大为 1024B,需要在一台内存为 2GB 的机器中完成。 + +PS: 面试官后面允许统计中可有一定的误差。 + +*面试过程中回答的并不好。* + +# Main + +## 方案一 排序 + +解决思路:通过外部排序将 50GB 的 URL 进行一次排序,排序后相同的 URL 会相邻,然后扫描 50GB 的文件,统计每一个 URL 出现的次数,使用小根堆维护 Top 10 的 URL。 + +面试官显然不满意这个答案,问以上解决方案的空间复杂度和时间复杂度。 + +时间复杂度和空间复杂度的瓶颈都在外部排序上,假设使用归并排序实现外部排序,那么时间复杂度为 `O(1024nlogn)`,空间复杂度为 `O(n)`,也就是需要额外的 50GB 的磁盘空间。`O(50GBlog(50GB))≈ 1913614957980.12`,即使 CPU 一秒能够执行 10亿 条指令,完成整个过程也至少需要 2000秒,如果加上磁盘频繁的读写,整个程序肯定会运行得很慢,这显然不是一个理想的解决方案。 + +如果有多台 2GB 内存的机器,可以借助 MapReduce 并行计算的威力。 + +## 方案二 哈希计数 + +如果 URL 大部分是重复的,那么 50GB 放入 2GB 内存中统计也是一个可以解决的方案,但是如果重复可能性不大,那么该方案也就无法在 2GB 内存中完成。 + +解决思路:每次从文件中读取一个 URL,对 URL 进行哈希,然后自增对应的哈希值,扫描一遍文件后,得出出现次数 Top 10 的哈希值,然后再对 50GB 文件做一次扫描,找出哈希值与 Top 10 哈希值一致的 URL,并对这些 URL 进行统计计数,由于哈希可能存在冲突,所以最后取出的 URL 不只有 10 个,再从这些 URL 中筛选出现次数 Top 10 的 URL。 + +假设每个 URL 都不同,且每个 URL 长度为 1024B,那么 50GB 里大概有 52428800 个 URL,大概需要 26 字节才能够标识每一个 URL(`2^25 < 52428800 < 2^26`),`(2^30 * 8) - 52428800 * 26 = 7226785792`,`7226785792 / 8 / 52428800 ≈ 17`,平均一个 URL 有 17字节做计数统计,足以做统计计数了。 + +哈希函数选择 MD5 足矣,MD5 有 128位,能够标识 340282366920938463463374607431768211456 个 URL,远超当前场景需求。 + +这个方案会在一个问题,可能不准确。 + +## 方案三 哈希切割文件 + +> 这是虎牙面试官给的思路,没错虎牙面试问了同一个问题。 + +如果能够将所有 URL 放进内存统计计数,那么这个这个问题也就迎刃而解。 + +内存不够一般做法: + +1. 使用位图等数据结构尽可能压缩数据 +2. 借助外存(磁盘) +3. 牺牲准确性,使用基于统计概率的算法 + +这个方案主要是借助磁盘空间,需要 O(n) 的磁盘空间,在这一题中是 50GB,将 50GB 的大文件,切分为每一个大小大约为 1GB 的小文件,足以放进内存进行统计计数。 + +步骤: + +1. 顺序读入 50GB URL,将每一个 URL 进行以下操作 +2. 计算 URL 的 MD5 值,将该 URL 写入文件名为 `Hash % 50` 文件中。得到名为 0 ~ 49 大小约为 1GB 的文件 +3. 分别计算每个文件 URL 出现次数 Top10 的 URL +4. 利用步骤 3 的结果,求出 50GB URL 中出现次数 Top10 + +解释: + +1. 哈希是为了将 URL 数值化,把 URL 归类 +2. `m = Hash(x)`,如果 x 一样,那么产生的 m 一定一样,也就是想同的 URL 一定被写入到同一个文件,方便统计计数 + +缺点:如果 50GB 文件中 URL 比较凌乱,随机写 50GB 数据到磁盘性能是很差的。 + +优化:扫描 50GB URL 50次,每次写入 `Hash(URL) % 50 == time` 的文件,批量读写,每次读写的大小近可能以磁盘页为单位进行,将磁盘的随机读写转化顺序读写。 + +## 方案四 Trie + +![Trie 图解](../img/400px-Trie_example.svg.png) + +[Trie](https://en.wikipedia.org/wiki/Trie) 前缀树,树中的每一个节点都存储键的一部分信息,因为一个节点存储的信息可以被多个键复用,大大地降低了数据的冗余,提升存储效率。在判断英文单词前缀是否正确,匹配最长前缀等有应用。 + +面试时候想到这个方案,但是没有说出这种方案,因为假设 URL 平均长度为 20 个字符且仅包含 ASCII 编码的字符,那么 Trie 树的高度为 20,每一个节点有 255 个子节点,也就是 Trie 树子节点的数量大概为 `255^20 == 1351461283755592687189686338827705478668212890625`,不可能存储在 2GB 的内存中。 + +## 方案五 布隆过滤器 + +![bloom filter解析](../img/649px-Bloom_filter.svg.png) + +[布隆过滤器](https://en.wikipedia.org/wiki/Bloom_filter)的优点:不存储具体的键数据,借助多个稀疏哈希函数来对数据进行快速地检索,能够保证布隆过滤器判断键不存在的情况下,该键一定不存在;如果布隆过滤器判断键存在的情况下,该键以一个可以推算的概率判断它存在。 + +后面被问将 50GB 的数据映射到 2GB 内存中,布隆过滤器的错误率是多少。之前简单看过布隆过滤器的错误率计算公式,但是没理解清楚,没答上。知其然不知其所以然。我预估冲突概率的思路: + +假设布隆过滤器占用内存为 1GB,有 10 个数组,每个数组长度约为 100MB 有 838860800位,能够标示 `2^838860800` 个元素,50GB URL 中大概有 52428800 个链接,每个 URL 通过稀疏哈希生成的 1 个数都 10 个,`52428800 * 10 / (2^838860800)` 错误率小到可以忽略。 + +解决思路:在内存中建立一个接近 2GB 大小的布隆过滤器,每取出一个 URL 都判断这个 URL 是否已经加入到布隆过滤器中,如果已经加入到就自增 URL 计数,否则将 URL 添加到布隆过滤器中。 + +上述方案最大的问题是如何以及在哪里进行统计计数。对于这个问题实在没有想到好的解决方案,能够想到的就是在磁盘上进行统计计数,说实话这个方案并不理想。 + +## 方案六 Count-Min Sketch + +![Count-Min Sketch解析图片](../img/count-min-sketch.jpg) + +[Count-Min Sketch](https://en.wikipedia.org/wiki/Count%E2%80%93min_sketch) 是布隆过滤器的变种,除了能够判断一个键是否存在外,还能够记录该键出现的次数,与本题目的要求非常符合。 + +Count-Min Sketch 适用于 Top K 都是出现次数非常多的,也就是与其他相差悬殊,但是如果每个 URL 出现次数相差不大,那么使用 Count-Mean-Min Sketch 会更好。 + +如果对于数据准确性要求更高,可通过 Count-Min Sketch 统计计数,将出现次数大于某个阈值的 URL 加入到堆中进行排序。 + +## 方案七 Lossy Count + +[Lossy Count](https://micvog.com/2015/07/18/frequency-counting-algorithms-over-data-streams/) 有损计数,有可能产生误报,但是可以将误报控制在一定的概率下,建议点开博客链接查看具体算法实现。 + +大概步骤: + +1. 将元素集合平均分为若干个窗口 +2. 合并相邻窗口,将相邻窗口相同的元素数量相加,并且每个元素数量减一 +3. 一直进行步骤 2,直到所有窗口合并完成 +4. 剩下的元素就是高频元素 + +核心思想:通过不断地筛选,最后剩下的都是高频元素。 + +如果应用在本例 URL TopK 的筛选,将 50GB 平均分为 100 个窗口处理,每个窗口大小约为 0.5GB,先加载第一个窗口进内存,并对 URL 进行统计计数,依次加载 2 ~ 100 号窗口进内存,统计计数,所有 URL 统计数量 -1,如果 -1 后变为 0 就舍弃该元素(该元素很有可能不是高频的元素),统计合并完所有窗口后,剩下的元素中,通过排序或堆得出 TopK URL。 + +## 方案八 HyperLogLog + +Redis 有一个基于概率的统计计数解决方案 HyperLogLog,该算法能够控制在 10^9 个元素下,只消耗 1.5KB 内存,且将错误率控制在 2% 以下。 + +# End + +上述很多方案都需要用到哈希运算,要注意的是不是所有哈希函数否符合条件的,对于布隆过滤器等应用应该选择 murmurhash 稀疏哈希(期待 0 远多于 1),而不是像 sha1、md5 这种每一位是 0 还是 1 的概率都是相等。 + +布隆过滤器、Count-Min Sketch以及 Lossy Count 都是基于概率的解决方案,能将保证误报率控制在某个阀值下,且相对其他绝对准确的解决方案,能够节省大量的内存以及处理时间。 + +Count-Min Sketch、Lossy Count 和 Redis HyperLogLog 在平常阅读博客中已经接触过,但是只是简单地知道它的存在,对两者没有很好的理解,关键时候没有联想到。还是书读的少,很多东西知其然不知其所以然(**You don't know what you can't build.**)。平时应该多关注其他公司比较先进的架构设计理念,成知识体系地看书。 + +在处理数据量大的情况下,应该借助位图、MapReduce和统计学方面的知识去解决问题。 + +对大数据下的统计计数感兴趣的可以参考: + +- [https://highlyscalable.wordpress.com/2012/05/01/probabilistic-structures-web-analytics-data-mining/](https://highlyscalable.wordpress.com/2012/05/01/probabilistic-structures-web-analytics-data-mining/) diff --git a/src/md/2018-09-22-knapsack.md b/src/md/2018-09-22-knapsack.md index 5778b77..4aaaec5 100644 --- a/src/md/2018-09-22-knapsack.md +++ b/src/md/2018-09-22-knapsack.md @@ -1,374 +1,374 @@ ---- -layout: post -title: "背包问题 - 动态规划" -date: 2018-09-22 20:00:05 +0800 -categories: 算法 ---- - -# In short - -常见的背包问题: - -1. 0-1 背包 -2. 多重背包 -3. 完全背包 -4. 多维背包 -5. 塞满背包 - -笔试题常遇到背包问题,做了一个简单的调研,mark down here,方便未来查阅。 - -# Main - -其他的背包问题都是 0-1 背包的延伸,解题思路也是借鉴 0-1 背包,所以重点是弄清楚最简单的 0-1 背包解题思路。 - -以下的所有问题都通过动态规划解决,动态规划适用于以下问题: - -1. 问题能够分解为子问题 -2. 全局最优依赖于局部最优,每次求局部最优能够求得全局最优 -3. 解空间有重叠,能够通过存储局部解,减少冗余计算 -4. 局部解不会失效 - -例如通过动态规划求 Fibonacci 数列: - -```python -def fibonacci(): - mark = {1: 1, 2: 1} - n = 1 - while True: - if n in mark: - ret = mark[n] - else: - ret = mark[n - 1] + mark[n - 2] - mark[n] = ret - yield ret - n += 1 -``` - -声明:下述代码采用 Go 编程语言,默认会将数据初始化为零值,比如整形数组会初始化为 0;创建动态数组没 Java 灵活,需 make 创建 slice,但不影响理解代码逻辑。 - -## 0-1 背包 - -题目描述:有 N 件物品,第 i 件物品的重量为 w[i],价值为 p[i],承重为 W 的背包,每件物品有且仅有一件,要求最大化背包中物品的价值。 - -对于每件物品都有两种选择(放进背包 or not),那么时间复杂度就是 `O(2 ^ N)`,问题复杂度成指数级别增长,但在 `O(2 ^ N)` 中有很多重复的子结构,有优化空间。 - -定义函数:`f(i, v)` 为在背包承重为 v 的情况下,在 1 ~ i 物品中选择若干件,最大化背包中物品的价值。显然 `f(N, W)` 是本题的最终答案。 - -初始条件下,`f(0, 0...W) = 0` 且 `f(0...N, 0) = 0`,显然,没有物品和背包容量为 0 的情况下,价值最大化是 0。 - -状态转移函数: - -``` -f(i, v) = max{ - f(i - 1, v), - f(i - 1, v - w[i]) + p[i] if v >= w[i] else 0 - } -``` - -为什么状态转移方程是这样? - -如果 w[i] <= v,物品都有两种选择,放入背包 or not。那怎么判断是否应该放入背包呢?答案是两种方案都尝试一下,比较两种方案价值,选择价值更大者,在状态转移函数中是通过查表而不是重复计算子结构。 - -```go -func zeroOneKnapsack(w, p []int, N, W int) int { - f := make([][]int, N+1) - for i := 0; i <= N; i++ { - f[i] = make([]int, W+1) - } - - for i := 1; i <= N; i++ { - for v := 1; v <= W; v++ { - if w[i] > v { - f[i][v] = f[i-1][v] - } else { - f[i][v] = max(f[i-1][v], f[i-1][v-w[i-1]]+p[i-1]) - } - } - } - - return f[N][W] -} -``` - -例子:4 件物品,大小分别为 2, 3, 1, 2,价值分别为 4, 3, 5, 2,背包容量为 7。 - -按照上述代码,需要构造一个表格,然后按照规律填写表格: - -初始状态下表格是这样的: - -| | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | -| -- | -- | -- | -- | -- | --| -- | -- | -- | -| 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | -| 1 | 0 | | | | | | | | -| 2 | 0 | | | | | | | | -| 3 | 0 | | | | | | | | -| 4 | 0 | | | | | | | | - -填写后: - -| | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | -| -- | -- | -- | -- | -- | --| -- | -- | -- | -| 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | -| 1 | 0 | 0 | 4 | 4 | 4 | 4 | 4 | 4 | -| 2 | 0 | 0 | 4 | 4 | 4 | 7 | 7 | 7 | -| 3 | 0 | 5 | 5 | 9 | 9 | 9 | 12 | 12 | -| 4 | 0 | 5 | 5 | 9 | 9 | 9 | 12 | 12 | - -最终返回 12。 - -使用动态规划复杂度是 `O(N * W)`,通过存储解记录,下次需要时查表,减少冗余计算。在没有存储解记录的情况下,不难发现上述算法遍历 `O(2 ^ N)` 的解空间,把所有可能都已经纳入考虑了,得到的自然是最优解。动态规划聪明的地方不是将解空间有效缩小,而是存储解记录减少冗余计算。有兴趣的读者可以一步一步试着推导。 - -### 数组空间缩小为 O(W) - -根据状态转移函数: - -``` -f(i, v) = max{ - f(i - 1, v), - f(i - 1, v - w[i]) + p[i] if v >= w[i] else 0 - } -``` - -`f(i, v)` 只依赖于 `f(i - 1, v)` 和 `f(i - 1, v - w[i])` 两个解,也就是求解 `f(i, 0...W)` 只需要依赖于 `f(i - 1, 0...W)`,可把存储空间降到 `O(N)`。 - -```go -func zeroOneKnapsackSpaceAdvance(w, p []int, N, W int) int { - f := make([]int, W+1) - for i := 1; i <= N; i++ { - for v := W; v > 0 && w[i] <= v; v-- { - // f(i, v) = max{ f(i - 1, v), f(i - 1, v - w[i]) + p[i]} - f[v] = max(f[v-w[i]]+p[i], f[v]) - } - } - return f[W] -} -``` - -计算 `f(i, v)` 是从 W ==> 0 计算的,而不能是 0 ==> W。因为 `f(i, v)` 需要依赖 `f(i - 1, v - w[i])`,如果是从 0 ==> W 计算,有可能覆盖了 `f(i - 1, v - w[i])` 从而丢失解记录。 - ---- - -## 多重背包 - -题目描述:有 N 件物品,第 i 件物品的重量为 w[i],价值为 p[i],数量为 n[i],背包承重为 W,要求最大化背包中物品的价值。 - -与 0-1 背包的区别: - -在 0-1 背包,第 i 件物品只有两个选择,放入 or not。而多重背包,第 i 件物品可选择放入 0 ~ n[i] 件。 - -多重背包可以转化为 0-1 背包解决,将第 i 件物品,看成是 n[i] 件独立的,重量和价值等价的商品,可以直接复用 0-1 背包。 - -不把多重背包问题直接转化为 0-1 背包问题,拟定一个多重背包的状态转移函数: - -``` -f(i, v) = max(f(i - 1, v - k * w[i]) for k := 0...n[i] if v >= k * w[i]) -``` - -```go -func multiKnapsack(w, p, n []int, N, W int) int { - f := make([][]int, N+1) - for i := 0; i <= N; i++ { - f[i] = make([]int, W+1) - } - for i := 1; i <= N; i++ { - for v := 1; v <= W; v++ { - for k := 0; k <= n[i]; k++ { - if v >= k*w[i] { - f[i][v] = max(f[i-1][v], f[i-1][v-k*w[i]]+k*p[i]) - } else { - f[i][v] = f[i - 1][v] - } - } - } - } - return f[N][W] -} -``` - -同样可以将空间复杂度降到 O(W)。 - -```go -func multiKnapsackSpaceAdvance(w, p, n []int, N, W int) int { - f := make([]int, W + 1) - for i := 0; i <= N; i++ { - for v := W; v > 0; v-- { - for k := 1; k <= n[i] && v >= k * w[i]; k++ { - f[v] = max(f[v], f[v - k * w[i]] + k * p[i]) - } - } - } - return f[W] -} -``` - ---- - -## 完全背包 - -问题描述:有 N 件物品,每件物品数量有无数多个,第 i 件物品的重量为 w[i],价值为 p[i],背包承重为 W,要求最大化背包中物品的价值。 - -与多重背包有什么关联? - -虽然物品的数量无上限,但是因为背包承重上限为 W,那么第 i 件商品最多只能够携带 `W / w[i]` 件,也就是完全背包可以转化为多重背包求解。只需要计算出每件物品的上限数量 `n[i] = W / w[i]` 就可以复用多重背包求解。 - -另一种方法是将有限资源(背包承重)的循环条件往外移动,确保背包的承重从小到大变化过程中保持局部最优解。 - -```go -func comleteKnapsack(w, p []int, N, W int) int { - f := make([]int, W+1) - for v := 1; v <= W; v++ { - for i := range w { - if v >= w[i] { - f[v] = max(f[v], f[v-w[i]]+p[i]) - } - } - } - return f[W] -} -``` - -[Leetcode 322. Coin Change](https://leetcode.com/problems/coin-change/description/) - -[Leetcode 377. Combination Sum IV](https://leetcode.com/problems/combination-sum-iv/description/) - ---- - -## 多维背包 - -问题描述:有 N 件物品,每件物品数量为 1,第 i 件物品的重量为 w[i],大小为 s[i],价值为 p[i],背包承重为 W,容量为 S,要求最大化背包中物品的价值。 - -与 0-1 背包有什么关联? - -物品属性的维度不再是单一的,除了重量还有大小,但动态规划的思路是一样的:探索整个解空间,并且存储解记录。 - -定义函数:`f(i, v, y)` 为在背包承重为 v,容量为 y 的情况下,在 1 ~ i 物品中选择若干件,最大化背包中物品价值。 - -状态转移函数: - -``` -f(i, v, y) = max { - f(i - 1, v, y), - f(i - 1, v - w[i], y - s[i]) if v >= w[i] and y >= s[i] else 0 - } -``` - -因为状态转移函数有三个变量,所以解空间大小为三维,需要一个三维数组存储解记录。 - -```go -func multiDimensionKnapsack(w, p, s []int, N, W, S int) int { - f := make([][][]int, N+1) - for i := 0; i <= N; i++ { - f[i] = make([][]int, W+1) - for j := 0; j <= S; j++ { - f[i][j] = make([]int, S+1) - } - } - for i := 1; i <= N; i++ { - for v := 0; v <= W; v++ { - for y := 0; y <= S; y++ { - if v >= w[i] && y >= s[i] { - f[i][v][y] = max(f[i-1][v][y], f[i-1][v-w[i]][y-s[i]]) - } else { - f[i][v][y] = f[i-1][v][y] - } - } - } - } - return f[N][W][S] -} -``` - -同样的,存储空间可做降维。 - -```go -func multiDimensionKnapsackSpaceAdvance(w, p, s []int, N, W, S int) int { - f := make([][]int, W+1) - for i := 0; i <= W; i++ { - f[i] = make([]int, S+1) - } - for i := 1; i <= N; i++ { - for v := W; v > 0 && v >= w[i]; v-- { - for y := S; y > 0 && y >= s[i]; y-- { - f[v][y] = max(f[v][y], f[v-w[i]][y-s[i]]+p[i]) - } - } - } - return f[W][S] -} -``` - -[Leetcode 474. Ones and Zeroes](https://leetcode.com/problems/ones-and-zeroes/description/) - ---- - -## 塞满背包 - -题目描述:有 N 件物品,第 i 件物品大小为 w[i],背包容量为 W,问是否存在一种方案刚好塞满背包。 - -相似的问题:给出一个*只有正数*的数组 nums 和一个特定值 target,问 nums 的子序列是否存在数字之和为 target。 - -解决方案:对数组 nums 进行排序,创建一个数组记录某个值是否可达,经过所有遍历后,判断是否能够达到某个值。 - -如果原来该顶点已经可达,那么 f(i) = true;如果新发现能够到达 i 的路径,那么 f(i) = f(i - v)。 - -状态转移函数: - -``` -f(i) = f(i) || f(i - v) -``` - -```go -func fullKnapsack(w []int, N, W int) bool { - // f mark every value is reachable or not. - f := make([]bool, W+1) - // 0 can be reach always - f[0] = true - // make sure every number will be used just once. - for _, v := range w { - // loop from W to 0 - // if loop from 0 to W, every f[k * v] will be set true. - for t := W; t > 0 && t >= v; t-- { - f[t] = f[t] || f[t-v] - } - } - // judge - return f[W] -} -``` - -**因为加法符合交换律,所以不需要对数组进行排序**。 - -dp 的值除了可以记录是否可达(true / false)外,还能够记录到达该顶点的路径数。 - -到达该值的方法 = 新发现路径数量 + 旧路径数量 - -状态转移函数: - -``` -f(i) = f(i) + f(i - v) -``` - -```go -func knapsackWaysCnt(w []int, N, W int) int { - f := make([]int, W+1) - f[0] = 1 - for _, v := range w { - for t := W; t >= v; t-- { - f[t] += f[t-v] - } - } - return f[W] -} -``` - -[Leetcode 416. Partition Equal Subset Sum](https://leetcode.com/problems/partition-equal-subset-sum/description/) - -[Leetcode 494. Target Sum](https://leetcode.com/problems/target-sum/description/) - -[Leetcode 139. Word Break](https://leetcode.com/problems/word-break/description/) - -# End - -上述四种背包问题都采用动态规划解决,所有方案都没有效地缩小解空间,而是通过存储解记录,减少冗余计算。如果分析上述方案,会发现动态规划遍历整个解空间。 - -不足:如果物品的重量为浮点数,无法采用本博文方法解决。可复用动态规划思路,将浮点数转化为整数处理,但存储空间会被放大。 +--- +layout: post +title: "背包问题 - 动态规划" +date: 2018-09-22 20:00:05 +0800 +categories: 算法 +--- + +# In short + +常见的背包问题: + +1. 0-1 背包 +2. 多重背包 +3. 完全背包 +4. 多维背包 +5. 塞满背包 + +笔试题常遇到背包问题,做了一个简单的调研,mark down here,方便未来查阅。 + +# Main + +其他的背包问题都是 0-1 背包的延伸,解题思路也是借鉴 0-1 背包,所以重点是弄清楚最简单的 0-1 背包解题思路。 + +以下的所有问题都通过动态规划解决,动态规划适用于以下问题: + +1. 问题能够分解为子问题 +2. 全局最优依赖于局部最优,每次求局部最优能够求得全局最优 +3. 解空间有重叠,能够通过存储局部解,减少冗余计算 +4. 局部解不会失效 + +例如通过动态规划求 Fibonacci 数列: + +```python +def fibonacci(): + mark = {1: 1, 2: 1} + n = 1 + while True: + if n in mark: + ret = mark[n] + else: + ret = mark[n - 1] + mark[n - 2] + mark[n] = ret + yield ret + n += 1 +``` + +声明:下述代码采用 Go 编程语言,默认会将数据初始化为零值,比如整形数组会初始化为 0;创建动态数组没 Java 灵活,需 make 创建 slice,但不影响理解代码逻辑。 + +## 0-1 背包 + +题目描述:有 N 件物品,第 i 件物品的重量为 w[i],价值为 p[i],承重为 W 的背包,每件物品有且仅有一件,要求最大化背包中物品的价值。 + +对于每件物品都有两种选择(放进背包 or not),那么时间复杂度就是 `O(2 ^ N)`,问题复杂度成指数级别增长,但在 `O(2 ^ N)` 中有很多重复的子结构,有优化空间。 + +定义函数:`f(i, v)` 为在背包承重为 v 的情况下,在 1 ~ i 物品中选择若干件,最大化背包中物品的价值。显然 `f(N, W)` 是本题的最终答案。 + +初始条件下,`f(0, 0...W) = 0` 且 `f(0...N, 0) = 0`,显然,没有物品和背包容量为 0 的情况下,价值最大化是 0。 + +状态转移函数: + +``` +f(i, v) = max{ + f(i - 1, v), + f(i - 1, v - w[i]) + p[i] if v >= w[i] else 0 + } +``` + +为什么状态转移方程是这样? + +如果 w[i] <= v,物品都有两种选择,放入背包 or not。那怎么判断是否应该放入背包呢?答案是两种方案都尝试一下,比较两种方案价值,选择价值更大者,在状态转移函数中是通过查表而不是重复计算子结构。 + +```go +func zeroOneKnapsack(w, p []int, N, W int) int { + f := make([][]int, N+1) + for i := 0; i <= N; i++ { + f[i] = make([]int, W+1) + } + + for i := 1; i <= N; i++ { + for v := 1; v <= W; v++ { + if w[i] > v { + f[i][v] = f[i-1][v] + } else { + f[i][v] = max(f[i-1][v], f[i-1][v-w[i-1]]+p[i-1]) + } + } + } + + return f[N][W] +} +``` + +例子:4 件物品,大小分别为 2, 3, 1, 2,价值分别为 4, 3, 5, 2,背包容量为 7。 + +按照上述代码,需要构造一个表格,然后按照规律填写表格: + +初始状态下表格是这样的: + +| | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | +| -- | -- | -- | -- | -- | --| -- | -- | -- | +| 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | +| 1 | 0 | | | | | | | | +| 2 | 0 | | | | | | | | +| 3 | 0 | | | | | | | | +| 4 | 0 | | | | | | | | + +填写后: + +| | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | +| -- | -- | -- | -- | -- | --| -- | -- | -- | +| 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | +| 1 | 0 | 0 | 4 | 4 | 4 | 4 | 4 | 4 | +| 2 | 0 | 0 | 4 | 4 | 4 | 7 | 7 | 7 | +| 3 | 0 | 5 | 5 | 9 | 9 | 9 | 12 | 12 | +| 4 | 0 | 5 | 5 | 9 | 9 | 9 | 12 | 12 | + +最终返回 12。 + +使用动态规划复杂度是 `O(N * W)`,通过存储解记录,下次需要时查表,减少冗余计算。在没有存储解记录的情况下,不难发现上述算法遍历 `O(2 ^ N)` 的解空间,把所有可能都已经纳入考虑了,得到的自然是最优解。动态规划聪明的地方不是将解空间有效缩小,而是存储解记录减少冗余计算。有兴趣的读者可以一步一步试着推导。 + +### 数组空间缩小为 O(W) + +根据状态转移函数: + +``` +f(i, v) = max{ + f(i - 1, v), + f(i - 1, v - w[i]) + p[i] if v >= w[i] else 0 + } +``` + +`f(i, v)` 只依赖于 `f(i - 1, v)` 和 `f(i - 1, v - w[i])` 两个解,也就是求解 `f(i, 0...W)` 只需要依赖于 `f(i - 1, 0...W)`,可把存储空间降到 `O(N)`。 + +```go +func zeroOneKnapsackSpaceAdvance(w, p []int, N, W int) int { + f := make([]int, W+1) + for i := 1; i <= N; i++ { + for v := W; v > 0 && w[i] <= v; v-- { + // f(i, v) = max{ f(i - 1, v), f(i - 1, v - w[i]) + p[i]} + f[v] = max(f[v-w[i]]+p[i], f[v]) + } + } + return f[W] +} +``` + +计算 `f(i, v)` 是从 W ==> 0 计算的,而不能是 0 ==> W。因为 `f(i, v)` 需要依赖 `f(i - 1, v - w[i])`,如果是从 0 ==> W 计算,有可能覆盖了 `f(i - 1, v - w[i])` 从而丢失解记录。 + +--- + +## 多重背包 + +题目描述:有 N 件物品,第 i 件物品的重量为 w[i],价值为 p[i],数量为 n[i],背包承重为 W,要求最大化背包中物品的价值。 + +与 0-1 背包的区别: + +在 0-1 背包,第 i 件物品只有两个选择,放入 or not。而多重背包,第 i 件物品可选择放入 0 ~ n[i] 件。 + +多重背包可以转化为 0-1 背包解决,将第 i 件物品,看成是 n[i] 件独立的,重量和价值等价的商品,可以直接复用 0-1 背包。 + +不把多重背包问题直接转化为 0-1 背包问题,拟定一个多重背包的状态转移函数: + +``` +f(i, v) = max(f(i - 1, v - k * w[i]) for k := 0...n[i] if v >= k * w[i]) +``` + +```go +func multiKnapsack(w, p, n []int, N, W int) int { + f := make([][]int, N+1) + for i := 0; i <= N; i++ { + f[i] = make([]int, W+1) + } + for i := 1; i <= N; i++ { + for v := 1; v <= W; v++ { + for k := 0; k <= n[i]; k++ { + if v >= k*w[i] { + f[i][v] = max(f[i-1][v], f[i-1][v-k*w[i]]+k*p[i]) + } else { + f[i][v] = f[i - 1][v] + } + } + } + } + return f[N][W] +} +``` + +同样可以将空间复杂度降到 O(W)。 + +```go +func multiKnapsackSpaceAdvance(w, p, n []int, N, W int) int { + f := make([]int, W + 1) + for i := 0; i <= N; i++ { + for v := W; v > 0; v-- { + for k := 1; k <= n[i] && v >= k * w[i]; k++ { + f[v] = max(f[v], f[v - k * w[i]] + k * p[i]) + } + } + } + return f[W] +} +``` + +--- + +## 完全背包 + +问题描述:有 N 件物品,每件物品数量有无数多个,第 i 件物品的重量为 w[i],价值为 p[i],背包承重为 W,要求最大化背包中物品的价值。 + +与多重背包有什么关联? + +虽然物品的数量无上限,但是因为背包承重上限为 W,那么第 i 件商品最多只能够携带 `W / w[i]` 件,也就是完全背包可以转化为多重背包求解。只需要计算出每件物品的上限数量 `n[i] = W / w[i]` 就可以复用多重背包求解。 + +另一种方法是将有限资源(背包承重)的循环条件往外移动,确保背包的承重从小到大变化过程中保持局部最优解。 + +```go +func comleteKnapsack(w, p []int, N, W int) int { + f := make([]int, W+1) + for v := 1; v <= W; v++ { + for i := range w { + if v >= w[i] { + f[v] = max(f[v], f[v-w[i]]+p[i]) + } + } + } + return f[W] +} +``` + +[Leetcode 322. Coin Change](https://leetcode.com/problems/coin-change/description/) + +[Leetcode 377. Combination Sum IV](https://leetcode.com/problems/combination-sum-iv/description/) + +--- + +## 多维背包 + +问题描述:有 N 件物品,每件物品数量为 1,第 i 件物品的重量为 w[i],大小为 s[i],价值为 p[i],背包承重为 W,容量为 S,要求最大化背包中物品的价值。 + +与 0-1 背包有什么关联? + +物品属性的维度不再是单一的,除了重量还有大小,但动态规划的思路是一样的:探索整个解空间,并且存储解记录。 + +定义函数:`f(i, v, y)` 为在背包承重为 v,容量为 y 的情况下,在 1 ~ i 物品中选择若干件,最大化背包中物品价值。 + +状态转移函数: + +``` +f(i, v, y) = max { + f(i - 1, v, y), + f(i - 1, v - w[i], y - s[i]) if v >= w[i] and y >= s[i] else 0 + } +``` + +因为状态转移函数有三个变量,所以解空间大小为三维,需要一个三维数组存储解记录。 + +```go +func multiDimensionKnapsack(w, p, s []int, N, W, S int) int { + f := make([][][]int, N+1) + for i := 0; i <= N; i++ { + f[i] = make([][]int, W+1) + for j := 0; j <= S; j++ { + f[i][j] = make([]int, S+1) + } + } + for i := 1; i <= N; i++ { + for v := 0; v <= W; v++ { + for y := 0; y <= S; y++ { + if v >= w[i] && y >= s[i] { + f[i][v][y] = max(f[i-1][v][y], f[i-1][v-w[i]][y-s[i]]) + } else { + f[i][v][y] = f[i-1][v][y] + } + } + } + } + return f[N][W][S] +} +``` + +同样的,存储空间可做降维。 + +```go +func multiDimensionKnapsackSpaceAdvance(w, p, s []int, N, W, S int) int { + f := make([][]int, W+1) + for i := 0; i <= W; i++ { + f[i] = make([]int, S+1) + } + for i := 1; i <= N; i++ { + for v := W; v > 0 && v >= w[i]; v-- { + for y := S; y > 0 && y >= s[i]; y-- { + f[v][y] = max(f[v][y], f[v-w[i]][y-s[i]]+p[i]) + } + } + } + return f[W][S] +} +``` + +[Leetcode 474. Ones and Zeroes](https://leetcode.com/problems/ones-and-zeroes/description/) + +--- + +## 塞满背包 + +题目描述:有 N 件物品,第 i 件物品大小为 w[i],背包容量为 W,问是否存在一种方案刚好塞满背包。 + +相似的问题:给出一个*只有正数*的数组 nums 和一个特定值 target,问 nums 的子序列是否存在数字之和为 target。 + +解决方案:对数组 nums 进行排序,创建一个数组记录某个值是否可达,经过所有遍历后,判断是否能够达到某个值。 + +如果原来该顶点已经可达,那么 f(i) = true;如果新发现能够到达 i 的路径,那么 f(i) = f(i - v)。 + +状态转移函数: + +``` +f(i) = f(i) || f(i - v) +``` + +```go +func fullKnapsack(w []int, N, W int) bool { + // f mark every value is reachable or not. + f := make([]bool, W+1) + // 0 can be reach always + f[0] = true + // make sure every number will be used just once. + for _, v := range w { + // loop from W to 0 + // if loop from 0 to W, every f[k * v] will be set true. + for t := W; t > 0 && t >= v; t-- { + f[t] = f[t] || f[t-v] + } + } + // judge + return f[W] +} +``` + +**因为加法符合交换律,所以不需要对数组进行排序**。 + +dp 的值除了可以记录是否可达(true / false)外,还能够记录到达该顶点的路径数。 + +到达该值的方法 = 新发现路径数量 + 旧路径数量 + +状态转移函数: + +``` +f(i) = f(i) + f(i - v) +``` + +```go +func knapsackWaysCnt(w []int, N, W int) int { + f := make([]int, W+1) + f[0] = 1 + for _, v := range w { + for t := W; t >= v; t-- { + f[t] += f[t-v] + } + } + return f[W] +} +``` + +[Leetcode 416. Partition Equal Subset Sum](https://leetcode.com/problems/partition-equal-subset-sum/description/) + +[Leetcode 494. Target Sum](https://leetcode.com/problems/target-sum/description/) + +[Leetcode 139. Word Break](https://leetcode.com/problems/word-break/description/) + +# End + +上述四种背包问题都采用动态规划解决,所有方案都没有效地缩小解空间,而是通过存储解记录,减少冗余计算。如果分析上述方案,会发现动态规划遍历整个解空间。 + +不足:如果物品的重量为浮点数,无法采用本博文方法解决。可复用动态规划思路,将浮点数转化为整数处理,但存储空间会被放大。 diff --git "a/src/md/2018-10-02-\347\247\213\346\213\233\347\273\223\346\235\237.md" "b/src/md/2018-10-02-\347\247\213\346\213\233\347\273\223\346\235\237.md" index 27ac443..9b3d39a 100644 --- "a/src/md/2018-10-02-\347\247\213\346\213\233\347\273\223\346\235\237.md" +++ "b/src/md/2018-10-02-\347\247\213\346\213\233\347\273\223\346\235\237.md" @@ -1,61 +1,61 @@ ---- -layout: post -title: "秋招结束" -date: 2018-10-02 20:00:05 +0800 -categories: 总结 ---- - -# In short - -> 本文非面经。 - -从腾讯实习回来已有一个多月,这段日子一直忙忙碌碌地复习、投简历、做笔试及跑面试。这几天陆陆续续地收到了几个比较满意的 offer,认为秋招可以结束了,在这段日子的所见所闻所想令我产生一些想法,在这里做一个记录和大家分享。 - -# Main - -## 秋招相关工作 - -得知没拿到腾讯实习留用 offer,整个实习过程都没有为秋招做准备,时间稍晚,错过了很多公司提前批,感觉非常被动。在焦虑中开始了海投,在牛客网上看到有任何公司招聘信息都不会错过,简历信息重复地录入非常浪费时间,心想怎么没有一个统一的平台去做简历管理呢(有好几个平台做简历管理,但不统一)?未来找工作的同学一定要好好准备简历,除了 pdf 外,还要准备好网上填写个人履历,因为部分面试官会看招聘网站的信息,而不是我们精心制作的简历。 - -每个面试官各有自己的面试风格,复习仅仅针对某一模块会吃亏,但问题总是相似的。技术面一般问的是算法、项目经历、逻辑数学题、编程语言基础及底层实现原理、计算机系统基础知识、系统设计、数据库。*注:应聘岗位为后台开发* - -算法可以通过有针对性地看书和成系统地刷题,《剑指 offer》和《编程之美》对于应对笔试和面试问题很有帮助,这类书信息密度非常大,应该耐心琢磨,不在多而在精;Leetcode 上有很多题目,部分题目是用户在真实面试中遇到并上传的,不应盲目地把所有题目刷一遍,这样不仅很耗费时间,而且重复的问题很多。应根据问题的难度(easy / medium / hard)和问题的类型来做更有针对性的练习,大部分公司对于非算法岗,算法要求都不会太高,medium 足以应对大部分笔试、面试中的问题,每天弄懂一类问题,一个月下来就 30 类,足以应付面试中常问的问题。 - -项目经历只能靠积累了,通过临时抱佛脚和造假是没有意义的,仔细一问就会露馅。有些面试官喜欢逮着项目经历问,对于项目中使用到的技术有深入的了解是很有必要的,做好对于项目能够有自己的看法,比如针对某个模块使用什么技术会更好,以及为什么。 - -逻辑数学题不仅仅出现在性格测评,而且面试官会问一些 IQ 题,我能够想起的有: - -1. 蚂蚁从杆的一端走到另一端需要 2 分钟,走到杆末尾蚂蚁会掉下;蚂蚁相遇后,在原地改变行走方向;现在杆上有无数只蚂蚁在杆上随机的位置和方向行走,问至少多长时间后,保证杆上一定没有蚂蚁。 -2. 现有一没有砝码的天平和 9 个物品,其中 1 个有毒物品比其他 8 个物品轻,其余 8 个物品重量相等,问至少经过几次测量可得知是哪个物品有毒? -3. 现有三个筐,里面分别装:苹果、橘子、苹果 + 橘子,有三张标签:苹果、橘子、苹果 + 橘子,现在三个筐上贴着的标签都是错的,每次能够从筐中取出一个水果,问取多少次能够把标签贴正确? -4. 足球比赛中,赢者积 3 分、平者积 1 分、负者积 0 分,一组内有 100 个队伍,前两名出线,问至少积分多少才能出线? - -关于编程语言,我路子走的比较野,一开始学的 Java,做过 Android 和 Java Web 开发,后面觉得人生苦短还是写 Python 吧,写了一段时间,享受过 Python 的语法糖后,觉得 Python 太慢了,它慢是因为解释型编程语言、动态类型和 GIL。后面开始写 Golang,Golang 算是奇葩,面向接口编程,内置 goroutine 和 channel,背后有 Google,在腾讯实习期间用 C++。如果再让我选择一次,我可能会选择 C++ 和 Python,因为 C++ 语法和特性十分复杂,能够把 C++ 语法、编译器优化等弄懂,后续去理解其他编程语言的思想也就不难了,而且 C++ 能够接触到比较底层的 API,对计算机系统的理解有帮助。上述提到的编程语言的岗位我都有投递,所以面试中遇到的问题比较广泛,我也没有花太多的时间去复习关于编程语言方面的知识。 - -计算机系统方面的知识需要成体系地看书,在网上搜索博客一小块一小块地看书很难整理出知识体系,而且这方面有不少经典的书籍。 - -如果对自己有实力有信心的读者请不要海投,太浪费时间精力了,对自己想去的公司有针对性地准备更好,比如了解一下他们的技术栈,这样也能在面试过程中能够问更多的问题,面试官能够更深入地问,这样可能更能够证明自己的实力。 - -在经过很多次笔试、面试后还是没有 offer 会很伤士气,有些公司在面试完两个星期后才会出结果。这时需要稳住心态,不要怕被拒绝,继续尝试。 - -## 我到底想干什么 - -应聘竞争者学历普遍比我高,很多都是研究生,找一份满意的工作真的不简单。 - -不能再随波逐流了,人生是一个多维度的竞赛,需要思考清楚自己对什么感兴趣,未来职业发展的大方向是什么。 - -身边很多同学选择计算机方面读研,去更好的学校进修。也有些同学发现自己对计算机不太感兴趣,跨专业读研,去做新的尝试,心里面很佩服这类人,“沉没成本不是成本”这句话都懂,但能做到知行合一并非易事。一些人选择做自媒体,借助今日头条、腾讯、百度等自媒体平台探索出一条新的出路。一些人选择考公务员,手握铁饭碗,未来生活质量有了一定的保证。当然永远少不了想要创业的人或从商的人。很高兴身边有形形色色的人,能够了解到更多人的想法,就像生态圈,多样性越高越稳定,社会需要有不同想法的人。 - -而我真正想要的是什么呢?这个问题花再长的时间去思考也不过分。 - -人生是一个多维度竞赛,光是在学历、薪资方面领先并不一定能做人生赢家,身体健康、幸福程度、朋友质量也都是重要的衡量标准。 - -面试中 HR 最常问的问题是你有女朋友吗?有人为了躲避 HR 后续的一系列问题会说没有,而我是真的没有。HR 问大学四年为什么不找一个女朋友呢?对于这个问题我往往没法回答。大学没谈恋爱是大学最后悔的事情了。 - -在腾讯实习的两个半月期间,做的事情我不感兴趣,令我真切地感受到如果对工作内容不感兴趣,工作是多么的无聊。想要有一定的建树,必须从事一个感兴趣的事。 - -国庆这几天,结束了秋招想要好好休息一段时间,却不知道应该干什么,想看点书,但是内心有点浮躁,最近坐的时间有点长,身体也有点受不了。运动又不能一天到晚地做,看电影又不知道有什么好片,我不大喜欢打游戏。**有一个业余爱好是多么的重要**。我最后选择的做法是到处找找朋友聊天,接下来的日子要去实习了,下次回学校可能是明年毕业答辩了,好好珍惜接下来的校园闲暇时光。 - -# End - -人生很漫长,所有当下觉得迈不过去的坎,放在若干年后回味都是小事。 +--- +layout: post +title: "秋招结束" +date: 2018-10-02 20:00:05 +0800 +categories: 总结 +--- + +# In short + +> 本文非面经。 + +从腾讯实习回来已有一个多月,这段日子一直忙忙碌碌地复习、投简历、做笔试及跑面试。这几天陆陆续续地收到了几个比较满意的 offer,认为秋招可以结束了,在这段日子的所见所闻所想令我产生一些想法,在这里做一个记录和大家分享。 + +# Main + +## 秋招相关工作 + +得知没拿到腾讯实习留用 offer,整个实习过程都没有为秋招做准备,时间稍晚,错过了很多公司提前批,感觉非常被动。在焦虑中开始了海投,在牛客网上看到有任何公司招聘信息都不会错过,简历信息重复地录入非常浪费时间,心想怎么没有一个统一的平台去做简历管理呢(有好几个平台做简历管理,但不统一)?未来找工作的同学一定要好好准备简历,除了 pdf 外,还要准备好网上填写个人履历,因为部分面试官会看招聘网站的信息,而不是我们精心制作的简历。 + +每个面试官各有自己的面试风格,复习仅仅针对某一模块会吃亏,但问题总是相似的。技术面一般问的是算法、项目经历、逻辑数学题、编程语言基础及底层实现原理、计算机系统基础知识、系统设计、数据库。*注:应聘岗位为后台开发* + +算法可以通过有针对性地看书和成系统地刷题,《剑指 offer》和《编程之美》对于应对笔试和面试问题很有帮助,这类书信息密度非常大,应该耐心琢磨,不在多而在精;Leetcode 上有很多题目,部分题目是用户在真实面试中遇到并上传的,不应盲目地把所有题目刷一遍,这样不仅很耗费时间,而且重复的问题很多。应根据问题的难度(easy / medium / hard)和问题的类型来做更有针对性的练习,大部分公司对于非算法岗,算法要求都不会太高,medium 足以应对大部分笔试、面试中的问题,每天弄懂一类问题,一个月下来就 30 类,足以应付面试中常问的问题。 + +项目经历只能靠积累了,通过临时抱佛脚和造假是没有意义的,仔细一问就会露馅。有些面试官喜欢逮着项目经历问,对于项目中使用到的技术有深入的了解是很有必要的,做好对于项目能够有自己的看法,比如针对某个模块使用什么技术会更好,以及为什么。 + +逻辑数学题不仅仅出现在性格测评,而且面试官会问一些 IQ 题,我能够想起的有: + +1. 蚂蚁从杆的一端走到另一端需要 2 分钟,走到杆末尾蚂蚁会掉下;蚂蚁相遇后,在原地改变行走方向;现在杆上有无数只蚂蚁在杆上随机的位置和方向行走,问至少多长时间后,保证杆上一定没有蚂蚁。 +2. 现有一没有砝码的天平和 9 个物品,其中 1 个有毒物品比其他 8 个物品轻,其余 8 个物品重量相等,问至少经过几次测量可得知是哪个物品有毒? +3. 现有三个筐,里面分别装:苹果、橘子、苹果 + 橘子,有三张标签:苹果、橘子、苹果 + 橘子,现在三个筐上贴着的标签都是错的,每次能够从筐中取出一个水果,问取多少次能够把标签贴正确? +4. 足球比赛中,赢者积 3 分、平者积 1 分、负者积 0 分,一组内有 100 个队伍,前两名出线,问至少积分多少才能出线? + +关于编程语言,我路子走的比较野,一开始学的 Java,做过 Android 和 Java Web 开发,后面觉得人生苦短还是写 Python 吧,写了一段时间,享受过 Python 的语法糖后,觉得 Python 太慢了,它慢是因为解释型编程语言、动态类型和 GIL。后面开始写 Golang,Golang 算是奇葩,面向接口编程,内置 goroutine 和 channel,背后有 Google,在腾讯实习期间用 C++。如果再让我选择一次,我可能会选择 C++ 和 Python,因为 C++ 语法和特性十分复杂,能够把 C++ 语法、编译器优化等弄懂,后续去理解其他编程语言的思想也就不难了,而且 C++ 能够接触到比较底层的 API,对计算机系统的理解有帮助。上述提到的编程语言的岗位我都有投递,所以面试中遇到的问题比较广泛,我也没有花太多的时间去复习关于编程语言方面的知识。 + +计算机系统方面的知识需要成体系地看书,在网上搜索博客一小块一小块地看书很难整理出知识体系,而且这方面有不少经典的书籍。 + +如果对自己有实力有信心的读者请不要海投,太浪费时间精力了,对自己想去的公司有针对性地准备更好,比如了解一下他们的技术栈,这样也能在面试过程中能够问更多的问题,面试官能够更深入地问,这样可能更能够证明自己的实力。 + +在经过很多次笔试、面试后还是没有 offer 会很伤士气,有些公司在面试完两个星期后才会出结果。这时需要稳住心态,不要怕被拒绝,继续尝试。 + +## 我到底想干什么 + +应聘竞争者学历普遍比我高,很多都是研究生,找一份满意的工作真的不简单。 + +不能再随波逐流了,人生是一个多维度的竞赛,需要思考清楚自己对什么感兴趣,未来职业发展的大方向是什么。 + +身边很多同学选择计算机方面读研,去更好的学校进修。也有些同学发现自己对计算机不太感兴趣,跨专业读研,去做新的尝试,心里面很佩服这类人,“沉没成本不是成本”这句话都懂,但能做到知行合一并非易事。一些人选择做自媒体,借助今日头条、腾讯、百度等自媒体平台探索出一条新的出路。一些人选择考公务员,手握铁饭碗,未来生活质量有了一定的保证。当然永远少不了想要创业的人或从商的人。很高兴身边有形形色色的人,能够了解到更多人的想法,就像生态圈,多样性越高越稳定,社会需要有不同想法的人。 + +而我真正想要的是什么呢?这个问题花再长的时间去思考也不过分。 + +人生是一个多维度竞赛,光是在学历、薪资方面领先并不一定能做人生赢家,身体健康、幸福程度、朋友质量也都是重要的衡量标准。 + +面试中 HR 最常问的问题是你有女朋友吗?有人为了躲避 HR 后续的一系列问题会说没有,而我是真的没有。HR 问大学四年为什么不找一个女朋友呢?对于这个问题我往往没法回答。大学没谈恋爱是大学最后悔的事情了。 + +在腾讯实习的两个半月期间,做的事情我不感兴趣,令我真切地感受到如果对工作内容不感兴趣,工作是多么的无聊。想要有一定的建树,必须从事一个感兴趣的事。 + +国庆这几天,结束了秋招想要好好休息一段时间,却不知道应该干什么,想看点书,但是内心有点浮躁,最近坐的时间有点长,身体也有点受不了。运动又不能一天到晚地做,看电影又不知道有什么好片,我不大喜欢打游戏。**有一个业余爱好是多么的重要**。我最后选择的做法是到处找找朋友聊天,接下来的日子要去实习了,下次回学校可能是明年毕业答辩了,好好珍惜接下来的校园闲暇时光。 + +# End + +人生很漫长,所有当下觉得迈不过去的坎,放在若干年后回味都是小事。 diff --git a/src/md/2018-10-10-RWLock-with-CAS.md b/src/md/2018-10-10-RWLock-with-CAS.md index 8c3a94b..af374e0 100644 --- a/src/md/2018-10-10-RWLock-with-CAS.md +++ b/src/md/2018-10-10-RWLock-with-CAS.md @@ -1,241 +1,241 @@ ---- -layout: post -title: "读写锁实现" -date: 2018-10-10 20:00:05 +0800 -categories: 系统 ---- - -# In Short - -面试中被问到如何实现读写锁,个人也比较感兴趣锁是如何实现的,最近也在看《深入理解计算机系统》看到相关内容,做了一个调研,在此 mark down。 - -> 以下代码是伪代码 - -# Main - -在使用 C 语言时,我们可以采用 Mutex 来进行线程同步,那么 Mutex 是如何实现的呢? - -每次调用 lock / unlock 都会触发一个系统调用,让内核来协调多个线程的同步,具体同步方式采用 FIFO 队列,避免饥饿。根据 Linux Man Page 中提到系统调用需要上百条指令,而且如果 lock 失败了,会导致进程(线程)上下文的切换,进程上下文切换涉及到,保存、恢复寄存器值,替换进程虚拟地址映射表,消耗较大。 - -如果竞争不是很激烈,可以采用 CAS(Compare And Swap)实现用户空间锁(user-space lock),从而避免系统调用和上下文切换。CAS 是原子操作,通过 CPU 指令实现,一直尝试直到获取到锁,这种实现方式称为自旋锁。 - -类似地 CPU 提供 Test-and-Set 原子指令也可以用作用户空间锁。 - -同是原子操作,Compare-and-Swap 和 Test-and-Set 区别(伪代码形式呈现,事实上只是一条指令): - -**Compare-and-Swap:** - -```c -if (*ptr == oldVal) { *ptr = newVal; return true; } -else return false; -``` - -**Test-and-Set:** - -```c -oldVal = *ptr; *ptr = newVal; return oldVal; -``` - -需要内核协调同步的方式是引入了内核这 **单点**,像 100 号人想进入由**门卫**把控的只允许若干个人(可能不止一个,如 Semaphore)同时进入的房间一样,线程就像人,内核就像门卫。 - -使用 CAS 原子操作实现的锁,像 100 号人想要进入没门卫把控的只允许若干个人同时进入的房间,没有门卫单点,但是所有人必须打死打残才能获取进入的机会。 - -## 借助 Mutex 实现的读写锁 - -C / C++ / Java 都有关键字 volatile,这关键字的作用是使编译器每次从内存中读取值 v,而不使用寄存器中的缓存。根据计算机系统中的存储结构,值 v 会被缓存在 CPU 中的高速缓存中,高速缓存一般是有多层的,有核独占的和共享的,所以是每次从共享高速缓存中读取值 v。 - -设有以下代码: - -```c -int v = 0; -++v; -++v; -``` - -翻译成汇编的执行顺序: - -1. 加载 v 到寄存器 r 中 -2. 自增 r 中的值 -3. 自增 r 中的值 -4. 将 r 中的值写回到内存 - -如果在多线程下,每个线程都引用寄存器中的值而不是从内存取,那么值 v 将会有多个不同版本的拷贝。 - -如果加上 volatile 关键字: - -```c -volatile int v = 0; -++v; -++v; -``` - -翻译成汇编的执行顺序: - -1. 加载 v 到寄存器 r 中 -2. 自增 r 中的值 -3. 将 r 的值覆盖旧 v 的值 -4. 加载 v 到寄存器 r 中 -5. 自增 r 中的值 -6. 将 r 的值覆盖旧 v 的值 - -仅仅通过 volatile 无法保证更新一个变量线程安全,因为 加载 ==> 更新 ==> 写回 被分开了多步骤实现,也就是读取的值有可能是脏值,写回有可能覆盖另一个线程的写入内容。 - -使用 kernel 提供的 mutex 实现读写锁的伪代码: - -```c -volatile int read_cnt = 0; -mutex_t m; // 保护 read_cnt 变量 -mutex_t w; // 写锁 - -void r_lock() -{ - lock(m); - ++read_cnt; - if (read_cnt == 1) { - // first reader access write lock - lock(w); - } - unlock(m); -} - -void r_unlock() -{ - lock(m); - --read_cnt; - if (read_cnt == 0) { - // last reader release write lock - unlock(w); - } - unlock(m); -} - -void w_lock() -{ - lock(w); -} - -void w_unlock() -{ - unlock(w); -} -``` - -上述代码不是很有必要使用 volatile 保护 read_cnt 变量,因为 read_cnt 只在 r_lock / r_unlock 中访问,而且两个函数边界都是用了 lock(m) / unlock(m) 做保护,在函数执行时都会重新将值从内存加载到寄存器中。 - -设 cas 的几个接口: - -```c -bool cas_set(void *ptr, int oldval, int newval) // return success or not -int cas_inc(void *ptr) // return old value -int cas_dec(void *ptr) // return old value -``` - -使用 CAS 实现读写锁的伪代码: - -```c -int read_cnt = 0; -int write_cnt = 0; -int mutex = 1; // protector - -void r_lock() -{ - while (cas_set(&mutex, 1, 0)) - ; - ++read_cnt; - // first reader access write lock - if (read_cnt == 1) { - while (cas_set(&write_cnt, 0, 1)) - ; - } - while (cas_set(&mutex, 0, 1)) - ; -} - -void r_unlock() -{ - while (cas_set(&mutex, 1, 0)) - ; - --read_cnt; - // last reader release write lock - if (read_cnt == 0) { - while (cas_set(&write_cnt, 1, 0)) - ; - } - while (cas_set(&mutex, 0, 1)) - ; -} - -void w_lock() -{ - while (cas_set(&write_cnt, 0, 1)) - ; -} - -void w_unlock() -{ - while (cas_set(&write_cnt, 1, 0)) - ; -} -``` - -以上方案会导致写饥饿,个人感觉写的优先级应该比读要高,应该优先处理写请求,改版为: - -```c -int read_cnt = 0; -int write_cnt = 0; -int mutex = 1; // protector -volatile int write_request = 0; - -void r_lock() -{ - // if write request exist, just wait. - while (write_request == 1) - ; - while (cas_set(&mutex, 1, 0)) - ; - ++read_cnt; - // first reader access write lock - if (read_cnt == 1) { - while (cas_set(&write_cnt, 0, 1)) - ; - } - while (cas_set(&mutex, 0, 1)) - ; -} - -void r_unlock() -{ - while (cas_set(&mutex, 1, 0)) - ; - --read_cnt; - // last reader release write lock - if (read_cnt == 0) { - while (cas_set(&write_cnt, 1, 0)) - ; - } - while (cas_set(&mutex, 0, 1)) - ; -} - -void w_lock() -{ - while (cas_set(&write_request, 0, 1)) - ; - while (cas_set(&write_cnt, 0, 1)) - ; -} - -void w_unlock() -{ - while (cas_set(&write_request, 1, 0)) - ; - while (cas(&write_cnt, 1, 0)) - ; -} -``` - -# End - -对于 Java / Golang 这种由虚拟机或运行时库调度用户级线程的编程语言,实现锁并不需要操作系统介入。 - -对于不同的需求,如每次随机选出一个线程执行,先申请先获得锁等,具体实现方式也不太一样,在解决业务需求如此复杂的场景 Java JDK 内置非常丰富的实现。 +--- +layout: post +title: "读写锁实现" +date: 2018-10-10 20:00:05 +0800 +categories: 系统 +--- + +# In Short + +面试中被问到如何实现读写锁,个人也比较感兴趣锁是如何实现的,最近也在看《深入理解计算机系统》看到相关内容,做了一个调研,在此 mark down。 + +> 以下代码是伪代码 + +# Main + +在使用 C 语言时,我们可以采用 Mutex 来进行线程同步,那么 Mutex 是如何实现的呢? + +每次调用 lock / unlock 都会触发一个系统调用,让内核来协调多个线程的同步,具体同步方式采用 FIFO 队列,避免饥饿。根据 Linux Man Page 中提到系统调用需要上百条指令,而且如果 lock 失败了,会导致进程(线程)上下文的切换,进程上下文切换涉及到,保存、恢复寄存器值,替换进程虚拟地址映射表,消耗较大。 + +如果竞争不是很激烈,可以采用 CAS(Compare And Swap)实现用户空间锁(user-space lock),从而避免系统调用和上下文切换。CAS 是原子操作,通过 CPU 指令实现,一直尝试直到获取到锁,这种实现方式称为自旋锁。 + +类似地 CPU 提供 Test-and-Set 原子指令也可以用作用户空间锁。 + +同是原子操作,Compare-and-Swap 和 Test-and-Set 区别(伪代码形式呈现,事实上只是一条指令): + +**Compare-and-Swap:** + +```c +if (*ptr == oldVal) { *ptr = newVal; return true; } +else return false; +``` + +**Test-and-Set:** + +```c +oldVal = *ptr; *ptr = newVal; return oldVal; +``` + +需要内核协调同步的方式是引入了内核这 **单点**,像 100 号人想进入由**门卫**把控的只允许若干个人(可能不止一个,如 Semaphore)同时进入的房间一样,线程就像人,内核就像门卫。 + +使用 CAS 原子操作实现的锁,像 100 号人想要进入没门卫把控的只允许若干个人同时进入的房间,没有门卫单点,但是所有人必须打死打残才能获取进入的机会。 + +## 借助 Mutex 实现的读写锁 + +C / C++ / Java 都有关键字 volatile,这关键字的作用是使编译器每次从内存中读取值 v,而不使用寄存器中的缓存。根据计算机系统中的存储结构,值 v 会被缓存在 CPU 中的高速缓存中,高速缓存一般是有多层的,有核独占的和共享的,所以是每次从共享高速缓存中读取值 v。 + +设有以下代码: + +```c +int v = 0; +++v; +++v; +``` + +翻译成汇编的执行顺序: + +1. 加载 v 到寄存器 r 中 +2. 自增 r 中的值 +3. 自增 r 中的值 +4. 将 r 中的值写回到内存 + +如果在多线程下,每个线程都引用寄存器中的值而不是从内存取,那么值 v 将会有多个不同版本的拷贝。 + +如果加上 volatile 关键字: + +```c +volatile int v = 0; +++v; +++v; +``` + +翻译成汇编的执行顺序: + +1. 加载 v 到寄存器 r 中 +2. 自增 r 中的值 +3. 将 r 的值覆盖旧 v 的值 +4. 加载 v 到寄存器 r 中 +5. 自增 r 中的值 +6. 将 r 的值覆盖旧 v 的值 + +仅仅通过 volatile 无法保证更新一个变量线程安全,因为 加载 ==> 更新 ==> 写回 被分开了多步骤实现,也就是读取的值有可能是脏值,写回有可能覆盖另一个线程的写入内容。 + +使用 kernel 提供的 mutex 实现读写锁的伪代码: + +```c +volatile int read_cnt = 0; +mutex_t m; // 保护 read_cnt 变量 +mutex_t w; // 写锁 + +void r_lock() +{ + lock(m); + ++read_cnt; + if (read_cnt == 1) { + // first reader access write lock + lock(w); + } + unlock(m); +} + +void r_unlock() +{ + lock(m); + --read_cnt; + if (read_cnt == 0) { + // last reader release write lock + unlock(w); + } + unlock(m); +} + +void w_lock() +{ + lock(w); +} + +void w_unlock() +{ + unlock(w); +} +``` + +上述代码不是很有必要使用 volatile 保护 read_cnt 变量,因为 read_cnt 只在 r_lock / r_unlock 中访问,而且两个函数边界都是用了 lock(m) / unlock(m) 做保护,在函数执行时都会重新将值从内存加载到寄存器中。 + +设 cas 的几个接口: + +```c +bool cas_set(void *ptr, int oldval, int newval) // return success or not +int cas_inc(void *ptr) // return old value +int cas_dec(void *ptr) // return old value +``` + +使用 CAS 实现读写锁的伪代码: + +```c +int read_cnt = 0; +int write_cnt = 0; +int mutex = 1; // protector + +void r_lock() +{ + while (cas_set(&mutex, 1, 0)) + ; + ++read_cnt; + // first reader access write lock + if (read_cnt == 1) { + while (cas_set(&write_cnt, 0, 1)) + ; + } + while (cas_set(&mutex, 0, 1)) + ; +} + +void r_unlock() +{ + while (cas_set(&mutex, 1, 0)) + ; + --read_cnt; + // last reader release write lock + if (read_cnt == 0) { + while (cas_set(&write_cnt, 1, 0)) + ; + } + while (cas_set(&mutex, 0, 1)) + ; +} + +void w_lock() +{ + while (cas_set(&write_cnt, 0, 1)) + ; +} + +void w_unlock() +{ + while (cas_set(&write_cnt, 1, 0)) + ; +} +``` + +以上方案会导致写饥饿,个人感觉写的优先级应该比读要高,应该优先处理写请求,改版为: + +```c +int read_cnt = 0; +int write_cnt = 0; +int mutex = 1; // protector +volatile int write_request = 0; + +void r_lock() +{ + // if write request exist, just wait. + while (write_request == 1) + ; + while (cas_set(&mutex, 1, 0)) + ; + ++read_cnt; + // first reader access write lock + if (read_cnt == 1) { + while (cas_set(&write_cnt, 0, 1)) + ; + } + while (cas_set(&mutex, 0, 1)) + ; +} + +void r_unlock() +{ + while (cas_set(&mutex, 1, 0)) + ; + --read_cnt; + // last reader release write lock + if (read_cnt == 0) { + while (cas_set(&write_cnt, 1, 0)) + ; + } + while (cas_set(&mutex, 0, 1)) + ; +} + +void w_lock() +{ + while (cas_set(&write_request, 0, 1)) + ; + while (cas_set(&write_cnt, 0, 1)) + ; +} + +void w_unlock() +{ + while (cas_set(&write_request, 1, 0)) + ; + while (cas(&write_cnt, 1, 0)) + ; +} +``` + +# End + +对于 Java / Golang 这种由虚拟机或运行时库调度用户级线程的编程语言,实现锁并不需要操作系统介入。 + +对于不同的需求,如每次随机选出一个线程执行,先申请先获得锁等,具体实现方式也不太一样,在解决业务需求如此复杂的场景 Java JDK 内置非常丰富的实现。 diff --git "a/src/md/2018-10-10-\345\274\202\345\270\270\345\244\204\347\220\206.md" "b/src/md/2018-10-10-\345\274\202\345\270\270\345\244\204\347\220\206.md" index 88d0b6a..24b6742 100644 --- "a/src/md/2018-10-10-\345\274\202\345\270\270\345\244\204\347\220\206.md" +++ "b/src/md/2018-10-10-\345\274\202\345\270\270\345\244\204\347\220\206.md" @@ -1,113 +1,113 @@ ---- -layout: post -title: "异常处理实现" -date: 2018-10-10 20:00:05 +0800 -categories: 系统 ---- - -# In Short - -现在较高级的编程语言都有异常处理机制(try-catch-finally),下面探讨一下异常处理实现方法,本文说说 C 语言的异常处理机制。没错,C 语言也有异常处理机制。 - -# Main - -C++ 中的异常处理机制(**并没有 finally 区块**): - -```cpp -try { - throw exception(); -} catch (const exception &e) { - // exception handler -} catch(const other_exception &e) { - // exception handler -} -``` - -下面介绍如何使用 C 代码实现类似异常处理机制。 - -我们都知道 C 中支持 goto 语句,但是 goto 只能够在本方法内实现跳转,因为跨方法的 goto 需要保存、恢复寄存器、栈指针、PC 等信息。C 中还有两个函数 setjmp / longjmp,他们可以支持跨函数的跳转,setjmp 调用一次,可返回多次,由于降低程序的可读性,setjmp / goto 都不被推荐使用。 - -先看一段小 demo: - -```c -#include -#include - -void fn(); - -jmp_buf A; - -int main() { - switch (setjmp(A)) { - case 0: - // first call normally return 0 - fn(); - break; - case 1: - // exception 1 - printf("1\n"); - break; - case 2: - // exception 2 - printf("2\n"); - break; - default: - // default handler - printf("default\n"); - break; - } - { - // finally - printf("finally\n"); - } - return 0; -} - -void fn() { - int input; - scanf("%d", &input); - longjmp(A, input); -} -``` - -setjump 将当前栈的信息保存(save stack context,信息包括当前栈指针、PC)。在第一次调用 `setjump(A)` 时,将当前栈信息保存在 A 中,然后返回 0。下次调用 `longjmp(A, input)`,将会跳回到 `setjmp(A)` 代码处,setjump(A) 返回 input,如果 input == 0,则返回 1,只允许第一次返回 0。 - -假如在 fn() 函数中,遇到异常情况,则调用 longjmp 调转到具体的异常处理函数中,需要给每一个异常定义一个编号(1\2\3)。如果没异常发生,不调用 longjmp,让 fn 正常返回,那么就不会触发异常处理。 - -相当于我们自己实现了 try-catch,注意,C++ 并不需要支持 finally 语句,因为 finally 语句块一定会被执行的,在真实编码中 finally 多用于资源的释放,C++ 的资源管理机制是 RAII,也就是通过构造哈数和析构函数来管理资源,在当前 scope 结束时,会调用对象的析构函数,可以理解为 C++ 在代码编译中进行了埋点。 - -setjmp 尽量只用作 switch 语句的选择,不然会产生令人费解的行为,比如以下代码: - -```c -#include -#include - -void fn(); - -jmp_buf A; - -int main() { - int ret = setjmp(A); - printf("%d\n", ret); - fn(); - return 0; -} - -void fn() { - int input; - scanf("%d", &input); - longjmp(A, input); -} -``` - -这段代码会是一个无限循环,即使没有显示的 while / for 循环语句,会在标准输出流输出标准输入流的内容。同样 goto 也可以实现循环,goto 相当于无条件循环,映射为汇编语言的 jmp 指令,其实 while / for / if 编译为汇编代码后都会被转化为跳转指令。 - -编码时有异常跨越函数捕获的需求,在函数 B 中抛出的异常,在函数 A 中捕获,或异常一直到 main 函数没有被捕获就会导致整个程序退出,这又是如何实现的呢? - -每一类异常定义唯一的 id 标识,每一个 try 块都对应一个 jmp_buf,每一个 catch 块都对应一个 switch 中的一个 case,如果所有 case 都没命中,那么跳转到 default,default 继续往上抛出异常。在函数的调用栈中维护一个有异常处理(try-catch)的块的 jmp_buf,那么 default 只需要找到栈顶 jmp_buf,然后调用 `longjmp(stack_top_jmp_buf, exception_id) ` 即可甩锅给调用栈中更上层的 try-catch 块处理。 - -# End - -在 C++ 编程语言中尽可能使用 try-catch 语言级别的异常处理机制代替 setjmp / longjmp,因为 C++ 对象有析构函数,调用 longjmp 会导致析构函数无法被调用,会造成资源泄露。如果存在上述情况,编译器不允许这种行为发生,会报错(不信试试)。 - -了解高级语言中的异常处理机制对真实开发帮助不大,但是对于了解整个系统很有帮助,就像学习 Java 的同学会去了解 JVM 的垃圾回收机制,知其所以然。 +--- +layout: post +title: "异常处理实现" +date: 2018-10-10 20:00:05 +0800 +categories: 系统 +--- + +# In Short + +现在较高级的编程语言都有异常处理机制(try-catch-finally),下面探讨一下异常处理实现方法,本文说说 C 语言的异常处理机制。没错,C 语言也有异常处理机制。 + +# Main + +C++ 中的异常处理机制(**并没有 finally 区块**): + +```cpp +try { + throw exception(); +} catch (const exception &e) { + // exception handler +} catch(const other_exception &e) { + // exception handler +} +``` + +下面介绍如何使用 C 代码实现类似异常处理机制。 + +我们都知道 C 中支持 goto 语句,但是 goto 只能够在本方法内实现跳转,因为跨方法的 goto 需要保存、恢复寄存器、栈指针、PC 等信息。C 中还有两个函数 setjmp / longjmp,他们可以支持跨函数的跳转,setjmp 调用一次,可返回多次,由于降低程序的可读性,setjmp / goto 都不被推荐使用。 + +先看一段小 demo: + +```c +#include +#include + +void fn(); + +jmp_buf A; + +int main() { + switch (setjmp(A)) { + case 0: + // first call normally return 0 + fn(); + break; + case 1: + // exception 1 + printf("1\n"); + break; + case 2: + // exception 2 + printf("2\n"); + break; + default: + // default handler + printf("default\n"); + break; + } + { + // finally + printf("finally\n"); + } + return 0; +} + +void fn() { + int input; + scanf("%d", &input); + longjmp(A, input); +} +``` + +setjump 将当前栈的信息保存(save stack context,信息包括当前栈指针、PC)。在第一次调用 `setjump(A)` 时,将当前栈信息保存在 A 中,然后返回 0。下次调用 `longjmp(A, input)`,将会跳回到 `setjmp(A)` 代码处,setjump(A) 返回 input,如果 input == 0,则返回 1,只允许第一次返回 0。 + +假如在 fn() 函数中,遇到异常情况,则调用 longjmp 调转到具体的异常处理函数中,需要给每一个异常定义一个编号(1\2\3)。如果没异常发生,不调用 longjmp,让 fn 正常返回,那么就不会触发异常处理。 + +相当于我们自己实现了 try-catch,注意,C++ 并不需要支持 finally 语句,因为 finally 语句块一定会被执行的,在真实编码中 finally 多用于资源的释放,C++ 的资源管理机制是 RAII,也就是通过构造哈数和析构函数来管理资源,在当前 scope 结束时,会调用对象的析构函数,可以理解为 C++ 在代码编译中进行了埋点。 + +setjmp 尽量只用作 switch 语句的选择,不然会产生令人费解的行为,比如以下代码: + +```c +#include +#include + +void fn(); + +jmp_buf A; + +int main() { + int ret = setjmp(A); + printf("%d\n", ret); + fn(); + return 0; +} + +void fn() { + int input; + scanf("%d", &input); + longjmp(A, input); +} +``` + +这段代码会是一个无限循环,即使没有显示的 while / for 循环语句,会在标准输出流输出标准输入流的内容。同样 goto 也可以实现循环,goto 相当于无条件循环,映射为汇编语言的 jmp 指令,其实 while / for / if 编译为汇编代码后都会被转化为跳转指令。 + +编码时有异常跨越函数捕获的需求,在函数 B 中抛出的异常,在函数 A 中捕获,或异常一直到 main 函数没有被捕获就会导致整个程序退出,这又是如何实现的呢? + +每一类异常定义唯一的 id 标识,每一个 try 块都对应一个 jmp_buf,每一个 catch 块都对应一个 switch 中的一个 case,如果所有 case 都没命中,那么跳转到 default,default 继续往上抛出异常。在函数的调用栈中维护一个有异常处理(try-catch)的块的 jmp_buf,那么 default 只需要找到栈顶 jmp_buf,然后调用 `longjmp(stack_top_jmp_buf, exception_id) ` 即可甩锅给调用栈中更上层的 try-catch 块处理。 + +# End + +在 C++ 编程语言中尽可能使用 try-catch 语言级别的异常处理机制代替 setjmp / longjmp,因为 C++ 对象有析构函数,调用 longjmp 会导致析构函数无法被调用,会造成资源泄露。如果存在上述情况,编译器不允许这种行为发生,会报错(不信试试)。 + +了解高级语言中的异常处理机制对真实开发帮助不大,但是对于了解整个系统很有帮助,就像学习 Java 的同学会去了解 JVM 的垃圾回收机制,知其所以然。 diff --git "a/src/md/2019-05-22-\346\234\254\347\247\221\346\257\225\344\270\232.md" "b/src/md/2019-05-22-\346\234\254\347\247\221\346\257\225\344\270\232.md" new file mode 100644 index 0000000..ba50f6d --- /dev/null +++ "b/src/md/2019-05-22-\346\234\254\347\247\221\346\257\225\344\270\232.md" @@ -0,0 +1,27 @@ +--- +layout: post +title: "本科毕业" +date: 2019-05-22 21:00:05 +0800 +categories: 总结 +--- + +# In short + +> 这篇文章具体想表达什么我也不清楚,我只知道我想输出。 + +大半年没有文章输出了。 +在字节跳动实习了半年,体验到了相对腾讯不一样的开发体验,整个组织更加年轻,富有活力,不断地吸收新鲜血液。实习期间,立下flag,每周输出一篇博客,后来因为懒惰、毕业设计等原因,计划没有得到执行。回到学校,去了一趟毕业旅行,拍毕业照,写毕业论文。从图书馆借《会计学原理》,才看了不到50页。想学习一下[boltdb](https://github.com/boltdb/bolt)和golang runtime实现开源代码,刚开始看又停了;看了好几本书,感觉挺有意思的,特别是《文明之光》;自学蛙泳,掌握正确的方法,做事情会简单很多;想在接下来的日子好好锻炼身体,没想到又病了。这段时间开始了很多事情,但真正结束并输出的不多,让我想起了一句话,**重要的是结束了多少,而不是开始了多少**。接下来的日子要将它们一一收尾。 + +属于我的毕业季来了,离别了旧的组织,拥抱新的组织。对我来说毕业季并不悲伤,借助发达的通信技术、完善的交通设置,友谊、爱情不畏惧距离,但时间依然是一把锋利的杀猪刀。过去一年几乎都在深圳实习,独自租房,习惯了长期一个人的生活,突然回到学校有些不适应集体生活。 + +过去一年社会舆论对贸易战从来没有消停过,我是乐观主义者,无论媒体描述地贸易战多么惨烈,我也相信中国能平稳过渡。一开始看不懂华为要自研芯片、操作系统,这两事多折腾,需要花费大量人力物力去研究,兼容现有的生态,甚至打造新的生态圈,直到战鼓打响。短期来看,我们会精力阵痛,长期来看,我们降低了对国外引入的核心技术的依赖,企业和消费者有更多的选择。由心佩服富有远见的企业家。 + +接下来又要解决租房问题了,一大波应届毕业生同时毕业,涌入各大城市,供需决定价格,租金又要上涨一点。租房需要在十几分钟内决定未来一年的选择,信息是极度不平衡的,甚至不知道邻居是怎样的人,毁约成本高。上次租房,楼上有个小孩早上喜欢砸地板,碰巧楼隔音差,每早6点多被吵醒,体验非常差,和家长沟通过几次,最后还被他们骂了一句,还让不让人活了。**熊孩子+熊家长** + +已经有20多天没有写代码了,一个月前某些觉得需要抽空学习的技术,现在觉得这多折腾,学而不用则废,有时间还不如抽空了解一下其他专业的知识,开阔一下视野,提升一下理财等能力,做更有趣的人。 + +简单地了解奢侈品市场,感觉挺有意思的。奢侈品往往质量更优,品牌宣传更好,各种服务更好,外观更美,贵的东西还是有它的道理,贵的东西除了贵,其他都好。 + +之前将云计算想象得太hack了,没想到大多数人还是写业务逻辑,我开始思考写业务难道就不是技术的一部分吗?写业务逻辑不就是为公司创造价值和体现个人价值吗?现在各个公司都大面积地拥抱开源,现在一想,感觉真正技术可能在开源社区。后续还得反复思考的一个问题是做偏技术还是偏业务的方向。做技术是一个较纯粹的过程,需求排期相对稳定,可控性更高,长期从事一个方向可能会让视野变窄;做业务需求依赖当前产品进度,相对灵活,业务发展得快会有special bonus。这是个艰难的选择。 + +即将离开大学,如何发展自己的职业生涯是一个大问题,所幸我选择的专业是软件工程,就业前景相对明朗。我理解的职业生涯并不仅仅是就业,或许我的职能还太低或我做事情的方式不对,做事情大多是别人安排的,自主能动性不高,事情往往不是自己真正想做的,我不太喜欢这种状态,我也渴望拥有话语权。 \ No newline at end of file diff --git "a/src/md/interview/\345\274\200\346\224\276\346\200\247\351\227\256\351\242\230.md" "b/src/md/interview/\345\274\200\346\224\276\346\200\247\351\227\256\351\242\230.md" index f3c17a7..72cf00a 100644 --- "a/src/md/interview/\345\274\200\346\224\276\346\200\247\351\227\256\351\242\230.md" +++ "b/src/md/interview/\345\274\200\346\224\276\346\200\247\351\227\256\351\242\230.md" @@ -1,58 +1,58 @@ -# 开发型问题 - -此处记录一些在面试中可能会遇到的开放性问题,包括面试官问面试者 or 面试者问面试官。 - -像刚哥教我的,说什么、做什么背后都要有为什么?比如跟 leader 说想要升职,那为什么提拔我,必须说服别人。 - -## 面试官问面试者 - -### 说说你的学习能力 - -主要围绕 **积极性、深度、广度、方法** - -1. 积极性:从大一开始自学编程,主要通过图书馆借书和慕课网两个学习渠道;大二申请加入了实验室,跟着学长开始学习 Web 后台开发;在大二下学期去老师公司实习 -2. 深度:在 Web 后台开发方面有比较深的了解,熟悉 Python / Golang,在腾讯公司实习期间,使用 C++ 改造腾讯游戏接口框架,熟悉 MySQL、Redis 数据库,通过阅读《大型网站架构》、《阿里巴巴中台服务》、《淘宝这十年》等技术书籍,对大型网站的架构有一定的了解,有两年的 Linux 使用经验,能够独自承担 Web 后台开发任务。 -3. 广度:在技术方面,除了 Web 后台开发,我还在 coursera 平台上学习过吴恩达《机器学习》方面的课程,通过阅读网上开源文档对比特币、以太坊、联盟链有所了解。在兴趣爱好方面,在得到APP方面订阅了《薛兆丰的北大经济学课》、《宁向东的清华管理学科》、《生命科学50讲》等,坚持每周学习 5 天以上。 -4. 方法:先打好基础,后面打算深入看《算法导论》、《深入理解计算机系统》等基础且经典的书籍;平时会通过 Github、博客、公众号等渠道了解技术走向;在开发中遇到问题时,首先会 Google,如果找不到满意的回答,会选择查阅官方文档,实在无法解决问题再请教身边或网上在这方面有研究的同学。 - -在腾讯实习期间,从 0 开始学习组内使用的技术,使用中心通用的技术栈搭建一个服务模型,后面有机会参与到 IDIP 框架的改造,我负责 HTTPS 介入、连接池构建、二进制加密、签名认证、框架性能热点定位等,很遗憾由于组内没有留用名额,没有拿到留用。 - -### 谈一下自身优缺点 - -如何进行优点、缺点衔接是一个很重要的技巧,先说优点还是先说缺点呢?这些都是值得斟酌的,为表谦虚先说说缺点,介绍的缺点最好最后都能够绕回来,比如我不帅,但我很幽默,介绍时候最好带点真实生活的例子。 - -缺点:表达能力不好,首先是我的普通话比较烂,其次是我内心害怕公共演讲,最后我书面表达能力欠缺,比较容易口语化。没说好是因为没写好,没写好是因为没想好。为了提高自身表达能力,我和朋友搭建了博客平台网站,在日常学习中,我会尝试着写总结、学习中遇到的问题、学到的新东西相关的文章,虽然当前网站没太多人访问,但是我相信坚持写对我的表达能力会有所提升,博客访问量也会慢慢上去的。 - -优点:如果我认为一件事情是有价值的,我会坚持做下去。比如运动健身、在得到 APP 上订阅课程、阅读经典书籍和善待家人朋友,我已经坚持健身两年了,每周至少运动 5 天,在得到 APP 完成了《薛兆丰的北大经济学科》、《生命科学 50 讲》的学习,每个人的时间都是有限的,怎么使时间产生最大的价值就需要借助复利和开阔视野。 - -### 职业生涯规划 - -我想成为一名企业家。马洛斯需求层次将需求分为 5 层,最顶层是自我实现,我的梦想是成为一名企业家。 - -作为一名刚刚毕业的大学生,可支配的资源非常匮乏,无论是资金、人脉、自身能力上都需要积累。我想在一个靠谱的团队中成长,踏踏实实地工作学习,在未来有机会我会尝试创业或加入创业团队。 - -### 觉得最适合做什么 - -运动员。这是我最喜欢的事情,如果时间允许,我会每天抽空运动。 - -### 最难忘的事情 - -有一次奶奶生病,我在医院里面照顾她。奶奶经常在客人面前提起这件事,夸我孝顺。 - -不是被人夸让我觉得难忘,而是我懂得了家人需要的陪伴是多么简单,并不需要到出人头地才报答,有时间应该多回家陪伴家人。 - -### 最有成功感的事情 - -没想到。 - -## 面试者问面试官 - -在面试末尾,面试官通常会问:“你有什么想问的吗?” - -推荐问题: - -1. 组内使用的技术栈 -2. 组内从事的业务 -3. 如果是总监面或者 HR 面可以问问关于员工职业上升通道等问题 -4. 亚马逊等公司在推 DevOps,某些公司开发、测试、运维有明确分工,当前组内的组织架构 -5. 健身房等基础设施 +# 开发型问题 + +此处记录一些在面试中可能会遇到的开放性问题,包括面试官问面试者 or 面试者问面试官。 + +像刚哥教我的,说什么、做什么背后都要有为什么?比如跟 leader 说想要升职,那为什么提拔我,必须说服别人。 + +## 面试官问面试者 + +### 说说你的学习能力 + +主要围绕 **积极性、深度、广度、方法** + +1. 积极性:从大一开始自学编程,主要通过图书馆借书和慕课网两个学习渠道;大二申请加入了实验室,跟着学长开始学习 Web 后台开发;在大二下学期去老师公司实习 +2. 深度:在 Web 后台开发方面有比较深的了解,熟悉 Python / Golang,在腾讯公司实习期间,使用 C++ 改造腾讯游戏接口框架,熟悉 MySQL、Redis 数据库,通过阅读《大型网站架构》、《阿里巴巴中台服务》、《淘宝这十年》等技术书籍,对大型网站的架构有一定的了解,有两年的 Linux 使用经验,能够独自承担 Web 后台开发任务。 +3. 广度:在技术方面,除了 Web 后台开发,我还在 coursera 平台上学习过吴恩达《机器学习》方面的课程,通过阅读网上开源文档对比特币、以太坊、联盟链有所了解。在兴趣爱好方面,在得到APP方面订阅了《薛兆丰的北大经济学课》、《宁向东的清华管理学科》、《生命科学50讲》等,坚持每周学习 5 天以上。 +4. 方法:先打好基础,后面打算深入看《算法导论》、《深入理解计算机系统》等基础且经典的书籍;平时会通过 Github、博客、公众号等渠道了解技术走向;在开发中遇到问题时,首先会 Google,如果找不到满意的回答,会选择查阅官方文档,实在无法解决问题再请教身边或网上在这方面有研究的同学。 + +在腾讯实习期间,从 0 开始学习组内使用的技术,使用中心通用的技术栈搭建一个服务模型,后面有机会参与到 IDIP 框架的改造,我负责 HTTPS 介入、连接池构建、二进制加密、签名认证、框架性能热点定位等,很遗憾由于组内没有留用名额,没有拿到留用。 + +### 谈一下自身优缺点 + +如何进行优点、缺点衔接是一个很重要的技巧,先说优点还是先说缺点呢?这些都是值得斟酌的,为表谦虚先说说缺点,介绍的缺点最好最后都能够绕回来,比如我不帅,但我很幽默,介绍时候最好带点真实生活的例子。 + +缺点:表达能力不好,首先是我的普通话比较烂,其次是我内心害怕公共演讲,最后我书面表达能力欠缺,比较容易口语化。没说好是因为没写好,没写好是因为没想好。为了提高自身表达能力,我和朋友搭建了博客平台网站,在日常学习中,我会尝试着写总结、学习中遇到的问题、学到的新东西相关的文章,虽然当前网站没太多人访问,但是我相信坚持写对我的表达能力会有所提升,博客访问量也会慢慢上去的。 + +优点:如果我认为一件事情是有价值的,我会坚持做下去。比如运动健身、在得到 APP 上订阅课程、阅读经典书籍和善待家人朋友,我已经坚持健身两年了,每周至少运动 5 天,在得到 APP 完成了《薛兆丰的北大经济学科》、《生命科学 50 讲》的学习,每个人的时间都是有限的,怎么使时间产生最大的价值就需要借助复利和开阔视野。 + +### 职业生涯规划 + +我想成为一名企业家。马洛斯需求层次将需求分为 5 层,最顶层是自我实现,我的梦想是成为一名企业家。 + +作为一名刚刚毕业的大学生,可支配的资源非常匮乏,无论是资金、人脉、自身能力上都需要积累。我想在一个靠谱的团队中成长,踏踏实实地工作学习,在未来有机会我会尝试创业或加入创业团队。 + +### 觉得最适合做什么 + +运动员。这是我最喜欢的事情,如果时间允许,我会每天抽空运动。 + +### 最难忘的事情 + +有一次奶奶生病,我在医院里面照顾她。奶奶经常在客人面前提起这件事,夸我孝顺。 + +不是被人夸让我觉得难忘,而是我懂得了家人需要的陪伴是多么简单,并不需要到出人头地才报答,有时间应该多回家陪伴家人。 + +### 最有成功感的事情 + +没想到。 + +## 面试者问面试官 + +在面试末尾,面试官通常会问:“你有什么想问的吗?” + +推荐问题: + +1. 组内使用的技术栈 +2. 组内从事的业务 +3. 如果是总监面或者 HR 面可以问问关于员工职业上升通道等问题 +4. 亚马逊等公司在推 DevOps,某些公司开发、测试、运维有明确分工,当前组内的组织架构 +5. 健身房等基础设施