为你的 Github Page 加上 HTTPS

概述

前几天突然收到一封邮件,长这样:

Hey Damon Zhao!

I am @nubela on Github and I found your project (se77en/se77en.github.io) on Github. There is a small chance you might have heard of some of my projects such as Javelin Browser or Gom VPN.

Anyways, I found that Github Page at se77en/se77en.github.io has a custom domain, and I was wondering if I can help you get it to HTTPS with a LetsEncrypt cert? (for free, of course!)

I work at Kloudsec (a free and minimal CDN for programmers) and I just built this tool to provision LetsEncrypt certs for github pages.

Will you like to try it? (and help me test it?) ;)

Steven.

意思就是他们做个一个可以免费帮你把 Github Page 加 HTTPS 的服务,活雷锋啊,谁信啊!反正我是没信,这哥们估计也猜到了我不会信,又连着发了几封,就是说反正不花钱,你的博客又没有秘密,试试呗。然后我就试试了。。。试完感觉上天了,赶紧回复这哥们这东西好啊,爽歪歪啊,你是活雷锋啊。他说我服务这么好你写篇博客替我宣传呗,我说必须哒!于是有个这篇博客。

先决条件

首先你得有个自定义域名,没有的话就别往下看了。

步骤

  1. 访问 活雷锋网站 Kloudsec,大概长这样,然后点击泛黄的按钮。 1
  2. 在弹出的白匡里输入你的域名。 2
  3. 输入邮箱和密码注册账号。 3
  4. 登录你的邮箱激活账号。 4
  5. 激活登录账号以后主页应该长这样。点击 GET INSTRUCTIONS5
  6. 接下来它会给出一些配置,登录你的域名 DNS 提供商修改这些配置,我用的是 dnspod,长这样(注意 A 记录只能留它给的这一个,之前的要全部删除掉),修改完之后点击 VERIFY DNS RECORDS6-2 6-2 6-3
  7. 验证完成(可能等待与人品成反比的时间)之后,回到主页点击类似播放键的按钮,会出现几个问题等待修复,一个一个修复就行,全程下一步。 7
  8. 然后等待获取 https 证书,同样等待时间与人品成反比,不要急,你人品不行。 8-2 8-2
  9. 现在可以访问 https://yourawesomeshittydomain.xxoo 来验证看是否成功了。
  10. 这时你可能会发现直接输入不加 https 前缀的域名不会自动跳转到 https 开头的域名,同样 www 开头的域名也不会,不用担心,活雷锋都替你想好了。
  11. 点击左侧边栏的 PROTECTION 然后把自动重定向到 https 都打开。 11-1 11-2
  12. 完成了,上天吧(额。。。上天是不可能的,不过谷歌会提高 https 网站的搜索排名,国内某(垃圾)搜索引擎不详)! 12

感谢

感觉那些活雷锋(steven@nubela.co)们。

探索 Rust 的所有权系统(Ownership System)

主要内容

这两个部分的介绍是为了给那些了解 rust 基本语法,写过一些小的 demo 代码,但却不是很清楚 ownershipborrowing 机制的码农看的。

我们从最简单的开始,然后一步一步逐渐复杂化,探索每一个细节。这个介绍文章假设你非常了解 letfnstructtraitimpl 概念。 我们的目标是学习如果写 rust 代码,而且在写的过程中不要碰到有关 ownershipborrowing 的墙。

首先是 ownership 部分:

  • 在简单介绍之后
  • 学习 Copy Traits,然后
  • 学习 不可变(Immutable)
  • 和 可变(Mutable) 所有权规则
  • 然后介绍一下所有权系统的强大之处
  • 体现在内存管理方面
  • 垃圾回收方面
  • 以及并发方面

先决条件 – 你应该预先知道的

基于 作用域/栈 的内存管理很简单,因为我们已经很熟悉它的工作方式了。比如下面的代码,imain 函数的最后发生了什么?

fn main() {
    let i = 5;
}

它离开了作用域然后释放了,对吗?

那如果我们把 i 传给另一个函数 foo,它释放了几次?

fn foo(i: i64) {
    // 干点啥
}

fn main() {
    let i = 5;
    foo(i);  // 调用 foo 函数
}

是的,它释放了两次。第一次是在 foo 函数结束时候,第二次是在 main 函数结束之后。如果你修改了 foo,那么完全不会影响 main

因为在调用 foo(i) 的时候值被拷贝了。

在 Rust 中,就像在 C艹 中(或者其他的语言),可以使用你的自定义类型来替代 Int。值将会被分配在当前栈,然后在离开作用域的时候被释放。

然而,Rust 编译器使用了不同的所有权规则,除非类型实现了 Copy 特质。 因此,我们先来讨论一下 Copy 特质,看看它是如何工作的。

Copy Trait

Copy 特质使类型的行为有了同样的方式:它每次赋值或者用作函数参数的时候,内存空间地址会被完整拷贝到另一个内存地址。实现这个特质允许你像使用内建 integer 一样使用你自定义类型。

内建的类型 i64(一种类型的 integer) 实现了这个特质,还有很多类型都实现了它。

如果我们有一个 Info 结构体,我们可以通过实现 Copy 特质来让它可拷贝。

struct Info {
    value: i64,
}
impl Copy for Info {}

或者,使用 #[derive(Copy)] 属性实现同样的功能

#[derive(Copy)]
struct Info {
    value: i64,
}

没有实现 Copy 特质的类型将会移动到另一个地址并且服从所有权规则。

所有权(Ownership)

所有权规则规定:对于一个不可被拷贝的值,在任意一个地方,只能有一个所有者可以改变它。

因此,如果一个函数有责任删除一个值,它可以确认未来没有其他的所有者会访问,修改或者删除它。

抽象的概念就说这么多,来看看具体的例子!

向 Bob 问好,我们的人体模型结构体。。。

为了证明数据是如何移动的,我们创建一个新的结构体,叫做 Bob

struct Bob {
    name: String,
}

在 Bob 的构造方法 new 中,我们宣布一下他的创建:

impl Bob {
    fn new(name: &str) -> Bob {
        println!("new bob {:?}", name);
        Bob { name: name.to_string() }
    }
}

当 Bob 被销毁时(对不起啦 Bob),我们通过实现内建的 Drop::drop 特质来打印一下他的名字:

impl Drop for Bob {
    fn drop(&mut self) {
        println!("del bob {:?}", self.name);
    }
}

为了让 bob 的值在打印时候可以格式化,我们试下一下内建的 Show::fmt 特质方法:

impl fmt::Show for Bob {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "bob {:?}", self.name)
    }
}

来测试一下

当我们在 main 函数中创建 Bob 时,我们得到一个预期的结果:

fn main() {
    Bob::new("A");
}
new bob "A"
del bob "A"

好的,bob 挂了,但是啥时候挂的?

让我们在函数结尾插入一个 “print” 语句:

fn main () {
    Bob::new("A");
    println!{"end is near"};
}
new bob "A"
del bob "A"
end is near

他在函数结束前就已经挂了。返回值并没有被赋值给任何东西,所以编译器调用了 drop 来在他创建的地方销毁他。

如果我们绑定返回值给一个变量呢?

fn main() {
    let bob = Bob::new("A")
    println!("end is near");
}
new bob "A"
end is near
del bob "A"

有了 let 绑定,他在函数结束时才会挂,也就是在离开作用域时。所以,编译器在作用域结束时销毁绑定值

销毁它,除非它移动了

值可以被移动到另一个地方,如果它移动了,它不会被销毁!

那么怎么移动呢?其实也就是简单地把它们作为值传给另一个函数。

让我们把我们的 bob 值传给一个叫 black_hole 的函数:

fn black_hole(bob: Bob) {
    println!("imminent shrinkage {:?}", bob);
}

fn main() {
    let bob = Bob::new("A");
    black_hole(bob);
    println!(""end is near);
}
new bob "A"
imminent shrikage bob "A"
del bob "A"
end is near

自己试一下

bob 在 black_hole 函数里挂了,而不是在 main 函数的结尾!

等一下,如果我们把 Bob 传给 black_hole 两次会发生什么呢?

fn main() {
    let bob = Bob::new("A");
    black_hole(bob);
    black_hole(bob);
}
<anon>:35:16: 35:19 error: use of moved value: `bob`
<anon>:35     black_hole(bob);
                         ^~~
<anon>:34:16: 34:19 note: `bob` moved here because it has type `Bob`, which is non-copyable
<anon>:34     black_hole(bob);
                         ^~~

编译器告诉我们不可以使用已经移动的值,而且做了详细的解释。

没有魔法 – 只是规则而已

为了实现 “不用垃圾回收机制的内存安全”,编译器不需要追踪你代码里的值。编译器可以通过观测函数体来确定函数需要销毁哪些值。

如果你知道规则,你也可以简单的做到这些。到目前为止,我们知道了一些规则:

  • 没有使用的返回值会被销毁
  • 所有被绑定到 let 的值都会在函数结尾处被销毁,除非它被移动了

现在我们知道了,内存安全实际是基于一个值只能有一个所有者。

然而,到目前为止我们只是讨论了不可变let 绑定 - 当我们的值可变时,规则会变得略微复杂。

所有权(可变性)

所有的值都可以被改变:我们只需要在变量名和 let 之间加入 mut。举个栗子,我们可以改变 bob 的一些地方,比如说名字:

fn main() {
    let mut bob = Bob::new("A");
    bob.name = String::from_str("mutant");
}
new bob "A"
del bob "mutant"

我们以名字 “A” 创建了他,但是以名字 “mutant” 销毁了他。

如果我们把这个值传给另一个函数 mutate,我们同样可以把它赋值给 mut 修饰的变量:

fn mutate(value: Bob) {
    let mut bob = value;
    bob.name = String::from_str("mutant");
}

fn main() {
    mutate(Bob::new("A"));
}
new bob "A"
del bob "mutant"

所以,可以在任意时刻改变可变类型的值。

一些需要了解的知识点:函数参数也可以变成用 mut 修饰的,因为它也是用于绑定的关键字,就像 let 一样。所以上面的例子可以被改写成:

fn mutate(mut value: Bob) {  // 在参数名前直接使用 mut
    value.name = String::from_str("mutant");
}

替换 mut 修饰的值

如果我们重写 mut 修饰的值会发生什么?来看看:

fn main() {
    let mut bob = Bob::new("A");
    println!("");
    for &name in ["B", "C"].iter() {
        println!("before overwrite");
        bob = Bob::new(name);
        println!("after overwrite");
        println!("");
    }
}
new bob "A"

before overwrite
new bob "B"
del bob "A"
after overwrite

before overwrite
new bob "C"
del bob "B"
after overwrite

del bob "C"

旧的值被销毁了。新的赋值会在作用域结尾处被销毁 – 除非它被移动了或者是被再次重写。

所有权(可变)规则

相对于不可变性,可变性只有一条附加规则:

  • 没有使用的返回值会被销毁
  • 所有被绑定到 let 的值都会在函数结尾处被销毁,除非它被移动了
  • 被替换的值将被销毁

很明显了,在 Rust 中,我们可以确定一个值没有所有者或者被引用。

所有权系统的威力

刚开始这些所有权规则可能看起来有一些限制性,这仅仅是因为你使用了一套新的规则集。他们实际上并没有任何限制,只是给了我们另外一个基础设施去创建高级别架构。

这些架构在别的语言中可能很难实现安全性。即使他们做到了安全性,他们也不能保证编译期就确定安全性。

下面我们来概览一下它们,它们在一个独立的库中。

内存分配

目前为止我们只讨论了类似 integer 的值,它们存活在上。我们的测试人偶 Bob 就是这样一个类型的值。一些流行的语言也会把值保持在栈上(比如 C# 中的 struct,C艹中非 new 实例化出来的值),其他大部分语言都不是。

相反的,一个新的构造对象实例(在很多语言中通过 new 操作符创建的)在叫做堆内存的地方创建。

堆内存有很多优点。第一,它不受栈大小的限制。把一个大的结构放到栈上可能会马上溢出。第二,它的内存地址不会改变,不想栈地址。每次一个栈内存上面的值被移动或者拷贝,它所有的比特都会被拷贝到栈的另一个地址。当结构小的时候它是很有效率的(因为这样值会挨着嘛),不过随着结构变大就会变的很慢。

Box 解决了这个问题,处理方法是把我们创建的值移动到上,然后在上存一个指向堆地址的指针。

举个栗子,我们像这样创建 Bob 在堆内存上:

fn main() {
    let bob = Box::new(Bob::new("A"));
}
new bob A
del bob A

bob 的返回值类型是 Box<Bob>。泛型类型使 Bob 的生命周期被 Box<Bob> 管理,同时当 Box 被删除时它也被随之删除。

Box 是不可拷贝的,其所有权机制跟上面提到的一样。当它在栈上的生命周期结束时,它的析构方法 drop 被调用,随后立即调用 Bob 的析构方法 drop,同样会清理堆上的内存。

这些看似琐碎的实现其实是重大的策略。如果我们跟其他语言的解决办法比较,它们大都做了两件事情中的一件。它们或者留给你自己清理内存(用一些讨厌的 delete 语句,可能忘了调用或者调用多次),或者依赖垃圾回收机制去跟踪内存指针,当这些指针不被引用时清理内存。

在 Rust 中,所有权跟踪不会有运行时消耗,而且会在编译器确认正确性。这个简单的通过 Box 的内存处理方案,小而美,而且经常已经足够用了。

当它真的不够用时,其它的工具会来帮忙。

垃圾回收

Rust 有足够的低级别(low-level)工具来用一个库的方式实现垃圾回收。最简单的方案已经存在于 Rust 中:基于引用计数(注意:不是自动引用计数)的垃圾回收。

引用计数的解决方案很容易实现,不过它不是我们所说的真正意义上的垃圾回收。

因此在 Rust 中我们给它起了个新名字叫做:*共享所有权*。std::rc 库提供了在不容 Rc 处理者(handler)中共享同一个值所有权的机制。只要有一个处理者作用于这个值上,这个值就会保持存活。

举个栗子,我们可以创建一个被 Rc 处理者所管理的 bob 实例:

use std::rc::Rc;

fn main() {
    let bob = Rc::new(Bob::new("A"));
    println!("{:?}", bob);
}
new bob A
Rc(bob A)
del bob A

自己试一下

我们可以改变 black_hole 函数来接受一个 Rc<Bob> 然后检查是否被销毁。但是我们可以更简单的让它接受任意类型T 然后实现 Show 特质来方便打印。让我们来让它泛型话:

fn black_hole<T>(value: T) where T: fmt::Show {
    println!("imminent shrinkage {:?}", value);
}

工作方式一样,不过我们以后有新的类型改变时就不需要改变这个函数啦~

现在,发送 Rc<Bob> 到 black_hole!

fn main() {
    let bob = Rc::new(Bob::new("A"));
    black_hole(bob.clone());
    print!("{:?}", bob);
}
new bob A
imminent shrinkage Rc(bob A)
Rc(bob A)
del bob A

自己试一下

它在 black_hole 里存活了下来!不过它是怎么工作的?

一旦被 Rc 处理,那么其它地方只要有任何 Rc 克隆存在,bob 就会一直存活。Rc 在内部使用 Box 把值同引用计数一起放到堆内存。

每次一个新的处理者克隆被创建(通过调用 clone 或者 Rc),引用计数就会加,当它生命周期结束时,引用计数会减。当引用计数达到 0 时,对象会被销毁,内存也会被释放。

注意:上面所说的 Rc 是不可变的。如果 Bob 的内容需要被改变,他可以被附加的用 RefCell 类型包装,这时就允许借用(borrow)单个 bob 实例的引用。下面的例子中它将能在 mutate 函数中被改变。

fn mutate(bob: Rc<RefCell<Bob>>) {
    bob.borrow_mut().name = String::from_str("mutant");
}

fn main() {
    let bob = Rc::new(RefCell::new(Bob::new("A")));
    mutate(bob.clone());
    println!("{:?}", bob);
}
new bob A
Rc(RefCell { value: bob mutant })
del bob mutant

这个例子证明了不同的低级别工具是如何以最小的代价来组合实现精确的垃圾回收。

举个栗子,Rc 只能被使用在同一线程中。不过另外一个类型 Arc 可以被不同线程使用。一个可变的 Rc 可能会被多个对象互相引用。不过,Rc 可以被克隆成弱引用(Weak) ,这样就不会参与引用计数了。更多的信息请查看官方文档

最重要的是,更多高级的垃圾回收机制可以(即将)被实现,而且都是使用库的形式。

并发

让我们看看 Rust 是如何改变我们使用线程的方式的。默认的模式是没有竞态数据。竞态数据不会发生是因为有很多特别的安全方式作用在线程上。原则上,你可以通过这些安全特性创建你自己的线程库,简单是因为所有权模型本身是线程安全的。

考虑一下我们将一个 Bob(可移动的) 和一个 integet(可拷贝的)发送到一个新的 Rust 线程中:

use std::thread::Thread

fn main() {
    let bob = Bob::new("A");
    let i : i64 = 12;
    let guard = Thread::scoped(move || {
        println!("From thread, {:?} and {:?}!", bob, i);
    });
    println!("waiting for thread to end");
}
new bob A
waiting for thread to end
From thread, bob A and 12i64
del bob A

自己试一下

发生了什么?首先,我们创建了两个值:bobi。然后我们通过 Thread::scoped 创建了一个新的线程并且传入一个闭包让它执行。闭包捕获了我们的两个值。

捕获对 bobi 来说意味着不同的事。bob 会移动到闭包中(对外部线程不可用),i 则会被拷贝到闭包中,不过它还会对外部线程可用。

主线程会停下来等到新创建的线程执行完毕,执行完毕的标志是 guard 达到生命周期的尽头(在这个例子中也就是 main 函数的结尾)。

你可能会指出这跟你之前使用线程没啥区别 – 我们大家都知道如果没有某些同步机制是不可以随便在不同线程之间共享内存地址的。Rust 的不同之处在于它在编译期就强制了这一点。

当然,我们能获取到 guard 的返回值,也可以创建一个管道在不同线程中来发送和接受值。更多的信息请查看官方线程文档管道文档,和这本书

还有啥?

现在我们熟悉了 Rust 中的所有权系统,可以查看文档写安全代码啦。

不过还有一部分还没有讲到:借用系统(borrowing system)。

在这个系列的第二部分中我们将学习为什么借用机制很有用,已经怎么最好的使用它。

原文地址:http://nercury.github.io/rust/guide/2015/01/19/ownership.html

使用 buffered channel 实现线程安全的 pool

概述

我们已经知道 Go 语言提供了 sync.Pool,但是做的不怎么好,所以有必要自己来实现一个 pool。

给我看代码

type Pool struct {
  pool chan *Client
}

// 创建一个新的 pool
func NewPool(max int) *Pool {
  return &Pool{
    pool: make(chan *Client, max),
  }
}

// 从 pool 里借一个 Client
func (p *Pool) Borrow() *Client {
  var cl *Client
  select {
  case cl = <-p.pool:
  default:
    cl = newClient()
  }
  return cl
}

// 还回去
func (p *Pool) Return(cl *Client) {
  select {
  case p.pool <- cl:
  default:
    // let it go, let it go...
  }
}

总结

现在不要使用 sync.Pool

Go 语言中的 Array,Slice,Map 和 Set

Array(数组)

内部机制

在 Go 语言中数组是固定长度的数据类型,它包含相同类型的连续的元素,这些元素可以是内建类型,像数字和字符串,也可以是结构类型,元素可以通过唯一的索引值访问,从 0 开始。

数组是很有价值的数据结构,因为它的内存分配是连续的,内存连续意味着可是让它在 CPU 缓存中待更久,所以迭代数组和移动元素都会非常迅速。

数组声明和初始化

通过指定数据类型和元素个数(数组长度)来声明数组。

// 声明一个长度为5的整数数组
var array [5]int

一旦数组被声明了,那么它的数据类型跟长度都不能再被改变。如果你需要更多的元素,那么只能创建一个你想要长度的新的数组,然后把原有数组的元素拷贝过去。

Go 语言中任何变量被声明时,都会被默认初始化为各自类型对应的 0 值,数组当然也不例外。当一个数组被声明时,它里面包含的每个元素都会被初始化为 0 值。

一种快速创建和初始化数组的方法是使用数组字面值。数组字面值允许我们声明我们需要的元素个数并指定数据类型:

// 声明一个长度为5的整数数组
// 初始化每个元素
array := [5]int{7, 77, 777, 7777, 77777}

如果你把长度写成 ...,Go 编译器将会根据你的元素来推导出长度:

// 通过初始化值的个数来推导出数组容量
array := [...]int{7, 77, 777, 7777, 77777}

如果我们知道想要数组的长度,但是希望对指定位置元素初始化,可以这样:

// 声明一个长度为5的整数数组
// 为索引为1和2的位置指定元素初始化
// 剩余元素为0值
array := [5]int{1: 77, 2: 777}

使用数组

使用 [] 操作符来访问数组元素:

array := [5]int{7, 77, 777, 7777, 77777}
// 改变索引为2的元素的值
array[2] = 1

我们可以定义一个指针数组:

array := [5]*int{0: new(int), 1: new(int)}

// 为索引为0和1的元素赋值
*array[0] = 7
*array[1] = 77

在 Go 语言中数组是一个值,所以可以用它来进行赋值操作。一个数组可以被赋值给任意相同类型的数组:

var array1 [5]string
array2 := [5]string{"Red", "Blue", "Green", "Yellow", "Pink"}
array1 = array2

注意数组的类型同时包括数组的长度和可以被存储的元素类型,数组类型完全相同才可以互相赋值,比如下面这样就不可以:

var array1 [4]string
array2 := [5]string{"Red", "Blue", "Green", "Yellow", "Pink"}
array1 = array2

// 编译器会报错
Compiler Error:
cannot use array2 (type [5]string) as type [4]string in assignment

拷贝一个指针数组实际上是拷贝指针值,而不是指针指向的值:

var array1 [3]*string
array2 := [3]*string{new(string), new(string), new(string)}
*array2[0] = "Red"
*array2[1] = "Blue"
*array2[2] = "Green"

array1 = array2
// 赋值完成后,两组指针数组指向同一字符串

多维数组

数组总是一维的,但是可以组合成多维的。多维数组通常用于有父子关系的数据或者是坐标系数据:

// 声明一个二维数组
var array [4][2]int

// 使用数组字面值声明并初始化
array := [4][2]int{{10, 11}, {20, 21}, {30, 31}, {40, 41}}

// 指定外部数组索引位置初始化
array := [4][2]int{1: {20, 21}, 3: {40, 41}}

// 同时指定内外部数组索引位置初始化
array := [4][2]int{1: {0: 20}, 3: {1: 41}}

同样通过 [] 操作符来访问数组元素:

var array [2][2]int

array[0][0] = 0
array[0][1] = 1
array[1][0] = 2
array[1][1] = 3

也同样的相同类型的多维数组可以相互赋值:

var array1 = [2][2]int
var array2 = [2][2]int

array[0][0] = 0
array[0][1] = 1
array[1][0] = 2
array[1][1] = 3

array1 = array2

因为数组是值,我们可以拷贝单独的维:

var array3 [2]int = array1[1]
var value int = array1[1][0]

在函数中传递数组

在函数中传递数组是非常昂贵的行为,因为在函数之间传递变量永远是传递值,所以如果变量是数组,那么意味着传递整个数组,即使它很大很大很大。。。

举个栗子,创建一个有百万元素的整形数组,在64位的机器上它需要8兆的内存空间,来看看我们声明它和传递它时发生了什么:

var array [1e6]int
foo(array)
func foo(array [1e6]int) {
  ...
}

每一次 foo 被调用,8兆内存将会被分配在栈上。一旦函数返回,会弹栈并释放内存,每次都需要8兆空间。

Go 语言当然不会这么傻,有更好的方法来在函数中传递数组,那就是传递指向数组的指针,这样每次只需要分配8字节内存:

var array [1e6]int
foo(&array)
func foo(array *[1e6]int){
  ...
}

但是注意如果你在函数中改变指针指向的值,那么原始数组的值也会被改变。幸运的是 slice(切片)可以帮我们处理好这些问题,来一起看看。

Slice(切片)

内部机制和基础

slice 是一种可以动态数组,可以按我们的希望增长和收缩。它的增长操作很容易使用,因为有内建的 append 方法。我们也可以通过 relice 操作化简 slice。因为 slice 的底层内存是连续分配的,所以 slice 的索引,迭代和垃圾回收性能都很好。

slice 是对底层数组的抽象和控制。它包含 Go 需要对底层数组管理的三种元数据,分别是:

  1. 指向底层数组的指针
  2. slice 中元素的长度
  3. slice 的容量(可供增长的最大值)

创建和初始化

Go 中创建 slice 有很多种方法,我们一个一个来看。

第一个方法是使用内建的函数 make。当我们使用 make 创建时,一个选项是可以指定 slice 的长度:

slice := make([]string, 5)

如果只指定了长度,那么容量默认等于长度。我们可以分别指定长度和容量:

slice := make([]int, 3, 5)

当我们分别指定了长度和容量,我们创建的 slice 就可以拥有一开始并没有访问的底层数组的容量。上面代码的 slice 中,可以访问3个元素,但是底层数组有5个元素。两个与长度不相干的元素可以被 slice 来用。新创建的 slice 同样可以共享底层数组和已存在的容量。

不允许创建长度大于容量的 slice:

slice := make([]int, 5, 3)

Compiler Error:
len larger than cap in make([]int)

惯用的创建 slice 的方法是使用 slice 字面量。跟创建数组很类似,不过不用指定 []里的值。初始的长度和容量依赖于元素的个数:

// 创建一个字符串 slice
// 长度和容量都是 5
slice := []string{"Red", "Blue", "Green", "Yellow", "Pink"}

在使用 slice 字面量创建 slice 时有一种方法可以初始化长度和容量,那就是初始化索引。下面是个例子:

// 创建一个字符串 slice
// 初始化一个有100个元素的空的字符串 slice
slice := []string{99: ""}

nil 和 empty slice

有的时候我们需要创建一个 nil slice,创建一个 nil slice 的方法是声明它但不初始化它:

var slice []int

创建一个 nil slice 是创建 slice 最基本的方法,很多标准库和内建函数都可以使用它。当我们想要表示一个并不存在的 slice 时它变得非常有用,比如一个返回 slice 的函数中发生异常的时候。

创建 empty slice 的方法就是声明并初始化一下:

// 使用 make 创建
silce := make([]int, 0)

// 使用 slice 字面值创建
slice := []int{}

empty slice 包含0个元素并且底层数组没有分配存储空间。当我们想要表示一个空集合时它很有用处,比如一个数据库查询返回0个结果。

不管我们用 nil slice 还是 empty slice,内建函数 appendlencap的工作方式完全相同。

使用 slice

为一个指定索引值的 slice 赋值跟之前数组赋值的做法完全相同。改变单个元素的值使用 [] 操作符:

slice := []int{10, 20, 30, 40, 50}
slice[1] = 25

我们可以在底层数组上对一部分数据进行 slice 操作,来创建一个新的 slice:

// 长度为5,容量为5
slice := []int{10, 20, 30, 40, 50}

// 长度为2,容量为4
newSlice := slice[1:3]

在 slice 操作之后我们得到了两个 slice,它们共享底层数组。但是它们能访问底层数组的范围却不同,newSlice 不能访问它头指针前面的值。

计算任意 new slice 的长度和容量可以使用下面的公式:

对于 slice[i:j] 和底层容量为 k 的数组
长度:j - i
容量:k - i

必须再次明确一下现在是两个 slice 共享底层数组,因此只要有一个 slice 改变了底层数组的值,那么另一个也会随之改变:

slice := []int{10, 20, 30, 40, 50}
newSlice := slice[1:3]
newSlice[1] = 35

改变 newSlice 的第二个元素的值,也会同样改变 slice 的第三个元素的值。

一个 slice 只能访问它长度范围内的索引,试图访问超出长度范围的索引会产生一个运行时错误。容量只可以用来增长,它只有被合并到长度才可以被访问:

slice := []int{10, 20, 30, 40, 50}
newSlice := slice[1:3]
newSlice[3] = 45

Runtime Exception:
panic: runtime error: index out of range

容量可以被合并到长度里,通过内建的 append 函数。

slice 增长

slice 比 数组的优势就在于它可以按照我们的需要增长,我们只需要使用 append 方法,然后 Go 会为我们做好一切。

使用 append 方法时我们需要一个源 slice 和需要附加到它里面的值。当 append 方法返回时,它返回一个新的 slice,append 方法总是增长 slice 的长度,另一方面,如果源 slice 的容量足够,那么底层数组不会发生改变,否则会重新分配内存空间。

// 创建一个长度和容量都为5的 slice
slice := []int{10, 20, 30, 40, 50}

// 创建一个新的 slice
newSlice := slice[1:3]

// 为新的 slice append 一个值
newSlice = append(newSlice, 60)

因为 newSlice 有可用的容量,所以在 append 操作之后 slice 索引为 3 的值也变成了 60,之前说过这是因为 slice 和 newSlice 共享同样的底层数组。

如果没有足够可用的容量,append 函数会创建一个新的底层数组,拷贝已存在的值和将要被附加的新值:

// 创建长度和容量都为4的 slice
slice := []int{10, 20, 30, 40}

// 附加一个新值到 slice,因为超出了容量,所以会创建新的底层数组
newSlice := append(slice, 50)

append 函数重新创建底层数组时,容量会是现有元素的两倍(前提是元素个数小于1000),如果元素个数超过1000,那么容量会以 1.25 倍来增长。

slice 的第三个索引参数

slice 还可以有第三个索引参数来限定容量,它的目的不是为了增加容量,而是提供了对底层数组的一个保护机制,以方便我们更好的控制 append 操作,举个栗子:

source := []string{"apple", "orange", "plum", "banana", "grape"}

// 接着我们在源 slice 之上创建一个新的 slice
slice := source[2:3:4]

新创建的 slice 长度为 1,容量为 2,可以看出长度和容量的计算公式也很简单:

对于 slice[i:j:k]  或者 [2:3:4]

长度: j - i       或者   3 - 2
容量: k - i       或者   4 - 2

如果我们试图设置比可用容量更大的容量,会得到一个运行时错误:

slice := source[2:3:6]


Runtime Error:
panic: runtime error: slice bounds out of range

限定容量最大的用处是我们在创建新的 slice 时候限定容量与长度相同,这样以后再给新的 slice 增加元素时就会分配新的底层数组,而不会影响原有 slice 的值:

source := []string{"apple", "orange", "plum", "banana", "grape"}

// 接着我们在源 slice 之上创建一个新的 slice
// 并且设置长度和容量相同
slice := source[2:3:3]

// 添加一个新元素
slice = append(slice, "kiwi")

如果没有第三个索引参数限定,添加 kiwi 这个元素时就会覆盖掉 banana。

内建函数 append 是一个变参函数,意思就是你可以一次添加多个元素,比如:

s1 := []int{1, 2}
s2 := []int{3, 4}

fmt.Printf("%v\n", append(s1, s2...))

Output:
[1 2 3 4]

迭代 slice

slice 也是一种集合,所以可以被迭代,用 for 配合 range 来迭代:

slice := []int{10, 20, 30, 40, 50}

for index, value := range slice {
  fmt.Printf("Index: %d  Value: %d\n", index, value)
}

Output:
Index: 0  Value: 10
Index: 1  Value: 20
Index: 2  Value: 30
Index: 3  Value: 40
Index: 4  Value: 50

当迭代时 range 关键字会返回两个值,第一个是索引值,第二个是索引位置值的拷贝。注意:返回的是值的拷贝而不是引用,如果我们把值的地址作为指针使用,会得到一个错误,来看看为啥:

slice := []int{10, 20, 30 ,40}

for index, value := range slice {
  fmt.Printf("Value: %d  Value-Addr: %X  ElemAddr: %X\n", value, &value, &slice[index])
}

Output:
Value: 10  Value-Addr: 10500168  ElemAddr: 1052E100
Value: 20  Value-Addr: 10500168  ElemAddr: 1052E104
Value: 30  Value-Addr: 10500168  ElemAddr: 1052E108
Value: 40  Value-Addr: 10500168  ElemAddr: 1052E10C

value 变量的地址总是相同的因为它只是包含一个拷贝。如果想得到每个元素的真是地址可以使用 &slice[index]。

如果不需要索引值,可以使用 _ 操作符来忽略它:

slice := []int{10, 20, 30, 40}

for _, value := range slice {
  fmt.Printf("Value: %d\n", value)
}


Output:
Value: 10
Value: 20
Value: 30
Value: 40

range 总是从开始一次遍历,如果你想控制遍历的step,就用传统的 for 循环:

slice := []int{10, 20, 30, 40}

for index := 2; index &lt; len(slice); index++ {
  fmt.Printf("Index: %d  Value: %d\n", index, slice[index])
}


Output:
Index: 2  Value: 30
Index: 3  Value: 40

同数组一样,另外两个内建函数 lencap 分别返回 slice 的长度和容量。

多维 slice

也是同数组一样,slice 可以组合为多维的 slice:

slice := [][]int{{10}, {20, 30}}

需要注意的是使用 append 方法时的行为,比如我们现在对 slice[0] 增加一个元素:

slice := [][]int{{10}, {20, 30}}
slice[0] = append(slice[0], 20)

那么只有 slice[0] 会重新创建底层数组,slice[1] 则不会。

在函数间传递 slice

在函数间传递 slice 是很廉价的,因为 slice 相当于是指向底层数组的指针,让我们创建一个很大的 slice 然后传递给函数调用它:

slice := make([]int, 1e6)

slice = foo(slice)

func foo(slice []int) []int {
    ...
    return slice
}

在 64 位的机器上,slice 需要 24 字节的内存,其中指针部分需要 8 字节,长度和容量也分别需要 8 字节。

Map

内部机制

map 是一种无序的键值对的集合。map 最重要的一点是通过 key 来快速检索数据,key 类似于索引,指向数据的值。

map 是一种集合,所以我们可以像迭代数组和 slice 那样迭代它。不过,map 是无序的,我们无法决定它的返回顺序,这是因为 map 是使用 hash 表来实现的。

map 的 hash 表包含了一个桶集合(collection of buckets)。当我们存储,移除或者查找键值对(key/value pair)时,都会从选择一个桶开始。在映射(map)操作过程中,我们会把指定的键值(key)传递给 hash 函数(又称散列函数)。hash 函数的作用是生成索引,索引均匀的分布在所有可用的桶上。hash 表算法详见:July的博客–从头到尾彻底解析 hash 表算法

创建和初始化

Go 语言中有多种方法创建和初始化 map。我们可以使用内建函数 make 也可以使用 map 字面值:

// 通过 make 来创建
dict := make(map[string]int)

// 通过字面值创建
dict := map[string]string{"Red": "#da1337", "Orange": "#e95a22"}

使用字面值是创建 map 惯用的方法(为什么不使用make)。初始化 map 的长度依赖于键值对的数量。

map 的键可以是任意内建类型或者是 struct 类型,map 的值可以是使用 ==操作符的表达式。slice,function 和 包含 slice 的 struct 类型不可以作为 map 的键,否则会编译错误:

dict := map[[]string]int{}

Compiler Exception:
invalid map key type []string

使用 map

给 map 赋值就是指定合法类型的键,然后把值赋给键:

colors := map[string]string{}
colors["Red"] = "#da1337"

如果不初始化 map,那么就会创建一个 nil map。nil map 不能用来存放键值对,否则会报运行时错误:

var colors map[string]string
colors["Red"] = "#da1337"

Runtime Error:
panic: runtime error: assignment to entry in nil map

测试 map 的键是否存在是 map 操作的重要部分,因为它可以让我们判断是否可以执行一个操作,或者是往 map 里缓存一个值。它也可以被用来比较两个 map 的键值对是否匹配或者缺失。

从 map 里检索一个值有两种选择,我们可以同时检索值并且判断键是否存在:

value, exists := colors["Blue"]
if exists {
  fmt.Println(value)
}

另一种选择是只返回值,然后判断是否是零值来确定键是否存在。但是只有你确定零值是非法值的时候这招才管用:

value := colors["Blue"]
if value != "" {
  fmt.Println(value)
}

当索引一个 map 取值时它总是会返回一个值,即使键不存在。上面的例子就返回了对应类型的零值。

迭代一个 map 和迭代数组和 slice 是一样的,使用 range 关键字,不过在迭代 map 时我们不使用 index/value 而使用 key/value 结构:

colors := map[string]string{
    "AliceBlue":   "#f0f8ff",
    "Coral":       "#ff7F50",
    "DarkGray":    "#a9a9a9",
    "ForestGreen": "#228b22",
}

for key, value := range colors {
  fmt.Printf("Key: %s  Value: %s\n", key, value)
}

如果我们想要从 map 中移除一个键值对,使用内建函数 delete(要是也能返回移除是否成功就好了,哎。。。):

delete(colors, "Coral")

for key, value := range colors {
  fmt.Println("Key: %s  Value: %s\n", key, value)
}

在函数间传递 map

在函数间传递 map 不是传递 map 的拷贝。所以如果我们在函数中改变了 map,那么所有引用 map 的地方都会改变:

func main() {
  colors := map[string]string{
     "AliceBlue":   "#f0f8ff",
     "Coral":       "#ff7F50",
     "DarkGray":    "#a9a9a9",
     "ForestGreen": "#228b22",
  }

  for key, value := range colors {
      fmt.Printf("Key: %s  Value: %s\n", key, value)
  }

  removeColor(colors, "Coral")

  for key, value := range colors {
      fmt.Printf("Key: %s  Value: %s\n", key, value)
  }
}

func removeColor(colors map[string]string, key string) {
    delete(colors, key)
}

执行会得到以下结果:

Key: AliceBlue Value: #F0F8FF
Key: Coral Value: #FF7F50
Key: DarkGray Value: #A9A9A9
Key: ForestGreen Value: #228B22
    
Key: AliceBlue Value: #F0F8FF
Key: DarkGray Value: #A9A9A9
Key: ForestGreen Value: #228B22

可以看出来传递 map 也是十分廉价的,类似 slice。

Set

Go 语言本身是不提供 set 的,但是我们可以自己实现它,下面就来试试:

package main

import(
  "fmt"
  "sync"
)

type Set struct {
  m map[int]bool
  sync.RWMutex
}

func New() *Set {
  return &Set{
    m: map[int]bool{},
  }
}

func (s *Set) Add(item int) {
  s.Lock()
  defer s.Unlock()
  s.m[item] = true
}

func (s *Set) Remove(item int) {
  s.Lock()
  s.Unlock()
  delete(s.m, item)
}

func (s *Set) Has(item int) bool {
  s.RLock()
  defer s.RUnlock()
  _, ok := s.m[item]
  return ok
}

func (s *Set) Len() int {
  return len(s.List())
}

func (s *Set) Clear() {
  s.Lock
  defer s.Unlock()
  s.m = map[int]bool{}
}

func (s *Set) IsEmpty() bool {
  if s.Len() == 0 {
    return true
  }
  return false
}

func (s *Set) List() []int {
  s.RLock()
  defer s.RUnlock()
  list := []int{}
  for item := range s.m {
    list = append(list, item)
  }
  return list
}

func main() {
  // 初始化
  s := New()
  
  s.Add(1)
  s.Add(1)
  s.Add(2)

  s.Clear()
  if s.IsEmpty() {
    fmt.Println("0 item")
  }
  
  s.Add(1)
  s.Add(2)
  s.Add(3)
  
  if s.Has(2) {
    fmt.Println("2 does exist")
  }
  
  s.Remove(2)
  s.Remove(3)
  fmt.Println("list of all items", S.List())
}

注意我们只是使用了 int 作为键,你可以自己实现用 interface{} 作为键,做成更通用的 Set,另外,这个实现是线程安全的。

总结

  • 数组是 slice 和 map 的底层结构。
  • slice 是 Go 里面惯用的集合数据的方法,map 则是用来存储键值对。
  • 内建函数 make 用来创建 slice 和 map,并且为它们指定长度和容量等等。slice 和 map 字面值也可以做同样的事。
  • slice 有容量的约束,不过可以通过内建函数 append 来增加元素。
  • map 没有容量一说,所以也没有任何增长限制。
  • 内建函数 len 可以用来获得 slice 和 map 的长度。
  • 内建函数 cap 只能作用在 slice 上。
  • 可以通过组合方式来创建多维数组和 slice。map 的值可以是 slice 或者另一个 map。slice 不能作为 map 的键。
  • 在函数之间传递 slice 和 map 是相当廉价的,因为他们不会传递底层数组的拷贝。

Go 语言中的方法,接口和嵌入类型

概述

在 Go 语言中,如果一个结构体和一个嵌入字段同时实现了相同的接口会发生什么呢?我们猜一下,可能有两个问题:

  • 编译器会因为我们同时有两个接口实现而报错吗?
  • 如果编译器接受这样的定义,那么当接口调用时编译器要怎么确定该使用哪个实现?

在写了一些测试代码并认真深入的读了一下标准之后,我发现了一些有意思的东西,而且觉得很有必要分享出来,那么让我们先从 Go 语言中的方法开始说起。

方法

Go 语言中同时有函数和方法。一个方法就是一个包含了接受者的函数,接受者可以是命名类型或者结构体类型的一个值或者是一个指针。所有给定类型的方法属于该类型的方法集。

下面定义一个结构体类型和该类型的一个方法:

type User struct {
  Name  string
  Email string
}

func (u User) Notify() error

首先我们定义了一个叫做 User 的结构体类型,然后定义了一个该类型的方法叫做 Notify,该方法的接受者是一个 User 类型的值。要调用 Notify 方法我们需要一个 User 类型的值或者指针:

// User 类型的值可以调用接受者是值的方法
damon := User{"AriesDevil", "ariesdevil@xxoo.com"}
damon.Notify()

// User 类型的指针同样可以调用接受者是值的方法
alimon := &User{"A-limon", "alimon@ooxx.com"}
alimon.Notify()

在这个例子中当我们使用指针时,Go 调整和解引用指针使得调用可以被执行。注意,当接受者不是一个指针时,该方法操作对应接受者的值的副本(意思就是即使你使用了指针调用函数,但是函数的接受者是值类型,所以函数内部操作还是对副本的操作,而不是指针操作,参见:http://play.golang.org/p/DBhWU0p1Pv)。

我们可以修改 Notify 方法,让它的接受者使用指针类型:

func (u *User) Notify() error

再来一次之前的调用(注意:当接受者是指针时,即使用值类型调用那么函数内部也是对指针的操作,参见:http://play.golang.org/p/SYBb4xPfPh):

// User 类型的值可以调用接受者是指针的方法
damon := User{"AriesDevil", "ariesdevil@xxoo.com"}
damon.Notify()

// User 类型的指针同样可以调用接受者是指针的方法
alimon := &User{"A-limon", "alimon@ooxx.com"}
alimon.Notify()

如果你不清楚到底什么时候该使用值,什么时候该使用指针作为接受者,你可以去看一下这篇介绍。这篇文章同时还包含了社区约定的接受者该如何命名。

接口

Go 语言中的接口很特别,而且提供了难以置信的一系列灵活性和抽象性。它们指定一个特定类型的值和指针表现为特定的方式。从语言角度看,接口是一种类型,它指定一个方法集,所有方法为接口类型就被认为是该接口。

下面定义一个接口:

type Notifier interface {
  Notify() error
}

我们定义了一个叫做 Notifier 的接口并包含一个 Notify 方法。当一个接口只包含一个方法时,按照 Go 语言的约定命名该接口时添加 -er 后缀。这个约定很有用,特别是接口和方法具有相同名字和意义的时候。

我们可以在接口中定义尽可能多的方法,不过在 Go 语言标准库中,你很难找到一个接口包含两个以上的方法。

实现接口

当涉及到我们该怎么让我们的类型实现接口时,Go 语言是特别的一个。Go 语言不需要我们显式的实现类型的接口。如果一个接口里的所有方法都被我们的类型实现了,那么我们就说该类型实现了该接口。

让我们继续之前的例子,定义一个函数来接受任意一个实现了接口 Notifier 的类型的值或者指针:

func SendNotification(notify Notifier) error {
  return notify.Notify()
}

SendNotification 函数调用 Notify 方法,这个方法被传入函数的一个值或者指针实现。这样一来一个函数就可以被用来执行任意一个实现了该接口的值或者指针的指定的行为。

用我们的 User 类型来实现该接口并且传入一个 User 类型的值来调用 SendNotification 方法:

func (u *User) Notify() error {
  log.Printf("User: Sending User Email To %s<%s>\n",
      u.Name,
      u.Email)
  return nil
}

func main() {
  user := User{
    Name:  "AriesDevil",
    Email: "ariesdevil@xxoo.com",
  }
  
  SendNotification(user)
}

// Output:
cannot use user (type User) as type Notifier in function argument:
User does not implement Notifier (Notify method has pointer receiver)

详细代码:http://play.golang.org/p/KG8-Qb7gqM

为什么编译器不考虑我们的值是实现该接口的类型?接口的调用规则是建立在这些方法的接受者和接口如何被调用的基础上。下面的是语言规范里定义的规则,这些规则用来说明是否我们一个类型的值或者指针实现了该接口:

  • 类型 *T 的可调用方法集包含接受者为 *TT 的所有方法集

这条规则说的是如果我们用来调用特定接口方法的接口变量是一个指针类型,那么方法的接受者可以是值类型也可以是指针类型。显然我们的例子不符合该规则,因为我们传入 SendNotification 函数的接口变量是一个值类型。

  • 类型 T 的可调用方法集包含接受者为 T 的所有方法

这条规则说的是如果我们用来调用特定接口方法的接口变量是一个值类型,那么方法的接受者必须也是值类型该方法才可以被调用。显然我们的例子也不符合这条规则,因为我们 Notify 方法的接受者是一个指针类型。

语言规范里只有这两条规则,我通过这两条规则得出了符合我们例子的规则:

  • 类型 T 的可调用方法集不包含接受者为 *T 的方法

我们碰巧赶上了我推断出的这条规则,所以编译器会报错。Notify 方法使用指针类型作为接受者而我们却通过值类型来调用该方法。解决办法也很简单,我们只需要传入 User 值的地址到 SendNotification 函数就好了:

func main() {
  user := &User{
    Name:  "AriesDevil",
    Email: "ariesdevil@xxoo.com",
  }
  
  SendNotification(user)
}

// Output:
User: Sending User Email To AriesDevil<ariesdevil@xxoo.com>

详细代码:http://play.golang.org/p/kEKzyTfLjA

嵌入类型

结构体类型可以包含匿名或者嵌入字段。也叫做嵌入一个类型。当我们嵌入一个类型到结构体中时,该类型的名字充当了嵌入字段的字段名。

下面定义一个新的类型然后把我们的 User 类型嵌入进去:

type Admin struct {
  User
  Level  string
}

我们定义了一个新类型 Admin 然后把 User 类型嵌入进去,注意这个不叫继承而叫组合。 User 类型跟 Admin 类型没有关系。

我们来改变一下 main 函数,创建一个 Admin 类型的变量并把变量的地址传入 SendNotification 函数中:

func main() {
  admin := &Admin{
    User: User{
      Name:  "AriesDevil",
      Email: "ariesdevil@xxoo.com",
    },
    Level: "master",
  }
  
  SendNotification(admin)
}

// Output
User: Sending User Email To AriesDevil<ariesdevil@xxoo.com>

详细代码:http://play.golang.org/p/ivzzzk78TC

事实证明,我们可以 Admin 类型的一个指针来调用 SendNotification 函数。现在 Admin 类型也通过来自嵌入的 User 类型的方法提升实现了该接口。

如果 Admin 类型包含了 User 类型的字段和方法,那么它们在结构体中的关系是怎么样的呢?

当我们嵌入一个类型,这个类型的方法就变成了外部类型的方法,但是当它被调用时,方法的接受者是内部类型(嵌入类型),而非外部类型。– Effective Go

因此嵌入类型的名字充当着字段名,同时嵌入类型作为内部类型存在,我们可以使用下面的调用方法:

admin.User.Notify()

// Output
User: Sending User Email To AriesDevil<ariesdevil@xxoo.com>

详细代码:http://play.golang.org/p/0WL_5Q6mao

这儿我们通过类型名称来访问内部类型的字段和方法。然而,这些字段和方法也同样被提升到了外部类型:

admin.Notify()

// Output
User: Sending User Email To AriesDevil<ariesdevil@xxoo.com>

详细代码:http://play.golang.org/p/2snaaJojRo

所以通过外部类型来调用 Notify 方法,本质上是内部类型的方法。

下面是 Go 语言中内部类型方法集提升的规则:

给定一个结构体类型 S 和一个命名为 T 的类型,方法提升像下面规定的这样被包含在结构体方法集中:

  • 如果 S 包含一个匿名字段 TS*S 的方法集都包含接受者为 T 的方法提升。

这条规则说的是当我们嵌入一个类型,嵌入类型的接受者为值类型的方法将被提升,可以被外部类型的值和指针调用。

  • 对于 *S 类型的方法集包含接受者为 *T 的方法提升

这条规则说的是当我们嵌入一个类型,可以被外部类型的指针调用的方法集只有嵌入类型的接受者为指针类型的方法集,也就是说,当外部类型使用指针调用内部类型的方法时,只有接受者为指针类型的内部类型方法集将被提升。

  • 如果 S 包含一个匿名字段 *TS*S 的方法集都包含接受者为 T 或者 *T 的方法提升

这条规则说的是当我们嵌入一个类型的指针,嵌入类型的接受者为值类型或指针类型的方法将被提升,可以被外部类型的值或者指针调用。

这就是语言规范里方法提升中仅有的三条规则。

回答开头的问题

现在我们可以写程序来回答开头提出的两个问题了,首先我们让 Admin 类型实现 Notifier 接口:

func (a *Admin) Notify() error {
  log.Printf("Admin: Sending Admin Email To %s<%s>\n",
      a.Name,
      a.Email)
      
  return nil
}

Admin 类型实现的接口显示一条 admin 方面的信息。当我们使用 Admin 类型的指针去调用函数 SendNotification 时,这将帮助我们确定到底是哪个接口实现被调用了。

现在创建一个 Admin 类型的值并把它的地址传入 SendNotification 函数,来看看发生了什么:

func main() {
  admin := &Admin{
    User: User{
      Name:  "AriesDevil",
      Email: "ariesdevil@xxoo.com",
    },
    Level: "master",
  }
  
  SendNotification(admin)
}

// Output
Admin: Sending Admin Email To AriesDevil<ariesdevil@xxoo.com>

详细代码:http://play.golang.org/p/JGhFaJnGpS

预料之中,Admin 类型的接口实现被 SendNotification 函数调用。现在我们用外部类型来调用 Notify 方法会发生什么呢:

admin.Notify()

// Output
Admin: Sending Admin Email To AriesDevil<ariesdevil@xxoo.com>

详细代码:http://play.golang.org/p/EGqK6DwBOi

我们得到了 Admin 类型的接口实现的输出。User 类型的接口实现不被提升到外部类型了。

现在我们有了足够的依据来回答问题了:

  • 编译器会因为我们同时有两个接口实现而报错吗?

不会,因为当我们使用嵌入类型时,类型名充当了字段名。嵌入类型作为结构体的内部类型包含了自己的字段和方法,且具有唯一的名字。所以我们可以有同一接口的内部实现和外部实现。

  • 如果编译器接受这样的定义,那么当接口调用时编译器要怎么确定该使用哪个实现?

如果外部类型包含了符合要求的接口实现,它将会被使用。否则,通过方法提升,任何内部类型的接口实现可以直接被外部类型使用。

总结

在 Go 语言中,方法,接口和嵌入类型一起工作方式是独一无二的。这些特性可以帮助我们像面向对象那样组织结构然后达到同样的目的,并且没有其它复杂的东西。用本文中谈到的语言特色,我们可以以极少的代码来构建抽象和可伸缩性的框架。

Go 语言方法接受者类型的选择

概述

很多人(特别是新手)在写 Go 语言代码时经常会问一个问题,那就是一个方法的接受者类型到底应该是值类型还是指针类型呢,Go 的 wiki 上对这点做了很好的解释,我来翻译一下。

何时使用值类型

  • 如果接受者是一个 mapfunc 或者 chan,使用值类型(因为它们本身就是引用类型)。
  • 如果接受者是一个 slice,并且方法不执行 reslice 操作,也不重新分配内存给 slice,使用值类型。
  • 如果接受者是一个小的数组或者原生的值类型结构体类型(比如 time.Time 类型),而且没有可修改的字段和指针,又或者接受者是一个简单地基本类型像是 intstring,使用值类型就好了。

一个值类型的接受者可以减少一定数量的垃圾生成,如果一个值被传入一个值类型接受者的方法,一个栈上的拷贝会替代在堆上分配内存(但不是保证一定成功),所以在没搞明白代码想干什么之前,别因为这个原因而选择值类型接受者。

何时使用指针类型

  • 如果方法需要修改接受者,接受者必须是指针类型。
  • 如果接受者是一个包含了 sync.Mutex 或者类似同步字段的结构体,接受者必须是指针,这样可以避免拷贝。
  • 如果接受者是一个大的结构体或者数组,那么指针类型接受者更有效率。(多大算大呢?假设把接受者的所有元素作为参数传给方法,如果你觉得参数有点多,那么它就是大)。
  • 从此方法中并发的调用函数和方法时,接受者可以被修改吗?一个值类型的接受者当方法调用时会创建一份拷贝,所以外部的修改不能作用到这个接受者上。如果修改必须被原始的接受者可见,那么接受者必须是指针类型。
  • 如果接受者是一个结构体,数组或者 slice,它们中任意一个元素是指针类型而且可能被修改,建议使用指针类型接受者,这样会增加程序的可读性

当你看完这个还是有疑虑,还是不知道该使用哪种接受者,那么记住使用指针接受者。

关于接受者的命名

社区约定的接受者命名是类型的一个或两个字母的缩写(像 c 或者 cl 对于 Client)。不要使用泛指的名字像是 methis 或者 self,也不要使用过度描述的名字,最后,如果你在一个地方使用了 c,那么就不要在别的地方使用 cl

Go 语言中的 new() 和 make() 的区别

概述

Go 语言中的 newmake 一直是新手比较容易混淆的东西,咋一看很相似。不过解释两者之间的不同也非常容易。

new 的主要特性

首先 new 是内建函数,你可以从 http://golang.org/pkg/builtin/#new 这儿看到它,它的定义也很简单:

func new(Type) *Type

官方文档对于它的描述是:

内建函数 new 用来分配内存,它的第一个参数是一个类型,不是一个值,它的返回值是一个指向新分配类型零值的指针

根据这段描述,我们可以自己实现一个类似 new 的功能:

func newInt() *int {
  var i int
  return &i
}

someInt := newInt()

我们这个函数的功能跟 someInt := new(int) 一模一样。所以在我们自己定义 new 开头的函数时,出于约定也应该返回类型的指针。

make 的主要特性

make 也是内建函数,你可以从 http://golang.org/pkg/builtin/#make 这儿看到它,它的定义比 new 多了一个参数,返回值也不同:

func make(Type, size IntegerType) Type

官方文档对于它的描述是:

内建函数 make 用来为 slicemapchan 类型分配内存和初始化一个对象(注意:只能用在这三种类型上),跟 new 类似,第一个参数也是一个类型而不是一个值,跟 new 不同的是,make 返回类型的引用而不是指针,而返回值也依赖于具体传入的类型,具体说明如下:

Slice: 第二个参数 size 指定了它的长度,它的容量和长度相同。
你可以传入第三个参数来指定不同的容量值,但必须不能比长度值小。
比如 make([]int, 0, 10)

Map: 根据 size 大小来初始化分配内存,不过分配后的 map 长度为 0,如果 size 被忽略了,那么会在初始化分配内存时分配一个小尺寸的内存

Channel: 管道缓冲区依据缓冲区容量被初始化。如果容量为 0 或者忽略容量,管道是没有缓冲区的

总结

new 的作用是初始化一个指向类型的指针(*T),make 的作用是为 slicemapchan 初始化并返回引用(T)。

基于 Martini 的跨域资源共享(CORS)

##概述

CORS 的全称是 Cross-Origin Resource Sharing,即:跨域资源共享

根据我的理解,就是马伊琍和文章结婚了,姚笛就不能和文章结了,如果还想在一起,那就得采用一定的方法,这个方法就是跨域,哦,不对,是当第三者:) 根据维基百科的解释,CORS 是一种机制,这个机制允许一个 Web 页面上 JavaScript 向另外的域发起 XMLHttpRequests 请求,注意不是向该 Web 页面所在域请求。这样的跨域请求,在 CORS 之前,根据同源安全策略是会被浏览器拒绝的。CORS 定义了一种方法,这个方法使浏览器和服务器相互作用来限定是否允许跨域请求。它显然比只有单纯的同源请求有用,而且还比简单的允许所有跨域访问要安全。

在 CORS 出现之前,已经有了很多种方法来实现跨域访问,其中最有名的就是 JSONP(JSON with Padding),JSONP 是一种使用 JavaScript 请求其它域服务器的一种通信技术,本质就是利用同源策略的漏洞,一般来说位于 xxoo.se77en.cc 的网页是无法与非 xxoo.se77en.cc 的服务器通信的,但是 HTML 里的 <script> 元素是一个例外,利用这一例外,可以通过 JavaScript 操作浏览器页面 DOM 来动态创建 Script 对象,再将 Script 的 src 属性指向另一个域的资源,服务器就会将数据伪装成一段 JavaScript 代码来实现跨域目的。不过这种技术只能发起 GET 请求,而且安全隐患极大,因为远程服务器可以发送 JavaScript 代码,所以极易受到跨网站伪造请求(CSRF/XSRF),所以使用 JSONP 要格外小心。

注:目前有个正在进行的计划定义 JSON-P 严格安全子集,使浏览器可以对 MIME 类别是 application/json-p 的请求做强制处理,如果不能被解析为严格的 JSON-P,浏览器则会抛出一个错误或者忽略整个响应,目前正确的 JSONP MIME 类型仍然是 application/javascript

对比 JSONP 的限制,CORS 的限制主要是浏览器支持的问题(不过已经很不错了,除了万恶的 IE6):

cors-in-broswer

##创建一个 CORS 请求

完成一个 CORS 需要前后端配合。

###前端

对前端而言,基本没什么变化,还是使用 XMLHttpRequest 对象(IE 使用 XDomainRequest),增加了参数和响应回调,当然如果你用 jQuery 可以不用考虑这么多了。下面用 JavaScript 和 jQuery 分别示例:

首先是 JavaScript,比较复杂,所以直接用大牛 Nicholas•Zakas 写的帮助方法:

function createCORSRequest(method, url) {
  var xhr = new XMLHttpRequest();
  if ("withCredentials" in xhr) {

    // 检查 XMLHttpRequest 对象是否包含 "withCredentials" 属性
    // "withCredentials" 只在 XMLHTTPRequest2 对象中存在
    xhr.open(method, url, true);

  } else if (typeof XDomainRequest != "undefined") {

    // 否则,检查是否是 XDomainRequest
    // XDomainRequest 只在 IE 中存在, 所以用 IE 的方式来创建 CORS 请求
    xhr = new XDomainRequest();
    xhr.open(method, url);

  } else {

    // 上述都不满足,说明浏览器不支持 CORS
    xhr = null;

  }
  return xhr;
}

var xhr = createCORSRequest('GET', url);
if (!xhr) {
  throw new Error('CORS not supported');
}

如果你想要提交 cookies 需要设置 XMLHttpRequest 的 withCredentials 属性为 true:

xhr.withCredentials = true;

然后处理服务端的返回结果:

xhr.onload = function() {
 var responseText = xhr.responseText;
 console.log(responseText);
 // 处理返回结果
};

xhr.onerror = function() {
  console.log('There was an error!');
};

坑爹的是,浏览器在发生错误时的处理方式并不好,FireFox 对于所有错误返回一个为0的状态值和一个空的信息。浏览器会在 console log 里打印一个错误信息,不过这个信息却不能被 JavaScript 访问。所以处理错误时,你只知道一个错误发生了,别的一概不知。

前端完整代码如下:

// 创建 XHR 对象
function createCORSRequest(method, url) {
  var xhr = new XMLHttpRequest();
  if ("withCredentials" in xhr) {
    // XHR for Chrome/Firefox/Opera/Safari.
    xhr.open(method, url, true);
  } else if (typeof XDomainRequest != "undefined") {
    // XDomainRequest for IE.
    xhr = new XDomainRequest();
    xhr.open(method, url);
  } else {
    // 不支持 CORS
    xhr = null;
  }
  return xhr;
}


//创建真正的一个 CORS 请求
function makeCorsRequest() {
  var url = 'http://ooxx.se77en.cc';

  var xhr = createCORSRequest('GET', url);
  if (!xhr) {
    alert('CORS not supported');
    return;
  }

  // 处理响应
  xhr.onload = function() {
    var text = xhr.responseText;
    alert('Response from CORS request to ' + url);
  };

  xhr.onerror = function() {
    alert('Woops, there was an error making the request.');
  };

  xhr.send();
}

###服务端

对服务端而言,最简单的处理方法就是增加下面一行到你的 Response Header 里:

Access-Control-Allow-Origin: *

使用 go 来实现就是:

func setAllowOrigin(writer http.ResponseWriter, r *http.Request) {
  writer.Header().Add("Access-Control-Allow-Origin", "*")
  return
}

当然,如果希望处理 POST,PUT 这类复杂的请求,或者是想要更加精确的控制 CORS,如:允许的域范围,是否允许 Cookie,允许哪些请求方法,那自然处理也会变得复杂一点。

对于任何非简单请求,浏览器都会先于服务器进行沟通,达成一致后,再发出实际请求。沟通的方式叫做 Preflight(起飞预备),在发起实际请求前,浏览器首先通过 OPTIONS 方式(这样才能从服务器收到响应)。

Preflight 请求:

OPTIONS /cors HTTP/1.1
Origin: http://ooxx.se77en.cc
Access-Control-Request-Method: POST, PUT
Access-Control-Request-Headers: X-Custom-Header
  • Access-Control-Request-Method 是浏览器要发出的请求类型
  • Access-Control-Request-Headers 是实际请求发送过来时额外的 Header 类型

以上这些参数都是可以用逗号分隔的多值字符串。

Preflight 响应:

Access-Control-Allow-Origin: http://ooxx.se77en.cc
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Content-Type: text/html; charset=utf-8
  • Access-Control-Allow-Origin 是 CORS 响应的标配
  • Access-Control-Allow-MethodsAccess-Control-Allow-Headers 是服务器支持的方法和头信息,值得注意的是,这里应该填写全集,而非对应 Preflight 请求里的项目

此外还有一些可选项:

  • Access-Control-Max-Age 是告诉浏览器多少秒以内,不再需要请求 Preflight
  • Access-Control-Allow-Credentials 是告诉浏览器是否支持 Cookie,对应上面

Preflight 沟通失败:

如果 Preflight 发送过来的请求权限超过了服务器所支持的,回复的方法是忽略掉 Access-Control-Allow-Origin 即可,就像一个普通的 HTTP 200 返回,这样浏览器就不会发起实际请求了:

Content-Type: text/html; charset=utf-8

沟通成功后的实际请求和响应:

当浏览器发起 Preflight,并确认服务器支持 CORS 无误,就可以发起实际请求步骤

实际请求:

POST /cors HTTP/1.1
Origin: http://ooxx.se77en.cc
Host: xxoo.wisteria.io
X-Custom-Header: value
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

实际响应:

Access-Control-Allow-Origin: http://ooxx.se77en.cc
Content-Type: text/html; charset=utf-8

交互过程:

cors_flow

服务端响应流程图:

cors_server_flowchart

###如何用 Go 语言实现?

按照上述过程,首先判断是 Preflight 还是 Actual Request:

func (cors *Cors) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  if origin := r.Header.Get("Origin"); origin == "" {
      cors.corsNotValid(w, r)
      return
  } else if r.Method != "OPTIONS" {
      //actual request.
      cors.actualRequest(w, r)
      return
  } else if acrm := r.Header.Get("Access-Control-Request-Method"); acrm == "" {
      //actual request.
      cors.actualRequest(w, r)
      return
  } else {
      //preflight request.
      cors.preflightRequest(w, r)
      return
  }
}

###在 Martini 中实现

上面代码只是说明意图,下面我们来示范一下 CORS 在 Martini 中的应用。

首先是页面所在域,假设为 xxoo.wisteria.io

<!DOCTYPE html>
<html>
<head>
  <title></title>
  <meta charset="utf-8">
  <script src="http://cdn.staticfile.org/jquery/1.8.2/jquery.min.js"></script>
  <script type="text/javascript">
    $(function() {
      $("#btn").click(function(e){
        e.preventDefault();  //感谢 @A-limon 提醒
        var btx = $("#btx").val();
        var url = "http://ooxx.se77en.cc/cors";
        $.ajax(url, {
          type:"POST",
          data:{"value":btx},
          dataType:"json",
          xhrFields:{
            withCredentials:false
          },
          success:function(data){alert(data.msg);},
          error:function(){alert("errror");}
        });
      });
    });
  </script>
</head>
<body>
  <h1>CORS</h1>
  <form>
    <textarea id="btx" cols="30" rows="10"></textarea><br />
    <button id="btn">submit</button>
  </form>
</body>
</html>

接下来是服务器所在域,假设为 ooxx.se77en.cc

package main

import (
	"github.com/go-martini/martini"
	"github.com/martini-contrib/binding"
	"github.com/martini-contrib/cors"
)

type xxoo struct {
	Value string `form:"value"`
}

func main() {
	m := martini.Classic()
	m.Use(cors.Allow(&cors.Options{
		AllowOrigins:     []string{"http://xxoo.wisteria.io"},
		AllowMethods:     []string{"POST"},
		AllowHeaders:     []string{"Origin", "x-requested-with", "Content-Type", "Content-Range", "Content-Disposition", "Content-Description"},
		ExposeHeaders:    []string{"Content-Length"},
		AllowCredentials: false,
	}))

	m.Post("/cors", binding.Form(xxoo{}), func(ooxx xxoo, writer http.ResponseWriter) (int, string) {
		writer.Header().Set("Content-Type", "application/json")
		log.Println("******* " + ooxx.Value + " *******")
		return http.StatusOK, `{"msg":"hello cors"}`
	})
	
  m.Run()
}

我们使用了 Martini 的一个叫 cors 的插件,可以看到 Martini 的 cors 插件已经为我们做了很多工作,详细说明请参见 cors 文档

##感谢

  1. http://en.wikipedia.org/wiki/Cross-origin_resource_sharing
  2. http://en.wikipedia.org/wiki/JSONP
  3. http://www.html5rocks.com/en/tutorials/cors/
  4. http://semicircle.github.io/blog/2013/09/29/go-with-cors/
  5. http://client.cors-api.appspot.com/client
  6. http://enable-cors.org/

Go 语言的并发模型--通过通信来共享内存

##概述

我一直在找一种好的方法来解释 go 语言的并发模型: > 不要通过共享内存来通信,相反,应该通过通信来共享内存

但是没有发现一个好的解释来满足我下面的需求:

  • 通过一个例子来说明最初的问题
  • 提供一个共享内存的解决方案
  • 提供一个通过通信的解决方案

这篇文章我就从这三个方面来做出解释。

读过这篇文章后你应该会了解通过通信来共享内存的模型,以及它和通过共享内存来通信的区别,你还将看到如何分别通过这两种模型来解决访问和修改共享资源的问题。 ##前提

设想一下我们要访问一个银行账号:

type Account interface {
  Withdraw(uint)
  Deposit(uint)
  Balance() int
}

type Bank struct {
  account Account
}

func NewBank(account Account) *Bank {
  return &Bank{account: account}
}

func (bank *Bank) Withdraw(amount uint, actor_name string) {
  fmt.Println("[-]", amount, actor_name)
  bank.account.Withdraw(amount)
}

func (bank *Bank) Deposit(amount uint, actor_name string) {
  fmt.Println("[+]", amount, actor_name)
  bank.account.Deposit(amount)
}

func (bank *Bank) Balance() int {
  return bank.account.Balance()
}

因为 Account 是一个接口,所以我们提供一个简单的实现:

type SimpleAccount struct{
  balance int
}

func NewSimpleAccount(balance int) *SimpleAccount {
  return &SimpleAccount{balance: balance}
}

func (acc *SimpleAccount) Deposit(amount uint) {
  acc.setBalance(acc.balance + int(amount))
}

func (acc *SimpleAccount) Withdraw(amount uint) {
  if acc.balance >= int(amount) {
    acc.setBalance(acc.balance - int(amount))
  } else {
    panic("杰克穷死")
  }
}

func (acc *SimpleAccount) Balance() int {
  return acc.balance
}

func (acc *SimpleAccount) setBalance(balance int) {
  acc.add_some_latency()  //增加一个延时函数,方便演示
  acc.balance = balance
}

func (acc *SimpleAccount) add_some_latency() {
  <-time.After(time.Duration(rand.Intn(100)) * time.Millisecond)
}

你可能注意到了 balance 没有被直接修改,而是被放到了 setBalance 方法里进行修改。这样设计是为了更好的描述问题。稍后我会做出解释。

把上面所有部分弄好以后我们就可以像下面这样使用它啦:

func main() {
  balance := 80
  b := NewBank(NewSimpleAccount(balance))
  
  fmt.Println("初始化余额", b.Balance())
  
  b.Withdraw(30, "马伊琍")
  
  fmt.Println("-----------------")
  fmt.Println("剩余余额", b.Balance())
}

运行上面的代码会输出:

初始化余额 80
[-] 30 马伊琍
-----------------
剩余余额 50

没错!

不错在现实生活中,一个银行账号可以有很多个附属卡,不同的附属卡都可以对同一个账号进行存取钱,所以我们来修改一下代码:

func main() {
  balance := 80
  b := NewBank(NewSimpleAccount(balance))
  
  fmt.Println("初始化余额", b.Balance())
  
  done := make(chan bool)
  
  go func() { b.Withdraw(30, "马伊琍"); done <- true }()
  go func() { b.Withdraw(10, "姚笛"); done <- true }()
  
  //等待 goroutine 执行完成
  <-done
  <-done
  
  fmt.Println("-----------------")
  fmt.Println("剩余余额", b.Balance())
}

这儿两个附属卡并发的从账号里取钱,来看看输出结果:

初始化余额 80
[-] 30 马伊琍
[-] 10 姚笛
-----------------
剩余余额 70

这下把文章高兴坏了:)

结果当然是错误的,剩余余额应该是40而不是70,那么让我们看看到底哪儿出问题了。

##问题

当并发访问共享资源时,无效状态有很大可能会发生。

在我们的例子中,当两个附属卡同一时刻从同一个账号取钱后,我们最后得到银行账号(即共享资源)错误的剩余余额(即无效状态)。

我们来看一下执行时候的情况:

                 处理情况
             --------------
             _马伊琍_|_姚笛_
 1. 获取余额     80  |  80
 2. 取钱       -30  | -10
 3. 当前剩余     50  |  70
                ... | ...
 4. 设置余额     50  ?  70  //该先设置哪个好呢?
 5. 后设置的生效了
             --------------
 6. 剩余余额        70

上面 ... 的地方描述了我们 add_some_latency 实现的延时状况,现实世界经常发生延迟情况。所以最后的剩余余额就由最后设置余额的那个附属卡决定。

##解决办法

我们通过两种方法来解决这个问题:

  • 共享内存的解决方案
  • 通过通信的解决方案

所有的解决方案都是简单的封装了一下 SimpleAccount 来实现保护机制。

###共享内存的解决方案

又叫 “通过共享内存来通信”。

这种方案暗示了使用锁机制来预防同时访问和修改共享资源。锁告诉其它处理程序这个资源已经被一个处理程序占用了,因此别的处理程序需要排队直到当前处理程序处理完毕。

让我们来看看 LockingAccount 是怎么实现的:

type LockingAccount struct {
  lock    sync.Mutex
  account *SimpleAccount
}

//封装一下 SimpleAccount
func NewLockingAccount(balance int) *LockingAccount {
  return &LockingAccount{account: NewSimpleAccount(balance)}
}

func (acc *LockingAccount) Deposit(amount uint) {
  acc.lock.Lock()
  defer acc.lock.Unlock()
  acc.account.Deposit(amount)
}

func (acc *LockingAccount) Withdraw(amount uint) {
  acc.lock.Lock()
  defer acc.lock.Unlock()
  acc.account.Withdraw(amount)
}

func (acc *LockingAccount) Balance() int {
  acc.lock.Lock()
  defer acc.lock.Unlock()
  return acc.account.Balance()
}

直接明了!注意 lock sync.Locklock.Lock()lock.Unlock()

这样每次一个附属卡访问银行账号(即共享资源),这个附属卡会自动获得锁直到最后操作完毕。

我们的 LockingAccount 像下面这样使用:

func main() {
  balance := 80
  b := NewBank(NewLockingAccount(balance))
  
  fmt.Println("初始化余额", b.Balance())
  
  done := make(chan bool)
  
  go func() { b.Withdraw(30, "马伊琍"); done <- true }()
  go func() { b.Withdraw(10, "姚笛"); done <- true }()
  
  //等待 goroutine 执行完成
  <-done
  <-done
  
  fmt.Println("-----------------")
  fmt.Println("剩余余额", b.Balance())
}

输出的结果是:

初始化余额 80
[-] 30 马伊琍
[-] 10 姚笛
-----------------
剩余余额 40

现在结果正确了!

在这个例子中第一个处理程序加锁后独享共享资源,其它处理程序只能等待它执行完成。

我们接着看一下执行时的情况,假设马伊琍先拿到了锁:

                            处理过程
                        ________________
                        _马伊琍_|__姚笛__
        加锁                   ><
        得到余额            80  |
        取钱               -30  |
        当前余额            50  |
                           ... |
        设置余额            50  |
        解除锁                 <>
                               |
        当前余额                50
                               |
        加锁                   ><
        得到余额                |  50
        取钱                    | -10
        当前余额                |  40
                               |  ...
        设置余额                |  40
        解除锁                  <>
                        ________________
        剩余余额                40

现在我们的处理程序在访问共享资源时相继的产生了正确的结果。

###通过通信的解决方案

又叫 “通过通信来共享内存”。

现在账号被命名为 ConcurrentAccount,像下面这样来实现:

type ConcurrentAccount struct {
  account     *SimpleAccount
  deposits    chan uint
  withdrawals chan uint
  balances    chan chan int
}

func NewConcurrentAccount(amount int) *ConcurrentAccount{
  acc := &ConcurrentAccount{
    account :    &SimpleAccount{balance: amount},
    deposits:    make(chan uint),
    withdrawals: make(chan uint),
    balances:    make(chan chan int),
  }
  acc.listen()
  
  return acc
}

func (acc *ConcurrentAccount) Balance() int {
  ch := make(chan int)
  acc.balances <- ch
  return <-ch
}

func (acc *ConcurrentAccount) Deposit(amount uint) {
  acc.deposits <- amount
}

func (acc *ConcurrentAccount) Withdraw(amount uint) {
  acc.withdrawals <- amount
}

func (acc *ConcurrentAccount) listen() {
  go func() {
    for {
      select {
      case amnt := <-acc.deposits:
        acc.account.Deposit(amnt)
      case amnt := <-acc.withdrawals:
        acc.account.Withdraw(amnt)
      case ch := <-acc.balances:
        ch <- acc.account.Balance()
      }
    }
  }()
}

ConcurrentAccount 同样封装了 SimpleAccount ,然后增加了通信通道

调用代码和加锁版本的一样,这里就不写了,唯一不一样的就是初始化银行账号的时候:

b := NewBank(NewConcurrentAccount(balance))

运行产生的结果和加锁版本一样:

初始化余额 80
[-] 30 马伊琍
[-] 10 姚笛
-----------------
剩余余额 40

让我们来深入了解一下细节。

###通过通信来共享内存是如何工作的

一些基本注意点:

  • 共享资源被封装在一个控制流程中。 结果就是资源成为了非共享状态。没有处理程序能够直接访问或者修改资源。你可以看到访问和修改资源的方法实际上并没有执行任何改变。
  func (acc *ConcurrentAccount) Balance() int {
    ch := make(chan int)
    acc.balances <- ch
    balance := <-ch
    return balance
  }
  func (acc *ConcurrentAccount) Deposit(amount uint) {
    acc.deposits <- amount
  }

  func (acc *ConcurrentAccount) Withdraw(amount uint) {
    acc.withdrawals <- amount
  }
  • 访问和修改是通过消息和控制流程通信。
  • 在控制流程中任何访问和修改的动作都是相继发生的。 当控制流程接收到访问或者修改的请求后会立即执行相关动作。让我们仔细看看这个流程:
  func (acc *ConcurrentAccount) listen() {
    // 执行控制流程
    go func() {
      for {
        select {
        case amnt := <-acc.deposits:
          acc.account.Deposit(amnt)
        case amnt := <-acc.withdrawals:
          acc.account.Withdraw(amnt)
        case ch := <-acc.balances:
          ch <- acc.account.Balance()
        }
      }
    }()
  }

select 不断地从各个通道中取出消息,每个通道都跟它们所要执行的操作相一致。

重要的一点是:在 select 声明内部的一切都是相继执行的(在同一个处理程序中排队执行)。一次只有一个事件(在通道中接受或者发送)发生,这样就保证了同步访问共享资源。

领会这个有一点绕。

让我们用例子来看看 Balance() 的执行情况:

         一张附属卡的流程      |   控制流程 
      ----------------------------------------------

 1.     b.Balance()         |
 2.             ch -> [acc.balances]-> ch
 3.             <-ch        |  balance = acc.account.Balance()
 4.     return  balance <-[ch]<- balance
 5                          |

这两个流程都干了点什么呢?

###附属卡的流程

  1. 调用 b.Balance()
  2. 新建通道 ch,将 ch 通道塞入通道 acc.balances 中与控制流程通信,这样控制流程也可以通过 ch 来返回余额
  3. 等待 <-ch 来取得要接受的余额
  4. 接受余额
  5. 继续

###控制流程

  1. 空闲或者处理
  2. 通过 acc.balances 通道里面的 ch 通道来接受余额请求
  3. 取得真正的余额值
  4. 将余额值发送到 ch 通道
  5. 准备处理下一个请求

控制流程每次只处理一个 事件。这也就是为什么除了描述出来的这些以外,第2-4步没有别的操作执行。

##总结

这篇博客描述了问题以及问题的解决办法,但那时没有深入去探究不同解决办法的优缺点。

其实这篇文章的例子更适合用 mutex,因为这样代码更加清晰。

最后,请毫无顾忌的指出我的错误!

使用 Koa 从零打造 TODO 应用

原文地址:Koa: Zero to Todo List

###注意:你需要使用node 0.11.x外加 -harmony 来执行代码

Express 团队利用新的 ECMAScript 6 的生成器语法创建了新的框架,Koa 框架是一个全新的 node web 框架,包含了很多有意思的东西。 ##之前的方式

在 node 标准库里,http 模块被用来创建服务。

var server = http.createServer(function(req, res) {
  res.writeHead(200, {'Content-Type': 'text/plain'});
  //这里写服务逻辑
  res.end('');
});

server.listen(3000, '127.0.0.1');
console.log('listening on port 3000');

Express 暴露一个方法使我们可以将 http.createServer 作为回调。Express 中间件是一个函数集合,每个函数包含了三个参数 req,res,next。中间件执行一些操作,修改请求或者返回对象然后通过调用 next() 来传递到堆栈里的下一个中间件。它类似一个瀑布模型,在中间件栈的底部结束响应。

##进入 Koa:建立在生成器机制上的框架

就像 Express,Koa 也是生成一个可以被传递到 http.createServer() 的回调。与 Express 不同的是,它使用生成器提供一个更加细粒度的控制流程。

下面是一个最基本的 Koa 应用,用来读取一个文件的内容

var koa      = require('koa');
var Promise  = require('bluebird');

//创建 promise 版本的 fs
var fs = Promise.promisifyAll(require('fs'));
//创建 koa 实例
var app = koa();

app.use(function *(next) {
  //这是一个示例中间件,在控制台记录一些东西
  console.log('timestamp: before request => ', time.now());
  yield next;
  console.log('timestamp: after request => ', time.now());
});

app.use(function *() {
  this.body = yield fs.readFileAsync('./app.js', 'utf8');
});

app.listen(3000);
console.log('now listening on port 3000');

不像 Express,Koa 中中间件使用生成器来编写。在 Koa 流中下游的中间件在返回时向上流动(回形针调用方式,具体参见:koajs.cn)。通过显式的调用 yield next 来执行下游中间件。当下游中间件返回时,控制流回溯到上游中间件。

Express 通过不同的函数来传递 node 原生的 req 和 res,Koa 则是通过讲它们装入一个借口来管理上下文。不过它们仍然可以通过 this 关键字获取到,像这样:this.req, this.res。然而,在文档中直接使用原生对象是不被推荐的。可以预测到当在控制流中调用 this.res.end('') 时会抛出一个 monkey wrench(猴子扳手?此处不会翻译欢迎指正)。所以建议你使用 this.requestthis.response 来代替直接调用原生对象。很多方法都起了别名指向直接用 this 调用,比如:this.body 就是 this.response.body 的别名。

目前似乎还没有出现可以直接得到请求体的办法。co-body 分析器可以直接的解析请求体,不过文档说别这么做,Koa 是一个年轻的框架,所以别让你的手闲下来。

##使用 Koa 做一个 TODO 应用

刚才我们已经简单的进行了介绍,现在来试着做一个复杂点的。一个 TODO 应用貌似不错,为了简化,我们把 todos 存放在内存里。

Koa 是一个极简的框架,它核心里并没有提供 body 解析,session 和 routing。不幸的是 Koa 太嫩了以至于还没有很多 npm 的模块是为它来写的。浏览了一下 Koa 介绍页面发现有一些必要的模块可以供给我们的基本 TODO 应用来使用。

  1. koa-route: 用作路由
  2. co-body: 用作解析 post 请求体
  3. koa-static: 用于处理静态文件

下面是基本的服务端 api

var koa          = require('koa');
var staticServer = require('koa-staitc');

//这个允许我们解析原生请求对象来获取请求内容
var parse        = require('co-body');

var router       = require('koa-route');
var _            = require('underscore');

var Promise      = require('bluebird');
var path         = require('path');

var fs           = Promise.promisifyAll(require('fs'));
var app          = koa();

//我们的最简单的存储方式
var todos = [];

//获取唯一的 id 值
var counter = (function() {
  var count = 0;
  return function() {
    count++;
    return count;
  }
})();

//处理静态资源文件夹
app.use(staticServer(path.join(__dirname, 'public')));

app.use(router.post('/todos', function *() {
  /*
    yield使我们可以传递异步函数,然后返回内容或者是 promises
    它会冻结当前中间件直到函数被执行完成,然后返回当前中间件继续解冻执行
  */
  var todo = (yield parse.json(this));
  
  todo.id = counter();
  todos.push(todo);
  this.body = JSON.stringify(todos);
}));

app.use(router.get('/todos', function *() {
  this.body = JSON.stringify(todos);
}));

app.use(router.delete('/todos/:id', function *(id) {
  todos = _(todos).reject(function(todo) {
    console.log('what? ', todo, id);
    return todo.id === parseInt(id, 10);
  }, this);
  this.body = JSON.stringify(todos.sort(function(a, b) {
    return a - b;
  }));
}));

app.listen(3000);
console.log('listening on port 3000');

github 上下载完整代码,github 上的版本包含了前端代码。

###一些需要注意的:

yield 关键字可以做一些有意思的事情。如果我们向当前中间件传递一个一步函数,这个函数返回数据块或者 promise,那么它会停止执行当前中间件直到函数完成。等它返回数据块或者 promise 后,会恢复生成器执行。这样更容易阅读。

###一些警告:

yield 关键字使我们可以写出一些安全的代码块,但它也不总是理想的解决办法。

举个栗子,如果我们执行三个相互不依赖的异步操作,像下面这样…

app.use(function *() {
  var a = yield async1();
  var b = yield async2();
  var c = yield async3();
});

这会使 node 的并发失效。当我们调用 async1,我们必须等待 async1 完成才能执行 async2。不过我们可以用 promise 来优化这3个函数,然后生成一个合并的 promise。

app.use(function *() {
  var a = async1();
  var b = async2();
  var c = async3();
  var result = yield Promise.all([a, b, c]);
});

注意:tjholowaychuk 大神在原文留言指出了一些问题,见下面图

当 Koa 框架成熟时,它将会允许更加细粒度的控制以便于我们写出下一代的 web 应用。

TJ

JavaScript 迭代器和生成器

迭代器和生成器

翻译自 MDN 官方文档,原文地址:Iterators and Generators 处理集合里的每一项是一个非常普通的操作,JavaScript提供了许多方法来迭代一个集合,从简单的forfor each循环到 map()filter()array comprehensions(数组推导式)。在JavaScript 1.7中,迭代器和生成器在JavaScript核心语法中带来了新的迭代机制,而且还提供了定制 for…infor each 循环行为的机制。

##迭代器

迭代器是一个每次访问集合序列中一个元素的对象,并跟踪该序列中迭代的当前位置。在JavaScript中迭代器是一个对象,这个对象提供了一个 next() 方法,next() 方法返回序列中的下一个元素。当序列中所有元素都遍历完成时,该方法抛出 StopIteration 异常。

迭代器对象一旦被建立,就可以通过显式的重复调用next(),或者使用JavaScript的 for…infor each 循环隐式调用。

简单的对对象和数组进行迭代的迭代器可以使用 Iterator() 被创建:

    var lang = { name: 'JavaScript', birthYear: 1995 };
    var it = Iterator(lang);

一旦初始化完成,next() 方法可以被调用来依次访问对象的键值对:

    var pair = it.next(); //键值对是["name", "JavaScript"]
    pair = it.next(); //键值对是["birthday", 1995]
    pair = it.next(); //一个 `StopIteration` 异常被抛出

for…in 循环可以被用来替换显式的调用 next() 方法。当 StopIteration 异常被抛出时,循环会自动终止。

    var it = Iterator(lang);
    for (var pair in it)
      print(pair); //每次输出 it 中的一个 [key, value] 键值对

如果你只想迭代对象的 key 值,可以往 Iterator() 函数中传入第二个参数,值为 true

    var it = Iterator(lang, true);
    for (var key in it)
      print(key); //每次输出 key 值

使用 Iterator() 访问对象的一个好处是,被添加到 Object.prototype 的自定义属性不会被包含在序列对象中。

Iterator() 同样可以被作用在数组上:

    var langs = ['JavaScript', 'Python', 'Haskell'];
    var it = Iterator(langs);
    for (var pair in it)
      print(pair); //每次迭代输出 [index, language] 键值对

就像遍历对象一样,把 true 当做第二个参数传入遍历的结果将会是数组索引:

    var langs = ['JavaScript', 'Python', 'Haskell'];
    var it = Iterator(langs, true);
    for (var i in it)
      print(i); //输出 0,然后是 1,然后是 2

使用 let 关键字可以在循环内部分别分配索引和值给块变量,还可以解构赋值(Destructuring Assignment):

    var langs = ['JavaScript', 'Python', 'Haskell'];
    var it = Iterators(langs);
    for (let [i, lang] in it)
      print(i + ': ' + lang); //输出 "0: JavaScript" 等

##声明自定义迭代器

一些代表元素集合的对象应该用一种指定的方式来迭代。

  • 迭代一个表示范围(Range)的对象应该一个接一个的返回这个范围包含的数字
  • 一个树的叶子节点可以使用深度优先或者广度优先访问到
  • 迭代一个代表数据库查询结果的对象应该一行一行的返回,即使整个结果集尚未全部加载到一个单一数组
  • 作用在一个无限数学序列(像斐波那契序列)上的迭代器应该在不创建无限长度数据结构的前提下一个接一个的返回结果

JavaScript 允许你写自定义迭代逻辑的代码,并把它作用在一个对象上

我们创建一个简单的 Range 对象,包含低和高两个值

    function Range(low, high){
      this.low = low;
      this.high = high;
    }

现在我们创建一个自定义迭代器,它返回一个包含范围内所有整数的序列。迭代器接口需要我们提供一个 next() 方法用来返回序列中的下一个元素或者是抛出 StopIteration 异常。

    function RangeIterator(range){
      this.range = range;
      this.current = this.range.low;
    }
    RangeIterator.prototype.next = function(){
      if (this.current > this.range.high)
        throw StopIteration;
      else
        return this.current++;
    };

我们的 RangeIterator 通过 range 实例来实例化,同时维持一个 current 属性来跟踪当前序列的位置。

最后,为了让 RangeIterator 可以和 Range 结合起来,我们需要为 Range 添加一个特殊的 __iterator__ 方法。当我们试图去迭代一个 Range 时,它将被调用,而且应该返回一个实现了迭代逻辑的 RangeIterator 实例。

    Range.prototype.__iterator__ = function(){
      return new RangeIterator(this);
    };

完成我们的自定义迭代器后,我们就可以迭代一个范围实例:

    var range = new Range(3, 5);
    for (var i in range)
      print(i); //输出 3,然后 4,然后 5

##生成器:一种更好的方式来构建迭代器

虽然自定义的迭代器是一种很有用的工具,但是创建它们的时候要仔细规划,因为需要显式的维护它们的内部状态。 生成器提供了很强大的功能:它允许你定义一个包含自有迭代算法的函数, 同时它可以自动维护自己的状态。

生成器是可以作为迭代器工厂的特殊函数。如果一个函数包含了一个或多个 yield 表达式,那么就称它为生成器(译者注:Node.js 还需要在函数名前加 * 来表示)。

注意:只有 HTML 中被包含在 <script type="application/javascript;version=1.7"> (或者更高版本)中的代码块才可以使用 yield 关键字。XUL (XML User Interface Language) 脚本标签不需要指定这个特殊的代码块也可以访问这些特性。

当一个生成器函数被调用时,函数体不会即刻执行,它会返回一个 generator-iterator 对象。每次调用 generator-iterator 的 next() 方法,函数体就会执行到下一个 yield 表达式,然后返回它的结果。当函数结束或者碰到 return 语句,一个 StopIteration 异常会被抛出。

用一个例子来更好的说明:

    function simpleGenerator(){
      yield "first";
      yield "second";
      yield "third";
      for (var i = 0; i < 3; i++)
        yield i;
    }
    
    var g = simpleGenerator();
    print(g.next()); //输出 "first"
    print(g.next()); //输出 "second"
    print(g.next()); //输出 "third"
    print(g.next()); //输出 0
    print(g.next()); //输出 1
    print(g.next()); //输出 2
    print(g.next()); //抛出 StopIteration 异常

生成器函数可以被一个类直接的当做 __iterator__ 方法使用,在需要自定义迭代器的地方可以有效的减少代码量。我们使用生成器重写一下 Range

    function Range(low, high){
      this.low = low;
      this.high = high;
    }
    Range.prototype.__iterator__ = function(){
      for (var i = this.low; i <= this.high; i++)
        yield i;
    };
    var range = new Range(3, 5);
    for (var i in range)
      print(i); //输出 3,然后 4,然后 5

不是所有的生成器都会终止,你可以创建一个代表无限序列的生成器。下面的生成器实现一个斐波那契序列,就是每一个元素都是前面两个的和:

    function fibonacci(){
      var fn1 = 1;
      var fn2 = 1;
      while (1) {
        var current = fn2;
        fn2 = fn1;
        fn1 = fn1 + current;
        yield current;
      }
    }
    
    var sequence = fibonacci();
    print(sequence.next()); // 1
    print(sequence.next()); // 1
    print(sequence.next()); // 2
    print(sequence.next()); // 3
    print(sequence.next()); // 5
    print(sequence.next()); // 8
    print(sequence.next()); // 13

生成器函数可以带有参数,并且会在第一次调用函数时使用这些参数。生成器可以被终止(引起它抛出 StopIteration 异常)通过使用 return 语句。下面的 fibonacci() 变体带有一个可选的 limit 参数,当条件被触发时终止函数。

    function fibonacci(limit){
      var fn1 = 1;
      var fn2 = 1;
      while(1){
        var current = fn2;
        fn2 = fn1;
        fn1 = fn1 + current;
        if (limit && current > limit)
          return;
        yield current;
      }
    }

##生成器高级特性

生成器可以根据需求计算yield返回值,这使得它可以表示以前昂贵的序列计算需求,甚至是上面所示的无限序列。

除了 next() 方法,generator-iterator 对象还有一个 send() 方法,该方法可以修改生成器的内部状态。传给 send() 的值将会被当做最后一个 yield 表达式的结果,并且会暂停生成器。在你使用 send() 方法传一个指定值前,你必须至少调用一次 next() 来启动生成器。

下面的斐波那契生成器使用 send() 方法来重启序列:

    function fibonacci(){
      var fn1 = 1;
      var fn2 = 1;
      while (1) {
        var current = fn2;
        fn2 = fn1;
        fn1 = fn1 + current;
        var reset = yield current;
        if (reset) {
          fn1 = 1;
          fn2 = 1;
        }
      }
    }
    
    var sequence = fibonacci();
    print(sequence.next());     //1
    print(sequence.next());     //1
    print(sequence.next());     //2
    print(sequence.next());     //3
    print(sequence.next());     //5
    print(sequence.next());     //8
    print(sequence.next());     //13
    print(sequence.send(true)); //1
    print(sequence.next());     //1
    print(sequence.next());     //2
    print(sequence.next());     //3

注意:有意思的一点是,调用 send(undefined) 和调用 next() 是完全同等的。不过,当调用 send() 方法启动一个新的生成器时,除了 undefined 其它的值都会抛出一个 TypeError 异常。

你可以调用 throw 方法并且传递一个它应该抛出的异常值来强制生成器抛出一个异常。此异常将从当前上下文抛出并暂停生成器,类似当前的 yield 执行,只不过换成了 throw value 语句。

如果在抛出异常的处理过程中没有遇到 yield ,该异常将会被传递直到调用 throw() 方法,并且随后调用 next() 将会导致 StopIteration 异常被抛出。

生成器拥有一个 close() 方法来强制生成器结束。结束一个生成器会产生如下影响:

  1. 所有生成器中有效的 finally 字句将会执行
  2. 如果 finally 字句抛出了除 StopIteration 以外的任何异常,该异常将会被传递到 close() 方法的调用者
  3. 生成器会终止

##生成器表达式

数组推导式的一个明显缺点是,它们会导致整个数组在内存中构造。当输入到推导式的本身是个小数组时它的开销是微不足道的–但是,当输入数组很大或者创建一个新的昂贵(或者是无限的)数组生成器时就可能出现问题。

生成器允许对序列延迟计算(lazy computation),在需要时按需计算元素。生成器表达式在句法上几乎和数组推导式相同–它用圆括号来代替方括号(而且用 for...in 代替 for each...in)–但是它创建一个生成器而不是数组,这样就可以延迟计算。你可以把它想象成创建生成器的简短语法。

假设我们有一个迭代器 it 来迭代一个巨大的整数序列。我们需要创建一个新的迭代器来迭代偶数。一个数组推导式将会在内存中创建整个包含所有偶数的数组:

    var doubles = [i * 2 for (i in it)];

而生成器表达式将会创建一个新的迭代器,并且在需要的时候按需来计算偶数值:

    var it2 = (i * 2 for (i in it));
    print(it2.next());  //it 里面的第一个偶数
    print(it2.next());  //it 里面的第二个偶数

当一个生成器被用做函数的参数,圆括号被用做函数调用,意味着最外层的圆括号可以被省略:

    var result = doSomething(i * 2 for (i in it));

Pacman 主题已华丽更新,再次推荐!

告别node-forever, 拥抱PM2

原文地址:Goodbye node-forever,hello PM2

pm2-logo

devo.ps团队对JavaScript的迷恋已经不是什么秘密了;node.js作为服务器端,AngularJS作为客户端,某种程度上说,我们的堆栈是用它建成的.我们构建静态客户端和RESTful JSON API的方法意味着我们跑了很多的node.js,我必须承认尽管node.js的一切都令人敬畏,但当我们在生产环境中运行它时它仍然会让我们感到头疼.相比一些更加成熟的语言,它的工具和最佳实践仍然缺乏(试想一下:监控,日志,错误处理). 到目前为止,我们仍然依赖漂亮俏皮的node-forever模块.它是非常伟大的模块,不过依然缺失一些功能:

  • 有限的监控和日志功能
  • 进程管理配置的支持差
  • 不支持集群
  • 代码库老化(意味着在升级node.js时频繁的失败)

这就是为什么我们要在过去的几个月里去写PM2模块.在我们即将发布针对生产环境的正式版之前我们想先让您看一眼.

###PM2到底是什么个东西呢?

首先第一件事,你需要先通过npm来安装它:

npm install -g pm2

让我们通过表格来对比下:

Feature Forever PM2
Keep Alive
Coffeescript  
Log aggregation  
API  
Terminal monitoring  
Clustering  
JSON configuration  

现在让我来介绍一点点主要特性…

###原生的集群化支持

Node v0.6引入了集群特性,允许你在多个Node应用中共享socket.问题在于,它不能在容器外运行而且需要一些额外的配置来处理主进程和子进程.

PM2原生支持处理这个问题,而且不需要额外的代码:PM2本身作为主进程,然后它将你的代码封装到一个特殊的集群进程里,就像node.js一样,为你的代码文件添加一些全局变量.

想要启动一个使用所有CPU核心的集群,你只需要键入如下的指令:

$ pm2 start app.js -i max

然后:

$ pm2 list

然后就会显示类似下面的东西(ASCII UI FTW);

pm2-list

就像你看到的,现在你的应用有多少个进程就取决于你的CPU核心数了

###按照termcaps-HTOP(Linux下的系统监控与进程管理软件)的方式管理

通过pm2 list命令来观察所有运行的进程以及它们的状态已经足够好了.但是怎么来追踪它们的资源消耗呢?别担心,用这个命令:

$ pm2 monit

你可以得到进程(以及集群)的CPU的使用率和内存占用.

pm2-monit

声明:node-usage到目前为止还不支持MacOS(随便什么性能要求),不过它在Linux下运行良好.

现在,让我们来核实一下我们的集群,还有对内存堆栈的垃圾回收,我们假设你已经有一个HTTP基准测试工具(如果没有,你一定要使用WRK):

    $ express bufallo    //Create an express app
    $ cd bufallo
    $ npm install
    $ pm2 start app.js -i max
    $ wrk -c 100 -d 100 http://localhost:3000/

在另一个终端,运行监控选项:

$ pm2 monit

耶~

###实时集中log处理

现在你不得不管理多个集群进程:一个爬取数据,一个处理数据,等等…这就意味着大量log,你可以按照老式的方法处理:

$ tail -f /path/to/log1 /path/to/log2 ...

但我们想的很周到,我们增加了logs功能:

$ pm2 logs

pm2-logs

###快速恢复

现在事情一切顺利,你的进程嗡嗡的运行着,你需要做一次硬重启(hard restart).现在吗?是的,首先,dump掉:

$ pm2 dump

然后,你可以从文件中恢复它:

$ pm2 kill      //让我们假设一个PM2停掉了
$ pm2 resurect  //我所有的进程又满血满状态复活了

###强健的API

比方说,你想要监控所有被PM2管理的进程,而且同时还想监控运行这些进程的机器的状态(甚至希望创建一个Angular应用来调用这些API…):

$ pm2 web

打开浏览器输入http://localhost:9615 ,我嘞个去!

###对了,还有很多特性…

  • 全部测试通过,
  • 新一代的update-rc.d(pm2 startup),当然它还是alpha版,
  • 开发模式下更改文件自动重启(pm2 dev),也同样还是草稿,
  • 自动刷新log,
  • 快捷的通过JSON文件管理你的应用,
  • 在error log里记录未捕获的异常,
  • 记录重启的次数和时间,
  • 退出时自动杀死进程.

##下一步计划?

首先,你可以去Github上粉我们(我们喜欢stars).

我们开发的PM2提供了先进完整的Node进程管理解决方案.我们希望能有更多的人来帮助我们:更多的pull requests.一些还停留在开发路线图上面的功能我们会尽快完成,下面这些就是:

  • 远程管理/状态校验,
  • 嵌入式跨进程通信通道(消息总线),
  • V8垃圾回收的内存泄漏检查,
  • Web界面,
  • 监控数据持久化,
  • 邮件通知.

特别感谢Makara Wang的观点和工具,还有Alex Kocharin提的建议和提交的代码.

本博客基于hexo搭建,推荐一个不错的主题Pacman,虽然我还没用上,不过可以去@A-limon的主页查看效果.

最后感谢@A-limon同学提供翻译建议和审读.