hello2dj

if you can't explain it simply, you don't understand it well enough

用来angular的体系来写node之nestjs简介

先上图

这几年前端发展的很快,出现了很多优秀的框架。例如Angular2, React, Vue等他们大大提升了开发者的生产效率,以及快速创建可测试化,可扩展的前端应用。但是在server端nodejs到没有出现如此的框架都是一些基础的框架,工具等等,虽然目前有eggjs,thinkjs,私以为他们架构成熟性还有待进步,他依然也只是提供了一些工具和方法等,不过egg还是要胜think一筹的(都是我瞎掰的),当然他们也都在不同程度上的解决了一些初步的架构问题。

这里我看到了一个新近的框架nestjs(基于express),他提供了一个开箱即用的架构体系,是啥样的架构体系呢?angular2的架构体系,可以说几乎是一样的架构体系,照着搬过来了,不过可以想一想用angular2的前端架构来写后端代码,也是一种酸爽!(typescript)在我看看来就是一个nodejs中ROR,flask或者是django。

概念普及

  • typescript js的超集,提供强类型校验,以及es6,7等js特性
  • express 一个node, httpserver,提供了最基础的框架如路由等。
  • 依赖注入,angular里面有个重要的概念就是依赖注入,nestjs中也是大量使用,有一个IOC容器。这里nest的注入和angular2的是一样的,注入可以是一个class, 也可以是一个值,可以是一个function等等不过不是一个class的就得用如下的方式

    1
    2
    3
    4
    5
    {
    provide: 'PhotoRepositoryToken',
    useFactory: (connection: Connection) => connection.getRepository(Photo),
    inject: ['DbConnectionToken'],
    }

    其中inject是这个注入值要使用的依赖注入,provide是提供给其他人要使用的注入标识符,useFactory代表要执行的方法,也可以是useValue,此时是一个普通对象即可。还有循环依赖,此时就要使用forwardRef了,就不细讲了,大家可以具体参见

  • 装饰器,要是写过angular2,或者flask等就会知道,都是一堆@name堆起来的了的

  • @Controller
    处理req的handler的。

    1
    2
    3
    4
    5
    6
    7
    @Controller('cats')
    export class CatsController {
    @Get()
    findAll(@Req() user) {
    return [];
    }
    }
  • @Component
    就是service

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @Component()
    export class CatsService {
    private readonly cats: Cat[] = [];
    create(cat: Cat) {
    this.cats.push(cat);
    }
    }
    // 使用
    export class CatsController {
    constructor(private readonly catsService: CatsService) {} // 注入

    @Post()
    async create(@Body() createCatDto: CreateCatDto) {
    this.catsService.create(createCatDto);
    }
    }
  • @Module
    在angualr中代码是以module为粗粒度单元进行组织的,就是rootModule包含其他module比如orderModule, reportModule, userModule,等等,

    1
    2
    3
    4
    @Module({
    imports: [CatsModule, OrderModule, ReportModule, UserModule],
    })
    export class ApplicationModule {}
  • @Middleware
    同理与express的middleware,只是写法有变化,需要包裹一下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Middleware()
    export class LoggerMiddleware implements NestMiddleware {
    resolve(...args: any[]): ExpressMiddleware {
    return (req, res, next) => {
    console.log('Request...');
    next();
    };
    }
    }
  • @Filter 和 HttpException
    用来处理handler执行过程产生exception或者说错误(但是异常和错误应当是区分开来的),Filter是区分全局和路由级的。 HttpException是nest提供的错误类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @Post()
    async create(@Body() createCatDto: CreateCatDto) {
    throw new HttpException({
    status: HttpStatus.FORBIDDEN,
    error: 'This is a custom message',
    });
    }
    /**
    {
    "status": 403,
    "error": "This is a custom message"
    }
    */

    expecptions的体系是可以扩展的,当然nest还提供很多的类型,如BadRequestExceptio, UnauthorizedExceptio等等

    1
    2
    3
    4
    5
    export class ForbiddenException extends HttpException {
    constructor() {
    super('Forbidden', HttpStatus.FORBIDDEN);
    }
    }

    @Filter的正式称呼应当是Exception Filters。这次望文生义是正确的。是的就是处理Exception的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    @Catch(HttpException)
    export class HttpExceptionFilter implements ExceptionFilter {
    catch(exception: HttpException, response) {
    const status = exception.getStatus();

    response
    .status(status)
    .json({
    statusCode: status,
    message: `It's a message from the exception filter`,
    });
    }
    }
    // 使用
    @Post()
    @UseFilters(new HttpExceptionFilter())
    async create(@Body() createCatDto: CreateCatDto) {
    throw new ForbiddenException();
    }

    // 全局的
    async function bootstrap() {
    const app = await NestFactory.create(ApplicationModule);
    app.useGlobalFilters(new HttpExceptionFilter());
    await app.listen(3000);
    }
    bootstrap();
  • @Guards
    用来决定请求是否要被handler处理,典型就是权限判断

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Guard()
    export class RolesGuard implements CanActivate {
    canActivate(dataOrRequest, context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
    return true;
    }
    }
    // 使用
    @Controller('cats')
    @UseGuards(RolesGuard)
    export class CatsController {}

    我们可以自定义一些装饰器,给controller添加必要属性以供guard来使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    // roles Deacator
    export const Roles = (...roles: string[]) => ReflectMetadata('roles', roles);
    // 使用
    @Post()
    @Roles('admin')
    async create(@Body() createCatDto: CreateCatDto) {
    this.catsService.create(createCatDto);
    }

    // Roles guards 改写
    @Guard()
    export class RolesGuard implements CanActivate {
    constructor(private readonly reflector: Reflector) {}

    canActivate(req, context: ExecutionContext): boolean {
    const { parent, handler } = context;
    const roles = this.reflector.get<string[]>('roles', handler);
    if (!roles) {
    return true;
    }

    const user = req.user;
    const hasRole = () => !!user.roles.find((role) => !!roles.find((item) => item === role));
    return user && user.roles && hasRole();
    }
    }
    // 使用同上
  • @Pipe
    使用来处理参数校验以及参数类型转换的,当然nest也提供了很多的内置pipe,
    参数解释一下,可能不太好看,value,是传进来的值,ArgumentMetadata的属性包含: type: 参数通过什么方式传进来的(body, query, param等等),metatype: 传进来的参数是啥类型,string, number .etc, data: 这个没搞太明白文档上说的是‘The string passed to the decorator, for example @Body(‘string’)’还没参悟处来。。。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @Pipe()
    export class ValidationPipe implements PipeTransform<any> {
    transform(value: any, metadata: ArgumentMetadata) {
    return value;
    }
    }
    // 使用
    @Post()
    // @UsePipes(new ValidationPipe())
    async create(@Body(new ValidationPipe()) createCatDto: CreateCatDto) {
    this.catsService.create(createCatDto);
    }
  • @Interceptor
    按照文档的说法就是受到AOP(面向切面编程)编程方式的启发。

  1. 在方法执行前后增加额外的逻辑(类似于koa中中间件的执行方式)
  2. 有了1,所以我们可以转换执行结果
  3. 转换执行时的异常
  4. 重写执行逻辑(比如根据缓存返回结果)
    这个例子太多了,就不贴代码了可以具体参见
  • 可以自定义装饰器,在guard里面我们已经见到过了。

  • nest还集成了graphql, websockets, microservice, 微服务这部分他提供两种通信方式,redis(pub/sub), tcp等等

    目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
app
-- modules
-- reports
-- report.controller.ts
-- report.service.ts
-- report.entity.ts
-- report.interface.ts
-- dto(data transfer object)
-- report.dto.ts(推荐class)
-- orders
...
-- common
-- db.provice.ts
...

实践一把

  1. git clone https://github.com/nestjs/typescript-starter
  2. npm install
  3. npm run start
  4. 看看实例代码就ok了。

最后 项目地址

nest项目也提供了很多的example, 总体来看写起来也还是很舒服的。集成了很多东西,点赞,希望下一个项目可以使用。

感悟

总会觉得自己不知道该怎么去更进一步的学习,一开始使用就觉得纯用express有很多问题,可是没有去思考怎么才能更好,总觉得见见世面,看看优秀的人都是怎么写的,可是,看完就完了,却没有想本项目做一样把看到的总结起来。不想动手写业余项目,因为就是觉得自己懂的太少了,还是得多看看,可是只看又有什么用呢?ps: 看的结果就是写出了屎一样的代码,还得努力啊!(有看的欢迎多多交流dj_amazing@sina.com

Oberservable之“hot”和“cold”的区别

我又要先上图了:

原文地址(english, 需翻墙)

TL;DR: 当你不想一次又一次的创建你的producer时,你需要一个HOT observable

COLD 就是你的Observable创建了生产者(就是当你创建observable时创建producer)

1
2
3
4
5
// COLD
const cold = new Observable((observer) => {
const producer = new Producer();
// have observer listen to producer here
});

HOT 就是你的observable 关闭你的生产者

1
2
3
4
5
// HOT
const producer = new Producer();
cosnt hot = new Observable((observer) => {
// have observer listen to producer here
})

继续深入

我的上篇文章介绍了observables就是函数。那篇文章的目的是解开observerable的神秘,但是没有深入那个困扰大家的问题:HOT VS COLD

Observables are just functions!

Observables 就是一个绑定observer和producer的函数。是的,那就是全部。他其实不需要创建producer,他们仅仅是建立observer对producer的监听,并且返回一个函数可以用来移除监听器。对obserable的调用就好像是在调用一个函数,并且传递一个observer

那么什么是producer?

生产者就是你的observable的数据来源。他可能是一个web socket, 或者是一个DOM event, 又或者是一个迭代器,甚至有可能是对一个数组的循环。总之,他可以是任何东西,可以用来获取数据并且传递给observer.next(value)

Cold observables: 生产者在内部创建

如果一个observable的producer是在subscribe时创建并且激活的那么他就是 ‘cold’。这意味着,如果observables是函数,那么生产者就是在调用这个函数是创建和激活的。

  1. creates the producer
  2. activates the producer
  3. start listening to the producer
  4. unicast

下面这个栗子就是‘cold’, 因为他对websocket的监听是在你subscribe observable时建立的:

1
2
3
4
5
const source = new Observable((observer) => {
const socket = new WebSocket('ws://someurl');
socket.addEventListener('message', (e) => observer.next(e));
return () => socket.close();
});

因此任何subscribe source的对象,都会有自己的WebSocket的实例,并且当他unsubscribe的时候,他将会关闭那个socket。这意味着我们source仅仅只是unicast,因为这个生产者仅仅只能给一个监听者发送数据。这里有一个简单的栗子jsbin

HOT observables: 生产者是在外部创建的

如果一个observable的生产者是在订阅之外(就是不是在订阅时产生的行为)创建或者是激活的那么他就是’hot’。

  1. 共享对生产者的引用
  2. 开始监听生产者
  3. 多播(通常是)
    如果我们把上面的栗子中对于WebSocket的创建挪到obserable的外面,那么他就是hot了
    1
    2
    3
    4
    const socket = new WebSocket('ws://someurl');
    const source = new Observable((observer) => {
    socket.addEventListener('message', (e) => observer.next(e));
    });

现在任何订阅source的对象都共享一个相同的WebSocket实例。他将会高效的多播数据到所有的订阅者。但是这里还有个小问题:我们的obserable没有了对socket的取消逻辑。这意味着一旦发生了错误或者结束后,甚至是取消订阅,我们都无法关闭socket。所以我们真正想要的是让我们的’cold’ observable 变成hot。这里有个例子就没有取消的逻辑jsbin

我们为什么需要 ’hot‘ observable?

从第一个cold observable的列子我可以看到如果所有的observable都是cold是会有一些问题的。比如,你不止一次的订阅一个observable,但是他有可能每次都创建了一些稀缺资源,好比 web socket connection, 可事实是你并不想创建很多的web socket连接。事实上你可能很容易的对一个observable创建很多的订阅,并且是在你没意识的情况下。假设我们需要从web socket的订阅中过滤出奇数和偶数,于是最终我们可能写出如下的代码:

1
2
3
4
source.filter(x => x % 2 === 0)
.subscribe(x => console.log('even', x));
source.filter(x => x % 2 ==== 1)
.subscribe(x => console.log('odd', x));

Rx 的subjects

在我们把’cold’ observable 变得 ’hot‘之前,我们需要先介绍一个新的类型:Rx Subject。他有如下特性:

  1. 他是一个obserable.他的结构类似一个observable, 并且用相同的操作符
  2. 他是一个observer, 他是一个鸭子类型的observer。当他订阅一个observable是,他会像一个observer那样把数据传递给’next‘方法
  3. 他是多播的。所有通过’subscribe‘犯法订阅的observers都会加入到一个内部的observers list
  4. 当他结束的时候就是真的结束了(意味着结束后不可以在重用)。无论是发生了unsubscribe(这我试出来了见下代码)或者complete, 亦或者是发生了error之后,Subjects是不可以再重用的。(When it’s done, it’s done. Subjects cannot be reused after they’re unsubscribed, completed or errored.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // The death of a Subject
    const subject = new Subject();
    subject.subscribe(x => console.log(x));
    subject.next(1); // 1
    subject.next(2); // 2
    subject.complete();
    subject.next(3); // silently ignored
    subject.unsubscribe();
    subject.next(4); // Unhandled ObjectUnsubscribedError
  5. 当你通过他自身传递数据时,他会进行#2,就是obser那一套。又若是你通过next传递数据,他又会表现出他observable的一面。

之所以称之为’subject‘是因为上述第三点。 在四人帮的设计模式里,’Subjects‘是一个拥有’addObserver‘方法的类。在这个例子里面,’addObserver‘方法是’subscribe‘,这里有一个jsbin的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const { Subject } = Rx;

const subject = new Subject();

// you can subscribe to them like any other observable

subject.subscribe(x => console.log('one', x), err => console.error('one', err));
subject.subscribe(x => console.log('two', x), err => console.error('two', err));
subject.subscribe(x => console.log('three', x), err => console.error('three', err));


// and you can next values into subjects.
// NOTICE: each value is sent to *all* subscribers. This is the multicast nature of subjects.

subject.next(1);
subject.next(2);
subject.next(3);

// An error will also be sent to all subscribers
subject.error(new Error('bad'));


// NOTICE: once it's errored or completed, you can't send new values into it
try {
subject.next(4); //throws ObjectUnsubscribedError
} catch (err) {
console.error('oops', err);
}

使一个 ’COLD‘ Observable ‘HOT’

当我们使用了Subject,我们可以使用一些函数式编程方法来使一个 ’cold‘ Observable ‘hot’:

1
2
3
4
5
6
7
function makeHot(cold) {
const subject = new Subject();
cold.subscribe(subject);
return new Observable((observer) => subject.subscribe(observer));
}

// 整体流程就是:observer -> subject -> cold(依次订阅下一个observable)

我们的makeHot方法可以接收任何的cold observable并且可以通过创建一个共享的subject。这里有个例子jsbin

但是我们仍然有一些小的问题,因为,我们仍然没有跟踪我们源observable的subscription(就是cancel函数),因此当我们想要取消的时候,我们该怎么取消呢?我们可以通过添加引用计数来解决这个问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function makeHotRefCounted(cold) {
const subject = new Subject();
const mainSub = cold.subscribe(subject);
let refs = 0;
return new Observable((observer) => {
refs++;
let sub = subject.subscribe(observer);
return () => {
refs--;
if (refs === 0) mainSub.unsubscribe();
sub.unsubscribe();
}
});
}

这样我们就有了unsubscribe函数了。jsbin

在Rxjs中,使用’publish()‘或者’share()‘

你应当使用publish或者share而不是上面造的makeHot。 有很多种方法可以是cold变为hot, 并且在Rx中有很多种高效简洁的方法来实现。

在rxjs 5中,share操作符,可以是cold变成hot,以及使用引用计数的observable。并且这个observable还可以重试当他失败或者成功。因为当他错误,完成或者取消订阅以后,subjects就不可以重新使用了,于是share()操作符会重新回收死掉的subjects并且在生成一个新的subject,使得我们可以重新订阅。
这里有一个栗子jsbin
经过尝试错误,取消订阅是可以的重新订阅的。

The “Warm” Observable

Given everything stated above, one might be able to see how an Observable, being that it’s just a function, could actually be both “hot” and “cold”. Perhaps it observes two producers? One it creates and one it closes over? That’s probably bad juju, but there are rare cases where it might be necessary. A multiplexed web socket for example, must share a socket, but send its own subscription and filter out a data stream.

温暖的 observable自己看吧

’hot‘和’cold‘都是针对生产者来说的

当你是用shared 引用来关闭producer,那么他是hot。 如果你是在你的observable中创建生产者那么他是cold,若是两者都做,那么我猜他是’warm‘吧!

hot Observable通常是多播的,但若是producer一次只提供一个监听器数据,此时再说他是多播的就会有些模糊了。

http2

  • 支持请求与响应的多路复用来减少延迟。

  • 压缩 HTTP 首部字段将协议开销降至最低。

  • 增加对请求优先级和服务器端推送的支持。

    同时由于考虑到庞大的 HTTP1.1 协议用户,所以 HTTP 方法、状态码、URI 及首部字段,等核心概念保持不变,也就是当前正在运行的网站不用做任何改变即可在 HTTP2 协议上运行。

  • 二进制分帧层:

  • 流、消息和帧:

    二进制分帧机制改变了客户端与服务器之间交互数据的方式,涉及到以下几个重要的概念:

  1. 流:已建立的 TCP 连接上的双向字节流,逻辑上可看做一个较为完整的交互处理单元,即表达一次完整的资源请求-响应数据交换流程;一个业务处理单元,在一个流内进行处理完毕,这个流生命周期完结。

  2. 消息:由一个或多个帧组合而成,例如请求和响应。

  3. 帧:HTTP2 通信的最小单位,每个帧包含帧首部,至少也会标识出当前帧所属的流。所有 HTTP2 通信都在一个连接上完成,此连接理论上可以承载任意数量的双向数据流。相应地,每个数据流以消息的形式发送,而消息由一或多个帧组成,这些帧可以乱序发送,然后再根据每个帧首部的流标识符重新组装。图示如下:

    HTTP2 的所有帧都采用二进制编码,所有首部数据都会被压缩。上图只是演示数据流、消息和帧之间的关系,而非实际传输时的编码结果。

  • 多向请求与响应:

    在 HTTP1.1 中,如果想使用多个并行 request 请求,必须多开 TCP 连接,但是一个域名对同一个浏览器客户端是有数量限制的(6 个左右),同时,每一个连接中的响应是按照顺序排队进行的,容易导致队头堵塞。

二进制分帧层实现了多向请求和响应,客户端和服务器可以把 HTTP 消息分解为互不依赖的帧,然后乱序发送,最后再在另一端把它们重新组合起来。图示如下:

由上图可以看出,同一个 TCP 连接可以传输多个数据流,并且服务器到客户端方向有多个数据流,流是一个逻辑信道,所以属于它的帧可以乱序发送,最后再根据标记组合起来即可。把 HTTP 消息分解为独立的帧,交错发送,然后在另一端重新组装是 HTTP2 最重要的改进,带来了巨大的性能提升,主要因为如下几个原因:

  1. 可以并行交错地发送请求,请求之间互不影响。
  2. 可以并行交错地发送响应,响应之间互不干扰。
  3. 只使用一个连接即可并行发送多个请求和响应。
  4. 消除不必要的延迟,从而减少页面加载的时间。

二进制分帧机制解决了 HTTP1.1 队头阻塞问题,也消除了并行处理和发送请求及响应时对多个 TCP 连接的依赖

  • 请求优先级:

    每个流都包含一个优先级,用来告诉对端哪个流更重要,当资源有限的时候,服务器会根据优先级来选择应该先发送哪些流。HTTP2 中,每个请求都可以带一个 31bit 的优先值,0 表示最高优先级。数值越大优先级越低。有了这个优先值,客户端和服务器就可以在处理不同的流时采取不同的策略,以最优的方式发送流、消息和帧。

  • 服务器推送:

    当前 web 页面的功能越来越强大,排版越来越精美,所以需要引用的 js 文件、css 文件或者图片等内容也越来越多,对每一个资源的外部引用,都是一次 request 请求。在 HTTP1.1 中,由于不具有多向请求与响应,所以可能需要额外的 TCP 连接,甚至导致队头堵塞,HTTP1.1 对此问题的解决方案可以参阅 HTTP 请求延迟解决方案一章节。当客户端获取服务器发送来的文档之后,通过分析获知需要引入额外的资源,然后再向服务器发送请求获取这些资源,如此大费周章,倒不如服务器主动推送这些额外资源。推送资源的特点如下:

  • 客户端可以缓存推送过来的资源。

  • 客户端可以拒绝推送过来的资源。

  • 推送资源可以由不同的页面共享。

  • 服务器可以按照优先级推送资源。

  • 首部压缩:

    在 HTTP1.1 中,每次请求或者响应都会发送一组首部信息,同时这些信息都是以文本形式发送,如果带有 cookie 信息的话,那么发送首部信息就是一份相当大的额外开销。为减少这些开销并提升性能,HTTP2 会压缩首部元数据,HTTP2 在客户端和服务器端使用“首部表”来跟踪和存储之前发送的键/值对,对于相同的数据,不再通过每次请求和响应发送,首部表在 HTTP2 的连接存续期内始终存在,由客户端和服务器共同渐进地更新。

http1 的一个问题

Hello English

step 1 每天一句

As long as you’re being a copycat, you will never be the best copycat.

一味的模仿他人,是做不到最好的

[1]: copycat百度翻译有道翻译 是指模仿者(特指盲目模仿)

step 2 音标学习

标准的中式发音,谁听谁服气(这说的是我。。。),下面给你列举一个学习资料(不知道是不是过时了,凑合先看),咱们先跟着第一个学习,每天学一丢丢。你也可以自己线下练习哦, 一天最少5-6个音标会读吧,然后再忘几个。。。就没了

  1. 美式音标(KK音标)在线发音课程

step 3 单词学习

在上面一起了

step 4 语法学习

这里给你一个英语语法学习的专门地址等你啥时候牛逼了,就可以去了, 英语语法网站

  • 先学习词性吧
1 名词 noun n. student 学生 2 代词 pronoun pron. you 你
3 形容词 adjective adj. happy 高兴的 4 副词 adverb adv. quickly 迅速地
5 动词 verb v. cut 砍、割 6 数词 numeral num. three 三
7 冠词 article art. a 一个 8 介词 preposition prep. at 在…
9 连词 conjunction conj. and 和 10 感叹词 interjection interj. oh 哦
前六类叫实词,后四类叫虚词

最后留点儿东西,省的以后还得再查

  1. 介词的理解
  2. wiki上的介词定义
  3. wiki上的英语书,很不错哦
  4. 英语学习的知乎专栏

memory snapshot的世界

snapshot(v8)

  • 既然要讲v8的snapshot那就得先看看snapshot的表示形式,整个snapshot里的对象是以graph的形式展示的,节点是对象(会以对象的构造函数的形式展示),边是属性值, 例如

    1
    2
    3
    4
    class A {
    a() {}
    }
    那么展示就是 A - a(边) -> Function

    此图是snapshot的json格式图

    此图中的索引都是从零开始的

    1
    2
    3
    4
    5
    6
    // 对应snapshot中的字段, 查找节点i的信息
    var i_type = nodes[i];
    var i_name = strings[nodes[i + 1]];
    var i_id = nodes[i + 2];
    var i_size = nodes[i + 3]
    var i_edge_count = nodes[i + 4];

    再具体的解释如下:

  • 那么接下来就要说一下snapshot里面是有哪些类型即节点的类型(当然这些类型都来自v8)完整的数据类型,而我们在这里要讲则是在snapshot里面定义的类型(来自v8), 这里我们要区分primitive(包扩三类number, string, symbol,js会替我们auto-boxing)和Object(不要问什么)

    从上图我们可以清晰节点的类型一共有13种,hidden类型可能不展示

  • 边的类型(即属性的表现形式) 从上图可以看出共有7种
    • map, array, symbol, object, regexp, number, boolean, string, date,typedarray…这些常见的内建对象我就不解释了,大家都知道
    • 我们在sanpshot中还会经常看见两类string比较生疏的两个(这两类算在pimitive中), sliced string和 concatenated string
      • 那我们就来看看string,在v8里的形式 在这里我们可以清晰的看出来,v8的字符串类型,里面有sliced的解释,是其他字符串的部分引用(因为字面量字符串是不可变的so可以这么办。。),而cons的就是(a, b),或者嵌套似的((a,b),c)这样的pairs(同样是因为字面量的字符串是不可变的)
  • 节点类型就上面列出的那些,但是根据具体的对象,他们又有好多不同的名字(Gc roots, map / tag, system / …)

chrome devtools 里的概念

  • shallow size:

    是指对象自己本身占用的大小, 不包含引用对象内容的大小

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const parent = {
    a: 23,
    child: child,
    }
    const child = {
    a: 23,
    c: 12,
    };
    /**
    child的大小就是8B(32为机器),parent大小就有得讨论了,若不算引用的具体内容大小,parent的大小也是8,而此时就是shallow size。那么包含引用内容呢?
    */

    *

  • retained size:

    是指不但包含对象自身,还包含该对象所能引用的或者间接引用(parent.child, parent.child.child)

  • GC roots

    GC roots的概念来自与垃圾回收算法,js的垃圾回收算法是基于根不可达来回收不使用的内存的,就选取某个对象作为初始点,沿着这个对象的引用链往下走,凡是通过这个对象无法访问到(是指通过引用可以获取到对象)的对象就认为是可以被回收的。

    如上图5,6,7就无法被访问到,此时就可以认为5,6,7可以被回收了
    

    1. 那么GC root 只有一个么?No.很明显不可能只有一个,因为有栈变量的存在
    2. 那么有哪些对象可以作为GC root呢, 对于js, 有Global, Window, 栈变量,内建对象等(我们是可以自己扩展内建对象的)
    3. chrome devtools 里面的distance是指什么? 是指通过多少次引用能访问对此对象,对于Window等就是1
  • 从上面我们可以看出来,所有的对象是以树的形式展示的,devtools中如何显示对象树的?
    对象的保留树
    就像我们前面所说的,堆就是由相互连接的对象构成的网络。在数学的世界中,这种结构称作图或者内存图。一个图是由节点和边构成的,而节点又是由边连接起来的,其中节点和边都有相应的标签。

    • 节点(或者对象)是用创建对象的构造函数标记的。

      1
      2
      3
      这里问题就来了那么js中的对象的构造函数都是啥呢
      const a = 'hello2dj';
      // a 的构造函数是 String, 但对于字面量来说还会有具体的展示类型,这与v8的内部实现相关
    • 边是用属性名来标记的
      对于不同的属性chrome 会标记不同的标识
      对象的属性以及属性值属于不同类型并且有着相应的颜色。每个属性都会有四种类型之一(更详细的上述snapshot里面又讲):

      • a:property - 有名称的常规属性,通过 .(点)操作符或者 [](方括号)符号来访问,例如 [“foo bar”];
      • 0:element - 有数字下标的常规属性,使用 [](方括号)来访问。
      • a:context var - 函数上下文中的某个变量,在相应的函数闭包中使用其名字就可以访问。
      • a:system prop - 由 JavaScript 虚拟机添加的属性,在 JavaScript 代码中无法访问。
  • Object count 挡在summary视图模式下查看时,会有这个,按照上述来说对象树的节点是constructor, 属性是边,那么object count 就是通过这个constructor 构造出来的对象实例数量

  • 巧了还有一个我们可以在devtools里经常看到的就是有些对象是黄颜色标识的有些是红色标识的,见图, 图中很明显标识红色和黄色的原因

    • 以黄色突出显示的节点具有 JavaScript 代码对它们的直接引用。 以红色突出显示的节点则没有直接引用。只有属于黄色节点的树时,它们才处于活动状态。 一般而言,您需要将注意力放在黄色节点上。 修复代码,使黄色节点处于活动状态的时间不长于需要的时间,您也需要消除属于黄色节点树的红色节点。点击黄色节点对其进行进一步调查。
    • 显然红色节点就是没有js代码直接引用的对象
  • 在summary视图下第一栏是从constructor而这一栏是分两类的

    • 不带()括号的是构造器,下面包含的是用这个构造器生成的对象,这个很好实验的,自己写一个类,实例化一下就可以了
    • 另一类带括号的又有如下区分

      见上图,他管()的行为叫tag,那就很明显了,在括号()下面的对象就是全部的这种对象了。

      我错了

      • (string, regexp) 显示的是literal string 即 a=’234’中的’23’, regexp类似
      • (num) 显示的是以number对象展示的对象
      • (array) 那些通过数组引用的对象,说白了就是数组对象
        • (code deopt data)[]: v8去优化时的数据
        • []:就是纯数组
        • (object properties)[]:通过对象属性引用的对象
        • (map descriptors)[]: map类型相关,暂时搁置
        • (object elements)[]: 暂时未知
        • (function scope info)[]: 暂时未知
      • (system) 那就是原生代码了
      • (compiled code) 编译过后的代码
      • (closure) 通过闭包引用的对象,但感觉更像是闭包自己
      • (sliced string): 搁置
      • (undefined): 搁置
      • (concatenated string): 搁置
  • 还有一些其他概念参见https://developers.google.com/web/tools/chrome-devtools/memory-problems/memory-101

看一次对其中两个对象的识别及分析

代码如下,num2是一个Number对象,不知道为啥,我以字面量分配的num1,我没找到。。。(有待继续)

1
num2 = new Number(234);

接下来的图示顺序分析的

常见的内存泄露种类

  • 全局变量

    1
    2
    3
    function globalLeak() {
    bar = 'hello2dj';
    }

    这里bar没有生命就意味着他被global引用了,那么他就不会被回收

  • 被遗忘的计时器或回调函数

    1
    2
    3
    4
    5
    6
    7
    8
    var someResource = getData();
    setInterval(function() {
    var node = document.getElementById('Node');
    if(node) {
    // 处理 node 和 someResource
    node.innerHTML = JSON.stringify(someResource));
    }
    }, 1000);

    此例说明:与节点或数据关联的计时器不再需要,node 对象可以删除,整个回调函数也不需要了。可是,计时器回调函数仍然没被回收(计时器停止才会被回收)。同时,someResource 如果存储了大量的数据,也是无法被回收的。

    还有时间监听:

    1
    2
    3
    4
    5
    var element = document.getElementById('button');
    function onClick(event) {
    element.innerHTML = 'text';
    }
    element.addEventListener('click', onClick);
  • 被引用的dom

    有时,保存 DOM 节点内部数据结构很有用。假如你想快速更新表格的几行内容,把每一行 DOM 存成字典(JSON 键值对)或者数组很有意义。此时,同样的 DOM 元素存在两个引用:一个在 DOM 树中,另一个在字典中。将来你决定删除这些行时,需要把两个引用都清除。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
      var elements = {
    button: document.getElementById('button'),
    image: document.getElementById('image'),
    text: document.getElementById('text')
    };
    function doStuff() {
    image.src = 'http://some.url/image';
    button.click();
    console.log(text.innerHTML);
    // 更多逻辑
    }
    function removeButton() {
    // 按钮是 body 的后代元素
    document.body.removeChild(document.getElementById('button'));
    // 此时,仍旧存在一个全局的 #button 的引用
    // elements 字典。button 元素仍旧在内存中,不能被 GC 回收。
    }

    此外还要考虑 DOM 树内部或子节点的引用问题。假如你的 JavaScript 代码中保存了表格某一个 的引用。将来决定删除整个表格的时候,直觉认为 GC 会回收除了已保存的 以外的其它节点。实际情况并非如此:此 是表格的子节点,子元素与父元素是引用关系。由于代码保留了 的引用,导致整个表格仍待在内存中。保存 DOM 元素引用的时候,要小心谨慎。

  • 闭包

    这段代码被引用了无数次了来自meteor

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    var theThing = null;
    var replaceThing = function () {
    var originalThing = theThing;
    var unused = function () {
    if (originalThing)
    console.log("hi");
    };
    theThing = {
    longStr: new Array(1000000).join('*'),
    someMethod: function () {
    console.log(someMessage);
    }
    };
    };
    setInterval(replaceThing, 1000);

    代码片段做了一件事情:每次调用 replaceThing ,theThing 得到一个包含一个大数组和一个新闭包(someMethod)的新对象。同时,变量 unused 是一个引用 originalThing 的闭包(先前的 replaceThing 又调用了 theThing )。思绪混乱了吗?最重要的事情是,闭包的作用域一旦创建,它们有同样的父级作用域,作用域是共享的。someMethod 可以通过 theThing 使用,someMethod 与 unused 分享闭包作用域,尽管 unused 从未使用,它引用的 originalThing 迫使它保留在内存中(防止被回收)。当这段代码反复运行,就会看到内存占用不断上升,垃圾回收器(GC)并无法降低内存占用。本质上,闭包的链表已经创建,每一个闭包作用域携带一个指向大数组的间接的引用,造成严重的内存泄漏。

    Meteor 的博文 解释了如何修复此种问题。在 replaceThing 的最后添加 originalThing = null 。

    参考

  1. v8类型(知乎上的一篇)
  2. v8类型源码注释
  3. v8-object-representation
  4. js内部编码介绍

  5. v8类型图

  6. snapshot的格式头文件

  7. 生成snapshot的cc文件

  8. heap profiling

  9. snapshot格式

  10. easy profiling

  11. 常见内存泄露copy于此处

Golang 高效的错误处理

原文地址

简介

golang 的错误处理方式一直是他遭受抨击的一个原因之一。探查每一个的错误然后处理确实是一个艰巨的任务,这里有几招可以让你减少错误的处理方式(处理错误的方法)。

错误处理靠前

当我们写golang的时候倾向于

1
2
3
4
5
f, err := os.Open(path)
if err != nil {
// handle error
}
// do stuff

而不是

1
2
3
4
5
f, err := os.Open(path)
if err == nil {
// do stuff
}
// handle error

这种方式可以让我们的正常处理方式一路看到底,而不是if之后是错误

定义属于自己的错误

处理错误的第一步得是知道错误是啥,如果你的package发生了错误,那么你的用户一定对错误的原因很感兴趣。要做到让你的user知道错误是啥,你只需要实现error interface, 如下就可以了

1
2
type Error string
func (e Error) Error() string { return string(e) }

这样你的用户就可以通过类型断言来判断是否是你的错误

1
2
3
4
result, err := yourpackage.Foo()
if err, ok := err.(yourpackage.Error); ok {
// use tp to handle err
}

你也可以暴露一个结构体的error给你的用户

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type OpenError struct {
File *File
Error string
}
func (oe *OpenError) Error() string {
// format error string here
}

func ParseFiles(files []*File) error {
for _, f := range files {
err := f.parse()
if err != nil {
return &OpenError{
File: f,
Error: err.Error(),
}
}
}
}

通过这种方式,你的用户就可以分辨具体是哪个文件解析失败了
但是当你包裹错误的时候你也应当注意,因为包裹一个error,会丢失一些信息,就好比上面那个你已经丢失了err的类型而只剩下了err里的信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var c net.Conn
f, err := DownloadFile(e, path)
switch e := err.(type) {
default:
// this will get executed if err == nil
case net.Error:
// close connection, not valid anymore
c.Close()
return e
case error:
// if err is non-nil
return err
}
// do other things

见上如果此时你包裹了net.Error, 那么这段代码就不会看到net.Error这个错误了,而只能是一段错误信息,此时就无法具体区分错误了(见前一段的包裹方式丢失了具体的错误类型)
一个好的处理方式是尽量不要包裹你调用的其他包自己产生的错误,因为用户可能更关心他们产生的错误而不是你的。

把错误当做状态

有时候你可能想要持有一个错误而不是抛出,不管你是打算随后上报或者是你知道这个错误很快就会再次出现

这种情况的一个栗子就是bufio这个包。当bufio.Reader遇到一个错误,他会持有这个错误一直到buffer空为止,只有此时他才会上报这个错误。
另一种栗子就是使用go/loader。当使用参数调用遇到错误时,他会持有这个错误,因为有很大的概率他会再次使用相同的参数又调用一遍

使用函数去避免重复

如果你有一段错误处理的代码是重复的,你可以把他提出来做一个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func handleError(c net.Conn, err error){
// repeated error handling
}
func DoStuff(c net.Conn) error{
f, err := downloadFile(c, path)
if err != nil {
handeError(c, err)
return err
}
f, err := doOtherThing(c)
if err != nil {
handleError(c, err)
return err
}
}

一个替换方案是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func handeError(c net.Conn, err error) {
if err == nil {
return
}
// repeat err handling
}

func DoStuff(c net.Conn) error {
// defer func(){ handleError(c, err) }()
// 这是原文在的位置,但明显有错误啊,err未定义啊
f, err := downloadFile(c, path)
defer func(){ handleError(c, err) }()// 所以我挪到这里了,但效果是否ok,暂未验证
if err != nil {
return err
}
f, err := doOtherThing(c)
if err != nil
}

ps: 一个golang官网的一段翻译

Why is my nil error value not equal to nil? 为什么我的nil error 不等于nil呢?

先上代码

1
2
3
4
5
6
7
func returnError() error{
var p *MyError = nil
if bad() {
p = ErrBad
}
return p // Will always return a non-nil error
}

如上这里的p明明是nil值为啥返回后就不是了呢?这里涉及到另外一个问题就是interface值,我们知道error是一个interface,而MyError是一个struct, 就是要把struct值赋值给interface,在golang里interface值是包括两个的一个type值,一个value值,只有当两者都是nil的时候interface值才是nil, 上述代码,很明显虽然value是nil,可是类型还在啊,就是说type不是你nil而是MyError, 改进如下

1
2
3
4
5
6
func returnsError() error {
if bad() {
return ErrBad
}
return nil
}

Rxjs之不要到处都是取消订阅

我又要先上图了:

原文地址(english, 需翻墙)

Rxjs: 不要 取消订阅!

好吧,我说的不要调用太多的取消的订阅

在我们的使用过程中我们可能会有很多的Observables, 那么这多的Observables该如何管理呢,有的人就会管理所有的subscription,然后一个一个的取消订阅,这样是不优雅的。当你管理了太多的subscriptions时那可能意味着你是必须要管理他们,而不是在充分利用Rx的强大。

我们来看一个不太好的管理方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class MyGenericComponent extends SomeFrameworkComponent {
updateData(data) {
// do something framework-specific to update your component here.
}

onMount() {
this.dataSub = this.getData()
.subscribe(data => this.updateData(data));

const cancelBtn = this.element.querySelector(‘.cancel-button’);
const rangeSelector = this.element.querySelector(‘.rangeSelector’);

this.cancelSub = Observable.fromEvent(cancelBtn, ‘click’)
.subscribe(() => {
this.dataSub.unsubscribe();
});

this.rangeSub = Observable.fromEvent(rangeSelector, ‘change’)
.map(e => e.target.value)
.subscribe((value) => {
if (+value > 500) {
this.dataSub.unsubscribe();
}
});
}

onUnmount() {
this.dataSub.unsubscribe();
this.cancelSub.unsubscribe();
this.rangeSub.unsubscribe();
}
}

我们从dataSub订阅数据直到发生了button click或者是range范围变化为止。就意味着当这两想发生时我们就会停止数据的订阅。我们可以看到上面管理了3个subscripitions,很复杂。
但其实我们可以把这些subscripition组合到一个subscripition里面来管理。

使用takeUntil来管理你的subscripition

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class MyGenericComponent extends SomeFrameworkComponent {
updateData(data) {
// do something framework-specific to update your component here.
}

onMount() {
const data$ = this.getData();
const cancelBtn = this.element.querySelector(‘.cancel-button’);
const rangeSelector = this.element.querySelector(‘.rangeSelector’);
const cancel$ = Observable.fromEvent(cancelBtn, 'click');
const range$ = Observable.fromEvent(rangeSelector, 'change').map(e => e.target.value);

const stop$ = Observable.merge(cancel$, range$.filter(x => x > 500))
this.subscription = data$.takeUntil(stop$).subscribe(data => this.updateData(data));
}

onUnmount() {
this.subscription.unsubscribe();
}
}

可以看到我们使用一个stop$就管理了所有的subscripitions。
优点:

  1. 很明显我们减少了代码量
  2. 这种方式给了我们一个可以complete observable的方法,当takeUntil条件满足后是会触发complete的,而单纯的调用unsubcribe只是不再订阅了,而source并不一定就结束了,这样就不会触发complete的
  3. 最后一个就是你把所有的东西都聚合在了一起,因为上述代码我们可以看到只有一个subscribe和一个unsubscribe, 而Observable,只有在调用了subscribe后才开始执行,这意味你把中心点都给聚合了,代码逻辑的开始和结束被归一了,代码会很清晰。

其他的操作符

  • take(n)
  • takeWhile(predicate): 只有predicate返回true,才会触发数据,当是false时就结束了
  • first()
  • first(predicate): 过滤所有的数据只有第一个满足predicate的数据会被触发,然后就结束了。

总结:使用takeUntil和takeWhile或者其他的

当你看到多个subscripitions的时候就应当尝试使用takeUntil来管理它们。

  • 更多的可组合性
  • 当你结束一个stream的时候会触发complete
  • 更少的代码
  • 更少需要管理的东西
  • 更少的subscribe调用

chrome-dev-you-may-dont-know

chrome devtools 中的一些实用技巧

在元素面板中拖拽元素

在控制台中引用元素面板中选中的元素

在控制台中使用上次操作的结果

修改元素的状态以及添加 css

查找 css 属性定义的位置

将修改的 css 样式保存到文件

对单个元素的截图

在控制台中使用 shift-enter(连续多行输入执行)

清理控制台

跳转…

监听表达式

XHR/FETCH 调试

DOM 修改的调试

———————————————–(华丽的分割线)

在元素面板中拖拽元素

在元素面板中我们是可以拖拽任意元素到页面中其他位置的(这个大家应该都知道。。。)

控制台中引用元素面板中选中的元素

在元素面板中选中元素,然后就可以在控制台中使用 $0 引用它,神奇了

大吃一惊还有这等操作(管用)

在控制台中使用上次操作的结果

在控制台中输入 $_ 引用上次操作结果

他要$?就更神奇了(管用)

修改元素的状态以及添加 css

这个大家都知道,直接截图就好了

在看下一个

查找 css 属性定义的位置

使用 cmd-click(ctrl-click on windows) 组合点击一个元素面板中的 css 属性就会跳转到 Source panel 中(连续两连点击才可以哦)

不错不错(管用)

将修改的 css 样式保存到文件

在元素面板右侧修改样式,然后使用上面方法跳 source panel 中,就会看到 css 源文件,右击文件名字,save as 保存到本地

对单个元素的截图

选中一个元素然后按组合键 cmd-shift-p(or crtl-shift-p on windows)打开命令菜单(command menu), 接着输入 screenshot 选择 Capture node screenshot

有意思的功能, 可惜我试验失败了。。。(66.0.3359.139chrome 版本,大家可以试试),其实里面的 screenshot 还有另外两个 capture full siz screenshot(全截,我也失败了), capture screenshot 接的是当文档展示的区域我成功了

在控制台中使用 shift-enter 连续多行输入执行

多行输入执行,啥也不说了,有用(管用)

每次换行使用 shit+enter 哦

清理控制台

清理使用 ctrl-l or cmd-k(管用)

跳转…

在 source panel 中

  • cmd-o(ctrl-o on windows) 展示当前页面加载的所有文件
  • cmd-shift-o(ctrl-shift-o in windows) 展示当前文件的符号表(属性,函数以及类)

  • ctrl-g 调到当前文件的指定行

亲测管用

监听表达式

添加一个表达式到 debug session 中,不用手动计算了

管用, 在 source panel 中,若是没有 consle 面板,右键-> evaluate in console 即可

XHR/FETCH 调试

可以在 source panel 中的右侧看到 XHR/FETCH breakpoints 中添加断点,不知道请 ajax 啥时候发的,没问题,他可以帮你。。。

并且我还在下面发现了 Event Listener Breakpoint,而它可以带来的断点位置是在是太多了,比如: 动画(animation), Canvas, Clipboard, DOM Mutaion, keyboard, 哎呀太多了,大家可以亲自试一试

DOM 修改的调试

右键选中的元素,选择 break on 展开后会有 subtree modifications, 就是说如果有脚本修改了子元素,断点就会触发,还有 atrribute modification, 以及 node removal

$$ 相当于 document.querySelectorAll()

文章尾部福利赠送

cmd-shfit-p 打开的命令行里面的有用命令(还有更多的等待大家一起探索)

  1. show layers 查看当前页面的渲染情况,合成层(关于合成层看淘宝)的状况,绘制的具体范围,等等检查性能一绝
  2. show perfomance monitor 顾名思义
  3. show frame per second(FPS) meter 顾名思义
  4. show paint flashing rectangles 展示当前页面重绘的区域

golang基础

随时会更新

golang 的所有的都是 copy 赋值,=,传参,channel 发送等等都是。

:= 这种声明方式若是已经定义了,则不会新建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import (
"fmt"
)

func main() {
pase_student()
}
type student struct {
Name string
Age int
}

func pase_student() {
m := make(map[string]*student)
stus := []student{
{Name: "zhou", Age: 24},
{Name: "li", Age: 23},
{Name: "wang", Age: 22},
}
for i, stu := range stus {
m[stu.Name] = &stu; // 应该改为m[stu.Name] = &stus[i]
}

for _, va := range m {
fmt.Printf("%p \n", va)
}
}

stus 是个 map, stu 并不会每次新生成一个, 其实循环时每次都是相同的 stu, 而且 golang 每次都是 copy 语义,你再看一眼会发现,给 map 赋值的是指针,那就意味着无论你如何改变 map 都会是相同的值。:= 多次声明不会重新定义新的变量, 可见规范

见下:

1
2
3
field1, offset := nextField(str, 0)
field2, offset := nextField(str, offset) // redeclares offset
a, a := 1, 2 // illegal: double declaration of a or no new variable if a was declared elsewhere

golang 的字符串是不可变的,要想使用可变字符串可以使用 bytes,或者[]rune 数组, string 类型不可变,他的 slice 也不可变

我们可以从关闭的 channel 中读取数据但是为空,就是说若是在 select 语句 case 中从关闭的 channel 是可以的。 参见

1
2
3
4
5
6
select {
case <- stopCh:
fmt.Println("go")
default:
fmt.Println("come")
}

在使用 Go channel 的时候,一个适用的原则是不要从接收端关闭 channel,也不要关闭有多个并发发送者的 channel。 优雅关闭 go channel(http://www.tapirgames.com/blog/golang-channel-closing)

channel 的使用,当 channel 是非缓冲的时候他就是阻塞读与写的, 所以使用 channel 的时候要小心同步阻塞,导致死锁 game over

1
2
3
ch := make(chan string)
ch <- "23" // 此时就会阻塞
// a <- ch 单写他也是会阻塞的

所以要小心不要出现只有读或者只有写的 channel 若是出现的话,分布在不同 goroutine 中时就会导致 golang 死锁,主在等子完成可是子阻塞在了 channel 读或者写上了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
package main

import (
"time"
"math/rand"
"sync"
"log"
"strconv"
)

func main() {
rand.Seed(time.Now().UnixNano())
log.SetFlags(0)

// ...
const MaxRandomNumber = 100000
const NumReceivers = 10
const NumSenders = 1000

wgReceivers := sync.WaitGroup{}
wgReceivers.Add(NumReceivers)

// ...
dataCh := make(chan int, 100)
stopCh := make(chan struct{})
// stopCh is an additional signal channel.
// Its sender is the moderator goroutine shown below.
// Its reveivers are all senders and receivers of dataCh.
toStop := make(chan string, 1)
// 设为缓冲1是为了防止moderator还未准备好就停止了
// the channel toStop is used to notify the moderator
// to close the additional signal channel (stopCh).
// Its senders are any senders and receivers of dataCh.
// Its reveiver is the moderator goroutine shown below.

var stoppedBy string

// moderator
go func() {
stoppedBy = <- toStop // part of the trick used to notify the moderator
// to close the additional signal channel.
close(stopCh)
}()

// senders
for i := 0; i < NumSenders; i++ {
go func(id string) {
for {
value := rand.Intn(MaxRandomNumber)
if value == 0 {
// here, a trick is used to notify the moderator
// to close the additional signal channel.
select {
case toStop <- "sender#" + id:
default:
}
return
}

// the first select here is to try to exit the
// goroutine as early as possible.
select {
case <- stopCh:
return
default:
}
// 为什么要在前面在加一个select stopCh呢? 因为若是到了这一步由于select的随机性(此时stopCh和dataCh都处于活跃状态select是随机选取的),有可能会继续发送,而没有选择stopCh
select {
case <- stopCh:
return
case dataCh <- value:
}
}
}(strconv.Itoa(i))
}

// receivers
for i := 0; i < NumReceivers; i++ {
go func(id string) {
defer wgReceivers.Done()

for {
// same as senders, the first select here is to
// try to exit the goroutine as early as possible.
select {
case <- stopCh:
return
default:
}

select {
case <- stopCh:
return
case value := <-dataCh:
if value == MaxRandomNumber-1 {
// the same trick is used to notify the moderator
// to close the additional signal channel.
select {
case toStop <- "receiver#" + id:
default:
}
return
}

log.Println(value)
}
}
}(strconv.Itoa(i))
}

// ...
wgReceivers.Wait()
log.Println("stopped by", stoppedBy)
}

golang make 返回的是值类型,用 slice, map, channel, 并且会给 map 和 slice 预分配空间

golang slice 语法可以对值也可以对引用

1
2
3
a = [1,2,34]
t := a[1:3] // ok
c := (&a)[1:2] // ok

golang 切片的用法是 [start:end] 但不包括 end

new 返回的是指针类型

golang 也存在 js 中的那个经典问题就是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func main() {
runtime.GOMAXPROCS(1)
wg := sync.WaitGroup{}
wg.Add(20)
for i := 0; i < 10; i++ {
go func() {
fmt.Println("i: ", i)
wg.Done()
}()
}
for i := 0; i < 10; i++ {
go func(i int) {
fmt.Println("i: ", i)
wg.Done()
}(i)
}
wg.Wait()
}

第一个打印的 i 都是 10, 因为他们打印的都是同一个变量 i。

golang 中 return defer 返回值的顺序

先来假设出结论,帮助大家理解原因:

多个 defer 的执行顺序为“后进先出”;

defer、return、返回值三者的执行逻辑应该是:return 最先执行,return 负责将结果写入返回值中;接着 defer 开始执行一些收尾工作;最后函数携带当前返回值退出。

如何解释两种结果的不同:

上面两段代码的返回结果之所以不同,其实从上面第 2 条结论很好理解。

a()int 函数的返回值没有被提前声名,其值来自于其他变量的赋值,而 defer 中修改的也是其他变量,而非返回值本身,因此函数退出时返回值并没有被改变。

b()(i int) 函数的返回值被提前声名,也就意味着 defer 中是可以调用到真实返回值的,因此 defer 在 return 赋值返回值 i 之后,再一次地修改了 i 的值,最终函数退出后的返回值才会是 defer 修改过的值。

defer 是在函数结束前执行的,当返回值的临时变量赋给外部时才算调用结束吧!a = fn(2) 当把值给了 a 才算结束

我们可以这么理解 return 肯定先执行执行的结果就是把返回值计算出来并且赋值给返回值所存在的临时变量, 但我们命名返回值的时候,返回值并不是临时变量而是函数中声明的变量

在函数有多个返回值时,只要有一个返回值有指定命名,其他的也必须有命名

append 只能作用在 slice 上而不能是指针

参见签名

1
func append(slice []Type, elems ...Type) []Type

错误的栗子

1
2
3
4
5
6
7
8
9
10
package main

import "fmt"

func main() {
s1 := []int{1, 2, 3}
s2 := []int{4, 5}
s1 = append(s1, s2)
fmt.Println(s1)
} // 错误因为 append接下来的参数是以一个一个传递的切片中的元素,而不是切片 正确的是 s1 = append(s1, ...s2)

进行结构体比较时候,只有相同类型的结构体才可以比较,结构体是否相同不但与属性类型个数有关,还与属性顺序相关。并且结构体可比较的前提是结构的属性都可比较。map 和 slice 还有 function 不可比较

就是说属性名字不同,类型不同或者顺序不同都是不能比较的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
func main() {

sn1 := struct {
age int
name string
}{age: 11, name: "qq"}
sn2 := struct {
age int
name string
}{age: 11, name: "qq"}

if sn1 == sn2 {
fmt.Println("sn1 == sn2")
}

sm1 := struct {
age int
m map[string]string
}{age: 11, m: map[string]string{"a": "1"}}
sm2 := struct {
age int
m map[string]string
}{age: 11, m: map[string]string{"a": "1"}}

if sm1 == sm2 {
fmt.Println("sm1 == sm2")
}
}

此处 sn1 可以和 sn2 使用==比较但是如下

1
2
3
4
sn3:= struct {
name string
age int
}{age:11,name:"qq"}

sn3 就不能比较了

还有上例中,含有不可比较的 map,slice,func 等,所以 sm1 和 sm2 是不可比较的但是我们可以使用 deepEqual 来进行比较

1
2
3
4
5
if reflect.DeepEqual(sn1, sm) {
fmt.Println("sn1 ==sm")
}else {
fmt.Println("sn1 !=sm")
}

:= 赋值模式的限制

定义变量同时显式初始化不能提供数据类型只能在函数内部使用

nil 可以用作 interface、function、pointer、map、slice 和 channel 的“空值”, 是不可以作为其他类型的空值的,比如 String, string 的空值是“”

1
2
3
4
5
6
7
8
var a chan int
// a 是空值nil
// chan的初始化一定是用make

var a map[int]string
// a 是空值nil
// map初始化一定使用make
// 使用new 生成的map也是nil的map

itoa

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const (
x = iota
y
z = "zz"
k
p = iota
)

func main() {
fmt.Println(x,y,z,k,p)
}
// 结果
0
1
zz
zz
4(直接计算当前的值)

golang const 常量可以使用 itoa 赋值, 甚至是自定义类型

参见

若是自定义类型类似于枚举,当我们传递字面量(如:2,3)时也是可以被识别为枚举值的。

  • 当在一行声明两个常量 itoa 时,itoa 是只有到了下一行才会增长
1
2
3
4
5
6
7
8
9
10
11
12
const (
Apple, Banana = iota + 1, iota + 2
Cherimoya, Durian
Elderberry, Fig
)
// 输出
// Apple: 1
// Banana: 2
// Cherimoya: 2
// Durian: 3
// Elderberry: 3
// Fig: 4
  • itoa 从 0 开始增长,当我们们不使用 itoa 时,且赋值一个则所有的都会是这个值
1
2
3
4
5
6
const (
a = "1"
b
c
d
)

goto 不能跳转到其他函数或者内层代码, 只能在本函数以及相同的 scope 内

下面的就是错的

1
2
3
4
5
6
7
8
9
10
package main

func main() {

for i:=0;i<10 ;i++ {
loop:
println(i)
}
goto loop
}

注意 defintion 和 type alias 的区别 defintion 是定义了一个新的类型,alias 仅仅是个别名

1
2
3
4
5
6
7
8
9
10
11
package main
import "fmt"

func main() {
type MyInt1 int
type MyInt2 = int
var i int =9
var i1 MyInt1 = i // 错误 MyInt1是个新类型
var i2 MyInt2 = i // yes 是个别名
fmt.Println(i1,i2)
}

1. 我们在返回值中定义的命名返回值,可以直接在函数中使用不用再重新定义 2. 内部 scope 中新定义的内部变量是会覆盖掉外部的变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import (
"errors"
"fmt"
)

var ErrDidNotWork = errors.New("did not work")

func DoTheThing(reallyDoIt bool) (err error) {
if reallyDoIt {
result, err := tryTheThing()
if err != nil || result != "it worked" {
err = ErrDidNotWork
}
fmt.Println(err)
}
return err
}

func tryTheThing() (string,error) {
return "",ErrDidNotWork
}

func main() {
fmt.Println(DoTheThing(true))
fmt.Println(DoTheThing(false))
}
// 输出两个
<nil>
<nil>

改为

1
2
3
4
5
6
7
8
9
10
func DoTheThing(reallyDoIt bool) (err error) {
var result string
if reallyDoIt {
result, err = tryTheThing() // 不要新定义变量
if err != nil || result != "it worked" {
err = ErrDidNotWork
}
}
return err
}

panic 仅有最后一个可以被 revover 捕获,panic 中可以传任何值,不仅仅可以传 string

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
func main()  {
defer func() {
if err:=recover();err!=nil{
fmt.Println("++++")
f:=err.(func()string)
fmt.Println(err,f(),reflect.TypeOf(err).Kind().String())
}else {
fmt.Println("fatal")
}
}()

defer func() {
// 这里就recover是捕获不到err的,所以这里的err是nil
if err:=recover();err!=nil{
fmt.Println("++++")
f:=err.(func()string)
fmt.Println(err,f(),reflect.TypeOf(err).Kind().String())
}else {
fmt.Println("fatal")
}
}()

defer func() {
panic(func() string {
return "defer panic"
})
}()
panic("panic")
}

不管运行顺序如何,当参数为函数的时候,要先计算参数的值

map 引用不存在的 key,不报错

map 使用 range 遍历顺序问题,并不是录入的顺序,而是随机顺序

append 函数返回更新后的 slice(长度和容量可能会变),必须重新用 slice 的变量接收,不然无法编译通过

golang channel 和一个 goroutine 组合起来就是一个 web-worker 的模式,只是我们在 js 里管理的是 web-worker 的句柄,而在 golang 里面我们要管理的是 channel,这里 golang 的一个优势是我可以把多个 channel 和一个 goroutine 绑定,而 web-worker 一个句柄就对应这个一个 web-worker 是不可能多对一的。

golang channel 的使用方式两种

  1. 调用者生成传递个 goroutine。
  2. 被调用者生成返回给调用者。

内嵌结构体,当内嵌结构体不是指针时,是会有默认值得,这就符合了 golang 得概念,所有的变量都有默认值,指针的默认值是 nil.

1
2
3
4
5
6
7
8
9
10
11
12
package main
import "sync"
type A struct {
sync.Mutex
}

func main() {
a := A{}
a.Lock()
a.Unlock()
fmt.Println("Mutex a ", a)
}

上面代码运行是 ok 的。

两个 golang 问题 http://colobu.com/2018/03/08/two-issues-in-go-development/

golang 包的引入路径 GOPATH/src/…一层一层查找

bin 是可执行的文件, pkg 是编译生成的.a 文件的存放位置是静态库

src 是存放源文件的命令

安装问题 golang/x/tools > https://github.com/golang/tools

下载放到 src 下的 golang/x/下

安装问题 golang/x/net > https://github.com/golang/net

下载放到 src 下的 golang/x/下

golint go get -u -v github.com/golang/lint/golint

编译时的使用第三方源码包 编译时的探索

  1. 在使用第三方包的时候,当源码和.a 均已安装的情况下,编译器链接的是源码
  2. 所谓的使用第三方包源码,实际上是链接了以该最新源码编译的临时目录下的.a 文件而已。
  3. 标准库在编译时也是必须要源码的。不过与自定义包不同的是,即便你修改了 fmt 包的源码(未重新编译 GO 安装包),用户源码编译时,也不会尝试重新编译 fmt 包的,依旧只是在链接时链接已经编译好的 fmt.a
  4. Go 语言中 import 后面路径中最后的一个元素到底是包名还是路径名?答案是目录名。按照 Golang 语言习惯,一个 go package 的所有源文件放在同一个目录下,且该目录名与该包名相同,比如 libproj1/foo 目录下的 package 为 foo,foo1.go、 foo2.go…共同组成 foo package 的源文件。但目录名与包名也可以不同。
  5. 一个路径下不允许有两个包

同其他语言 nodejs 一样,一个包被导入一次后就会被缓存,再次被导入会从缓存获取

init 函数的执行顺序

  1. 对同一个 go 文件的 init()调用顺序是从上到下的
  2. 对同一个 package 中不同文件是按文件名字符串比较“从小到大”顺序调用各文件中的 init()函数,对于
  3. 对不同的 package,如果不相互依赖的话,按照 main 包中”先 import 的后调用”的顺序调用其包中的 init()
  4. 如果 package 存在依赖,则先调用最早被依赖的 package 中的 init()
  5. 最后调用 main 函数

关于关闭 channel 有几点需要注意的是:

  1. 重复关闭 channel 会导致 panic。
  2. 向关闭的 channel 发送数据会 panic。
  3. 从关闭的 channel 读数据不会 panic,读出 channel 中已有的数据之后再读就是 channel 类似的默认值,比如 chan int 类型的 channel 关闭之后读取到的值为 0。

make 和 new

  1. new allocate the memory and return the pointer
  2. make allocate the memory and initialize the memory cause slice, map and channel must be initialized before use
    new 的作用是 初始化 一个指向类型的指针 (T), make 的作用是为 slice, map 或者 channel 初始化,并且返回引用 T
    make(T, args)函数的目的与 new(T)不同。它仅仅用于创建 Slice, Map 和 Channel,并且返回类型是 T(不是 T
    )的一个初始化的(不是零值)的实例。 这中差别的出现是由于这三种类型实质上是对在使用前必须进行初始化的数据结构的引用。 例如, Slice 是一个 具有三项内容的描述符,包括 指向数据(在一个数组内部)的指针,长度以及容量。在这三项内容被初始化之前,Slice 的值为 nil。对于 Slice,Map 和 Channel, make()函数初始化了其内部的数据结构,并且准备了将要使用的值。

产品中一定不要使用默认的 http.Get

如果你觉得方便,直接使用 http.Get 或者类似的方法发送请求,可能会导致一些问题, 因为这默认是使用 DefaultClient 作为 client:

  1. 多 goroutine 共享,这意味着在别处对 DefaultClient 的改动会影响你当前的使用
  2. 未设置 connection timeout 和 read/write timeout
  3. 默认的 idle connection 等设置可能不满足你的需求

检查网络错误或者超时 net.Error 和 err.Timeout() 可以检查是不是超时错误

1
2
3
if err, ok := err.(net.Error); ok && err.Timeout() {
……
}

golang 命名返回值

1
2
3
func test()(a int) {
return
}

实际上 a 相当于已经命名了,相当于

1
2
3
4
func test() int {
var a int
return a
}

channel 关闭问题

  • 通过 recover 来恢复来恢复
  • 通过 Mutex 来保证不会向已关闭的 channel 发送消息
  • 通过 sync.Once 来关闭,保证不会重复关闭好的原则是:
  1. 不要在接收端关闭 channel, 避免向关闭的 channel 发送消息
  2. 不要关闭有多个并发发送者的 channel

golang 里面所有未赋值的变量都会赋值为默认值,就连内嵌 struct, infterface 也一样

  • 内嵌 struct 会变为相应字段的默认值
  • 内嵌 interface 为赋值就是 nil, 因为接口变量的默认值就是 nil(但 nil 和 nil 是不同的, 这里是个坑)

若是要 marshal 时,不要输出 null 字段或者是为空的字段(结构体),需要把它赋值为指针才可以然后加上 omitempty

https://stackoverflow.com/questions/18088294/how-to-not-marshal-an-empty-struct-into-json-with-go?utm_medium=organic&utm_source=google_rich_qa&utm_campaign=google_rich_qa

http://colobu.com/2017/05/12/call-private-functions-in-other-packages/ 突破 golang 的访问限制

reflect

  • reflect.TypeOf,是获取类型的元数据
1
reflect.TypeOf(i).Elem().Field(0).Tag //获取定义在struct里面的标签
  • reflect.ValueOf,是获取类型的值
1
reflect.ValueOf(i).Elem().Field(0).String() //获取存储在第一个字段里面的值

http://www.01happy.com/p3206/

最后再次重复一遍反射三定律:

  • 反射可以将“接口类型变量”转换为“反射类型对象”。
  • 反射可以将“反射类型对象”转换为“接口类型变量”。
  • 如果要修改“反射类型对象”,其值必须是“可写的”(settable)。

一旦你理解了这些定律,使用反射将会是一件非常简单的事情。它是一件强大的工具,使用时务必谨慎使用,更不要滥用。

只有可以 addressable 的变量才可以使用 reflect 进行赋值修改即:

wrong

1
2
3
var x float64 = 3.4
v := reflect.ValueOf(x)
v.SetFloat(7.1) // Error: will panic.

yes

1
2
3
4
var x float64 = 3.4
p := reflect.ValueOf(&x) // Note: take the address of x.
fmt.Println("type of p:", p.Type())
fmt.Println("settability of p:", p.CanSet())

存入 map 的是值是不可以 addressable 的 a = map [string]int; &a[“s”], 这个很好理解因为,map 回去做冲突处理,地址有可能发生变化。

[go web 的一些建议](https://medium.com/@matryer/how-i-write-go-http-services-after-seven-years-37c208122831

  1. Shared dependencies are fields of the structure
    通过结构体的字段共享依赖,而不是到处引入(适用于所有的)

  2. I have a single file inside every component called routes.go where all the routing can live:

1
2
3
4
5
6
package app
func (s *server) routes() {
s.router.HandleFunc("/api/", s.handleAPI())
s.router.HandleFunc("/about", s.handleAbout())
s.router.HandleFunc("/", s.handleIndex())
}

This is handy because most code maintenance starts with a URL and an error report — so one glance at routes.go will direct us where to look.

  1. If a particular handler has a dependency, take it as an argument. 若是有特殊依赖则作为参数传入

4.

https://github.com/dgryski/go-perfbook/blob/master/performance.md

https://my.oschina.net/xinxingegeya/blog/729673 golang unsafe 包的 unsafe 特性

错误和异常,意料之内的是错误,意料之外的是异常(https://studygolang.com/articles/11753?fr=sidebar)

从标准输入读取

  1. os.Stdin 实现了 Reader 接口可以直接 Read
1
2
s := make([]byte, 10)
n, err := os.Stdin.Read(s)
  1. fmt.Scanf() 可以读取 so.Stdin
1
2
3
var firstname, secondname string
fmt.Scanln(&FirstName, &SecondNames) //Scanln 扫描来自标准输入的文本,将空格分隔的值依次存放到后续的参数内,直到碰到换行。
fmt.Scanf("%s %s", &firstName, &lastName) //Scanf与其类似,除了 Scanf 的第一个参数用作格式字符串,用来决定如何读取。
  1. os.Stdin 是个 Reader 但是没有缓冲, 可以使用 bufio.NewReader()生成一个带缓冲的
1
2
3
4
5
inputReader := bufio.NewReader(os.Stdin)
for {
input, _ := inputReader.ReadString('\n')
fmt.Println(input)
}

Reader 接口

  1. Read 方法
    也就是说,当 Read 方法返回错误时,不代表没有读取到任何数据。调用者应该处理返回的任何数据,之后才处理可能的错误。
    io.EOF 变量的定义:var EOF = errors.New(“EOF”),是 error 类型。根据 reader 接口的说明,在 n > 0 且数据被读完了的情况下,返回的 error 有可能是 EOF 也有可能是 nil。

Writer 接口

  1. Write 方法
    Write 将 len(p) 个字节从 p 中写入到基本数据流中。它返回从 p 中被写入的字节数 n(0 <= n <= len(p))以及任何遇到的引起写入提前停止的错误。若 Write 返回的 n < len(p),它就必须返回一个 非nil 的错误。

golang 比较行为

  1. 指针只有指针指向的内容是同一个时才相等
  2. 接口值可以比较, 但若是接口值得类型相同,但是值却是不可比较的例如slice等,就会报runtime error

Slice, map, and function values are not comparable

func的receiver是 值时 无论调用者是指针还是值都可以调用func

如下若是使用指针作为receiver,则print 值时无法调用到String

1
2
3
4
5
6
7
8
9
10
11
12
13
type A struct {
c int
}

func (a A) String() string {
return "dj"
}

func format() {

a := &A{c: 22}
fmt.Println(a)
}

bufio的readSlice返回的是bufio.Reader的里的buf(Reader缓存的slice),因此当再此读取以后返回的slice的值是改变的

一般的reader都是没有缓冲的,bufio就是给reader加缓冲的

1
2
3
4
5
6
7
func readSlice() {
reader := bufio.NewReader(strings.NewReader("http://studygolang.com.\nIt is the home of gophers"))
line, _ := reader.ReadSlice('\n')
fmt.Println("the line is ", string(line))
n, _ := reader.ReadSlice('\n')
fmt.Println(string(n), string(line))
}

最后n和slice的值相同

而ReadBytes和ReadString就不会出现这个问题了,他们都新开辟的空间

如果ReadSlice在找到界定符之前遇到了error,
它就会返回缓存中所有的数据和错误本身(经常是 io.EOF)。
如果在找到界定符之前缓存已经满了,ReadSlice会返回bufio.ErrBufferFull错误。
当且仅当返回的结果(line)没有以界定符结束的时候,ReadSlice返回err != nil,
也就是说,如果ReadSlice返回的结果line不是以界定符delim结尾,那么返回的err也一定不等于nil
(可能是bufio.ErrBufferFull或io.EOF)。
并且当下次在读取时会继续向前

1
2
3
4
5
6
7
func ReadSliceSize() {
reader := bufio.NewReaderSize(strings.NewReader("http://studygolang.com.\nIt is the home of gophers"), 10)
line, _ := reader.ReadSlice('\n')
fmt.Println("the line is ", string(line))
n, _ := reader.ReadSlice('\n')
fmt.Println(string(n), string(line))
}

然而使用ReadBytes和ReadString就没有这个问题了,bufio.reader会自动为我们处理。

golang排序

对基本类型排序

  1. int, float等
    sort包有sort.Ints, sort.Float64s等方法排序,会改变原数组
  2. 其他类型则需要实现 sort.Interface接口了 ,如less,等

程序中使用time.Time

程序中应使用 Time 类型值来保存和传递时间,而不是指针。就是说,表示时间的变量和字段,应为time.Time类型,而不是*time.Time.类型。一个Time类型值可以被多个go程同时使用

time 格式化

这是实际开发中常用到的。

  1. time.Parse 和 time.ParseInLocation
  2. time.Time.Format
    解析
    对于解析,要特别注意时区问题,否则很容易出 bug。比如:

t, _ := time.Parse(“2006-01-02 15:04:05”, “2016-06-13 09:14:00”)
fmt.Println(time.Now().Sub(t).Hours())
2016-06-13 09:14:00 这个时间可能是参数传递过来的。这段代码的结果跟预期的不一样。

原因是 time.Now() 的时区是 time.Local,而 time.Parse 解析出来的时区却是 time.UTC(可以通过 Time.Location() 函数知道是哪个时区)。在中国,它们相差 8 小时。

所以,一般的,我们应该总是使用 time.ParseInLocation 来解析时间,并给第三个参数传递 time.Local。

string和number互转

  1. ParseBool, ParseFloat, ParseInt, and ParseUint convert strings to values
  2. FormatBool, FormatFloat, FormatInt, and FormatUint convert values to strings:

number和byte (数字和字节互转)

想进行地址的加减操作就得使用uintptr

1
2
3
4
bts := [5]byte{2, 3, 4, 5, 6}
fmt.Println(bts)
c := (*[4]byte)(unsafe.Pointer((uintptr(unsafe.Pointer(&bts)) + uintptr(1))))[:]
fmt.Println(c)

输出正确,直接使用 slice就错误(bts := []byte{2,3,4,5,6})
原因是:bts为slice的时候 bts的内部结构是

1
2
3
4
5
type SliceHeader struct {
Data uintptr
Len int
Cap int
}

也就是说转化为[]byte以后 只有Data的地址,Len和Cap的值被转化为了字节

string 和 []byte 无copy转换

// reflect.SliceHeader and reflect.StringHeader

1
2
3
4
5
6
7
8
9
10
type SliceHeader struct {
Data uintptr
Len int
Cap int
}

type StringHeader struct {
Data uintptr
Len int
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
struct string{
uint8 *str;
int len;
}
struct []uint8{
uint8 *array;
int len;
int cap;
}
uintptr是golang的内置类型,是能存储指针的整型,uintptr的底层类型是int,它和unsafe.Pointer可相互转换。
但是转换后的string与[]byte共享底层空间,如果修改了[]byte那么string的值也会改变,就违背了string应该是只读的规范了,可能会造成难以预期的影响。
*/
func str2byte(s string) []byte {
x := (*[2]uintptr)unsafe.Pointer(&s)
h := [3]uintptr{x[0],x[1],x[1]}
return *(*[]byte)(unsafe.Pointer(&h))
}
func byte2str(b []byte) string{
return *(*string)(unsafe.Pointer(&b))
}

golang json问题

  1. json tag json:"name,omitempty,type" json: “name,[option]”
  2. int 为0时,若tag 为omitempty, int会不被序列化出来, omitempty,tag里面加上omitempy,可以在序列化的时候忽略0值或者空值
    解决办法是使用 指针

    1
    2
    3
    4
    type Test struct {
    String *string `json:"string,omitempty"`
    Integer *int `json:"integer,omitempty"`
    }
  3. int8 byte uint8 json序列化的时候 会被当做字符串处理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    type CA struct {  
    List []uint8
    }

    func main() {
    ca := CA{[]uint8{1,2,3,4,5,6,7,8,9,0}}
    r, _ := json.Marshal(ca)
    fmt.Println(string(r)) //{"List":"AQIDBAUGBwgJAA=="}
    }

当我们想给某操作加缓冲区时就可以使用bytes.Buffer

instanceof带来的思考与es规范的研究

源码面前了无秘密,规范面前也一样啊!

问题

今天在jjc的群里正美大大抛出了一个问题

答案全是false

郁闷点

我很郁闷了,我觉得按照instanceof的执行过程应该是true啊!
为啥呢?因为一下两点

  • 我记忆中instanceof的流程是,比如: “k instanceof v”就是验证v.prototype 在 k的原型链中

证明如下

  • 而在REPL中查看 如下
    1
    2
    (2).\__proto\__ === Number.prototype
    // true

那么为啥 2 instanceof Number 是false呢

接下来我要开始装逼了

下面的es规范是基于ecma-262/5.1的,es6的规范有了一些变化,有了更多的新东西,更加严谨,但原理是一致的。

  • 先来讲讲js里的原始类型

我们知道js中的原始类型(primitive) 只有如下几种

1
Undefined, Null, Boolean, String, Number

es6中多了一个原始类型 Symbol

要注意,Number是指原始类型而不是内置的Number对象, 其他类似

因此 3, ‘123’, true, false, null, undefined等是原始类型,不是对象(Object)哦。

也就是说3 和 new Number(3)或者Number(3)不是一个类型哦,后两者是对象
参见下图

各位看官有没有看到里面的另一句话啊: Number object 可以通过是用Number函数方式的调用在转换为Number value。 Number(new Number(3)) instanceof Number === false

剩下的就是对象了Object,各种对象,就不缺对象

接下来上 instanceof 的规范, 一步一步揭开秘密

例子

1
3 instanceof Number

我们一步一步讲来

1. The instanceof operator

The production RelationalExpression : RelationalExpression instanceof ShiftExpression is evaluated as follows:

  1. Let lref be the result of evaluating RelationalExpression.

    计算左侧 为3

  2. Let lval be GetValue(lref).

    GetValue 调用返回值赋值给lval(此处我们只要知道GetValue(3) === 3就好了,后面会细讲)

  3. Let rref be the result of evaluating ShiftExpression.

    计算Number ,那就是Number

  4. Let rval be GetValue(rref).

    同2 这里rval = Number

  5. If Type(rval) is not Object, throw a TypeError exception.

    Type 调用会返回rval的类型,此处是对象Object[1]。

  6. If rval does not have a [[HasInstance]] internal method, throw a TypeError exception.

    判断rval 是否有HasInstance 方法

  7. Return the result of calling the [[HasInstance]] internal method of rval with argument lval.
    使用lval作为参数调用rval的[[HasInstance方法]] 就是 Number[[HasInstance]] (3)

接下来就该[[HasInstance]]了

2. [[HasInstance]] (V)

Assume F is a Function object.

假设F是一个函数对象

When the [[HasInstance]] internal method of F is called with value V, the following steps are taken:

当我们调用F的[[HasInstance]]方法时走如下步骤

  1. If V is not an object, return false.

    如果v不是对象,返回false
    很明显我们上面的调用就结束了。。。因为3不是个对象,而是个Number value

  2. Let O be the result of calling the [[Get]] internal method of F with property name “prototype”.

    获取 F的prototype 给 O

  3. If Type(O) is not Object, throw a TypeError exception.
  4. Repeat
    • Let V be the value of the [[Prototype]] internal property of V.
    • If V is null, return false.
    • If O and V refer to the same object, return true.

      找到V的原型链,然后依次向上查找,直到结束,若O与其中一个原型是同一个对象返回true, 否则false

NOTE Function objects created using Function.prototype.bind have a different implementation of [[HasInstance]] defined in 15.3.4.5.3.

到此我们已经搞定为了为啥 3 instanceof Number 是false了,打完,不收工。

还有一个Symbol啊,在此有个很魔性的地方,我上面引入的ecma262/5.1是没有Symbol的,我们去看ecma262/6.0,里面是有Symbol的,可是在那里我们可以看到Symbol也进级为了原始类型,因此Symbol(‘a’) instanceof Symbol自然也是 false了,你说魔性不魔性。

结束了?no 不结束!GetValue我们还没讲呢

在此我们先来看看 Reference是啥

官方解释如下

1
2
3
4
The Reference type is used to explain the behaviour of such operators
as delete, typeof, the assignment operators, the super keyword and
other language features. For example, the left-hand operand of an
assignment is expected to produce a reference.

我只看明白是为了解释某些操作而存在的,例如delete, typeof, super, left-hand operand等等,还有super keyword等等

Reference 含有3个component

  1. base value: any 甚至是Environment Record
  2. referenced name: string or symbol
  3. strict reference flag: true or false

那什么时候会创建一个Reference呢 规范并没有说怎么设置,我在stackoverflow 上看到了以下三个会创建并返回Reference的情况

  1. identifier reference expressions, that resolve the identifier in the current lexical environment (or one of its parents)
    标识符,解析标识符时会创建一个reference, base value is envRec, referenced name is identifier

  2. property accessor expressions, i.e. the .… and […] operators
    属性获取时会创建一个reference(这个就是我们后面要用到的哦)

  3. function calls to host functions are permitted to return them, but such don’t exist.
    这个在es5规范里有说,是可以返回,但没有规定一定返回

接下来说说我的另一个问题了,就是下面的类型转换是怎么发生的?Number value => Number object

1
2
(2).\__proto\__
// [Number: 0]

上面的转换很显然是获取属性发生的转换,我们一步一步解析,请看下面

1. “(2).__proto__” 是啥, 从表达式走起
1
2
3
4
5
6
7
8
9
10
11
11 Expressions

11.1 Primary Expressions
Syntax
PrimaryExpression :
this
Identifier
Literal
ArrayLiteral
ObjectLiteral
( Expression )

参见上面,我们”(2)”是PrimaryExpression, 再来看看 (Expression)是啥

1
2
3
4
11.1.6 The Grouping Operator
The production PrimaryExpression : ( Expression ) is evaluated as follows:

1. Return the result of evaluating Expression. This may be of type Reference.

NOTE This algorithm does not apply GetValue to the result of evaluating Expression. The principal motivation for this is so that operators such as delete and typeof may be applied to parenthesised expressions.
很显然我们”(2)”是group expression,按照规范的描述,group expression返回的有可能是Reference

2. “2” 咋计算

看了上面我们知道了”(2)”是啥,那按照规范所说的,”(2)”属于Express, 也可以看出来”2” 属于PrimaryExpression 中的Literal, 于是就有了下面的

1
2
11.1.3 Literal Reference
A Literal is evaluated as described in 7.8.

看到了Literal 参见7.8 具体就是规定了Number literal的语法

3. (2)我们解决了,那么就该”(2).__proto__”了, 这是一个属性获取,于是有了下面的规范, 规范先描述了属性获取的语法”.”和”[]”,然后是算法描述

The production MemberExpression : MemberExpression [ Expression ] is evaluated as follows:

  1. Let baseReference be the result of evaluating MemberExpression.

    左侧计算 得到 2 赋值给baseReference

  2. Let baseValue be GetValue(baseReference).

    调用GetValue, 对2 ,依然返回2

  3. Let propertyNameReference be the result of evaluating Expression.

    计算属性Expression 赋值给 propertyNameReference, 对于’.’调用会变为’[]’ 于是 就是”__proto__”字符串

  4. Let propertyNameValue be GetValue(propertyNameReference).

    调用GetValue, 对于propertyNameReference ,依然返回”__proto__”字符串

  5. Call CheckObjectCoercible(baseValue).

    判断baseValue不是Null或者Undefined

  6. Let propertyNameString be ToString(propertyNameValue).

    转为String

  7. If the syntactic production that is being evaluated is contained in strict mode code, let strict be true, else let strict be false.

  8. Return a value of type Reference whose base value is baseValue and whose referenced name is propertyNameString, and whose strict mode flag is strict.

    返回 Reference, base value是baseValue,对于我们的baseValue就是2,referenced name 是 propertyNameString,对于我们就是”__proto__”

The production CallExpression : CallExpression [ Expression ] is evaluated in exactly the same manner, except that the contained CallExpression is evaluated in step 1.

这句话的意思就是 CallExpress和属性获取类似。

到这里我们就完成了 “(2).__proto__”的完整解析,这个完整解析返回了一个Reference, 可是类型转换呢?

4. 对属性获取返回的Reference的使用

上面的步骤我们是完成了整个表达式的解析,但还没有使用表达式返回的结果,那我们来使用一下,比如 “(2).__proto__ === 2”
11.9.4 The Strict Equals Operator ( === )
The production EqualityExpression : EqualityExpression === RelationalExpression is evaluated as follows:

  1. Let lref be the result of evaluating EqualityExpression.

    解析 EqualityExpression ,我们从前面可以得知,”(2).__proto__”, 解析之后是一个Reference, 类似于 {baseValue: 2, referencedName: “__proto__”, strict:…}

  2. Let lval be GetValue(lref).

    重头戏来了 把我们上一步得到的Reference 作为参数调用 GetValue会返回什么呢? 见下

  3. Let rref be the result of evaluating RelationalExpression.

  4. Let rval be GetValue(rref).
  5. Return the result of performing the strict equality comparison rval === lval. (See 11.9.6)
5. GetValue调用

上面的严格等于我们就不分析了,不是重点,来看看GetValue吧。很长很高能

8.7.1 GetValue (V)

  1. If Type(V) is not Reference, return V.

    判断V是不是一个Reference,不是直接返回V, 现在明白了GetValue(2)为啥返回2了吧。

  2. Let base be the result of calling GetBase(V).

    获取base value, 对我们的Ref 就是2

  3. If IsUnresolvableReference(V), throw a ReferenceError exception.

    这是用来判断base value为非null和undefined的

  4. If IsPropertyReference(V), then

    1. If HasPrimitiveBase(V) is false, then let get be the [[Get]] internal method of base, otherwise let get be the special [[Get]] internal method defined below.

      base value 不是原始类型,则让get为base的内部[[Get]]方法,若是原始类型则参见下面的internal [[GET]]方法的逻辑,嗯,我们的base value是2,于是就得走下面的internal [[GET]]方法了

    2. Return the result of calling the get internal method using base as its this value, and passing GetReferencedName(V) for the argument.

      调用get,使用referenceName作为参数, base value作为this
      判断V的base value是不是object, number, string, Boolean

  5. Else, base must be an environment record.

    不是 上面列举的几类,那就一定是environment record

    1. Return the result of calling the GetBindingValue (see 10.2.1) concrete method of base passing GetReferencedName(V) and IsStrictReference(V) as arguments.

The following [[Get]] internal method is used by GetValue when V is a property reference with a primitive base value. It is called using base as its this value and with property P as its argument. The following steps are taken:

internal [[GET]]method

  1. Let O be ToObject(base).

    我的神啊,神啊,神啊,神啊,神啊,神啊,神啊,神啊,神啊,神啊,神啊,神啊,神啊,神啊,神啊, 终于看到了类型转换啊!!! ToObject(base), 这个很简单了,把原始类型转换为对应Object类型,比如2(Number Value), 转换为Number Object, 其他类似 参见

  2. Let desc be the result of calling the [[GetProperty]] internal method of O with property name P.

    …不解释了,往下就很容易理解了

  3. If desc is undefined, return undefined.
  4. If IsDataDescriptor(desc) is true, return desc.[[Value]].
  5. Otherwise, IsAccessorDescriptor(desc) must be true so, let getter be desc.[[Get]] (see 8.10).
  6. If getter is undefined, return undefined.
  7. Return the result calling the [[Call]] internal method of getter providing base as the this value and providing no arguments.

NOTE The object that may be created in step 1 is not accessible outside of the above method. An implementation might choose to avoid the actual creation of the object. The only situation where such an actual property access that uses this internal method can have visible effect is when it invokes an accessor function. 这些说的就是step 1创建的object,不应当被外部访问到。

总结

源码面前了无秘密,规范面前也一样啊!其实但我们理解了Reference 类型,那么this绑定的问题也就迎刃而解了,大家可以再去看看this的绑定问题也是基于Reference的


[1]Type(v)解释

根据具体的内容返回相应的类型,如字面3,返回Number Type,new Number(3) 返回Object 类型