IT牛人博客聚合网站 发现IT技术最优秀的内容, 寻找IT技术的价值 http://www.udpwork.com/ zh_CN http://www.udpwork.com/about hourly 1 Thu, 23 Mar 2017 10:08:20 +0800 <![CDATA[一个有特色的有限状态机]]> http://www.udpwork.com/item/16029.html http://www.udpwork.com/item/16029.html#reviews Wed, 22 Mar 2017 23:18:06 +0800 鸟窝 http://www.udpwork.com/item/16029.html gofsm是一个简单、小巧而又特色的有限状态机(FSM)。

github已经有了很多状态机的实现,比如文末列出的一些,还为什么要再发明轮子呢?

原因在于这些状态机有一个特点,就是一个状态机维护一个对象的状态,这样一个状态机就和一个具体的图像实例关联在一起,在有些情况下,这没有什么问题,而且是很好的设计,而且比较符合状态机的定义。但是在有些情况下,当我们需要维护成千上百个对象的时候,需要创建成千上百个状态机对象,这其实是很大的浪费,因为在大部分情况下,对象本身自己会维护/保持自己当前的状态,我们只需把对象当前的状态传递给一个共用的状态机就可以了,也就是gofsm本身是“stateless”,本身它包维护一个或者多个对象的状态,所有需要的输入由调用者输入,它只负责状态的转换的逻辑,所以它的实现非常的简洁实用,这是创建gofsm的一个目的。

第二个原因它提供了Moore和Mealy两种状态机的统一接口,并且提供了UML状态机风格的Action处理,以程序员更熟悉的方式处理状态的改变。

第三个原因,当我们谈论起状态机的时候,我们总会画一个状态转换图,大家可以根据这这张图进行讨论、设计、实现和验证状态的迁移。但是对于代码来说,实现真的和你的设计是一致的吗,你怎么保证?gofsm提供了一个简单的方法,那就是它可以输出图片或者pdf文件,你可以利用输出的状态机图和你的设计进行比较,看看实现和设计是否一致。

gofsm生成的闸门状态图

有限状态机

有限状态机(finite-state machine)常常用于计算机程序和时序逻辑电路的设计数学模型。它被看作是一种抽象的机器,可以有有限个状态。任意时刻这个机器只有唯一的一个状态,这个状态称为当前状态。当有外部的事件或者条件被触发,它可以从一个状态转换到另一个状态,这就是转换(transition)。一个FSM就是由所有的状态、初始状态和每个转换的触发条件所定义。有时候,当这个转换发生的时候,我们可以要执行一些事情,我们称之为动作(Action)。

现实情况中,我们实际上遇到了很多的这种状态机的情况,只不过我们并没有把它们抽象出来,比如路口的红绿灯,总是在红、黄、绿的状态之间转变,比如电梯的状态,包括开、关、上、下等几个状态。

有限状态机可以有效清晰的为一大堆的问题建立模型,大量应用于电子设计、通讯协议、语言解析和其它的工程应用中,比如TCP/IP协议栈。

图像来源 http://www.tcpipguide.com

以一个转门为例,这种专门在一些会展、博物馆、公园的门口很常见,顾客可以投币或者刷卡刷票进入,我们下面以投币(Coin)统称这个触发事件。如果你不投币,闸门是锁着的,你推不动它的转臂,而且投一次币只能进去一个人,过去之后闸门又是锁着的,挺智能的 :)。

图片来源 wikipedia

如果我们抽象出来它的状态图,可以用下图表示:
图片来源 wikipedia

它有两个状态:Locked、Unlocked。有两个输入(input)会影响它的状态,投币(coin)和推动转臂(push)。

  1. 在Locked状态, push没有作用。不管比push多少次闸门的状态还是lock
  2. 在Locked状态,投币会让闸门开锁,闸门可以让一个人通过
  3. 在Unlocked状态,投币不起作用,闸门还是开着
  4. 在Unlocked状态,如果有人push通过,人通过后闸门会由Unlocked状态转变成Locked状态。

这是一个简单的闸门的状态转换,却是一个很好的理解状态的典型例子。

以表格来表示:

Current State Input Next State Output
Locked coin Unlocked Unlock turnstile so customer can push through
push Locked None
Unlocked coin Unlocked None
push Locked When customer has pushed through, lock turnstile

UML也有状态图的改变,它扩展了FSM的概念,提供了层次化的嵌套状态(Hierarchically nested states)和正交区域(orthogonal regions),当然这和本文没有太多的关系,有兴趣的读者可以找UML的资料看看。但是它提供了一个很好的概念,也就是动作(Action)。就像Mealy状态机所需要的一样,动作依赖系统的状态和触发事件,而它的Entry Action和Exit Action,却又像Moore 状态机一样,不依赖输入,只依赖状态。所以UML的动作有三种,一种是事件被处理的时候,状态机会执行特定的动作,比如改变变量、执行I/O、调用方法、触发另一个事件等。而离开一个状态,可以执行Exit action,进入一个状态,则执行Entry action。记住,收到一个事件,对象的状态不会改变,比如上边闸门的例子,在Locked状态下push多少次状态都没改变,这这种情况下,不会执行Exit和Entry action。

gofsm提供了这种扩展的模型,当然如果你不想使用这种扩展,你也可以不去实现Entry和Exit。

可以提到了两种状态机,这两种状态机是这样来区分的:

  • Moore machine
    Moore状态机只使用entry action,输出只依赖状态,不依赖输入。
  • Mealy machine
    Mealy状态机只使用input action,输出依赖输入input和状态state。使用这种状态机通常可以减少状态的数量。

gofsm提供了一个通用的接口,你可以根据需要确定使用哪个状态机。从软件开发的实践上来看,有时候你并不一定要关注状态机的区分,而是清晰的抽象、设计你所关注的对象的状态、触发条件以及要执行的动作。

gofsm

gofsm参考了elimisteve/fsm的实现,实现了一种单一状态机处理多个对象的方法,并且提供了输出状态图的功能。

它除了定义对象的状态外,还定义了触发事件以及处理的Action,这些都是通过字符串来表示的,在使用的时候很容易的和你的对象、方法对应起来。

使用gofsm也很简单,当然第一步将库拉到本地:

1
go get -u github.com/smallnest/gofsm

我们以上面的闸门为例,看看gofsm是如何使用的。

注意下面的单个状态机可以处理并行地的处理多个闸门的状态改变,虽然例子中只生成了一个闸门对象。

首先定义一个闸门对象,它包含一个State,表示它当前的状态:

12345678
type Turnstile struct {	ID         uint64	EventCount uint64 //事件统计	CoinCount  uint64 //投币事件统计	PassCount  uint64 //顾客通过事件统计	State      string //当前状态	States     []string //历史经过的状态}

状态机的初始化简单直接:

123456789101112
func initFSM() *StateMachine {	delegate := &DefaultDelegate{p: &TurnstileEventProcessor{}}	transitions := []Transition{		Transition{From: "Locked", Event: "Coin", To: "Unlocked", Action: "check"},		Transition{From: "Locked", Event: "Push", To: "Locked", Action: "invalid-push"},		Transition{From: "Unlocked", Event: "Push", To: "Locked", Action: "pass"},		Transition{From: "Unlocked", Event: "Coin", To: "Unlocked", Action: "repeat-check"},	}	return NewStateMachine(delegate, transitions...)}

你定义好转换对应关系transitions,一个Transition代表一个转换,从某个状态到另外一个状态,触发的事件名,要执行的Action。
因为Action是字符串,所以你需要实现delegate将Action和对应的要处理的方法对应起来。

注意from和to的状态可以一样,在这种情况下,状态没有发生改变,只是需要处理Action就可以了。

如果Action为空,也就是不需要处理事件,只是发生状态的改变而已。

处理Action的类型如下:

12345678910111213
type TurnstileEventProcessor struct{}func (p *TurnstileEventProcessor) OnExit(fromState string, args []interface{}) {	……}func (p *TurnstileEventProcessor) Action(action string, fromState string, toState string, args []interface{}) {	……}func (p *TurnstileEventProcessor) OnEnter(toState string, args []interface{}) {    ……}

然后我们就可以触发一些事件看看闸门的状态机是否正常工作:

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263
ts := &Turnstile{	ID:     1,	State:  "Locked",	States: []string{"Locked"},}fsm := initFSM()//推门//没刷卡/投币不可进入err := fsm.Trigger(ts.State, "Push", ts)if err != nil {	t.Errorf("trigger err: %v", err)}//推门//没刷卡/投币不可进入err = fsm.Trigger(ts.State, "Push", ts)if err != nil {	t.Errorf("trigger err: %v", err)}//刷卡或者投币//不容易啊,终于解锁了err = fsm.Trigger(ts.State, "Coin", ts)if err != nil {	t.Errorf("trigger err: %v", err)}//刷卡或者投币//这时才解锁err = fsm.Trigger(ts.State, "Coin", ts)if err != nil {	t.Errorf("trigger err: %v", err)}//推门//这时才能进入,进入后闸门被锁err = fsm.Trigger(ts.State, "Push", ts)if err != nil {	t.Errorf("trigger err: %v", err)}//推门//无法进入,闸门已锁err = fsm.Trigger(ts.State, "Push", ts)if err != nil {	t.Errorf("trigger err: %v", err)}lastState := Turnstile{	ID:         1,	EventCount: 6,	CoinCount:  2,	PassCount:  1,	State:      "Locked",	States:     []string{"Locked", "Unlocked", "Locked"},}if !compareTurnstile(&lastState, ts) {	t.Errorf("Expected last state: %+v, but got %+v", lastState, ts)} else {	t.Logf("最终的状态: %+v", ts)}

如果想将状态图输出图片,可以调用下面的方法,它实际是调用graphviz生成的,所以请确保你的机器上是否安装了这个软件,你可以执行dot -h检查一下:

1
fsm.Export("state.png")

生成的图片就是文首的闸门的状态机的图片。

如果你想定制graphviz的参数,你可以调用另外一个方法:

1
func (m *StateMachine) ExportWithDetails(outfile string, format string, layout string, scale string, more string) error

其它Go语言实现的FSM

如果你发现gofsm的功能需要改进,或者有一些想法、或者发现了bug,请不用迟疑,在issue中提交你的意见和建议,我会及时的进行反馈。

如果你觉得本项目有用,或者将来可能会使用,请star这个项目smallnest/gofsm

如果你想比较其它的Go语言实现的fsm,可以参考下面的列表:

参考资料

  1. https://en.wikipedia.org/wiki/Finite-state_machine
]]>
gofsm是一个简单、小巧而又特色的有限状态机(FSM)。

github已经有了很多状态机的实现,比如文末列出的一些,还为什么要再发明轮子呢?

原因在于这些状态机有一个特点,就是一个状态机维护一个对象的状态,这样一个状态机就和一个具体的图像实例关联在一起,在有些情况下,这没有什么问题,而且是很好的设计,而且比较符合状态机的定义。但是在有些情况下,当我们需要维护成千上百个对象的时候,需要创建成千上百个状态机对象,这其实是很大的浪费,因为在大部分情况下,对象本身自己会维护/保持自己当前的状态,我们只需把对象当前的状态传递给一个共用的状态机就可以了,也就是gofsm本身是“stateless”,本身它包维护一个或者多个对象的状态,所有需要的输入由调用者输入,它只负责状态的转换的逻辑,所以它的实现非常的简洁实用,这是创建gofsm的一个目的。

第二个原因它提供了Moore和Mealy两种状态机的统一接口,并且提供了UML状态机风格的Action处理,以程序员更熟悉的方式处理状态的改变。

第三个原因,当我们谈论起状态机的时候,我们总会画一个状态转换图,大家可以根据这这张图进行讨论、设计、实现和验证状态的迁移。但是对于代码来说,实现真的和你的设计是一致的吗,你怎么保证?gofsm提供了一个简单的方法,那就是它可以输出图片或者pdf文件,你可以利用输出的状态机图和你的设计进行比较,看看实现和设计是否一致。

gofsm生成的闸门状态图

有限状态机

有限状态机(finite-state machine)常常用于计算机程序和时序逻辑电路的设计数学模型。它被看作是一种抽象的机器,可以有有限个状态。任意时刻这个机器只有唯一的一个状态,这个状态称为当前状态。当有外部的事件或者条件被触发,它可以从一个状态转换到另一个状态,这就是转换(transition)。一个FSM就是由所有的状态、初始状态和每个转换的触发条件所定义。有时候,当这个转换发生的时候,我们可以要执行一些事情,我们称之为动作(Action)。

现实情况中,我们实际上遇到了很多的这种状态机的情况,只不过我们并没有把它们抽象出来,比如路口的红绿灯,总是在红、黄、绿的状态之间转变,比如电梯的状态,包括开、关、上、下等几个状态。

有限状态机可以有效清晰的为一大堆的问题建立模型,大量应用于电子设计、通讯协议、语言解析和其它的工程应用中,比如TCP/IP协议栈。

图像来源 http://www.tcpipguide.com

以一个转门为例,这种专门在一些会展、博物馆、公园的门口很常见,顾客可以投币或者刷卡刷票进入,我们下面以投币(Coin)统称这个触发事件。如果你不投币,闸门是锁着的,你推不动它的转臂,而且投一次币只能进去一个人,过去之后闸门又是锁着的,挺智能的 :)。

图片来源 wikipedia

如果我们抽象出来它的状态图,可以用下图表示:
图片来源 wikipedia

它有两个状态:Locked、Unlocked。有两个输入(input)会影响它的状态,投币(coin)和推动转臂(push)。

  1. 在Locked状态, push没有作用。不管比push多少次闸门的状态还是lock
  2. 在Locked状态,投币会让闸门开锁,闸门可以让一个人通过
  3. 在Unlocked状态,投币不起作用,闸门还是开着
  4. 在Unlocked状态,如果有人push通过,人通过后闸门会由Unlocked状态转变成Locked状态。

这是一个简单的闸门的状态转换,却是一个很好的理解状态的典型例子。

以表格来表示:

Current State Input Next State Output
Locked coin Unlocked Unlock turnstile so customer can push through
push Locked None
Unlocked coin Unlocked None
push Locked When customer has pushed through, lock turnstile

UML也有状态图的改变,它扩展了FSM的概念,提供了层次化的嵌套状态(Hierarchically nested states)和正交区域(orthogonal regions),当然这和本文没有太多的关系,有兴趣的读者可以找UML的资料看看。但是它提供了一个很好的概念,也就是动作(Action)。就像Mealy状态机所需要的一样,动作依赖系统的状态和触发事件,而它的Entry Action和Exit Action,却又像Moore 状态机一样,不依赖输入,只依赖状态。所以UML的动作有三种,一种是事件被处理的时候,状态机会执行特定的动作,比如改变变量、执行I/O、调用方法、触发另一个事件等。而离开一个状态,可以执行Exit action,进入一个状态,则执行Entry action。记住,收到一个事件,对象的状态不会改变,比如上边闸门的例子,在Locked状态下push多少次状态都没改变,这这种情况下,不会执行Exit和Entry action。

gofsm提供了这种扩展的模型,当然如果你不想使用这种扩展,你也可以不去实现Entry和Exit。

可以提到了两种状态机,这两种状态机是这样来区分的:

  • Moore machine
    Moore状态机只使用entry action,输出只依赖状态,不依赖输入。
  • Mealy machine
    Mealy状态机只使用input action,输出依赖输入input和状态state。使用这种状态机通常可以减少状态的数量。

gofsm提供了一个通用的接口,你可以根据需要确定使用哪个状态机。从软件开发的实践上来看,有时候你并不一定要关注状态机的区分,而是清晰的抽象、设计你所关注的对象的状态、触发条件以及要执行的动作。

gofsm

gofsm参考了elimisteve/fsm的实现,实现了一种单一状态机处理多个对象的方法,并且提供了输出状态图的功能。

它除了定义对象的状态外,还定义了触发事件以及处理的Action,这些都是通过字符串来表示的,在使用的时候很容易的和你的对象、方法对应起来。

使用gofsm也很简单,当然第一步将库拉到本地:

1
go get -u github.com/smallnest/gofsm

我们以上面的闸门为例,看看gofsm是如何使用的。

注意下面的单个状态机可以处理并行地的处理多个闸门的状态改变,虽然例子中只生成了一个闸门对象。

首先定义一个闸门对象,它包含一个State,表示它当前的状态:

12345678
type Turnstile struct {	ID         uint64	EventCount uint64 //事件统计	CoinCount  uint64 //投币事件统计	PassCount  uint64 //顾客通过事件统计	State      string //当前状态	States     []string //历史经过的状态}

状态机的初始化简单直接:

123456789101112
func initFSM() *StateMachine {	delegate := &DefaultDelegate{p: &TurnstileEventProcessor{}}	transitions := []Transition{		Transition{From: "Locked", Event: "Coin", To: "Unlocked", Action: "check"},		Transition{From: "Locked", Event: "Push", To: "Locked", Action: "invalid-push"},		Transition{From: "Unlocked", Event: "Push", To: "Locked", Action: "pass"},		Transition{From: "Unlocked", Event: "Coin", To: "Unlocked", Action: "repeat-check"},	}	return NewStateMachine(delegate, transitions...)}

你定义好转换对应关系transitions,一个Transition代表一个转换,从某个状态到另外一个状态,触发的事件名,要执行的Action。
因为Action是字符串,所以你需要实现delegate将Action和对应的要处理的方法对应起来。

注意from和to的状态可以一样,在这种情况下,状态没有发生改变,只是需要处理Action就可以了。

如果Action为空,也就是不需要处理事件,只是发生状态的改变而已。

处理Action的类型如下:

12345678910111213
type TurnstileEventProcessor struct{}func (p *TurnstileEventProcessor) OnExit(fromState string, args []interface{}) {	……}func (p *TurnstileEventProcessor) Action(action string, fromState string, toState string, args []interface{}) {	……}func (p *TurnstileEventProcessor) OnEnter(toState string, args []interface{}) {    ……}

然后我们就可以触发一些事件看看闸门的状态机是否正常工作:

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263
ts := &Turnstile{	ID:     1,	State:  "Locked",	States: []string{"Locked"},}fsm := initFSM()//推门//没刷卡/投币不可进入err := fsm.Trigger(ts.State, "Push", ts)if err != nil {	t.Errorf("trigger err: %v", err)}//推门//没刷卡/投币不可进入err = fsm.Trigger(ts.State, "Push", ts)if err != nil {	t.Errorf("trigger err: %v", err)}//刷卡或者投币//不容易啊,终于解锁了err = fsm.Trigger(ts.State, "Coin", ts)if err != nil {	t.Errorf("trigger err: %v", err)}//刷卡或者投币//这时才解锁err = fsm.Trigger(ts.State, "Coin", ts)if err != nil {	t.Errorf("trigger err: %v", err)}//推门//这时才能进入,进入后闸门被锁err = fsm.Trigger(ts.State, "Push", ts)if err != nil {	t.Errorf("trigger err: %v", err)}//推门//无法进入,闸门已锁err = fsm.Trigger(ts.State, "Push", ts)if err != nil {	t.Errorf("trigger err: %v", err)}lastState := Turnstile{	ID:         1,	EventCount: 6,	CoinCount:  2,	PassCount:  1,	State:      "Locked",	States:     []string{"Locked", "Unlocked", "Locked"},}if !compareTurnstile(&lastState, ts) {	t.Errorf("Expected last state: %+v, but got %+v", lastState, ts)} else {	t.Logf("最终的状态: %+v", ts)}

如果想将状态图输出图片,可以调用下面的方法,它实际是调用graphviz生成的,所以请确保你的机器上是否安装了这个软件,你可以执行dot -h检查一下:

1
fsm.Export("state.png")

生成的图片就是文首的闸门的状态机的图片。

如果你想定制graphviz的参数,你可以调用另外一个方法:

1
func (m *StateMachine) ExportWithDetails(outfile string, format string, layout string, scale string, more string) error

其它Go语言实现的FSM

如果你发现gofsm的功能需要改进,或者有一些想法、或者发现了bug,请不用迟疑,在issue中提交你的意见和建议,我会及时的进行反馈。

如果你觉得本项目有用,或者将来可能会使用,请star这个项目smallnest/gofsm

如果你想比较其它的Go语言实现的fsm,可以参考下面的列表:

参考资料

  1. https://en.wikipedia.org/wiki/Finite-state_machine
]]>
0
<![CDATA[[译]Go Slice 秘籍]]> http://www.udpwork.com/item/16187.html http://www.udpwork.com/item/16187.html#reviews Wed, 22 Mar 2017 20:17:19 +0800 鸟窝 http://www.udpwork.com/item/16187.html 这是 Golang官方的一个总结:SliceTricks

由于引入了内建的append的方法, 包container/vector的很多方法都被移除了,可以被内建的append和copy方法代替。

下面是栈vector的操作方法的实现,使用slice实现相关的操作。

AppendVector

1
a = append(a, b...)

Copy

1234
b = make([]T, len(a))copy(b, a)// 如果a不为空,也可以用下面的方式b = append([]T(nil), a...)

Cut

切掉一段数据

1
a = append(a[:i], a[j:]...)

Delete

删除一个元素

123
a = append(a[:i], a[i+1:]...)// ora = a[:i+copy(a[i:], a[i+1:])]

Delete,但不保持原来顺序

12
a[i] = a[len(a)-1] a = a[:len(a)-1]

注意 :如果元素是一个指针,或者是一个包含指针字段的struct,上面的cut、delete实现可能会有潜在的内存泄漏的问题。一些元素的值可能会被a一直引用而不被释放,下面的代码可以解决这个问题。

Cut

12345
copy(a[i:], a[j:])for k, n := len(a)-j+i, len(a); k < n; k++ {	a[k] = nil // or the zero value of T}a = a[:len(a)-j+i]

Delete

123
copy(a[i:], a[i+1:])a[len(a)-1] = nil // or the zero value of Ta = a[:len(a)-1]

Delete,但不保持原来顺序

123
a[i] = a[len(a)-1]a[len(a)-1] = nila = a[:len(a)-1]

Expand

插入一段到中间

1
a = append(a[:i], append(make([]T, j), a[i:]...)...)

Extend

插入一段到尾部

1
a = append(a, make([]T, j)...)

Insert

1
a = append(a[:i], append([]T{x}, a[i:]...)...)

注意 : 第二个append会使用它底层的存储创建一个新的slice,然后复制a[i:]到这个slice,然后把这个slice再复制回s。 新的slice的创建和第二次copy可以使用下面的方式来避免:

Insert

123
s = append(s, 0)copy(s[i+1:], s[i:])s[i] = x

InsertVector

1
a = append(a[:i], append(b, a[i:]...)...)

Pop

1
x, a = a[len(a)-1], a[:len(a)-1]

Push

1
a = append(a, x)

Shift

1
x, a := a[0], a[1:]

Unshift

1
a = append([]T{x}, a...)

其它技巧

无额外对象分配的filter

这个技巧利用了slice会共享它的底层的数据存储和容量。

123456
b := a[:0]for _, x := range a {	if f(x) {		b = append(b, x)	}}

反转

将slice中的元素反转。

1234
for i := len(a)/2-1; i >= 0; i-- {	opp := len(a)-1-i	a[i], a[opp] = a[opp], a[i]}

下面的代码类似,只不过使用了两个索引变量

123
for left, right := 0, len(a)-1; left < right; left, right = left+1, right-1 {	a[left], a[right] = a[right], a[left]}
]]>
这是 Golang官方的一个总结:SliceTricks

由于引入了内建的append的方法, 包container/vector的很多方法都被移除了,可以被内建的append和copy方法代替。

下面是栈vector的操作方法的实现,使用slice实现相关的操作。

AppendVector

1
a = append(a, b...)

Copy

1234
b = make([]T, len(a))copy(b, a)// 如果a不为空,也可以用下面的方式b = append([]T(nil), a...)

Cut

切掉一段数据

1
a = append(a[:i], a[j:]...)

Delete

删除一个元素

123
a = append(a[:i], a[i+1:]...)// ora = a[:i+copy(a[i:], a[i+1:])]

Delete,但不保持原来顺序

12
a[i] = a[len(a)-1] a = a[:len(a)-1]

注意 :如果元素是一个指针,或者是一个包含指针字段的struct,上面的cut、delete实现可能会有潜在的内存泄漏的问题。一些元素的值可能会被a一直引用而不被释放,下面的代码可以解决这个问题。

Cut

12345
copy(a[i:], a[j:])for k, n := len(a)-j+i, len(a); k < n; k++ {	a[k] = nil // or the zero value of T}a = a[:len(a)-j+i]

Delete

123
copy(a[i:], a[i+1:])a[len(a)-1] = nil // or the zero value of Ta = a[:len(a)-1]

Delete,但不保持原来顺序

123
a[i] = a[len(a)-1]a[len(a)-1] = nila = a[:len(a)-1]

Expand

插入一段到中间

1
a = append(a[:i], append(make([]T, j), a[i:]...)...)

Extend

插入一段到尾部

1
a = append(a, make([]T, j)...)

Insert

1
a = append(a[:i], append([]T{x}, a[i:]...)...)

注意 : 第二个append会使用它底层的存储创建一个新的slice,然后复制a[i:]到这个slice,然后把这个slice再复制回s。 新的slice的创建和第二次copy可以使用下面的方式来避免:

Insert

123
s = append(s, 0)copy(s[i+1:], s[i:])s[i] = x

InsertVector

1
a = append(a[:i], append(b, a[i:]...)...)

Pop

1
x, a = a[len(a)-1], a[:len(a)-1]

Push

1
a = append(a, x)

Shift

1
x, a := a[0], a[1:]

Unshift

1
a = append([]T{x}, a...)

其它技巧

无额外对象分配的filter

这个技巧利用了slice会共享它的底层的数据存储和容量。

123456
b := a[:0]for _, x := range a {	if f(x) {		b = append(b, x)	}}

反转

将slice中的元素反转。

1234
for i := len(a)/2-1; i >= 0; i-- {	opp := len(a)-1-i	a[i], a[opp] = a[opp], a[i]}

下面的代码类似,只不过使用了两个索引变量

123
for left, right := 0, len(a)-1; left < right; left, right = left+1, right-1 {	a[left], a[right] = a[right], a[left]}
]]>
0
<![CDATA[skynet 1.1 发布候选版本]]> http://www.udpwork.com/item/16184.html http://www.udpwork.com/item/16184.html#reviews Wed, 22 Mar 2017 11:23:33 +0800 云风 http://www.udpwork.com/item/16184.html skynet 1.0 于 2016 年 8 月 1 日正式发布,到今天已经有 7 个多月了。这段时间积累了很多小修改,我想是时候发布 1.1 版了。

很高兴这段时间 skynet 社区继续壮大,有更多的公司选择基于 skynet 开发。

现打算在下个月以目前 github 仓库 master 分支为基础发布 1.1 正式版,这两周如果同学们还有什么问题请尽快提 issue 。

下面是从 1.0 开始积累的更新:

  • debug console : 可用户指定绑定 ip 。
  • debug console : 增加 call 指令向服务发送消息。
  • debug console : 反馈 inject code 的错误信息。
  • debug console : 修改命令确认信息,方便自动化处理。
  • sharedata : 增加 flush 。
  • sharedata : 增加 deepcopy 。
  • cluster : 增加 send 。
  • cluster : 支持绕过配置文件直接传递配置表。
  • skynet : 增加 state 指令查询服务的 cpu 开销。
  • skynet : wakeup 保证次序。
  • httpc : 支持 timeout 。
  • mongo driver : sort 支持多个 key 。
  • bson : 对 string 类型做 utf8 编码检查。
  • daemon 模式 : 可正确输出错误信息。
  • sproto : 支持定点数。
  • sproto: 支持 binary 类型。
  • jemalloc : 更新到 4.5.0
  • lua : 更新到 5.3.4

还有一些次要的 bugfix 及代码调整没有列出。

]]>
skynet 1.0 于 2016 年 8 月 1 日正式发布,到今天已经有 7 个多月了。这段时间积累了很多小修改,我想是时候发布 1.1 版了。

很高兴这段时间 skynet 社区继续壮大,有更多的公司选择基于 skynet 开发。

现打算在下个月以目前 github 仓库 master 分支为基础发布 1.1 正式版,这两周如果同学们还有什么问题请尽快提 issue 。

下面是从 1.0 开始积累的更新:

  • debug console : 可用户指定绑定 ip 。
  • debug console : 增加 call 指令向服务发送消息。
  • debug console : 反馈 inject code 的错误信息。
  • debug console : 修改命令确认信息,方便自动化处理。
  • sharedata : 增加 flush 。
  • sharedata : 增加 deepcopy 。
  • cluster : 增加 send 。
  • cluster : 支持绕过配置文件直接传递配置表。
  • skynet : 增加 state 指令查询服务的 cpu 开销。
  • skynet : wakeup 保证次序。
  • httpc : 支持 timeout 。
  • mongo driver : sort 支持多个 key 。
  • bson : 对 string 类型做 utf8 编码检查。
  • daemon 模式 : 可正确输出错误信息。
  • sproto : 支持定点数。
  • sproto: 支持 binary 类型。
  • jemalloc : 更新到 4.5.0
  • lua : 更新到 5.3.4

还有一些次要的 bugfix 及代码调整没有列出。

]]>
0
<![CDATA[控制RecyclerView item的宽度]]> http://www.udpwork.com/item/16186.html http://www.udpwork.com/item/16186.html#reviews Tue, 21 Mar 2017 21:54:00 +0800 技术小黑屋 http://www.udpwork.com/item/16186.html 自从Android中引入RecyclerView之后,它就逐步的替换掉了ListView和GridView。本文很简单,行文目的是记录和备忘。如果能帮到你,那再好不过了。

关于控制RecyclerView item的宽度,说起来还不是那么清晰,上一张图,就明白了。

http://7jpolu.com1.z0.glb.clouddn.com/recyclerview_item_width.png

  • 上面的实际上是一个Grid布局
  • 前三行每个item均分RecyclerView的宽度
  • 最后一行的Others占大概三分之一,而Flipboard则占据了三分之二。

上面的图和描述就是我们今天想要实现的效果。

方法很简单,主要使用了GridLayoutManager的setSpanSizeLookup方法

1
2
3
4
5
6
7
8
9
10
11
mLayoutManager = new GridLayoutManager(this, 3);
mLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
    @Override
    public int getSpanSize(int position) {
        if (position == mAdapter.getItemCount() - 1) {
            return 2;
        } else {
            return 1;
        }
    }
});
  • GridLayoutManager构造方法中传入了一个spanCount,这里值为3
  • getSpanSize方法中,最后一个item占据2个span,其他占据一个span

完整示例源码

]]>
自从Android中引入RecyclerView之后,它就逐步的替换掉了ListView和GridView。本文很简单,行文目的是记录和备忘。如果能帮到你,那再好不过了。

关于控制RecyclerView item的宽度,说起来还不是那么清晰,上一张图,就明白了。

http://7jpolu.com1.z0.glb.clouddn.com/recyclerview_item_width.png

  • 上面的实际上是一个Grid布局
  • 前三行每个item均分RecyclerView的宽度
  • 最后一行的Others占大概三分之一,而Flipboard则占据了三分之二。

上面的图和描述就是我们今天想要实现的效果。

方法很简单,主要使用了GridLayoutManager的setSpanSizeLookup方法

1
2
3
4
5
6
7
8
9
10
11
mLayoutManager = new GridLayoutManager(this, 3);
mLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
    @Override
    public int getSpanSize(int position) {
        if (position == mAdapter.getItemCount() - 1) {
            return 2;
        } else {
            return 1;
        }
    }
});
  • GridLayoutManager构造方法中传入了一个spanCount,这里值为3
  • getSpanSize方法中,最后一个item占据2个span,其他占据一个span

完整示例源码

]]>
0
<![CDATA[前端架构的设计与进化]]> http://www.udpwork.com/item/16185.html http://www.udpwork.com/item/16185.html#reviews Tue, 21 Mar 2017 10:04:01 +0800 s5s5 http://www.udpwork.com/item/16185.html 上周参加了一门这个课,笔记一下

架构

  • 开发模式
  • 通用模型
  • 模板引擎
  • 基础类库

组件框架

  • 避免GOOGLE
  • 大厂
  • 选合适的
  • 不用时怎么办

代码结构

  • 团队和业务快速变化
  • 开发行为耦合
  • 多特性多并行
  • 主线是什么

开发规范

  • 编码
  • 设计思路
  • 模块拆分
  • 结构分层

工具平台

工程化过程

  • 框架支撑
  • 研发效率(质量)
  • 代码管理
  • 运营支撑
  • 运维监控

流程边界

  • 流程
  • 分工跨界
  • 活动开发
  • 平台化

经验沉淀

核心理念

扫码关注米随随

]]>
上周参加了一门这个课,笔记一下

架构

  • 开发模式
  • 通用模型
  • 模板引擎
  • 基础类库

组件框架

  • 避免GOOGLE
  • 大厂
  • 选合适的
  • 不用时怎么办

代码结构

  • 团队和业务快速变化
  • 开发行为耦合
  • 多特性多并行
  • 主线是什么

开发规范

  • 编码
  • 设计思路
  • 模块拆分
  • 结构分层

工具平台

工程化过程

  • 框架支撑
  • 研发效率(质量)
  • 代码管理
  • 运营支撑
  • 运维监控

流程边界

  • 流程
  • 分工跨界
  • 活动开发
  • 平台化

经验沉淀

核心理念

扫码关注米随随

]]>
0
<![CDATA[还是猴子]]> http://www.udpwork.com/item/16183.html http://www.udpwork.com/item/16183.html#reviews Sun, 19 Mar 2017 18:21:41 +0800 qyjohn http://www.udpwork.com/item/16183.html thumb_IMG_2539_1024thumb_IMG_2540_1024thumb_IMG_2541_1024thumb_IMG_2543_1024thumb_IMG_2544_1024

]]>
thumb_IMG_2539_1024thumb_IMG_2540_1024thumb_IMG_2541_1024thumb_IMG_2543_1024thumb_IMG_2544_1024

]]>
0
<![CDATA[Reduce 和 Transduce 的含义]]> http://www.udpwork.com/item/16182.html http://www.udpwork.com/item/16182.html#reviews Sat, 18 Mar 2017 16:50:20 +0800 阮一峰 http://www.udpwork.com/item/16182.html 学习函数式编程,必须掌握很多术语,否则根本看不懂文档。

本文介绍两个基本术语:reduce和transduce。它们非常重要,也非常有用。

一、reduce 的用法

reduce是一种数组运算,通常用于将数组的所有成员"累积"为一个值。

var arr = [1, 2, 3, 4];

var sum = (a, b) => a + b;

arr.reduce(sum, 0) // 10

上面代码中,reduce对数组arr的每个成员执行sum函数。sum的参数a是累积变量,参数b是当前的数组成员。每次执行时,b会加到a,最后输出a。

累积变量必须有一个初始值,上例是reduce函数的第二个参数0。如果省略该参数,那么初始值默认是数组的第一个成员。

var arr = [1, 2, 3, 4];

var sum = function (a, b) {
  console.log(a, b);
  return a + b;
};

arr.reduce(sum) // => 10
// 1 2
// 3 3
// 6 4

上面代码中,reduce方法省略了初始值。通过sum函数里面的打印语句,可以看到累积变量每一次的变化。

总之,reduce方法提供了一种遍历手段,对数组所有成员进行"累积"处理。

二、map 是 reduce 的特例

累积变量的初始值也可以是一个数组。

var arr = [1, 2, 3, 4];

var handler = function (newArr, x) {
  newArr.push(x + 1);
  return newArr;
};

arr.reduce(handler, [])
// [2, 3, 4, 5]

上面代码中,累积变量的初始值是一个空数组,结果reduce就返回了一个新数组,等同于执行map方法,对原数组进行一次"变形"。下面是使用map改写上面的例子。

var arr = [1, 2, 3, 4];
var plusOne = x => x + 1;
arr.map(plusOne) // [2, 3, 4, 5]

事实上,所有的map方法都可以基于reduce实现。

function map(f, arr) {
  return arr.reduce(function(result, x) {
    result.push(f(x));
    return result;
  }, []);
}

因此,map只是reduce的一种特例。

三、reduce的本质

本质上,reduce是三种运算的合成。

  • 遍历
  • 变形
  • 累积

还是来看上面的例子。

var arr = [1, 2, 3, 4];
var handler = function (newArr, x) {
  newArr.push(x + 1);
  return newArr;
};

arr.reduce(handler, [])
// [2, 3, 4, 5]

上面代码中,首先,reduce遍历了原数组,这是它能够取代map方法的根本原因;其次,reduce对原数组的每个成员进行了"变形"(上例是加1);最后,才是把它们累积起来(上例是push方法)。

四、 transduce 的含义

reduce包含了三种运算,因此非常有用。但也带来了一个问题:代码的复用性不高。在reduce里面,变形和累积是耦合的,不太容易拆分。

每次使用reduce,开发者往往都要从头写代码,重复实现很多基本功能,很难复用别人的代码。

var handler = function (newArr, x) {
  newArr.push(x + 1);
  return newArr;
};

上面的这个处理函数,就很难用在其他场合。

有没有解决方法呢?回答是有的,就是把"变形"和"累积"这两种运算分开。如果reduce允许变形运算和累积运算分开,那么代码的复用性就会大大增加。这就是transduce方法的由来。

transduce这个名字来自 transform(变形)和 reduce 这两个单词的合成。它其实就是reduce方法的一种不那么耦合的写法。

// 变形运算
var plusOne = x => x + 1;

// 累积运算
var append = function (newArr, x) {
  newArr.push(x);
  return newArr;
}; 

R.transduce(R.map(plusOne), append, [], arr);
// [2, 3, 4, 5]

上面代码中,plusOne是变形操作,append是累积操作。我使用了Ramda 函数库的transduce实现。可以看到,transduce就是将变形和累积从reduce拆分出来,其他并无不同。

五、transduce 的用法

transduce最大的好处,就是代码复用更容易。

var arr = [1, 2, 3, 4];
var append = function (newArr, x) {
  newArr.push(x);
  return newArr;
}; 

// 示例一
var plusOne = x => x + 1;
var square = x => x * x;

R.transduce(
  R.map(R.pipe(plusOne, square)), 
  append, 
  [], 
  arr
); // [4, 9, 16, 25]

// 示例二
var isOdd = x => x % 2 === 1;

R.transduce(
  R.pipe(R.filter(isOdd), R.map(square)), 
  append, 
  [], 
  arr
); // [1, 9]

上面代码中,示例一是两个变形操作的合成,示例二是过滤操作与变形操作的合成。这两个例子都使用了Pointfree 风格

可以看到,transduce非常有利于代码的复用,可以将一系列简单的、可复用的函数合成为复杂操作。作为练习,有兴趣的读者可以试试,使用reduce方法完成上面两个示例。你会发现,代码的复杂度和行数大大增加。

六、Transformer 对象

transduce函数的第一个参数是一个对象,称为 Transformer 对象(变形器)。前面例子中,R.map(plusOne)返回的就是一个 Transformer 对象。

事实上,任何一个对象只要遵守Transformer 协议,就是 Transformer 对象。

var Map = function(f, xf) {
    return {
       "@@transducer/init": function() { 
           return xf["@@transducer/init"](); 
       },
       "@@transducer/result": function(result) { 
           return xf["@@transducer/result"](result); 
       },
       "@@transducer/step": function(result, input) {
           return xf["@@transducer/step"](result, f(input)); 
       }
    };
};

上面代码中,Map函数返回的就是一个 Transformer 对象。它必须具有以下三个属性。

  • @@transducer/step:执行变形操作
  • @@transducer/init:返回初始值
  • @@transducer/result:返回变形后的最终值

所有符合这个协议的对象,都可以与其他 Transformer 对象合成,充当transduce函数的第一个参数。

因此,transduce函数的参数类型如下。

transduce(
  变形器 : Object,
  累积器 : Function,
  初始值 : Any,
  原始数组 : Array
)

七、into 方法

最后,你也许发现了,前面所有示例使用的都是同一个累积器。

var append = function (newArr, x) {
  newArr.push(x);
  return newArr;
}; 

上面代码的append函数是一个常见累积器。因此, Ramda 函数库提供了into方法,将它内置了。也就是说,into方法相当于默认提供append的transduce函数。

R.transduce(R.map(R.add(1)), append, [], [1,2,3,4]);
// 等同于
R.into([], R.map(R.add(1)), [1,2,3,4]);

上面代码中,into方法的第一个参数是初始值,第二个参数是变形器,第三个参数是原始数组,不需要提供累积器。

下面是另外一个例子。

R.into(
  [5, 6],
  R.pipe(R.take(2), R.map(R.add(1))),
  [1, 2, 3, 4]
) // [5, 6, 2, 3]

八、参考链接

(完)

文档信息

]]>
学习函数式编程,必须掌握很多术语,否则根本看不懂文档。

本文介绍两个基本术语:reduce和transduce。它们非常重要,也非常有用。

一、reduce 的用法

reduce是一种数组运算,通常用于将数组的所有成员"累积"为一个值。

var arr = [1, 2, 3, 4];

var sum = (a, b) => a + b;

arr.reduce(sum, 0) // 10

上面代码中,reduce对数组arr的每个成员执行sum函数。sum的参数a是累积变量,参数b是当前的数组成员。每次执行时,b会加到a,最后输出a。

累积变量必须有一个初始值,上例是reduce函数的第二个参数0。如果省略该参数,那么初始值默认是数组的第一个成员。

var arr = [1, 2, 3, 4];

var sum = function (a, b) {
  console.log(a, b);
  return a + b;
};

arr.reduce(sum) // => 10
// 1 2
// 3 3
// 6 4

上面代码中,reduce方法省略了初始值。通过sum函数里面的打印语句,可以看到累积变量每一次的变化。

总之,reduce方法提供了一种遍历手段,对数组所有成员进行"累积"处理。

二、map 是 reduce 的特例

累积变量的初始值也可以是一个数组。

var arr = [1, 2, 3, 4];

var handler = function (newArr, x) {
  newArr.push(x + 1);
  return newArr;
};

arr.reduce(handler, [])
// [2, 3, 4, 5]

上面代码中,累积变量的初始值是一个空数组,结果reduce就返回了一个新数组,等同于执行map方法,对原数组进行一次"变形"。下面是使用map改写上面的例子。

var arr = [1, 2, 3, 4];
var plusOne = x => x + 1;
arr.map(plusOne) // [2, 3, 4, 5]

事实上,所有的map方法都可以基于reduce实现。

function map(f, arr) {
  return arr.reduce(function(result, x) {
    result.push(f(x));
    return result;
  }, []);
}

因此,map只是reduce的一种特例。

三、reduce的本质

本质上,reduce是三种运算的合成。

  • 遍历
  • 变形
  • 累积

还是来看上面的例子。

var arr = [1, 2, 3, 4];
var handler = function (newArr, x) {
  newArr.push(x + 1);
  return newArr;
};

arr.reduce(handler, [])
// [2, 3, 4, 5]

上面代码中,首先,reduce遍历了原数组,这是它能够取代map方法的根本原因;其次,reduce对原数组的每个成员进行了"变形"(上例是加1);最后,才是把它们累积起来(上例是push方法)。

四、 transduce 的含义

reduce包含了三种运算,因此非常有用。但也带来了一个问题:代码的复用性不高。在reduce里面,变形和累积是耦合的,不太容易拆分。

每次使用reduce,开发者往往都要从头写代码,重复实现很多基本功能,很难复用别人的代码。

var handler = function (newArr, x) {
  newArr.push(x + 1);
  return newArr;
};

上面的这个处理函数,就很难用在其他场合。

有没有解决方法呢?回答是有的,就是把"变形"和"累积"这两种运算分开。如果reduce允许变形运算和累积运算分开,那么代码的复用性就会大大增加。这就是transduce方法的由来。

transduce这个名字来自 transform(变形)和 reduce 这两个单词的合成。它其实就是reduce方法的一种不那么耦合的写法。

// 变形运算
var plusOne = x => x + 1;

// 累积运算
var append = function (newArr, x) {
  newArr.push(x);
  return newArr;
}; 

R.transduce(R.map(plusOne), append, [], arr);
// [2, 3, 4, 5]

上面代码中,plusOne是变形操作,append是累积操作。我使用了Ramda 函数库的transduce实现。可以看到,transduce就是将变形和累积从reduce拆分出来,其他并无不同。

五、transduce 的用法

transduce最大的好处,就是代码复用更容易。

var arr = [1, 2, 3, 4];
var append = function (newArr, x) {
  newArr.push(x);
  return newArr;
}; 

// 示例一
var plusOne = x => x + 1;
var square = x => x * x;

R.transduce(
  R.map(R.pipe(plusOne, square)), 
  append, 
  [], 
  arr
); // [4, 9, 16, 25]

// 示例二
var isOdd = x => x % 2 === 1;

R.transduce(
  R.pipe(R.filter(isOdd), R.map(square)), 
  append, 
  [], 
  arr
); // [1, 9]

上面代码中,示例一是两个变形操作的合成,示例二是过滤操作与变形操作的合成。这两个例子都使用了Pointfree 风格

可以看到,transduce非常有利于代码的复用,可以将一系列简单的、可复用的函数合成为复杂操作。作为练习,有兴趣的读者可以试试,使用reduce方法完成上面两个示例。你会发现,代码的复杂度和行数大大增加。

六、Transformer 对象

transduce函数的第一个参数是一个对象,称为 Transformer 对象(变形器)。前面例子中,R.map(plusOne)返回的就是一个 Transformer 对象。

事实上,任何一个对象只要遵守Transformer 协议,就是 Transformer 对象。

var Map = function(f, xf) {
    return {
       "@@transducer/init": function() { 
           return xf["@@transducer/init"](); 
       },
       "@@transducer/result": function(result) { 
           return xf["@@transducer/result"](result); 
       },
       "@@transducer/step": function(result, input) {
           return xf["@@transducer/step"](result, f(input)); 
       }
    };
};

上面代码中,Map函数返回的就是一个 Transformer 对象。它必须具有以下三个属性。

  • @@transducer/step:执行变形操作
  • @@transducer/init:返回初始值
  • @@transducer/result:返回变形后的最终值

所有符合这个协议的对象,都可以与其他 Transformer 对象合成,充当transduce函数的第一个参数。

因此,transduce函数的参数类型如下。

transduce(
  变形器 : Object,
  累积器 : Function,
  初始值 : Any,
  原始数组 : Array
)

七、into 方法

最后,你也许发现了,前面所有示例使用的都是同一个累积器。

var append = function (newArr, x) {
  newArr.push(x);
  return newArr;
}; 

上面代码的append函数是一个常见累积器。因此, Ramda 函数库提供了into方法,将它内置了。也就是说,into方法相当于默认提供append的transduce函数。

R.transduce(R.map(R.add(1)), append, [], [1,2,3,4]);
// 等同于
R.into([], R.map(R.add(1)), [1,2,3,4]);

上面代码中,into方法的第一个参数是初始值,第二个参数是变形器,第三个参数是原始数组,不需要提供累积器。

下面是另外一个例子。

R.into(
  [5, 6],
  R.pipe(R.take(2), R.map(R.add(1))),
  [1, 2, 3, 4]
) // [5, 6, 2, 3]

八、参考链接

(完)

文档信息

]]>
0
<![CDATA[画个猴子]]> http://www.udpwork.com/item/16181.html http://www.udpwork.com/item/16181.html#reviews Fri, 17 Mar 2017 19:09:38 +0800 qyjohn http://www.udpwork.com/item/16181.html thumb_IMG_2536_1024thumb_IMG_2534_1024thumb_IMG_2537_1024thumb_IMG_2538_1024

]]>
thumb_IMG_2536_1024thumb_IMG_2534_1024thumb_IMG_2537_1024thumb_IMG_2538_1024

]]>
0
<![CDATA[关于租赁单车的东拉西扯]]> http://www.udpwork.com/item/16180.html http://www.udpwork.com/item/16180.html#reviews Fri, 17 Mar 2017 18:19:42 +0800 魏武挥 http://www.udpwork.com/item/16180.html 这两天关于租赁单车——媒体们投资人们创业者们更喜欢用“共享单车”这个词——的讨论变得比较热闹。我周末还接了财经的采访电话,前前后后说了一个小时有余。有几个颇具流量的媒体也做了长篇文章,比如财新,比如腾讯科技。

这篇文章我谈谈我对这个事的看法,有点不成体系,所以叫“东拉西扯”。

 

一个奇怪的词

这两年,有两个词在特别奇怪地流行。

第一个词是自媒体。自媒体本身并不奇怪,但把很多明明是机构团队做的也不太彰显个人风格的媒体称为“自媒体”,实在是很奇怪。

有人和我说过,中国媒体开办要证照,言下之意就是自媒体感觉不需要证照。朝野朝野,大概属于“野”的。好吧,自媒体=野媒体,你们可同意?

第二个词就是共享单车。这个词本身就有很大问题。如果说还有相当多的个人公号之类的确是自媒体的话,那么,搞共享单车的企业,其实没有一家是“共享单车”。

统一着色、统一配置、统一运营,计入自家公司资产的这些单车们,不知道共享个什么鬼。

投资人、创业者喜欢沾点总理的“欧气”来表示自己的政治正确(总理表示过对分享经济的重视)以至于慌不择词,媒体跟风这么写,只能说是人云亦云完全不动脑子了。

这是一个非常标准的“租赁模式”,不能因为张三骑了李四骑,李四骑了王五骑,就说是“共享”,那么低至如家高至凯悦之类,统统都是共享酒店。它们也是张三睡完李四睡,李四睡完王五睡呀。

这一波租赁单车,要说有些新鲜地方,其实是“分时租赁”,或者是“按次数租赁”。这是拜移动互联网所赐,以前还真搞不了。

叫分时租赁单车的确有些拗口,可以叫租赁单车嘛。

所有着笔写共享单车且不说明是为了讨论方便的,我都会先鄙视三分钟。

 

为什么要出手管制

前几天一个晚上,在一个群里,有个朋友提到,在一次饭桌上,几个政协委员们提到了单车这个生意。

貌似其中一位委员就很担心供给太大,城市容量不足,需求也没那么多,言下之意,就是会不会出现“毫无必要的最终是浪费的制造”。

第二天,我就看到新闻,说上海有关部门约谈几个单车运营商,要求停止投放,说是过量了。后者还能怎么办,只好诺诺称是。

我是不太容易理解这种担忧的。

市场经济不是计划经济,重复建设是必然会发生的。正统的马克思主义里面对此进行了批判,为了避免重复建设,所以要做计划。很可惜,这个想法实践证明是错的。

两千万公号呐,有必要吗?9成9是重复建设啊。怎么不一纸文书给关它一大半?

至于城市容量问题。

我不禁想起了腾讯总编辑李方以前的一篇批评电动车的文章,说快递业的电动车在城市里到处流窜,严重干扰交通。

李方的观点对不对可以再讨论,但至少好像也没禁止电动车的规模化流窜。自行车不过是放在路边,压根没动,不晓得城市容量哪里受不了了?

归根到底,还是觉得,一个城市可能只需要50万单车,所以你投放的数字不能是500万。

咋不说说那些空关的房子呐。

 

单车的生意模式到底好不好

其实这个生意比专车好,因为它理论上的商业模式是很清晰的。

一台单车300块,一天被十个人次租,一次一块,理论上一个月收回成本,后面都是白赚的。

它比专车好就好在:没有司机。人力成本其实非常高,无论你是雇佣还是合作分成,都是开支。

而且,物料成本长远来看只会越来越便宜,规模越大也越便宜。人力不是,人力成本一直是上升的,且与规模没有太大的关系。

但请注意我前面写的“理论上”三个字。

困扰单车们的第一个很大的问题是盗损。

我有位朋友在某租赁单车公司任高管,他告诉我,摩拜的盗损率在10%左右,OFO则会高达25%以上。

第二个问题是补贴大战。

是的,如专车一般,单车也开始大打出手。

OFO和摩拜都宣称两会期间,帝都可免费骑车。

我收到过摩拜“充100得210”的短信,最近继续收到OFO说还可以免费骑车的短信。

第三个问题是地域限制,这会限制住这个行业的规模。

单车基本上是北上广杭深之类的一线大城市的需求,而不是中国所有城市的需求。

交通堵不堵是影响单车发展的一个变量。

另外,话虽政治不正确,但却是实情:大城市普遍意义上,人们的素养会略高点,可能盗损率相对较低。

不过,单车生意里,有一个有利于公司的东西,那就是:押金。

 

押金是个什么鬼

前阵子有媒体报道说,单车押金退回非常麻烦。说得狠的,还有说这个类似于大规模非法集资。

在一个电视节目里,我当面和摩拜联合创始人CEO王晓峰提到了这个问题。王晓峰说这并不是他们故意扣着钱不给,并要求我当场再试。

我立刻试了试,不到一分钟,押金退回。

我然后又试了下OFO的,也是不到一分钟,退回。

单车的押金和类似神州租车的押金非常不同,后者是按次收取,且在租赁完成后退回(考虑到违章因素,这笔押金会延迟一段时间)。酒店业的押金也是离店就退。

但单车押金不是,它和你租车还是不租车无关。

类似的模式有吗?

其实是有的。

我一个做电商的朋友认为,这非常像公交卡押金。

在上海,一张公交卡100块,只有70块可以使用,另外30块是押金。只有你退回这张卡,这笔钱才会返还你。但它不是工本费,工本费是不需要退还的。

我总觉得上海公交卡人手一张都是低估了,人手几张才是常态。这背后的数字,事实上非常庞大。

单车们的押金不是非法集资,性质没那么恶劣。

对于企业来说,的确手握一笔非常巨额的现金流,这个倒没疑问。

这可能会派生出一点机会来。

 

单车会不会合并?

专车里的大打出手然后一笑泯恩仇的故事已经发生了,单车会不会?

在前文提到的电视节目里,王晓峰认为不会。

他的理由是:单车公司很重。专车公司合并其实就是账户的打通问题。但单车会涉及到线下庞大的车辆至少是重漆一遍的问题。

我觉得这个理由蛮牵强的,虽然有那么一分道理。

这档电视节目曾经邀请过柳甄,录制的时候uber中国还在和滴滴奋战,柳姑娘彰显了强大的信心,认为一定能和滴滴抗衡。

好像是播出的时候,滴滴就吞了uber中国。

单车业的合并,其实可以不用油漆。反正盗损率摆在那里,有的公司的单车也不结实,过个一年半载的,就可以用新车了。

在过渡期,两种颜色都存在,没什么了不得的。

关键是,这个行业,消费者的忠诚度是不高的。有什么骑什么呗。

单车本身的规模受限,使得我倾向于认为,这个行业出现并购的可能性非常大。

 

我更看好谁?

与人聊天或接受采访时,都会提到这个其实是有点无厘头的问题。

因为我对具体数据并不掌握,很难非常有说服力。

以前我表达过我更看好OFO,因为它便宜,成本回收快。

现在的我,并没有改变这个看法。

我甚至多了一个理由。

OFO创始人戴威,当年的北大学生会主席。能坐上这个位子的,而且还是在北大,都是人精。

而且戴威还摆平过一次举报,让后者不了了之。

控盘能力相当强。

摩拜就有些小纠结。

王晓峰的履历相当光鲜,光鲜得不像话。有兴趣的,可以去搜搜。

在我们天奇,把一个创业项目按照人和事两个维度会切成四类:好人好事、坏人坏事、好人坏事、坏人好事。这里的好坏,说的是合适不合适。好人好事没疑问,赶紧投,坏人坏事,没疑问,PASS。花功夫的是中间两种。

好人坏事,创业者很棒,但项目似乎有些问题。这个还好点,可以和创业者沟通,看看能不能就ta的想法做一些调整——当然,我们有可能是错的。

坏人好事,说的是项目非常有前途,但创业者似乎缺了点什么。这种情况下,我们会满世界找个CEO。这个情况是最麻烦的。

麻烦就麻烦在找起来不容易,找来了需要磨合。磨合不好,一定会宫斗。

磨合的好的,例子当然是有的,比如谷歌就是这样的。

我们觉得我们没那么大本事,一般就小心翼翼地躲过去了。

也许人有人的本事吧。

 

谁是这个生意的最大受益者?

给大家看两幅图

20块以下是去年10月的事。

 

无人自行车?好吧,这是一个笑话

谷歌搞了一个无人自动单车。

但这是去年的一个段子。

 

—— 首发 扯氮集 ——

版权说明 及 商业合作

作者执教于上海交通大学媒体与设计学院,天奇阿米巴创投基金投资合伙人

关于租赁单车的东拉西扯,首发于扯氮集

]]>
这两天关于租赁单车——媒体们投资人们创业者们更喜欢用“共享单车”这个词——的讨论变得比较热闹。我周末还接了财经的采访电话,前前后后说了一个小时有余。有几个颇具流量的媒体也做了长篇文章,比如财新,比如腾讯科技。

这篇文章我谈谈我对这个事的看法,有点不成体系,所以叫“东拉西扯”。

 

一个奇怪的词

这两年,有两个词在特别奇怪地流行。

第一个词是自媒体。自媒体本身并不奇怪,但把很多明明是机构团队做的也不太彰显个人风格的媒体称为“自媒体”,实在是很奇怪。

有人和我说过,中国媒体开办要证照,言下之意就是自媒体感觉不需要证照。朝野朝野,大概属于“野”的。好吧,自媒体=野媒体,你们可同意?

第二个词就是共享单车。这个词本身就有很大问题。如果说还有相当多的个人公号之类的确是自媒体的话,那么,搞共享单车的企业,其实没有一家是“共享单车”。

统一着色、统一配置、统一运营,计入自家公司资产的这些单车们,不知道共享个什么鬼。

投资人、创业者喜欢沾点总理的“欧气”来表示自己的政治正确(总理表示过对分享经济的重视)以至于慌不择词,媒体跟风这么写,只能说是人云亦云完全不动脑子了。

这是一个非常标准的“租赁模式”,不能因为张三骑了李四骑,李四骑了王五骑,就说是“共享”,那么低至如家高至凯悦之类,统统都是共享酒店。它们也是张三睡完李四睡,李四睡完王五睡呀。

这一波租赁单车,要说有些新鲜地方,其实是“分时租赁”,或者是“按次数租赁”。这是拜移动互联网所赐,以前还真搞不了。

叫分时租赁单车的确有些拗口,可以叫租赁单车嘛。

所有着笔写共享单车且不说明是为了讨论方便的,我都会先鄙视三分钟。

 

为什么要出手管制

前几天一个晚上,在一个群里,有个朋友提到,在一次饭桌上,几个政协委员们提到了单车这个生意。

貌似其中一位委员就很担心供给太大,城市容量不足,需求也没那么多,言下之意,就是会不会出现“毫无必要的最终是浪费的制造”。

第二天,我就看到新闻,说上海有关部门约谈几个单车运营商,要求停止投放,说是过量了。后者还能怎么办,只好诺诺称是。

我是不太容易理解这种担忧的。

市场经济不是计划经济,重复建设是必然会发生的。正统的马克思主义里面对此进行了批判,为了避免重复建设,所以要做计划。很可惜,这个想法实践证明是错的。

两千万公号呐,有必要吗?9成9是重复建设啊。怎么不一纸文书给关它一大半?

至于城市容量问题。

我不禁想起了腾讯总编辑李方以前的一篇批评电动车的文章,说快递业的电动车在城市里到处流窜,严重干扰交通。

李方的观点对不对可以再讨论,但至少好像也没禁止电动车的规模化流窜。自行车不过是放在路边,压根没动,不晓得城市容量哪里受不了了?

归根到底,还是觉得,一个城市可能只需要50万单车,所以你投放的数字不能是500万。

咋不说说那些空关的房子呐。

 

单车的生意模式到底好不好

其实这个生意比专车好,因为它理论上的商业模式是很清晰的。

一台单车300块,一天被十个人次租,一次一块,理论上一个月收回成本,后面都是白赚的。

它比专车好就好在:没有司机。人力成本其实非常高,无论你是雇佣还是合作分成,都是开支。

而且,物料成本长远来看只会越来越便宜,规模越大也越便宜。人力不是,人力成本一直是上升的,且与规模没有太大的关系。

但请注意我前面写的“理论上”三个字。

困扰单车们的第一个很大的问题是盗损。

我有位朋友在某租赁单车公司任高管,他告诉我,摩拜的盗损率在10%左右,OFO则会高达25%以上。

第二个问题是补贴大战。

是的,如专车一般,单车也开始大打出手。

OFO和摩拜都宣称两会期间,帝都可免费骑车。

我收到过摩拜“充100得210”的短信,最近继续收到OFO说还可以免费骑车的短信。

第三个问题是地域限制,这会限制住这个行业的规模。

单车基本上是北上广杭深之类的一线大城市的需求,而不是中国所有城市的需求。

交通堵不堵是影响单车发展的一个变量。

另外,话虽政治不正确,但却是实情:大城市普遍意义上,人们的素养会略高点,可能盗损率相对较低。

不过,单车生意里,有一个有利于公司的东西,那就是:押金。

 

押金是个什么鬼

前阵子有媒体报道说,单车押金退回非常麻烦。说得狠的,还有说这个类似于大规模非法集资。

在一个电视节目里,我当面和摩拜联合创始人CEO王晓峰提到了这个问题。王晓峰说这并不是他们故意扣着钱不给,并要求我当场再试。

我立刻试了试,不到一分钟,押金退回。

我然后又试了下OFO的,也是不到一分钟,退回。

单车的押金和类似神州租车的押金非常不同,后者是按次收取,且在租赁完成后退回(考虑到违章因素,这笔押金会延迟一段时间)。酒店业的押金也是离店就退。

但单车押金不是,它和你租车还是不租车无关。

类似的模式有吗?

其实是有的。

我一个做电商的朋友认为,这非常像公交卡押金。

在上海,一张公交卡100块,只有70块可以使用,另外30块是押金。只有你退回这张卡,这笔钱才会返还你。但它不是工本费,工本费是不需要退还的。

我总觉得上海公交卡人手一张都是低估了,人手几张才是常态。这背后的数字,事实上非常庞大。

单车们的押金不是非法集资,性质没那么恶劣。

对于企业来说,的确手握一笔非常巨额的现金流,这个倒没疑问。

这可能会派生出一点机会来。

 

单车会不会合并?

专车里的大打出手然后一笑泯恩仇的故事已经发生了,单车会不会?

在前文提到的电视节目里,王晓峰认为不会。

他的理由是:单车公司很重。专车公司合并其实就是账户的打通问题。但单车会涉及到线下庞大的车辆至少是重漆一遍的问题。

我觉得这个理由蛮牵强的,虽然有那么一分道理。

这档电视节目曾经邀请过柳甄,录制的时候uber中国还在和滴滴奋战,柳姑娘彰显了强大的信心,认为一定能和滴滴抗衡。

好像是播出的时候,滴滴就吞了uber中国。

单车业的合并,其实可以不用油漆。反正盗损率摆在那里,有的公司的单车也不结实,过个一年半载的,就可以用新车了。

在过渡期,两种颜色都存在,没什么了不得的。

关键是,这个行业,消费者的忠诚度是不高的。有什么骑什么呗。

单车本身的规模受限,使得我倾向于认为,这个行业出现并购的可能性非常大。

 

我更看好谁?

与人聊天或接受采访时,都会提到这个其实是有点无厘头的问题。

因为我对具体数据并不掌握,很难非常有说服力。

以前我表达过我更看好OFO,因为它便宜,成本回收快。

现在的我,并没有改变这个看法。

我甚至多了一个理由。

OFO创始人戴威,当年的北大学生会主席。能坐上这个位子的,而且还是在北大,都是人精。

而且戴威还摆平过一次举报,让后者不了了之。

控盘能力相当强。

摩拜就有些小纠结。

王晓峰的履历相当光鲜,光鲜得不像话。有兴趣的,可以去搜搜。

在我们天奇,把一个创业项目按照人和事两个维度会切成四类:好人好事、坏人坏事、好人坏事、坏人好事。这里的好坏,说的是合适不合适。好人好事没疑问,赶紧投,坏人坏事,没疑问,PASS。花功夫的是中间两种。

好人坏事,创业者很棒,但项目似乎有些问题。这个还好点,可以和创业者沟通,看看能不能就ta的想法做一些调整——当然,我们有可能是错的。

坏人好事,说的是项目非常有前途,但创业者似乎缺了点什么。这种情况下,我们会满世界找个CEO。这个情况是最麻烦的。

麻烦就麻烦在找起来不容易,找来了需要磨合。磨合不好,一定会宫斗。

磨合的好的,例子当然是有的,比如谷歌就是这样的。

我们觉得我们没那么大本事,一般就小心翼翼地躲过去了。

也许人有人的本事吧。

 

谁是这个生意的最大受益者?

给大家看两幅图

20块以下是去年10月的事。

 

无人自行车?好吧,这是一个笑话

谷歌搞了一个无人自动单车。

但这是去年的一个段子。

 

—— 首发 扯氮集 ——

版权说明 及 商业合作

作者执教于上海交通大学媒体与设计学院,天奇阿米巴创投基金投资合伙人

关于租赁单车的东拉西扯,首发于扯氮集

]]>
0
<![CDATA[如何优雅的实现一个 lua 调试器]]> http://www.udpwork.com/item/15933.html http://www.udpwork.com/item/15933.html#reviews Fri, 17 Mar 2017 15:52:03 +0800 云风 http://www.udpwork.com/item/15933.html 最近一段时间在帮公司一个项目组的客户端 review 代码。

我们的所有项目,无论渲染底层是用的 ejoy2d 还是 Unity3d ,实际开发的时候都基本是使用 lua 。所以开发人员日常工作基本是在和 Lua 打交道。

虽然我个人挺反感围绕着调试的开发方式,也就是不断的在测试、试错,纠正的循环中奔波。我认为好的程序应该努力在编写的过程中,在头脑中排错;在预感到坏味道时,就赶快重写。而坏味道通常指代码陷入了复杂度太高的境地,无法一眼看出潜在的问题。对付复杂度最好的武器是简化代码,而非调试器。

在真正遇到 bug 时,应该仔细浏览代码,设想各种出错的可能。而不是将错误的代码运行起来,查看运行中的状态变化。

话说回来,看到项目组的同学真的碰到 bug 时,不断的启动 Unity 客户端,把时间浪费在等待那几行 debug log 上,我觉得效率还是很低。必要的调试工具应该能提升一些开发效率的。

lua 官方提供了完善的 debug api 可以查询所有的信息;但并没有一套官方的调试工具。我都不记得是第几次写调试工具了。至少在这个 blog 上就记录了好几次。最近的一次是 3 年前

每次做完送给人用了两天就扔掉了。这次一时兴起,周末又做了一个。当然每次都会有一些不一样的想法。

这次的版本只开了个头,把构想中的基础架构搭好了。那就是,我认为一个优雅的调试器不应该过多的干涉被调试的实体(比方说你想监控被调试的程序内存使用的情况,和 gc 的工作)。

过去的一些版本都是把调试器代码直接嵌入被调试的虚拟机的,调试器本身和被调试的代码并没有明显的界限。调试过程也会在同一个虚拟机中运行。

我这次想玩点不一样的,让调试器运行在一个独立的虚拟机内,它通过一组接口来观察被调试程序。这样,在这个基础上制作的调试器,可以更放心的添加一些花哨的功能了。比如启动一个图形界面、或是提供一个 web server ,调试者可以通过浏览器来监控内部状态,发送调试指令。

当然最简单的用法是非侵入式的输出 log 。不必在被调试代码中硬加上几句 print 输出 log (这是没有调试工具时,大家最常用的调试方法),而可以把 print 查看内部状态的代码写在独立的调试器模块中。我们可以用编程的方式来编写调试过程,而不局限于一个交互式调试工具提供的有限手段。

我这次设计的调试模块,只提供一个概念:探测点。

你可以在被调试代码中设置探测点,探测点并不是断点,而更像一个观测点。在这个点上,调试器并不会停下来等待调试者的指令,而是运行调试器里的一个函数。(这个函数是运行在独立的虚拟机里的,完全不用担心代码有什么副作用)

你可以在探测点函数中,访问被调试代码在该处的状态,做一些合适的事情。比如把状态输出到 log 文件中,比如根据状态条件来选择做些事情;当然也可以暂停下来,交互式等待调试命令。

探测点可以分为两种,一种是在调试前,硬编码在代码中的,只要运行到那里,就会触发一下探测行为;还有一种是利用 lua 的 Hook 透明添加的。

两种探测点可以混合使用。比如在游戏主循环中预先硬编码进一个探测点,平时不启动调试器,运行到探测点时就自动忽略;当需要的时候,让这个探测点起作用,然后在探测函数中,设置 hook 点,进一步的调试。

初步的代码放在 github 上,这段时间会慢慢完善。不过期待它短期内发展成为一个图形式的漂亮交互调试器可能有点不现实,除非做前端的朋友有兴趣来完善它。(比如增加一个 web server ,直接可以通过浏览器连接到程序里交互调试)

最后说点这个东西的实现中一个有趣的部分:

由于调试器和被调试程序处于两个不同的 VM 中,所以调试器代码并不能直接引用被调试代码环境中的 table 。这里是怎么做到的呢?

我设计了一个 C 结构(封装成 userdata),里面保存了一个无法被直接引用的 lua 对象的引用路径。

比如,从探测点出发,你想获得某个对象的状态,无非只有几个途径。获取某个栈帧的 local 变量、upvalue 、或是从全局表中检索到一个对象等等,如果这个对象是一个 table ,可以进一步的去取 table 里的子域。总之,你总是通过一层层的简洁途径获得最终想观察的变量的。

那么,在调试器中,只需要把这个过程记录下来、而不需要铆定一个特定的对象。这个过程封装成一个 userdata ,它的实际含义和最终对应的对象是一致的。

]]>
最近一段时间在帮公司一个项目组的客户端 review 代码。

我们的所有项目,无论渲染底层是用的 ejoy2d 还是 Unity3d ,实际开发的时候都基本是使用 lua 。所以开发人员日常工作基本是在和 Lua 打交道。

虽然我个人挺反感围绕着调试的开发方式,也就是不断的在测试、试错,纠正的循环中奔波。我认为好的程序应该努力在编写的过程中,在头脑中排错;在预感到坏味道时,就赶快重写。而坏味道通常指代码陷入了复杂度太高的境地,无法一眼看出潜在的问题。对付复杂度最好的武器是简化代码,而非调试器。

在真正遇到 bug 时,应该仔细浏览代码,设想各种出错的可能。而不是将错误的代码运行起来,查看运行中的状态变化。

话说回来,看到项目组的同学真的碰到 bug 时,不断的启动 Unity 客户端,把时间浪费在等待那几行 debug log 上,我觉得效率还是很低。必要的调试工具应该能提升一些开发效率的。

lua 官方提供了完善的 debug api 可以查询所有的信息;但并没有一套官方的调试工具。我都不记得是第几次写调试工具了。至少在这个 blog 上就记录了好几次。最近的一次是 3 年前

每次做完送给人用了两天就扔掉了。这次一时兴起,周末又做了一个。当然每次都会有一些不一样的想法。

这次的版本只开了个头,把构想中的基础架构搭好了。那就是,我认为一个优雅的调试器不应该过多的干涉被调试的实体(比方说你想监控被调试的程序内存使用的情况,和 gc 的工作)。

过去的一些版本都是把调试器代码直接嵌入被调试的虚拟机的,调试器本身和被调试的代码并没有明显的界限。调试过程也会在同一个虚拟机中运行。

我这次想玩点不一样的,让调试器运行在一个独立的虚拟机内,它通过一组接口来观察被调试程序。这样,在这个基础上制作的调试器,可以更放心的添加一些花哨的功能了。比如启动一个图形界面、或是提供一个 web server ,调试者可以通过浏览器来监控内部状态,发送调试指令。

当然最简单的用法是非侵入式的输出 log 。不必在被调试代码中硬加上几句 print 输出 log (这是没有调试工具时,大家最常用的调试方法),而可以把 print 查看内部状态的代码写在独立的调试器模块中。我们可以用编程的方式来编写调试过程,而不局限于一个交互式调试工具提供的有限手段。

我这次设计的调试模块,只提供一个概念:探测点。

你可以在被调试代码中设置探测点,探测点并不是断点,而更像一个观测点。在这个点上,调试器并不会停下来等待调试者的指令,而是运行调试器里的一个函数。(这个函数是运行在独立的虚拟机里的,完全不用担心代码有什么副作用)

你可以在探测点函数中,访问被调试代码在该处的状态,做一些合适的事情。比如把状态输出到 log 文件中,比如根据状态条件来选择做些事情;当然也可以暂停下来,交互式等待调试命令。

探测点可以分为两种,一种是在调试前,硬编码在代码中的,只要运行到那里,就会触发一下探测行为;还有一种是利用 lua 的 Hook 透明添加的。

两种探测点可以混合使用。比如在游戏主循环中预先硬编码进一个探测点,平时不启动调试器,运行到探测点时就自动忽略;当需要的时候,让这个探测点起作用,然后在探测函数中,设置 hook 点,进一步的调试。

初步的代码放在 github 上,这段时间会慢慢完善。不过期待它短期内发展成为一个图形式的漂亮交互调试器可能有点不现实,除非做前端的朋友有兴趣来完善它。(比如增加一个 web server ,直接可以通过浏览器连接到程序里交互调试)

最后说点这个东西的实现中一个有趣的部分:

由于调试器和被调试程序处于两个不同的 VM 中,所以调试器代码并不能直接引用被调试代码环境中的 table 。这里是怎么做到的呢?

我设计了一个 C 结构(封装成 userdata),里面保存了一个无法被直接引用的 lua 对象的引用路径。

比如,从探测点出发,你想获得某个对象的状态,无非只有几个途径。获取某个栈帧的 local 变量、upvalue 、或是从全局表中检索到一个对象等等,如果这个对象是一个 table ,可以进一步的去取 table 里的子域。总之,你总是通过一层层的简洁途径获得最终想观察的变量的。

那么,在调试器中,只需要把这个过程记录下来、而不需要铆定一个特定的对象。这个过程封装成一个 userdata ,它的实际含义和最终对应的对象是一致的。

]]>
0
<![CDATA[Lua 调试器]]> http://www.udpwork.com/item/16179.html http://www.udpwork.com/item/16179.html#reviews Fri, 17 Mar 2017 15:43:56 +0800 云风 http://www.udpwork.com/item/16179.html 又一篇谈 Lua debugger 的 blog 了。但这次,并不是我的个人作品 :) 。

去年底我写了如何优雅的实现一个 lua 调试器。正如我的 blog 中所写:“不过期待它短期内发展成为一个图形式的漂亮交互调试器可能有点不现实,除非做前端的朋友有兴趣来完善它。”

ok 。这次,真的有人来完善它了。

我公司的前端大神突然对实现一个 lua debugger 产生了兴趣。他觉得既然 chrome 可以用来调试 javascript ,那么魔改一下后,调试 lua 也完全没有问题。利用几个月的业余时间,他完成了这么个东西:

http://mare.ejoy.com/

ps. 不愧是做前端出身啊,开源项目的主页比 skynet 好看多了。

]]>
又一篇谈 Lua debugger 的 blog 了。但这次,并不是我的个人作品 :) 。

去年底我写了如何优雅的实现一个 lua 调试器。正如我的 blog 中所写:“不过期待它短期内发展成为一个图形式的漂亮交互调试器可能有点不现实,除非做前端的朋友有兴趣来完善它。”

ok 。这次,真的有人来完善它了。

我公司的前端大神突然对实现一个 lua debugger 产生了兴趣。他觉得既然 chrome 可以用来调试 javascript ,那么魔改一下后,调试 lua 也完全没有问题。利用几个月的业余时间,他完成了这么个东西:

http://mare.ejoy.com/

ps. 不愧是做前端出身啊,开源项目的主页比 skynet 好看多了。

]]>
0
<![CDATA[[转]Protobuf3 语法指南]]> http://www.udpwork.com/item/16178.html http://www.udpwork.com/item/16178.html#reviews Thu, 16 Mar 2017 19:52:32 +0800 鸟窝 http://www.udpwork.com/item/16178.html

以前我翻译了Protobuf2 语法指南,现在千念飞羽把protobuf3的语法指南也翻译了,我也转载一下,读者可以有个参考。 译文地址是:Protobuf3语言指南

英文原文:
Language Guide (proto3)
中文出处:
Protobuf语言指南
[译]Protobuf 语法指南
中文出处是proto2的译文,proto3的英文出现后在原来基础上增改了,水平有限,还请指正

这个指南描述了如何使用Protocol buffer 语言去描述你的protocol buffer 数据, 包括 .proto文件符号和如何从.proto文件生成类。包含了proto2版本的protocol buffer语言:对于老版本的proto3 符号,请见Proto2 Language Guide(以及中文译本,抄了很多这里的感谢下老版本的翻译者)

本文是一个参考指南——如果要查看如何使用本文中描述的多个特性的循序渐进的例子,请在教程中查找需要的语言的教程。

定义一个消息类型

先来看一个非常简单的例子。假设你想定义一个“搜索请求”的消息格式,每一个请求含有一个查询字符串、你感兴趣的查询结果所在的页数,以及每一页多少条查询结果。可以采用如下的方式来定义消息类型的.proto文件了:

1234567
syntax = "proto3";message SearchRequest {  string query = 1;  int32 page_number = 2;  int32 result_per_page = 3;}
  • 文件的第一行指定了你正在使用proto3语法:如果你没有指定这个,编译器会使用proto2。这个指定语法行必须是文件的非空非注释的第一个行。
  • SearchRequest消息格式有3个字段,在消息中承载的数据分别对应于每一个字段。其中每个字段都有一个名字和一种类型。

指定字段类型

在上面的例子中,所有字段都是标量类型:两个整型(page_number和result_per_page),一个string类型(query)。当然,你也可以为字段指定其他的合成类型,包括枚举(enumerations)或其他消息类型。

分配标识号

正如你所见,在消息定义中,每个字段都有唯一的一个数字标识符。这些标识符是用来在消息的二进制格式中识别各个字段的,一旦开始使用就不能够再改变。注:[1,15]之内的标识号在编码的时候会占用一个字节。[16,2047]之内的标识号则占用2个字节。所以应该为那些频繁出现的消息元素保留 [1,15]之内的标识号。切记:要为将来有可能添加的、频繁出现的标识号预留一些标识号。

最小的标识号可以从1开始,最大到2^29 - 1, or 536,870,911。不可以使用其中的[19000-19999]( (从FieldDescriptor::kFirstReservedNumber 到 FieldDescriptor::kLastReservedNumber))的标识号, Protobuf协议实现中对这些进行了预留。如果非要在.proto文件中使用这些预留标识号,编译时就会报警。同样你也不能使用早期保留的标识号。

指定字段规则

所指定的消息字段修饰符必须是如下之一:

  • singular:一个格式良好的消息应该有0个或者1个这种字段(但是不能超过1个)。
  • repeated:在一个格式良好的消息中,这种字段可以重复任意多次(包括0次)。重复的值的顺序会被保留。

在proto3中,repeated的标量域默认情况虾使用packed。

你可以了解更多的pakced属性在Protocol Buffer 编码

添加更多消息类型

在一个.proto文件中可以定义多个消息类型。在定义多个相关的消息的时候,这一点特别有用——例如,如果想定义与SearchResponse消息类型对应的回复消息格式的话,你可以将它添加到相同的.proto文件中,如:

123456789
message SearchRequest {  string query = 1;  int32 page_number = 2;  int32 result_per_page = 3;}message SearchResponse { ...}

添加注释

向.proto文件添加注释,可以使用C/C++/Java风格的双斜杠(//) 语法格式,如:

12345
message SearchRequest {  string query = 1;  int32 page_number = 2;  // Which page number do we want?  int32 result_per_page = 3;  // Number of results to return per page.}

保留标识符(Reserved)

如果你通过删除或者注释所有域,以后的用户在更新这个类型的时候可能重用这些标识号。如果你使用旧版本加载相同的.proto文件会导致严重的问题,包括数据损坏、隐私错误等等。现在有一种确保不会发生这种情况的方法就是为字段tag(reserved name可能会JSON序列化的问题)指定reserved标识符,protocol buffer的编译器会警告未来尝试使用这些域标识符的用户。

1234
message Foo {  reserved 2, 15, 9 to 11;  reserved "foo", "bar";}

注:不要在同一行reserved声明中同时声明域名字和tag number。

从.proto文件生成了什么?

当用protocol buffer编译器来运行.proto文件时,编译器将生成所选择语言的代码,这些代码可以操作在.proto文件中定义的消息类型,包括获取、设置字段值,将消息序列化到一个输出流中,以及从一个输入流中解析消息。

  • 对C++来说,编译器会为每个.proto文件生成一个.h文件和一个.cc文件,.proto文件中的每一个消息有一个对应的类。
  • 对Java来说,编译器为每一个消息类型生成了一个.java文件,以及一个特殊的Builder类(该类是用来创建消息类接口的)。
  • 对Python来说,有点不太一样——Python编译器为.proto文件中的每个消息类型生成一个含有静态描述符的模块,,该模块与一个元类(metaclass)在运行时(runtime)被用来创建所需的Python数据访问类。
  • 对go来说,编译器会位每个消息类型生成了一个.pd.go文件。
  • 对于Ruby来说,编译器会为每个消息类型生成了一个.rb文件。
  • javaNano来说,编译器输出类似域java但是没有Builder类
  • 对于Objective-C来说,编译器会为每个消息类型生成了一个pbobjc.h文件和pbobjcm文件,.proto文件中的每一个消息有一个对应的类。
  • 对于C#来说,编译器会为每个消息类型生成了一个.cs文件,.proto文件中的每一个消息有一个对应的类。
    你可以从如下的文档链接中获取每种语言更多API(proto3版本的内容很快就公布)。API Reference

标量数值类型

一个标量消息字段可以含有一个如下的类型——该表格展示了定义于.proto文件中的类型,以及与之对应的、在自动生成的访问类中定义的类型:

.proto Type Notes C++ Type Java Type Python Type[2] Go Type Ruby Type C# Type PHP Type
double double double float float64 Float double float
float float float float float32 Float float float
int32 使用变长编码,对于负值的效率很低,如果你的域有可能有负值,请使用sint64替代 int32 int int int32 Fixnum 或者 Bignum(根据需要) int integer
uint32 使用变长编码 uint32 int int/long uint32 Fixnum 或者 Bignum(根据需要) uint integer
uint64 使用变长编码 uint64 long int/long uint64 Bignum ulong integer/string
sint32 使用变长编码,这些编码在负值时比int32高效的多 int32 int int int32 Fixnum 或者 Bignum(根据需要) int integer
sint64 使用变长编码,有符号的整型值。编码时比通常的int64高效。 int64 long int/long int64 Bignum long integer/string
fixed32 总是4个字节,如果数值总是比总是比228大的话,这个类型会比uint32高效。 uint32 int int uint32 Fixnum 或者 Bignum(根据需要) uint integer
fixed64 总是8个字节,如果数值总是比总是比256大的话,这个类型会比uint64高效。 uint64 long int/long uint64 Bignum ulong integer/string
sfixed32 总是4个字节 int32 int int int32 Fixnum 或者 Bignum(根据需要) int integer
sfixed64 总是8个字节 int64 long int/long int64 Bignum long integer/string
bool bool boolean bool bool TrueClass/FalseClass bool boolean
string 一个字符串必须是UTF-8编码或者7-bit ASCII编码的文本。 string String str/unicode string String (UTF-8) string string
bytes 可能包含任意顺序的字节数据。 string ByteString str []byte String (ASCII-8BIT) ByteString string

你可以在文章Protocol Buffer 编码中,找到更多“序列化消息时各种类型如何编码”的信息。

  1. 在java中,无符号32位和64位整型被表示成他们的整型对应形式,最高位被储存在标志位中。
  2. 对于所有的情况,设定值会执行类型检查以确保此值是有效。
  3. 64位或者无符号32位整型在解码时被表示成为ilong,但是在设置时可以使用int型值设定,在所有的情况下,值必须符合其设置其类型的要求。
  4. python中string被表示成在解码时表示成unicode。但是一个ASCIIstring可以被表示成str类型。
  5. Integer在64位的机器上使用,string在32位机器上使用

默认值

当一个消息被解析的时候,如果被编码的信息不包含一个特定的singular元素,被解析的对象锁对应的域被设置位一个默认值,对于不同类型指定如下:

  • 对于string,默认是一个空string
  • 对于bytes,默认是一个空的bytes
  • 对于bool,默认是false
  • 对于数值类型,默认是0
  • 对于枚举,默认是第一个定义的枚举值,必须为0;
  • 对于消息类型(message),域没有被设置,确切的消息是根据语言确定的,详见generated code guide

对于可重复域的默认值是空(通常情况下是对应语言中空列表)。

注:对于标量消息域,一旦消息被解析,就无法判断域释放被设置为默认值(例如,例如boolean值是否被设置为false)还是根本没有被设置。你应该在定义你的消息类型时非常注意。例如,比如你不应该定义boolean的默认值false作为任何行为的触发方式。也应该注意如果一个标量消息域被设置为标志位,这个值不应该被序列化传输。

查看generated code guide选择你的语言的默认值的工作细节。

枚举

当需要定义一个消息类型的时候,可能想为一个字段指定某“预定义值序列”中的一个值。例如,假设要为每一个SearchRequest消息添加一个 corpus字段,而corpus的值可能是UNIVERSAL,WEB,IMAGES,LOCAL,NEWS,PRODUCTS或VIDEO中的一个。 其实可以很容易地实现这一点:通过向消息定义中添加一个枚举(enum)并且为每个可能的值定义一个常量就可以了。

在下面的例子中,在消息格式中添加了一个叫做Corpus的枚举类型——它含有所有可能的值 ——以及一个类型为Corpus的字段:

123456789101112131415
message SearchRequest {  string query = 1;  int32 page_number = 2;  int32 result_per_page = 3;  enum Corpus {    UNIVERSAL = 0;    WEB = 1;    IMAGES = 2;    LOCAL = 3;    NEWS = 4;    PRODUCTS = 5;    VIDEO = 6;  }  Corpus corpus = 4;}

如你所见,Corpus枚举的第一个常量映射为0:每个枚举类型必须将其第一个类型映射为0,这是因为:

  • 必须有有一个0值,我们可以用这个0值作为默认值。
  • 这个零值必须为第一个元素,为了兼容proto2语义,枚举类的第一个值总是默认值。

你可以通过将不同的枚举常量指定位相同的值。如果这样做你需要将allow_alias设定位true,否则编译器会在别名的地方产生一个错误信息。

1234567891011
enum EnumAllowingAlias {  option allow_alias = true;  UNKNOWN = 0;  STARTED = 1;  RUNNING = 1;}enum EnumNotAllowingAlias {  UNKNOWN = 0;  STARTED = 1;  // RUNNING = 1;  // Uncommenting this line will cause a compile error inside Google and a warning message outside.}

枚举常量必须在32位整型值的范围内。因为enum值是使用可变编码方式的,对负数不够高效,因此不推荐在enum中使用负数。如上例所示,可以在 一个消息定义的内部或外部定义枚举——这些枚举可以在.proto文件中的任何消息定义里重用。当然也可以在一个消息中声明一个枚举类型,而在另一个不同 的消息中使用它——采用MessageType.EnumType的语法格式。

当对一个使用了枚举的.proto文件运行protocol buffer编译器的时候,生成的代码中将有一个对应的enum(对Java或C++来说),或者一个特殊的EnumDescriptor类(对 Python来说),它被用来在运行时生成的类中创建一系列的整型值符号常量(symbolic constants)。

在反序列化的过程中,无法识别的枚举值会被保存在消息中,虽然这种表示方式需要依据所使用语言而定。在那些支持开放枚举类型超出指定范围之外的语言中(例如C++和Go),为识别的值会被表示成所支持的整型。在使用封闭枚举类型的语言中(Java),使用枚举中的一个类型来表示未识别的值,并且可以使用所支持整型来访问。在其他情况下,如果解析的消息被序列号,未识别的值将保持原样。

关于如何在你的应用程序的消息中使用枚举的更多信息,请查看所选择的语言generated code guide

使用其他消息类型

你可以将其他消息类型用作字段类型。例如,假设在每一个SearchResponse消息中包含Result消息,此时可以在相同的.proto文件中定义一个Result消息类型,然后在SearchResponse消息中指定一个Result类型的字段,如:

123456789
message SearchResponse {  repeated Result results = 1;}message Result {  string url = 1;  string title = 2;  repeated string snippets = 3;}

导入定义

在上面的例子中,Result消息类型与SearchResponse是定义在同一文件中的。如果想要使用的消息类型已经在其他.proto文件中已经定义过了呢?
你可以通过导入(importing)其他.proto文件中的定义来使用它们。要导入其他.proto文件的定义,你需要在你的文件中添加一个导入声明,如:

1
import "myproject/other_protos.proto";

默认情况下你只能使用直接导入的.proto文件中的定义. 然而, 有时候你需要移动一个.proto文件到一个新的位置, 可以不直接移动.proto文件, 只需放入一个伪 .proto 文件在老的位置, 然后使用import public转向新的位置。import public 依赖性会通过任意导入包含import public声明的proto文件传递。例如:

12
// 这是新的proto// All definitions are moved here
1234
// 这是久的proto// 这是所有客户端正在导入的包import public "new.proto";import "other.proto";
123
// 客户端protoimport "old.proto";// 现在你可以使用新旧两种包的proto定义了。

通过在编译器命令行参数中使用-I/--proto_pathprotocal 编译器会在指定目录搜索要导入的文件。如果没有给出标志,编译器会搜索编译命令被调用的目录。通常你只要指定proto_path标志为你的工程根目录就好。并且指定好导入的正确名称就好。

使用proto2消息类型

在你的proto3消息中导入proto2的消息类型也是可以的,反之亦然,然后proto2枚举不可以直接在proto3的标识符中使用(如果仅仅在proto2消息中使用是可以的)。

嵌套类型

你可以在其他消息类型中定义、使用消息类型,在下面的例子中,Result消息就定义在SearchResponse消息内,如:

12345678
message SearchResponse {  message Result {    string url = 1;    string title = 2;    repeated string snippets = 3;  }  repeated Result results = 1;}

如果你想在它的父消息类型的外部重用这个消息类型,你需要以Parent.Type的形式使用它,如:

123
message SomeOtherMessage {  SearchResponse.Result result = 1;}

当然,你也可以将消息嵌套任意多层,如:

1234567891011121314
message Outer {                  // Level 0  message MiddleAA {  // Level 1    message Inner {   // Level 2      int64 ival = 1;      bool  booly = 2;    }  }  message MiddleBB {  // Level 1    message Inner {   // Level 2      int32 ival = 1;      bool  booly = 2;    }  }}

更新一个消息类型

如果一个已有的消息格式已无法满足新的需求——如,要在消息中添加一个额外的字段——但是同时旧版本写的代码仍然可用。不用担心!更新消息而不破坏已有代码是非常简单的。在更新时只要记住以下的规则即可。

  • 不要更改任何已有的字段的数值标识。
  • 如果你增加新的字段,使用旧格式的字段仍然可以被你新产生的代码所解析。你应该记住这些元素的默认值这样你的新代码就可以以适当的方式和旧代码产生的数据交互。相似的,通过新代码产生的消息也可以被旧代码解析:只不过新的字段会被忽视掉。注意,未被识别的字段会在反序列化的过程中丢弃掉,所以如果消息再被传递给新的代码,新的字段依然是不可用的(这和proto2中的行为是不同的,在proto2中未定义的域依然会随着消息被序列化)
  • 非required的字段可以移除——只要它们的标识号在新的消息类型中不再使用(更好的做法可能是重命名那个字段,例如在字段前添加“OBSOLETE_”前缀,那样的话,使用的.proto文件的用户将来就不会无意中重新使用了那些不该使用的标识号)。
  • int32, uint32, int64, uint64,和bool是全部兼容的,这意味着可以将这些类型中的一个转换为另外一个,而不会破坏向前、 向后的兼容性。如果解析出来的数字与对应的类型不相符,那么结果就像在C++中对它进行了强制类型转换一样(例如,如果把一个64位数字当作int32来 读取,那么它就会被截断为32位的数字)。
  • sint32和sint64是互相兼容的,但是它们与其他整数类型不兼容。
  • string和bytes是兼容的——只要bytes是有效的UTF-8编码。
  • 嵌套消息与bytes是兼容的——只要bytes包含该消息的一个编码过的版本。
  • fixed32与sfixed32是兼容的,fixed64与sfixed64是兼容的。
  • 枚举类型与int32,uint32,int64和uint64相兼容(注意如果值不相兼容则会被截断),然而在客户端反序列化之后他们可能会有不同的处理方式,例如,未识别的proto3枚举类型会被保留在消息中,但是他的表示方式会依照语言而定。int类型的字段总会保留他们的

Any

Any类型消息允许你在没有指定他们的.proto定义的情况下使用消息作为一个嵌套类型。一个Any类型包括一个可以被序列化bytes类型的任意消息,以及一个URL作为一个全局标识符和解析消息类型。为了使用Any类型,你需要导入import google/protobuf/any.proto。

123456
import "google/protobuf/any.proto";message ErrorStatus {  string message = 1;  repeated google.protobuf.Any details = 2;}

对于给定的消息类型的默认类型URL是type.googleapis.com/packagename.messagename。

不同语言的实现会支持动态库以线程安全的方式去帮助封装或者解封装Any值。例如在java中,Any类型会有特殊的pack()和unpack()访问器,在C++中会有PackFrom()和UnpackTo()方法。

1234567891011121314
// Storing an arbitrary message type in Any.NetworkErrorDetails details = ...;ErrorStatus status;status.add_details()->PackFrom(details);// Reading an arbitrary message from Any.ErrorStatus status = ...;for (const Any& detail : status.details()) {  if (detail.Is<NetworkErrorDetails>()) {    NetworkErrorDetails network_error;    detail.UnpackTo(&network_error);    ... processing network_error ...  }}

目前,用于Any类型的动态库仍在开发之中
如果你已经很熟悉proto2语法,使用Any替换扩展

Oneof

如果你的消息中有很多可选字段, 并且同时至多一个字段会被设置, 你可以加强这个行为,使用oneof特性节省内存.

Oneof字段就像可选字段, 除了它们会共享内存, 至多一个字段会被设置。 设置其中一个字段会清除其它字段。 你可以使用case()或者WhichOneof()方法检查哪个oneof字段被设置, 看你使用什么语言了.

使用Oneof

为了在.proto定义Oneof字段, 你需要在名字前面加上oneof关键字, 比如下面例子的test_oneof:

123456
message SampleMessage {  oneof test_oneof {    string name = 4;    SubMessage sub_message = 9;  }}

然后你可以增加oneof字段到 oneof 定义中. 你可以增加任意类型的字段, 但是不能使用repeated 关键字.

在产生的代码中, oneof字段拥有同样的 getters 和setters, 就像正常的可选字段一样. 也有一个特殊的方法来检查到底那个字段被设置. 你可以在相应的语言API指南中找到oneof API介绍.

Oneof 特性

  • 设置oneof会自动清楚其它oneof字段的值. 所以设置多次后,只有最后一次设置的字段有值.
12345
SampleMessage message;message.set_name("name");CHECK(message.has_name());message.mutable_sub_message();   // Will clear name field.CHECK(!message.has_name());
  • 如果解析器遇到同一个oneof中有多个成员,只有最会一个会被解析成消息。
  • oneof不支持repeated.
  • 反射API对oneof 字段有效.
  • 如果使用C++,需确保代码不会导致内存泄漏. 下面的代码会崩溃, 因为sub_message 已经通过set_name()删除了
1234
SampleMessage message;SubMessage* sub_message = message.mutable_sub_message();message.set_name("name");      // Will delete sub_messagesub_message->set_...            // Crashes here
  • 在C++中,如果你使用Swap()两个oneof消息,每个消息,两个消息将拥有对方的值,例如在下面的例子中,msg1会拥有sub_message并且msg2会有name。
1234567
SampleMessage msg1;msg1.set_name("name");SampleMessage msg2;msg2.mutable_sub_message();msg1.swap(&msg2);CHECK(msg1.has_sub_message());CHECK(msg2.has_name());

向后兼容性问题

当增加或者删除oneof字段时一定要小心. 如果检查oneof的值返回None/NOT_SET, 它意味着oneof字段没有被赋值或者在一个不同的版本中赋值了。 你不会知道是哪种情况,因为没有办法判断如果未识别的字段是一个oneof字段。

Tag 重用问题:

  • 将字段移入或移除oneof :在消息被序列号或者解析后,你也许会失去一些信息(有些字段也许会被清除)
  • 删除一个字段或者加入一个字段 :在消息被序列号或者解析后,这也许会清除你现在设置的oneof字段
  • 分离或者融合oneof :行为与移动常规字段相似。

Map

如果你希望创建一个关联映射,protocol buffer提供了一种快捷的语法:

1
map<key_type, value_type> map_field = N;

其中key_type可以是任意Integer或者string类型(所以,除了floating和bytes的任意标量类型都是可以的)value_type可以是任意类型。

例如,如果你希望创建一个project的映射,每个Projecct使用一个string作为key,你可以像下面这样定义:

1
map<string, Project> projects = 3;
  • Map的字段可以是repeated。
  • 序列化后的顺序和map迭代器的顺序是不确定的,所以你不要期望以固定顺序处理Map
  • 当为.proto文件产生生成文本格式的时候,map会按照key 的顺序排序,数值化的key会按照数值排序。
  • 从序列化中解析或者融合时,如果有重复的key则后一个key不会被使用,当从文本格式中解析map时,如果存在重复的key。

生成map的API现在对于所有proto3支持的语言都可用了,你可以从API指南找到更多信息。

向后兼容性问题

map语法序列化后等同于如下内容,因此即使是不支持map语法的protocol buffer实现也是可以处理你的数据的:

123456
message MapFieldEntry {  key_type key = 1;  value_type value = 2;}repeated MapFieldEntry map_field = N;

Package

当然可以为.proto文件新增一个可选的package声明符,用来防止不同的消息类型有命名冲突。如:

12
package foo.bar;message Open { ... }

在其他的消息格式定义中可以使用包名+消息名的方式来定义域的类型,如:

12345
message Foo {  ...  required foo.bar.Open open = 1;  ...}

包的声明符会根据使用语言的不同影响生成的代码。

  • 对于C++,产生的类会被包装在C++的命名空间中,如上例中的Open会被封装在 foo::bar空间中; - 对于Java,包声明符会变为java的一个包,除非在.proto文件中提供了一个明确有java_package;
  • 对于 Python,这个包声明符是被忽略的,因为Python模块是按照其在文件系统中的位置进行组织的。
  • 对于Go,包可以被用做Go包名称,除非你显式的提供一个option go_package在你的.proto文件中。
  • 对于Ruby,生成的类可以被包装在内置的Ruby名称空间中,转换成Ruby所需的大小写样式 (首字母大写;如果第一个符号不是一个字母,则使用PB_前缀),例如Open会在Foo::Bar名称空间中。
  • 对于javaNano包会使用Java包,除非你在你的文件中显式的提供一个option java_package。
  • 对于C#包可以转换为PascalCase后作为名称空间,除非你在你的文件中显式的提供一个option csharp_namespace,例如,Open会在Foo.Bar名称空间中

包及名称的解析

Protocol buffer语言中类型名称的解析与C++是一致的:首先从最内部开始查找,依次向外进行,每个包会被看作是其父类包的内部类。当然对于 (foo.bar.Baz)这样以“.”分隔的意味着是从最外围开始的。

ProtocolBuffer编译器会解析.proto文件中定义的所有类型名。 对于不同语言的代码生成器会知道如何来指向每个具体的类型,即使它们使用了不同的规则。

定义服务(Service)

如果想要将消息类型用在RPC(远程方法调用)系统中,可以在.proto文件中定义一个RPC服务接口,protocol buffer编译器将会根据所选择的不同语言生成服务接口代码及存根。如,想要定义一个RPC服务并具有一个方法,该方法能够接收 SearchRequest并返回一个SearchResponse,此时可以在.proto文件中进行如下定义:

123
service SearchService {  rpc Search (SearchRequest) returns (SearchResponse);}

最直观的使用protocol buffer的RPC系统是gRPC,一个由谷歌开发的语言和平台中的开源的PRC系统,gRPC在使用protocl buffer时非常有效,如果使用特殊的protocol buffer插件可以直接为您从.proto文件中产生相关的RPC代码。

如果你不想使用gRPC,也可以使用protocol buffer用于自己的RPC实现,你可以从proto2语言指南中找到更多信息

还有一些第三方开发的PRC实现使用Protocol Buffer。参考第三方插件wiki查看这些实现的列表。

JSON 映射

Proto3 支持JSON的编码规范,使他更容易在不同系统之间共享数据,在下表中逐个描述类型。

如果JSON编码的数据丢失或者其本身就是null,这个数据会在解析成protocol buffer的时候被表示成默认值。如果一个字段在protocol buffer中表示为默认值,体会在转化成JSON的时候编码的时候忽略掉以节省空间。具体实现可以提供在JSON编码中可选的默认值。

proto3 JSON JSON示例 注意
message object {“fBar”: v, “g”: null, …} 产生JSON对象,消息字段名可以被映射成lowerCamelCase形式,并且成为JSON对象键,null被接受并成为对应字段的默认值
enum string “FOO_BAR” 枚举值的名字在proto文件中被指定
map object {“k”: v, …} 所有的键都被转换成string
repeated V array [v, …] null被视为空列表
bool true, false true, false
string string “Hello World!”
bytes base64 string “YWJjMTIzIT8kKiYoKSctPUB+”
int32, fixed32, uint32 number 1, -10, 0 JSON值会是一个十进制数,数值型或者string类型都会接受
int64, fixed64, uint64 string “1”, “-10” JSON值会是一个十进制数,数值型或者string类型都会接受
float, double number 1.1, -10.0, 0, “NaN”, “Infinity” JSON值会是一个数字或者一个指定的字符串如”NaN”,”infinity”或者”-Infinity”,数值型或者字符串都是可接受的,指数符号也可以接受
Any object {“@type”: “url”, “f”: v, … } 如果一个Any保留一个特上述的JSON映射,则它会转换成一个如下形式:{"@type": xxx, "value": yyy}否则,该值会被转换成一个JSON对象,@type字段会被插入所指定的确定的值
Timestamp string “1972-01-01T10:00:20.021Z” 使用RFC 339,其中生成的输出将始终是Z-归一化啊的,并且使用0,3,6或者9位小数
Duration string “1.000340012s”, “1s” 生成的输出总是0,3,6或者9位小数,具体依赖于所需要的精度,接受所有可以转换为纳秒级的精度
Struct object { … } 任意的JSON对象,见struct.proto
Wrapper types various types 2, “2”, “foo”, true, “true”, null, 0, … 包装器在JSON中的表示方式类似于基本类型,但是允许nulll,并且在转换的过程中保留null
FieldMask string “f.fooBar,h” 见fieldmask.proto
ListValue array [foo, bar, …]
Value value 任意JSON值
NullValue null JSON null

选项

定义.proto文件时能够标注一系列的option。Option并不改变整个文件声明的含义,但却能够影响特定环境下处理方式。完整的可用选项可以在google/protobuf/descriptor.proto找到。

一些选项是文件级别的,意味着它可以作用于最外范围,不包含在任何消息内部、enum或服务定义中。一些选项是消息级别的,意味着它可以用在消息定义的内部。当然有些选项可以作用在域、enum类型、enum值、服务类型及服务方法中。到目前为止,并没有一种有效的选项能作用于所有的类型。

如下就是一些常用的选项:

  • java_package (文件选项) :这个选项表明生成java类所在的包。如果在.proto文件中没有明确的声明java_package,就采用默认的包名。当然了,默认方式产生的 java包名并不是最好的方式,按照应用名称倒序方式进行排序的。如果不需要产生java代码,则该选项将不起任何作用。如:
1
option java_package = "com.example.foo";
  • java_outer_classname (文件选项): 该选项表明想要生成Java类的名称。如果在.proto文件中没有明确的java_outer_classname定义,生成的class名称将会根据.proto文件的名称采用驼峰式的命名方式进行生成。如(foo_bar.proto生成的java类名为FooBar.java),如果不生成java代码,则该选项不起任何作用。如:
1
option java_outer_classname = "Ponycopter";
  • optimize_for(文件选项): 可以被设置为 SPEED, CODE_SIZE,或者LITE_RUNTIME。这些值将通过如下的方式影响C++及java代码的生成:
    • SPEED (default): protocol buffer编译器将通过在消息类型上执行序列化、语法分析及其他通用的操作。这种代码是最优的。
    • CODE_SIZE: protocol buffer编译器将会产生最少量的类,通过共享或基于反射的代码来实现序列化、语法分析及各种其它操作。采用该方式产生的代码将比SPEED要少得多, 但是操作要相对慢些。当然实现的类及其对外的API与SPEED模式都是一样的。这种方式经常用在一些包含大量的.proto文件而且并不盲目追求速度的 应用中。
    • LITE_RUNTIME: protocol buffer编译器依赖于运行时核心类库来生成代码(即采用libprotobuf-lite 替代libprotobuf)。这种核心类库由于忽略了一 些描述符及反射,要比全类库小得多。这种模式经常在移动手机平台应用多一些。编译器采用该模式产生的方法实现与SPEED模式不相上下,产生的类通过实现 MessageLite接口,但它仅仅是Messager接口的一个子集。
1
option optimize_for = CODE_SIZE;
  • cc_enable_arenas(文件选项):对于C++产生的代码启用arena allocation
  • objc_class_prefix(文件选项):设置Objective-C类的前缀,添加到所有Objective-C从此.proto文件产生的类和枚举类型。没有默认值,所使用的前缀应该是苹果推荐的3-5个大写字符,注意2个字节的前缀是苹果所保留的。
  • deprecated(字段选项):如果设置为true则表示该字段已经被废弃,并且不应该在新的代码中使用。在大多数语言中没有实际的意义。在java中,这回变成@Deprecated注释,在未来,其他语言的代码生成器也许会在字标识符中产生废弃注释,废弃注释会在编译器尝试使用该字段时发出警告。如果字段没有被使用你也不希望有新用户使用它,尝试使用保留语句替换字段声明。
1
int32 old_field = 6 [deprecated=true];

自定义选项

ProtocolBuffers允许自定义并使用选项。该功能应该属于一个高级特性,对于大部分人是用不到的。如果你的确希望创建自己的选项,请参看 Proto2 Language Guide。注意创建自定义选项使用了拓展,拓展只在proto3中可用。

生成访问类

可以通过定义好的.proto文件来生成Java,Python,C++, Ruby, JavaNano, Objective-C,或者C# 代码,需要基于.proto文件运行protocol buffer编译器protoc。如果你没有安装编译器,下载安装包并遵照README安装。对于Go,你还需要安装一个特殊的代码生成器插件。你可以通过GitHub上的protobuf库找到安装过程

通过如下方式调用protocol编译器:

1
protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR --go_out=DST_DIR --ruby_out=DST_DIR --javanano_out=DST_DIR --objc_out=DST_DIR --csharp_out=DST_DIR path/to/file.proto
  • IMPORT_PATH声明了一个.proto文件所在的解析import具体目录。如果忽略该值,则使用当前目录。如果有多个目录则可以多次调用--proto_path,它们将会顺序的被访问并执行导入。-I=IMPORT_PATH是--proto_path的简化形式。
  • 当然也可以提供一个或多个输出路径:
    • --cpp_out 在目标目录DST_DIR中产生C++代码,可以在C++代码生成参考中查看更多。
    • --java_out 在目标目录DST_DIR中产生Java代码,可以在Java代码生成参考中查看更多。
    • --python_out 在目标目录 DST_DIR 中产生Python代码,可以在Python代码生成参考中查看更多。
    • --go_out 在目标目录 DST_DIR 中产生Go代码,可以在GO代码生成参考中查看更多。
    • --ruby_out在目标目录 DST_DIR 中产生Ruby代码,参考正在制作中。
    • --javanano_out在目标目录DST_DIR中生成JavaNano,JavaNano代码生成器有一系列的选项用于定制自定义生成器的输出:你可以通过生成器的README查找更多信息,JavaNano参考正在制作中。
    • --objc_out在目标目录DST_DIR中产生Object代码,可以在Objective-C代码生成参考中查看更多。
    • --csharp_out在目标目录DST_DIR中产生Object代码,可以在C#代码生成参考中查看更多。
    • --php_out在目标目录DST_DIR中产生Object代码,可以在PHP代码生成参考中查看更多。

作为一个方便的拓展,如果DST_DIR以.zip或者.jar结尾,编译器会将输出写到一个ZIP格式文件或者符合JAR标准的.jar文件中。注意如果输出已经存在则会被覆盖,编译器还没有智能到可以追加文件。

  • 你必须提议一个或多个.proto文件作为输入,多个.proto文件可以只指定一次。虽然文件路径是相对于当前目录的,每个文件必须位于其IMPORT_PATH下,以便每个文件可以确定其规范的名称。
]]>

以前我翻译了Protobuf2 语法指南,现在千念飞羽把protobuf3的语法指南也翻译了,我也转载一下,读者可以有个参考。 译文地址是:Protobuf3语言指南

英文原文:
Language Guide (proto3)
中文出处:
Protobuf语言指南
[译]Protobuf 语法指南
中文出处是proto2的译文,proto3的英文出现后在原来基础上增改了,水平有限,还请指正

这个指南描述了如何使用Protocol buffer 语言去描述你的protocol buffer 数据, 包括 .proto文件符号和如何从.proto文件生成类。包含了proto2版本的protocol buffer语言:对于老版本的proto3 符号,请见Proto2 Language Guide(以及中文译本,抄了很多这里的感谢下老版本的翻译者)

本文是一个参考指南——如果要查看如何使用本文中描述的多个特性的循序渐进的例子,请在教程中查找需要的语言的教程。

定义一个消息类型

先来看一个非常简单的例子。假设你想定义一个“搜索请求”的消息格式,每一个请求含有一个查询字符串、你感兴趣的查询结果所在的页数,以及每一页多少条查询结果。可以采用如下的方式来定义消息类型的.proto文件了:

1234567
syntax = "proto3";message SearchRequest {  string query = 1;  int32 page_number = 2;  int32 result_per_page = 3;}
  • 文件的第一行指定了你正在使用proto3语法:如果你没有指定这个,编译器会使用proto2。这个指定语法行必须是文件的非空非注释的第一个行。
  • SearchRequest消息格式有3个字段,在消息中承载的数据分别对应于每一个字段。其中每个字段都有一个名字和一种类型。

指定字段类型

在上面的例子中,所有字段都是标量类型:两个整型(page_number和result_per_page),一个string类型(query)。当然,你也可以为字段指定其他的合成类型,包括枚举(enumerations)或其他消息类型。

分配标识号

正如你所见,在消息定义中,每个字段都有唯一的一个数字标识符。这些标识符是用来在消息的二进制格式中识别各个字段的,一旦开始使用就不能够再改变。注:[1,15]之内的标识号在编码的时候会占用一个字节。[16,2047]之内的标识号则占用2个字节。所以应该为那些频繁出现的消息元素保留 [1,15]之内的标识号。切记:要为将来有可能添加的、频繁出现的标识号预留一些标识号。

最小的标识号可以从1开始,最大到2^29 - 1, or 536,870,911。不可以使用其中的[19000-19999]( (从FieldDescriptor::kFirstReservedNumber 到 FieldDescriptor::kLastReservedNumber))的标识号, Protobuf协议实现中对这些进行了预留。如果非要在.proto文件中使用这些预留标识号,编译时就会报警。同样你也不能使用早期保留的标识号。

指定字段规则

所指定的消息字段修饰符必须是如下之一:

  • singular:一个格式良好的消息应该有0个或者1个这种字段(但是不能超过1个)。
  • repeated:在一个格式良好的消息中,这种字段可以重复任意多次(包括0次)。重复的值的顺序会被保留。

在proto3中,repeated的标量域默认情况虾使用packed。

你可以了解更多的pakced属性在Protocol Buffer 编码

添加更多消息类型

在一个.proto文件中可以定义多个消息类型。在定义多个相关的消息的时候,这一点特别有用——例如,如果想定义与SearchResponse消息类型对应的回复消息格式的话,你可以将它添加到相同的.proto文件中,如:

123456789
message SearchRequest {  string query = 1;  int32 page_number = 2;  int32 result_per_page = 3;}message SearchResponse { ...}

添加注释

向.proto文件添加注释,可以使用C/C++/Java风格的双斜杠(//) 语法格式,如:

12345
message SearchRequest {  string query = 1;  int32 page_number = 2;  // Which page number do we want?  int32 result_per_page = 3;  // Number of results to return per page.}

保留标识符(Reserved)

如果你通过删除或者注释所有域,以后的用户在更新这个类型的时候可能重用这些标识号。如果你使用旧版本加载相同的.proto文件会导致严重的问题,包括数据损坏、隐私错误等等。现在有一种确保不会发生这种情况的方法就是为字段tag(reserved name可能会JSON序列化的问题)指定reserved标识符,protocol buffer的编译器会警告未来尝试使用这些域标识符的用户。

1234
message Foo {  reserved 2, 15, 9 to 11;  reserved "foo", "bar";}

注:不要在同一行reserved声明中同时声明域名字和tag number。

从.proto文件生成了什么?

当用protocol buffer编译器来运行.proto文件时,编译器将生成所选择语言的代码,这些代码可以操作在.proto文件中定义的消息类型,包括获取、设置字段值,将消息序列化到一个输出流中,以及从一个输入流中解析消息。

  • 对C++来说,编译器会为每个.proto文件生成一个.h文件和一个.cc文件,.proto文件中的每一个消息有一个对应的类。
  • 对Java来说,编译器为每一个消息类型生成了一个.java文件,以及一个特殊的Builder类(该类是用来创建消息类接口的)。
  • 对Python来说,有点不太一样——Python编译器为.proto文件中的每个消息类型生成一个含有静态描述符的模块,,该模块与一个元类(metaclass)在运行时(runtime)被用来创建所需的Python数据访问类。
  • 对go来说,编译器会位每个消息类型生成了一个.pd.go文件。
  • 对于Ruby来说,编译器会为每个消息类型生成了一个.rb文件。
  • javaNano来说,编译器输出类似域java但是没有Builder类
  • 对于Objective-C来说,编译器会为每个消息类型生成了一个pbobjc.h文件和pbobjcm文件,.proto文件中的每一个消息有一个对应的类。
  • 对于C#来说,编译器会为每个消息类型生成了一个.cs文件,.proto文件中的每一个消息有一个对应的类。
    你可以从如下的文档链接中获取每种语言更多API(proto3版本的内容很快就公布)。API Reference

标量数值类型

一个标量消息字段可以含有一个如下的类型——该表格展示了定义于.proto文件中的类型,以及与之对应的、在自动生成的访问类中定义的类型:

.proto Type Notes C++ Type Java Type Python Type[2] Go Type Ruby Type C# Type PHP Type
double double double float float64 Float double float
float float float float float32 Float float float
int32 使用变长编码,对于负值的效率很低,如果你的域有可能有负值,请使用sint64替代 int32 int int int32 Fixnum 或者 Bignum(根据需要) int integer
uint32 使用变长编码 uint32 int int/long uint32 Fixnum 或者 Bignum(根据需要) uint integer
uint64 使用变长编码 uint64 long int/long uint64 Bignum ulong integer/string
sint32 使用变长编码,这些编码在负值时比int32高效的多 int32 int int int32 Fixnum 或者 Bignum(根据需要) int integer
sint64 使用变长编码,有符号的整型值。编码时比通常的int64高效。 int64 long int/long int64 Bignum long integer/string
fixed32 总是4个字节,如果数值总是比总是比228大的话,这个类型会比uint32高效。 uint32 int int uint32 Fixnum 或者 Bignum(根据需要) uint integer
fixed64 总是8个字节,如果数值总是比总是比256大的话,这个类型会比uint64高效。 uint64 long int/long uint64 Bignum ulong integer/string
sfixed32 总是4个字节 int32 int int int32 Fixnum 或者 Bignum(根据需要) int integer
sfixed64 总是8个字节 int64 long int/long int64 Bignum long integer/string
bool bool boolean bool bool TrueClass/FalseClass bool boolean
string 一个字符串必须是UTF-8编码或者7-bit ASCII编码的文本。 string String str/unicode string String (UTF-8) string string
bytes 可能包含任意顺序的字节数据。 string ByteString str []byte String (ASCII-8BIT) ByteString string

你可以在文章Protocol Buffer 编码中,找到更多“序列化消息时各种类型如何编码”的信息。

  1. 在java中,无符号32位和64位整型被表示成他们的整型对应形式,最高位被储存在标志位中。
  2. 对于所有的情况,设定值会执行类型检查以确保此值是有效。
  3. 64位或者无符号32位整型在解码时被表示成为ilong,但是在设置时可以使用int型值设定,在所有的情况下,值必须符合其设置其类型的要求。
  4. python中string被表示成在解码时表示成unicode。但是一个ASCIIstring可以被表示成str类型。
  5. Integer在64位的机器上使用,string在32位机器上使用

默认值

当一个消息被解析的时候,如果被编码的信息不包含一个特定的singular元素,被解析的对象锁对应的域被设置位一个默认值,对于不同类型指定如下:

  • 对于string,默认是一个空string
  • 对于bytes,默认是一个空的bytes
  • 对于bool,默认是false
  • 对于数值类型,默认是0
  • 对于枚举,默认是第一个定义的枚举值,必须为0;
  • 对于消息类型(message),域没有被设置,确切的消息是根据语言确定的,详见generated code guide

对于可重复域的默认值是空(通常情况下是对应语言中空列表)。

注:对于标量消息域,一旦消息被解析,就无法判断域释放被设置为默认值(例如,例如boolean值是否被设置为false)还是根本没有被设置。你应该在定义你的消息类型时非常注意。例如,比如你不应该定义boolean的默认值false作为任何行为的触发方式。也应该注意如果一个标量消息域被设置为标志位,这个值不应该被序列化传输。

查看generated code guide选择你的语言的默认值的工作细节。

枚举

当需要定义一个消息类型的时候,可能想为一个字段指定某“预定义值序列”中的一个值。例如,假设要为每一个SearchRequest消息添加一个 corpus字段,而corpus的值可能是UNIVERSAL,WEB,IMAGES,LOCAL,NEWS,PRODUCTS或VIDEO中的一个。 其实可以很容易地实现这一点:通过向消息定义中添加一个枚举(enum)并且为每个可能的值定义一个常量就可以了。

在下面的例子中,在消息格式中添加了一个叫做Corpus的枚举类型——它含有所有可能的值 ——以及一个类型为Corpus的字段:

123456789101112131415
message SearchRequest {  string query = 1;  int32 page_number = 2;  int32 result_per_page = 3;  enum Corpus {    UNIVERSAL = 0;    WEB = 1;    IMAGES = 2;    LOCAL = 3;    NEWS = 4;    PRODUCTS = 5;    VIDEO = 6;  }  Corpus corpus = 4;}

如你所见,Corpus枚举的第一个常量映射为0:每个枚举类型必须将其第一个类型映射为0,这是因为:

  • 必须有有一个0值,我们可以用这个0值作为默认值。
  • 这个零值必须为第一个元素,为了兼容proto2语义,枚举类的第一个值总是默认值。

你可以通过将不同的枚举常量指定位相同的值。如果这样做你需要将allow_alias设定位true,否则编译器会在别名的地方产生一个错误信息。

1234567891011
enum EnumAllowingAlias {  option allow_alias = true;  UNKNOWN = 0;  STARTED = 1;  RUNNING = 1;}enum EnumNotAllowingAlias {  UNKNOWN = 0;  STARTED = 1;  // RUNNING = 1;  // Uncommenting this line will cause a compile error inside Google and a warning message outside.}

枚举常量必须在32位整型值的范围内。因为enum值是使用可变编码方式的,对负数不够高效,因此不推荐在enum中使用负数。如上例所示,可以在 一个消息定义的内部或外部定义枚举——这些枚举可以在.proto文件中的任何消息定义里重用。当然也可以在一个消息中声明一个枚举类型,而在另一个不同 的消息中使用它——采用MessageType.EnumType的语法格式。

当对一个使用了枚举的.proto文件运行protocol buffer编译器的时候,生成的代码中将有一个对应的enum(对Java或C++来说),或者一个特殊的EnumDescriptor类(对 Python来说),它被用来在运行时生成的类中创建一系列的整型值符号常量(symbolic constants)。

在反序列化的过程中,无法识别的枚举值会被保存在消息中,虽然这种表示方式需要依据所使用语言而定。在那些支持开放枚举类型超出指定范围之外的语言中(例如C++和Go),为识别的值会被表示成所支持的整型。在使用封闭枚举类型的语言中(Java),使用枚举中的一个类型来表示未识别的值,并且可以使用所支持整型来访问。在其他情况下,如果解析的消息被序列号,未识别的值将保持原样。

关于如何在你的应用程序的消息中使用枚举的更多信息,请查看所选择的语言generated code guide

使用其他消息类型

你可以将其他消息类型用作字段类型。例如,假设在每一个SearchResponse消息中包含Result消息,此时可以在相同的.proto文件中定义一个Result消息类型,然后在SearchResponse消息中指定一个Result类型的字段,如:

123456789
message SearchResponse {  repeated Result results = 1;}message Result {  string url = 1;  string title = 2;  repeated string snippets = 3;}

导入定义

在上面的例子中,Result消息类型与SearchResponse是定义在同一文件中的。如果想要使用的消息类型已经在其他.proto文件中已经定义过了呢?
你可以通过导入(importing)其他.proto文件中的定义来使用它们。要导入其他.proto文件的定义,你需要在你的文件中添加一个导入声明,如:

1
import "myproject/other_protos.proto";

默认情况下你只能使用直接导入的.proto文件中的定义. 然而, 有时候你需要移动一个.proto文件到一个新的位置, 可以不直接移动.proto文件, 只需放入一个伪 .proto 文件在老的位置, 然后使用import public转向新的位置。import public 依赖性会通过任意导入包含import public声明的proto文件传递。例如:

12
// 这是新的proto// All definitions are moved here
1234
// 这是久的proto// 这是所有客户端正在导入的包import public "new.proto";import "other.proto";
123
// 客户端protoimport "old.proto";// 现在你可以使用新旧两种包的proto定义了。

通过在编译器命令行参数中使用-I/--proto_pathprotocal 编译器会在指定目录搜索要导入的文件。如果没有给出标志,编译器会搜索编译命令被调用的目录。通常你只要指定proto_path标志为你的工程根目录就好。并且指定好导入的正确名称就好。

使用proto2消息类型

在你的proto3消息中导入proto2的消息类型也是可以的,反之亦然,然后proto2枚举不可以直接在proto3的标识符中使用(如果仅仅在proto2消息中使用是可以的)。

嵌套类型

你可以在其他消息类型中定义、使用消息类型,在下面的例子中,Result消息就定义在SearchResponse消息内,如:

12345678
message SearchResponse {  message Result {    string url = 1;    string title = 2;    repeated string snippets = 3;  }  repeated Result results = 1;}

如果你想在它的父消息类型的外部重用这个消息类型,你需要以Parent.Type的形式使用它,如:

123
message SomeOtherMessage {  SearchResponse.Result result = 1;}

当然,你也可以将消息嵌套任意多层,如:

1234567891011121314
message Outer {                  // Level 0  message MiddleAA {  // Level 1    message Inner {   // Level 2      int64 ival = 1;      bool  booly = 2;    }  }  message MiddleBB {  // Level 1    message Inner {   // Level 2      int32 ival = 1;      bool  booly = 2;    }  }}

更新一个消息类型

如果一个已有的消息格式已无法满足新的需求——如,要在消息中添加一个额外的字段——但是同时旧版本写的代码仍然可用。不用担心!更新消息而不破坏已有代码是非常简单的。在更新时只要记住以下的规则即可。

  • 不要更改任何已有的字段的数值标识。
  • 如果你增加新的字段,使用旧格式的字段仍然可以被你新产生的代码所解析。你应该记住这些元素的默认值这样你的新代码就可以以适当的方式和旧代码产生的数据交互。相似的,通过新代码产生的消息也可以被旧代码解析:只不过新的字段会被忽视掉。注意,未被识别的字段会在反序列化的过程中丢弃掉,所以如果消息再被传递给新的代码,新的字段依然是不可用的(这和proto2中的行为是不同的,在proto2中未定义的域依然会随着消息被序列化)
  • 非required的字段可以移除——只要它们的标识号在新的消息类型中不再使用(更好的做法可能是重命名那个字段,例如在字段前添加“OBSOLETE_”前缀,那样的话,使用的.proto文件的用户将来就不会无意中重新使用了那些不该使用的标识号)。
  • int32, uint32, int64, uint64,和bool是全部兼容的,这意味着可以将这些类型中的一个转换为另外一个,而不会破坏向前、 向后的兼容性。如果解析出来的数字与对应的类型不相符,那么结果就像在C++中对它进行了强制类型转换一样(例如,如果把一个64位数字当作int32来 读取,那么它就会被截断为32位的数字)。
  • sint32和sint64是互相兼容的,但是它们与其他整数类型不兼容。
  • string和bytes是兼容的——只要bytes是有效的UTF-8编码。
  • 嵌套消息与bytes是兼容的——只要bytes包含该消息的一个编码过的版本。
  • fixed32与sfixed32是兼容的,fixed64与sfixed64是兼容的。
  • 枚举类型与int32,uint32,int64和uint64相兼容(注意如果值不相兼容则会被截断),然而在客户端反序列化之后他们可能会有不同的处理方式,例如,未识别的proto3枚举类型会被保留在消息中,但是他的表示方式会依照语言而定。int类型的字段总会保留他们的

Any

Any类型消息允许你在没有指定他们的.proto定义的情况下使用消息作为一个嵌套类型。一个Any类型包括一个可以被序列化bytes类型的任意消息,以及一个URL作为一个全局标识符和解析消息类型。为了使用Any类型,你需要导入import google/protobuf/any.proto。

123456
import "google/protobuf/any.proto";message ErrorStatus {  string message = 1;  repeated google.protobuf.Any details = 2;}

对于给定的消息类型的默认类型URL是type.googleapis.com/packagename.messagename。

不同语言的实现会支持动态库以线程安全的方式去帮助封装或者解封装Any值。例如在java中,Any类型会有特殊的pack()和unpack()访问器,在C++中会有PackFrom()和UnpackTo()方法。

1234567891011121314
// Storing an arbitrary message type in Any.NetworkErrorDetails details = ...;ErrorStatus status;status.add_details()->PackFrom(details);// Reading an arbitrary message from Any.ErrorStatus status = ...;for (const Any& detail : status.details()) {  if (detail.Is<NetworkErrorDetails>()) {    NetworkErrorDetails network_error;    detail.UnpackTo(&network_error);    ... processing network_error ...  }}

目前,用于Any类型的动态库仍在开发之中
如果你已经很熟悉proto2语法,使用Any替换扩展

Oneof

如果你的消息中有很多可选字段, 并且同时至多一个字段会被设置, 你可以加强这个行为,使用oneof特性节省内存.

Oneof字段就像可选字段, 除了它们会共享内存, 至多一个字段会被设置。 设置其中一个字段会清除其它字段。 你可以使用case()或者WhichOneof()方法检查哪个oneof字段被设置, 看你使用什么语言了.

使用Oneof

为了在.proto定义Oneof字段, 你需要在名字前面加上oneof关键字, 比如下面例子的test_oneof:

123456
message SampleMessage {  oneof test_oneof {    string name = 4;    SubMessage sub_message = 9;  }}

然后你可以增加oneof字段到 oneof 定义中. 你可以增加任意类型的字段, 但是不能使用repeated 关键字.

在产生的代码中, oneof字段拥有同样的 getters 和setters, 就像正常的可选字段一样. 也有一个特殊的方法来检查到底那个字段被设置. 你可以在相应的语言API指南中找到oneof API介绍.

Oneof 特性

  • 设置oneof会自动清楚其它oneof字段的值. 所以设置多次后,只有最后一次设置的字段有值.
12345
SampleMessage message;message.set_name("name");CHECK(message.has_name());message.mutable_sub_message();   // Will clear name field.CHECK(!message.has_name());
  • 如果解析器遇到同一个oneof中有多个成员,只有最会一个会被解析成消息。
  • oneof不支持repeated.
  • 反射API对oneof 字段有效.
  • 如果使用C++,需确保代码不会导致内存泄漏. 下面的代码会崩溃, 因为sub_message 已经通过set_name()删除了
1234
SampleMessage message;SubMessage* sub_message = message.mutable_sub_message();message.set_name("name");      // Will delete sub_messagesub_message->set_...            // Crashes here
  • 在C++中,如果你使用Swap()两个oneof消息,每个消息,两个消息将拥有对方的值,例如在下面的例子中,msg1会拥有sub_message并且msg2会有name。
1234567
SampleMessage msg1;msg1.set_name("name");SampleMessage msg2;msg2.mutable_sub_message();msg1.swap(&msg2);CHECK(msg1.has_sub_message());CHECK(msg2.has_name());

向后兼容性问题

当增加或者删除oneof字段时一定要小心. 如果检查oneof的值返回None/NOT_SET, 它意味着oneof字段没有被赋值或者在一个不同的版本中赋值了。 你不会知道是哪种情况,因为没有办法判断如果未识别的字段是一个oneof字段。

Tag 重用问题:

  • 将字段移入或移除oneof :在消息被序列号或者解析后,你也许会失去一些信息(有些字段也许会被清除)
  • 删除一个字段或者加入一个字段 :在消息被序列号或者解析后,这也许会清除你现在设置的oneof字段
  • 分离或者融合oneof :行为与移动常规字段相似。

Map

如果你希望创建一个关联映射,protocol buffer提供了一种快捷的语法:

1
map<key_type, value_type> map_field = N;

其中key_type可以是任意Integer或者string类型(所以,除了floating和bytes的任意标量类型都是可以的)value_type可以是任意类型。

例如,如果你希望创建一个project的映射,每个Projecct使用一个string作为key,你可以像下面这样定义:

1
map<string, Project> projects = 3;
  • Map的字段可以是repeated。
  • 序列化后的顺序和map迭代器的顺序是不确定的,所以你不要期望以固定顺序处理Map
  • 当为.proto文件产生生成文本格式的时候,map会按照key 的顺序排序,数值化的key会按照数值排序。
  • 从序列化中解析或者融合时,如果有重复的key则后一个key不会被使用,当从文本格式中解析map时,如果存在重复的key。

生成map的API现在对于所有proto3支持的语言都可用了,你可以从API指南找到更多信息。

向后兼容性问题

map语法序列化后等同于如下内容,因此即使是不支持map语法的protocol buffer实现也是可以处理你的数据的:

123456
message MapFieldEntry {  key_type key = 1;  value_type value = 2;}repeated MapFieldEntry map_field = N;

Package

当然可以为.proto文件新增一个可选的package声明符,用来防止不同的消息类型有命名冲突。如:

12
package foo.bar;message Open { ... }

在其他的消息格式定义中可以使用包名+消息名的方式来定义域的类型,如:

12345
message Foo {  ...  required foo.bar.Open open = 1;  ...}

包的声明符会根据使用语言的不同影响生成的代码。

  • 对于C++,产生的类会被包装在C++的命名空间中,如上例中的Open会被封装在 foo::bar空间中; - 对于Java,包声明符会变为java的一个包,除非在.proto文件中提供了一个明确有java_package;
  • 对于 Python,这个包声明符是被忽略的,因为Python模块是按照其在文件系统中的位置进行组织的。
  • 对于Go,包可以被用做Go包名称,除非你显式的提供一个option go_package在你的.proto文件中。
  • 对于Ruby,生成的类可以被包装在内置的Ruby名称空间中,转换成Ruby所需的大小写样式 (首字母大写;如果第一个符号不是一个字母,则使用PB_前缀),例如Open会在Foo::Bar名称空间中。
  • 对于javaNano包会使用Java包,除非你在你的文件中显式的提供一个option java_package。
  • 对于C#包可以转换为PascalCase后作为名称空间,除非你在你的文件中显式的提供一个option csharp_namespace,例如,Open会在Foo.Bar名称空间中

包及名称的解析

Protocol buffer语言中类型名称的解析与C++是一致的:首先从最内部开始查找,依次向外进行,每个包会被看作是其父类包的内部类。当然对于 (foo.bar.Baz)这样以“.”分隔的意味着是从最外围开始的。

ProtocolBuffer编译器会解析.proto文件中定义的所有类型名。 对于不同语言的代码生成器会知道如何来指向每个具体的类型,即使它们使用了不同的规则。

定义服务(Service)

如果想要将消息类型用在RPC(远程方法调用)系统中,可以在.proto文件中定义一个RPC服务接口,protocol buffer编译器将会根据所选择的不同语言生成服务接口代码及存根。如,想要定义一个RPC服务并具有一个方法,该方法能够接收 SearchRequest并返回一个SearchResponse,此时可以在.proto文件中进行如下定义:

123
service SearchService {  rpc Search (SearchRequest) returns (SearchResponse);}

最直观的使用protocol buffer的RPC系统是gRPC,一个由谷歌开发的语言和平台中的开源的PRC系统,gRPC在使用protocl buffer时非常有效,如果使用特殊的protocol buffer插件可以直接为您从.proto文件中产生相关的RPC代码。

如果你不想使用gRPC,也可以使用protocol buffer用于自己的RPC实现,你可以从proto2语言指南中找到更多信息

还有一些第三方开发的PRC实现使用Protocol Buffer。参考第三方插件wiki查看这些实现的列表。

JSON 映射

Proto3 支持JSON的编码规范,使他更容易在不同系统之间共享数据,在下表中逐个描述类型。

如果JSON编码的数据丢失或者其本身就是null,这个数据会在解析成protocol buffer的时候被表示成默认值。如果一个字段在protocol buffer中表示为默认值,体会在转化成JSON的时候编码的时候忽略掉以节省空间。具体实现可以提供在JSON编码中可选的默认值。

proto3 JSON JSON示例 注意
message object {“fBar”: v, “g”: null, …} 产生JSON对象,消息字段名可以被映射成lowerCamelCase形式,并且成为JSON对象键,null被接受并成为对应字段的默认值
enum string “FOO_BAR” 枚举值的名字在proto文件中被指定
map object {“k”: v, …} 所有的键都被转换成string
repeated V array [v, …] null被视为空列表
bool true, false true, false
string string “Hello World!”
bytes base64 string “YWJjMTIzIT8kKiYoKSctPUB+”
int32, fixed32, uint32 number 1, -10, 0 JSON值会是一个十进制数,数值型或者string类型都会接受
int64, fixed64, uint64 string “1”, “-10” JSON值会是一个十进制数,数值型或者string类型都会接受
float, double number 1.1, -10.0, 0, “NaN”, “Infinity” JSON值会是一个数字或者一个指定的字符串如”NaN”,”infinity”或者”-Infinity”,数值型或者字符串都是可接受的,指数符号也可以接受
Any object {“@type”: “url”, “f”: v, … } 如果一个Any保留一个特上述的JSON映射,则它会转换成一个如下形式:{"@type": xxx, "value": yyy}否则,该值会被转换成一个JSON对象,@type字段会被插入所指定的确定的值
Timestamp string “1972-01-01T10:00:20.021Z” 使用RFC 339,其中生成的输出将始终是Z-归一化啊的,并且使用0,3,6或者9位小数
Duration string “1.000340012s”, “1s” 生成的输出总是0,3,6或者9位小数,具体依赖于所需要的精度,接受所有可以转换为纳秒级的精度
Struct object { … } 任意的JSON对象,见struct.proto
Wrapper types various types 2, “2”, “foo”, true, “true”, null, 0, … 包装器在JSON中的表示方式类似于基本类型,但是允许nulll,并且在转换的过程中保留null
FieldMask string “f.fooBar,h” 见fieldmask.proto
ListValue array [foo, bar, …]
Value value 任意JSON值
NullValue null JSON null

选项

定义.proto文件时能够标注一系列的option。Option并不改变整个文件声明的含义,但却能够影响特定环境下处理方式。完整的可用选项可以在google/protobuf/descriptor.proto找到。

一些选项是文件级别的,意味着它可以作用于最外范围,不包含在任何消息内部、enum或服务定义中。一些选项是消息级别的,意味着它可以用在消息定义的内部。当然有些选项可以作用在域、enum类型、enum值、服务类型及服务方法中。到目前为止,并没有一种有效的选项能作用于所有的类型。

如下就是一些常用的选项:

  • java_package (文件选项) :这个选项表明生成java类所在的包。如果在.proto文件中没有明确的声明java_package,就采用默认的包名。当然了,默认方式产生的 java包名并不是最好的方式,按照应用名称倒序方式进行排序的。如果不需要产生java代码,则该选项将不起任何作用。如:
1
option java_package = "com.example.foo";
  • java_outer_classname (文件选项): 该选项表明想要生成Java类的名称。如果在.proto文件中没有明确的java_outer_classname定义,生成的class名称将会根据.proto文件的名称采用驼峰式的命名方式进行生成。如(foo_bar.proto生成的java类名为FooBar.java),如果不生成java代码,则该选项不起任何作用。如:
1
option java_outer_classname = "Ponycopter";
  • optimize_for(文件选项): 可以被设置为 SPEED, CODE_SIZE,或者LITE_RUNTIME。这些值将通过如下的方式影响C++及java代码的生成:
    • SPEED (default): protocol buffer编译器将通过在消息类型上执行序列化、语法分析及其他通用的操作。这种代码是最优的。
    • CODE_SIZE: protocol buffer编译器将会产生最少量的类,通过共享或基于反射的代码来实现序列化、语法分析及各种其它操作。采用该方式产生的代码将比SPEED要少得多, 但是操作要相对慢些。当然实现的类及其对外的API与SPEED模式都是一样的。这种方式经常用在一些包含大量的.proto文件而且并不盲目追求速度的 应用中。
    • LITE_RUNTIME: protocol buffer编译器依赖于运行时核心类库来生成代码(即采用libprotobuf-lite 替代libprotobuf)。这种核心类库由于忽略了一 些描述符及反射,要比全类库小得多。这种模式经常在移动手机平台应用多一些。编译器采用该模式产生的方法实现与SPEED模式不相上下,产生的类通过实现 MessageLite接口,但它仅仅是Messager接口的一个子集。
1
option optimize_for = CODE_SIZE;
  • cc_enable_arenas(文件选项):对于C++产生的代码启用arena allocation
  • objc_class_prefix(文件选项):设置Objective-C类的前缀,添加到所有Objective-C从此.proto文件产生的类和枚举类型。没有默认值,所使用的前缀应该是苹果推荐的3-5个大写字符,注意2个字节的前缀是苹果所保留的。
  • deprecated(字段选项):如果设置为true则表示该字段已经被废弃,并且不应该在新的代码中使用。在大多数语言中没有实际的意义。在java中,这回变成@Deprecated注释,在未来,其他语言的代码生成器也许会在字标识符中产生废弃注释,废弃注释会在编译器尝试使用该字段时发出警告。如果字段没有被使用你也不希望有新用户使用它,尝试使用保留语句替换字段声明。
1
int32 old_field = 6 [deprecated=true];

自定义选项

ProtocolBuffers允许自定义并使用选项。该功能应该属于一个高级特性,对于大部分人是用不到的。如果你的确希望创建自己的选项,请参看 Proto2 Language Guide。注意创建自定义选项使用了拓展,拓展只在proto3中可用。

生成访问类

可以通过定义好的.proto文件来生成Java,Python,C++, Ruby, JavaNano, Objective-C,或者C# 代码,需要基于.proto文件运行protocol buffer编译器protoc。如果你没有安装编译器,下载安装包并遵照README安装。对于Go,你还需要安装一个特殊的代码生成器插件。你可以通过GitHub上的protobuf库找到安装过程

通过如下方式调用protocol编译器:

1
protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR --go_out=DST_DIR --ruby_out=DST_DIR --javanano_out=DST_DIR --objc_out=DST_DIR --csharp_out=DST_DIR path/to/file.proto
  • IMPORT_PATH声明了一个.proto文件所在的解析import具体目录。如果忽略该值,则使用当前目录。如果有多个目录则可以多次调用--proto_path,它们将会顺序的被访问并执行导入。-I=IMPORT_PATH是--proto_path的简化形式。
  • 当然也可以提供一个或多个输出路径:
    • --cpp_out 在目标目录DST_DIR中产生C++代码,可以在C++代码生成参考中查看更多。
    • --java_out 在目标目录DST_DIR中产生Java代码,可以在Java代码生成参考中查看更多。
    • --python_out 在目标目录 DST_DIR 中产生Python代码,可以在Python代码生成参考中查看更多。
    • --go_out 在目标目录 DST_DIR 中产生Go代码,可以在GO代码生成参考中查看更多。
    • --ruby_out在目标目录 DST_DIR 中产生Ruby代码,参考正在制作中。
    • --javanano_out在目标目录DST_DIR中生成JavaNano,JavaNano代码生成器有一系列的选项用于定制自定义生成器的输出:你可以通过生成器的README查找更多信息,JavaNano参考正在制作中。
    • --objc_out在目标目录DST_DIR中产生Object代码,可以在Objective-C代码生成参考中查看更多。
    • --csharp_out在目标目录DST_DIR中产生Object代码,可以在C#代码生成参考中查看更多。
    • --php_out在目标目录DST_DIR中产生Object代码,可以在PHP代码生成参考中查看更多。

作为一个方便的拓展,如果DST_DIR以.zip或者.jar结尾,编译器会将输出写到一个ZIP格式文件或者符合JAR标准的.jar文件中。注意如果输出已经存在则会被覆盖,编译器还没有智能到可以追加文件。

  • 你必须提议一个或多个.proto文件作为输入,多个.proto文件可以只指定一次。虽然文件路径是相对于当前目录的,每个文件必须位于其IMPORT_PATH下,以便每个文件可以确定其规范的名称。
]]>
0
<![CDATA[sproto 的一些更新]]> http://www.udpwork.com/item/16176.html http://www.udpwork.com/item/16176.html#reviews Tue, 14 Mar 2017 22:05:06 +0800 云风 http://www.udpwork.com/item/16176.html sproto是我设计的一个类 google protocol buffers 的东西。

在很多年前,我在我经手的一些项目中使用 google protocol buffers 。用了好几年,经历了几个项目后,我感觉到它其实是为静态编译型语言设计的协议,其实并没有脱离语言的普适性。在动态语言中,大家都不太愿意使用它(json 更为流行)。一个很大的原因是,protobuffers 是基于代码生成工作的,如果你不使用代码生成,那么它自身的 bootstrap 就非常难实现。

因为它的协议本身是用自身描述的,如果你要解析协议,必须先有解析自己的能力。这是个先有鸡还是先有蛋的矛盾。过去很多动态语言的 binding 都逃不掉引入负责的 C++ 库再加上一部分动态代码生成。我对这点很不爽,后来重头实现了pbc这个库。虽然它还有一些问题,并且我不再想维护它,这个库加上 lua 的 binding 依然是 lua 中使用 protobuffer 的首选。

protobuffer 自身有很长的历史,历史带来了包袱,实际上在我放弃 protobuffer 后不久,google 也大刀阔斧的更新了 3.0 版,对之前 2.x 版做了很多必要的,不兼容的改进。

我仿照 protobuffer 的基本理念设计了 sproto ,设计上的考虑在之前的 blog 中已经写了很多。

sproto 的设计理念之一:保证基本能用的基础上足够简单,不增加太多特性。如果要增加新特性,都是仔细考虑过,并保持不破坏兼容性。最近一段时间,我给 sproto 加了两个特性,这里简单介绍一下:

其一,sproto 没有内置的浮点数类型。因为我觉得大多数项目中,传输浮点数的必要性都不大。如果必须传输的话,可以用字符串,或二进制串来替代。由传输的双方来共同约定保证一致性。

但是,有时候传输小数的需求依然存在。我认为定点数可以是一种选择,而且不必破坏原有的协议,也不必增加新的类型。只需要在 sproto 的协议描述中加一些备注。

之前给 sproto 加字典支持就是靠备注实现的

这次我选择在 integer 类型后面加一个备注,比如写 integer(2) 就表示,这个整数类型是一个有两位十进制小数位的定点数。两端在解析同样的协议时,需要在编码的时候 * 100 取整再编码,解码的时候 /100 还原。

如果有一端使用老版本的实现,那么可以在跟上层自行乘除 100 来兼容。

第二,sproto 用 string 类型来传输字符串和二进制串。它可以自然对应到 lua 中的 string 类型。但是,很多其它语言中,可阅读的 string 和 binary 串是两种不同的类型。其 string 类型需要额外指定编码。

sproto 缺乏 binary 串的描述,会在 python c# 等语言中遇到一些麻烦。

考虑之后,我决定给 sproto 增加 binary 类型,但为了兼容,把它作为 string 的一个子类型实现。在新版的 sproto 中,你可以在协议文件中定义 binary 字段。但编码时,还是按 string 编码的,不会破坏兼容性。

但新版本的代码在解码的时候,会根据协议定义,正确的通知 binding 层,这是一个 binary 串。

]]>
sproto是我设计的一个类 google protocol buffers 的东西。

在很多年前,我在我经手的一些项目中使用 google protocol buffers 。用了好几年,经历了几个项目后,我感觉到它其实是为静态编译型语言设计的协议,其实并没有脱离语言的普适性。在动态语言中,大家都不太愿意使用它(json 更为流行)。一个很大的原因是,protobuffers 是基于代码生成工作的,如果你不使用代码生成,那么它自身的 bootstrap 就非常难实现。

因为它的协议本身是用自身描述的,如果你要解析协议,必须先有解析自己的能力。这是个先有鸡还是先有蛋的矛盾。过去很多动态语言的 binding 都逃不掉引入负责的 C++ 库再加上一部分动态代码生成。我对这点很不爽,后来重头实现了pbc这个库。虽然它还有一些问题,并且我不再想维护它,这个库加上 lua 的 binding 依然是 lua 中使用 protobuffer 的首选。

protobuffer 自身有很长的历史,历史带来了包袱,实际上在我放弃 protobuffer 后不久,google 也大刀阔斧的更新了 3.0 版,对之前 2.x 版做了很多必要的,不兼容的改进。

我仿照 protobuffer 的基本理念设计了 sproto ,设计上的考虑在之前的 blog 中已经写了很多。

sproto 的设计理念之一:保证基本能用的基础上足够简单,不增加太多特性。如果要增加新特性,都是仔细考虑过,并保持不破坏兼容性。最近一段时间,我给 sproto 加了两个特性,这里简单介绍一下:

其一,sproto 没有内置的浮点数类型。因为我觉得大多数项目中,传输浮点数的必要性都不大。如果必须传输的话,可以用字符串,或二进制串来替代。由传输的双方来共同约定保证一致性。

但是,有时候传输小数的需求依然存在。我认为定点数可以是一种选择,而且不必破坏原有的协议,也不必增加新的类型。只需要在 sproto 的协议描述中加一些备注。

之前给 sproto 加字典支持就是靠备注实现的

这次我选择在 integer 类型后面加一个备注,比如写 integer(2) 就表示,这个整数类型是一个有两位十进制小数位的定点数。两端在解析同样的协议时,需要在编码的时候 * 100 取整再编码,解码的时候 /100 还原。

如果有一端使用老版本的实现,那么可以在跟上层自行乘除 100 来兼容。

第二,sproto 用 string 类型来传输字符串和二进制串。它可以自然对应到 lua 中的 string 类型。但是,很多其它语言中,可阅读的 string 和 binary 串是两种不同的类型。其 string 类型需要额外指定编码。

sproto 缺乏 binary 串的描述,会在 python c# 等语言中遇到一些麻烦。

考虑之后,我决定给 sproto 增加 binary 类型,但为了兼容,把它作为 string 的一个子类型实现。在新版的 sproto 中,你可以在协议文件中定义 binary 字段。但编码时,还是按 string 编码的,不会破坏兼容性。

但新版本的代码在解码的时候,会根据协议定义,正确的通知 binding 层,这是一个 binary 串。

]]>
0
<![CDATA[Why 370Z]]> http://www.udpwork.com/item/16177.html http://www.udpwork.com/item/16177.html#reviews Tue, 14 Mar 2017 18:30:00 +0800 IT牛人.117 http://www.udpwork.com/item/16177.html TL;DR

I am going to buy a manual Nissan 370Z.

Why 370Z?

  • Engine: VQ37VHR 3.7L V6 245kW/363Nm (reline @ 7500rpm). The best NA V6 engine, I lost count on how many times it has won Ward’s best engine. In addition, I have never owned a V6 before -_-z
  • Transmission: 6 Speed close-ratio Manual with SynchroRev Match (auto-blip) which can be turned off with a button, yes!
  • RWD with Viscous Limited Slip Differential (VLSD)
  • Driveshaft: Carbon-fibre composite (although kerb weight is still 1468kg)
  • Steering: Hydraulic power steering (Vehicle speed sensitive)
  • Exterior design, sexy ;-)
  • Room for modification (doesn’t necessarily mean I’ll go down that path ;-)
  • Price (A$25k - 30k for 2011-2015 1 owner pre-owned, bang for the buck)
  • Relatively good reliability, not expensive to own and keep for 3-5 years as weekend car.

Known issue : my mechanic friend told me the worst thing can happen to 370Z is that if the clutch breaks, the flywheel needs to be replaced as well due to its design. Parts and labour can cost A$3k, not too bad if you’ve owned a warranty expired BMW before ;-) Plus it may NOT happen during the lifetime of the car (depending on if the previous owner knows how to drive a stick correctly).

Thoughts on other cars : BMW M2 and Focus RS has been so hot and definitely draw too much attention, the direct result is high demand and low allocation for Australia, in turn heavily overpriced. I’ve test driven both MX-5 and 86, they are different cars and have unique characters, both are very good look cheap -_-z

I am a fan of the legendary Godzilla GT-R. Nissan has done the 370Z right, even automatic models have column-mounted paddle shifters - bingo! Who the hell designed the paddles on the bloody steering wheel?

UPDATE : The famousZ car line may end with current 370Z.

]]>
TL;DR

I am going to buy a manual Nissan 370Z.

Why 370Z?

  • Engine: VQ37VHR 3.7L V6 245kW/363Nm (reline @ 7500rpm). The best NA V6 engine, I lost count on how many times it has won Ward’s best engine. In addition, I have never owned a V6 before -_-z
  • Transmission: 6 Speed close-ratio Manual with SynchroRev Match (auto-blip) which can be turned off with a button, yes!
  • RWD with Viscous Limited Slip Differential (VLSD)
  • Driveshaft: Carbon-fibre composite (although kerb weight is still 1468kg)
  • Steering: Hydraulic power steering (Vehicle speed sensitive)
  • Exterior design, sexy ;-)
  • Room for modification (doesn’t necessarily mean I’ll go down that path ;-)
  • Price (A$25k - 30k for 2011-2015 1 owner pre-owned, bang for the buck)
  • Relatively good reliability, not expensive to own and keep for 3-5 years as weekend car.

Known issue : my mechanic friend told me the worst thing can happen to 370Z is that if the clutch breaks, the flywheel needs to be replaced as well due to its design. Parts and labour can cost A$3k, not too bad if you’ve owned a warranty expired BMW before ;-) Plus it may NOT happen during the lifetime of the car (depending on if the previous owner knows how to drive a stick correctly).

Thoughts on other cars : BMW M2 and Focus RS has been so hot and definitely draw too much attention, the direct result is high demand and low allocation for Australia, in turn heavily overpriced. I’ve test driven both MX-5 and 86, they are different cars and have unique characters, both are very good look cheap -_-z

I am a fan of the legendary Godzilla GT-R. Nissan has done the 370Z right, even automatic models have column-mounted paddle shifters - bingo! Who the hell designed the paddles on the bloody steering wheel?

UPDATE : The famousZ car line may end with current 370Z.

]]>
0
<![CDATA[Android中一个简单有用的发现性能问题的方法]]> http://www.udpwork.com/item/16175.html http://www.udpwork.com/item/16175.html#reviews Mon, 13 Mar 2017 21:57:00 +0800 技术小黑屋 http://www.udpwork.com/item/16175.html 在Android中,性能优化是我们持之不懈的工作。这其中,在主线程执行耗时的任务,可能会导致界面卡顿,甚至是ANR(程序未响应)。当然Android提供了很多优秀的工具,比如StrictMode,Method Tracing等,便于我们检测问题。

这里,本文将介绍一个更加简单有效的方法。相比StrictMode来说更加便于发现问题,相比Method Tracing来说更加容易操作。

首先,我们有这样一个程序代码

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
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    writeContentToFile();
}

private void writeContentToFile() {
    File log = new File(Environment.getExternalStorageDirectory(), "Log.txt");
    Writer outWriter = null;
    try {
        outWriter = new BufferedWriter(new FileWriter(log.getAbsolutePath(), false));
        outWriter.write(new Date().toString());
        outWriter.write(" : \n");
        outWriter.flush();
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        if (outWriter != null) {
            try {
                outWriter.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

上面的代码需要优化,因为

  • writeContentToFile 是一个本地文件写操作,比较耗时
  • 而writeContentToFile 这个方法却放在了主线程中,必然会阻塞主线程其他的工作顺利执行。

上面介绍StrictMode和Method Traing都可以检测这个问题,这里我们我们用一个更简单的方法

1
2
3
4
5
6
7
8
public void checkWorkerThread() {
    boolean isMainThread = Looper.myLooper() == Looper.getMainLooper();
    if (isMainThread) {
        if (BuildConfig.DEBUG) {
            throw new RuntimeException("Do not do time-consuming work in the Main thread");
        }
    }
}

这段方法有几点注意的。

  • 主线程判断,使用Looper.myLooper() == Looper.getMainLooper()可以准确判断当前线程是否为主线程。
  • BuildConfig.DEBUG 条件控制,只有在debug环境下抛出异常,给予开发者明显的提示。当然也可以使用自定义的是否抛出异常的逻辑
  • 如果当前线程不是主线程,那么就被认为是工作者线程。

比如上面的方法加入checkWorkerThread检查

1
2
3
4
private void writeContentToFile() {
    checkWorkerThread();
    //代码省略,具体实现参考上面
}

再次执行程序,会曝出异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.droidyue.checkthreadsample/com.droidyue.checkthreadsample.MainActivity}: java.lang.RuntimeException: Do not do time-consuming work in the Main thread
          at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2664)
          at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2733)
          at android.app.ActivityThread.access$900(ActivityThread.java:187)
          at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1584)
          at android.os.Handler.dispatchMessage(Handler.java:111)
          at android.os.Looper.loop(Looper.java:194)
          at android.app.ActivityThread.main(ActivityThread.java:5869)
          at java.lang.reflect.Method.invoke(Native Method)
          at java.lang.reflect.Method.invoke(Method.java:372)
          at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1019)
          at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:814)
 Caused by: java.lang.RuntimeException: Do not do time-consuming work in the Main thread
          at com.droidyue.checkthreadsample.MainActivity.checkWorkerThread(MainActivity.java:34)
          at com.droidyue.checkthreadsample.MainActivity.writeContentToFile(MainActivity.java:40)
          at com.droidyue.checkthreadsample.MainActivity.onCreate(MainActivity.java:27)
          at android.app.Activity.performCreate(Activity.java:6127)
          at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1123)
          at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2617)
          ... 10 more

通过分析crash stacktrace 我们可以很轻松的发现问题的根源并解决。

哪些方法需要加上检查

  • 本地IO读写
  • 网络操作
  • Bitmap相关的缩放等
  • 其他耗时的任务

如何选择工作者线程

Android中的工作者线程API有很多,简单的有Thread,AsyncTask,也有ThreadPool,HandlerThread等。关于如何选择,可以参考这篇文章。关于Android中工作者线程的思考

对比

  • StrictMode 是一把利器,但是检测的东西很多,打印出来的日志可能也有很多,查找定位问题可能不如文章的方法方便。
  • Method Tracing,需要刻意并时不时进行设置start和stop操作,文章的方法,可以说是一劳永逸。

检测会不会有性能问题

  • 理论上是不会的,通常这个检测的代价要远远比耗时任务要小很多。
  • 如果想进一步优化的,可以在编译期屏蔽这个方法的调用,即assumenosideeffects,具体可以参考关于Android Log的一些思考中的编译期屏蔽 的内容。

延伸阅读

当你刚刚写完一个方法时,考虑这一下这个方法会不会很耗时,如果耗时,不妨增加一个线程的check。注意,一定要加载debug版,不要影响到线上的用户。

]]>
在Android中,性能优化是我们持之不懈的工作。这其中,在主线程执行耗时的任务,可能会导致界面卡顿,甚至是ANR(程序未响应)。当然Android提供了很多优秀的工具,比如StrictMode,Method Tracing等,便于我们检测问题。

这里,本文将介绍一个更加简单有效的方法。相比StrictMode来说更加便于发现问题,相比Method Tracing来说更加容易操作。

首先,我们有这样一个程序代码

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
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    writeContentToFile();
}

private void writeContentToFile() {
    File log = new File(Environment.getExternalStorageDirectory(), "Log.txt");
    Writer outWriter = null;
    try {
        outWriter = new BufferedWriter(new FileWriter(log.getAbsolutePath(), false));
        outWriter.write(new Date().toString());
        outWriter.write(" : \n");
        outWriter.flush();
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        if (outWriter != null) {
            try {
                outWriter.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

上面的代码需要优化,因为

  • writeContentToFile 是一个本地文件写操作,比较耗时
  • 而writeContentToFile 这个方法却放在了主线程中,必然会阻塞主线程其他的工作顺利执行。

上面介绍StrictMode和Method Traing都可以检测这个问题,这里我们我们用一个更简单的方法

1
2
3
4
5
6
7
8
public void checkWorkerThread() {
    boolean isMainThread = Looper.myLooper() == Looper.getMainLooper();
    if (isMainThread) {
        if (BuildConfig.DEBUG) {
            throw new RuntimeException("Do not do time-consuming work in the Main thread");
        }
    }
}

这段方法有几点注意的。

  • 主线程判断,使用Looper.myLooper() == Looper.getMainLooper()可以准确判断当前线程是否为主线程。
  • BuildConfig.DEBUG 条件控制,只有在debug环境下抛出异常,给予开发者明显的提示。当然也可以使用自定义的是否抛出异常的逻辑
  • 如果当前线程不是主线程,那么就被认为是工作者线程。

比如上面的方法加入checkWorkerThread检查

1
2
3
4
private void writeContentToFile() {
    checkWorkerThread();
    //代码省略,具体实现参考上面
}

再次执行程序,会曝出异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.droidyue.checkthreadsample/com.droidyue.checkthreadsample.MainActivity}: java.lang.RuntimeException: Do not do time-consuming work in the Main thread
          at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2664)
          at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2733)
          at android.app.ActivityThread.access$900(ActivityThread.java:187)
          at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1584)
          at android.os.Handler.dispatchMessage(Handler.java:111)
          at android.os.Looper.loop(Looper.java:194)
          at android.app.ActivityThread.main(ActivityThread.java:5869)
          at java.lang.reflect.Method.invoke(Native Method)
          at java.lang.reflect.Method.invoke(Method.java:372)
          at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1019)
          at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:814)
 Caused by: java.lang.RuntimeException: Do not do time-consuming work in the Main thread
          at com.droidyue.checkthreadsample.MainActivity.checkWorkerThread(MainActivity.java:34)
          at com.droidyue.checkthreadsample.MainActivity.writeContentToFile(MainActivity.java:40)
          at com.droidyue.checkthreadsample.MainActivity.onCreate(MainActivity.java:27)
          at android.app.Activity.performCreate(Activity.java:6127)
          at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1123)
          at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2617)
          ... 10 more

通过分析crash stacktrace 我们可以很轻松的发现问题的根源并解决。

哪些方法需要加上检查

  • 本地IO读写
  • 网络操作
  • Bitmap相关的缩放等
  • 其他耗时的任务

如何选择工作者线程

Android中的工作者线程API有很多,简单的有Thread,AsyncTask,也有ThreadPool,HandlerThread等。关于如何选择,可以参考这篇文章。关于Android中工作者线程的思考

对比

  • StrictMode 是一把利器,但是检测的东西很多,打印出来的日志可能也有很多,查找定位问题可能不如文章的方法方便。
  • Method Tracing,需要刻意并时不时进行设置start和stop操作,文章的方法,可以说是一劳永逸。

检测会不会有性能问题

  • 理论上是不会的,通常这个检测的代价要远远比耗时任务要小很多。
  • 如果想进一步优化的,可以在编译期屏蔽这个方法的调用,即assumenosideeffects,具体可以参考关于Android Log的一些思考中的编译期屏蔽 的内容。

延伸阅读

当你刚刚写完一个方法时,考虑这一下这个方法会不会很耗时,如果耗时,不妨增加一个线程的check。注意,一定要加载debug版,不要影响到线上的用户。

]]>
0
<![CDATA[iOS App 签名的原理]]> http://www.udpwork.com/item/16174.html http://www.udpwork.com/item/16174.html#reviews Mon, 13 Mar 2017 20:46:39 +0800 bang http://www.udpwork.com/item/16174.html iOS 签名机制挺复杂,各种证书,Provisioning Profile,entitlements,CertificateSigningRequest,p12,AppID,概念一堆,也很容易出错,本文尝试从原理出发,一步步推出为什么会有这么多概念,希望能有助于理解 iOS App 签名的原理和流程。

目的

先来看看苹果的签名机制是为了做什么。在 iOS 出来之前,在主流操作系统(Mac/Windows/Linux)上开发和运行软件是不需要签名的,软件随便从哪里下载都能运行,导致平台对第三方软件难以控制,盗版流行。苹果希望解决这样的问题,在 iOS 平台对第三方 APP 有绝对的控制权,一定要保证每一个安装到 iOS 上的 APP 都是经过苹果官方允许的,怎样保证呢?就是通过签名机制。

非对称加密

通常我们说的签名就是数字签名,它是基于非对称加密算法实现的。对称加密是通过同一份密钥加密和解密数据,而非对称加密则有两份密钥,分别是公钥和私钥,用公钥加密的数据,要用私钥才能解密,用私钥加密的数据,要用公钥才能解密。

简单说一下常用的非对称加密算法 RSA 的数学原理,理解简单的数学原理,就可以理解非对称加密是怎么做到的,为什么会是安全的:

  1. 选两个质数 p 和 q,相乘得出一个大整数n,例如 p = 61,q = 53,n = pq = 3233
  2. 选 1-n 间的随便一个质数e,例如 e = 17
  3. 经过一系列数学公式,算出一个数字 d,满足:
    a.通过 n 和 e 这两个数据一组数据进行数学运算后,可以通过 n 和 d 去反解运算,反过来也可以。
    b.如果只知道 n 和 e,要推导出 d,需要知道 p 和 q,也就是要需要把 n 因数分解。

上述的 (n,e) 这两个数据在一起就是公钥,(n,d) 这两个数据就是私钥,满足用私钥加密,公钥解密,或反过来公钥加密,私钥解密,也满足在只暴露公钥 (只知道 n 和 e)的情况下,要推导出私钥 (n,d),需要把大整数 n 因数分解。目前因数分解只能靠暴力穷举,而 n 数字越大,越难以用穷举计算出因数 p 和 q,也就越安全,当 n 大到二进制 1024 位或 2048 位时,以目前技术要破解几乎不可能,所以非常安全。

若对数字 d 是怎样计算出来的感兴趣,可以详读这两篇文章:RSA 算法原理(一)(二)

数字签名

现在知道了有非对称加密这东西,那数字签名是怎么回事呢?

数字签名的作用是我对某一份数据打个标记,表示我认可了这份数据(签了个名),然后我发送给其他人,其他人可以知道这份数据是经过我认证的,数据没有被篡改过。

有了上述非对称加密算法,就可以实现这个需求:

sign0

  1. 首先用一种算法,算出原始数据的摘要。需满足 a.若原始数据有任何变化,计算出来的摘要值都会变化。 b.摘要要够短。这里最常用的算法是MD5。
  2. 生成一份非对称加密的公钥和私钥,私钥我自己拿着,公钥公布出去。
  3. 对一份数据,算出摘要后,用私钥加密这个摘要,得到一份加密后的数据,称为原始数据的签名。把它跟原始数据一起发送给用户。
  4. 用户收到数据和签名后,用公钥解密得到摘要。同时用户用同样的算法计算原始数据的摘要,对比这里计算出来的摘要和用公钥解密签名得到的摘要是否相等,若相等则表示这份数据中途没有被篡改过,因为如果篡改过,摘要会变化。

之所以要有第一步计算摘要,是因为非对称加密的原理限制可加密的内容不能太大(不能大于上述 n 的位数,也就是一般不能大于 1024 位 / 2048 位),于是若要对任意大的数据签名,就需要改成对它的特征值签名,效果是一样的。

好了,有了非对称加密的基础,知道了数字签名是什么,怎样可以保证一份数据是经过某个地方认证的,来看看怎样通过数字签名的机制保证每一个安装到 iOS 上的 APP 都是经过苹果认证允许的。

最简单的签名

要实现这个需求很简单,最直接的方式,苹果官方生成一对公私钥,在 iOS 里内置一个公钥,私钥由苹果后台保存,我们传 App 上 AppStore 时,苹果后台用私钥对 APP 数据进行签名,iOS 系统下载这个 APP 后,用公钥验证这个签名,若签名正确,这个 APP 肯定是由苹果后台认证的,并且没有被修改过,也就达到了苹果的需求:保证安装的每一个 APP 都是经过苹果官方允许的。

sign1

如果我们 iOS 设备安装 APP 只有从 AppStore 下载这一种方式的话,这件事就结束了,没有任何复杂的东西,只有一个数字签名,非常简单地解决问题。

但实际上因为除了从 AppStore 下载,我们还可以有三种方式安装一个 App:

  1. 开发 App 时可以直接把开发中的应用安装进手机进行调试。
  2. In-House 企业内部分发,可以直接安装企业证书签名后的 APP。
  3. AD-Hoc 相当于企业分发的限制版,限制安装设备数量,较少用。

苹果要对用这三种方式安装的 App 进行控制,就有了新的需求,无法像上面这样简单了。

新的需求

我们先来看第一个,开发时安装APP,它有两个个需求:

  1. 安装包不需要传到苹果服务器,可以直接安装到手机上。如果你编译一个 APP 到手机前要先传到苹果服务器签名,这显然是不能接受的。
  2. 苹果必须对这里的安装有控制权,包括
    a. 经过苹果允许才可以这样安装。
    b. 不能被滥用导致非开发app也能被安装。

为了实现这些需求,iOS 签名的复杂度也就开始增加了。

苹果这里给出的方案是使用了双层签名,会比较绕,流程大概是这样的:

sign2

  1. 在你的 Mac 开发机器生成一对公私钥,这里称为公钥L私钥L 。L:Local
  2. 苹果自己有固定的一对公私钥,跟上面 AppStore 例子一样,私钥在苹果后台,公钥在每个 iOS 设备上。这里称为公钥A私钥A 。A:Apple
  3. 把公钥 L 传到苹果后台,用苹果后台里的私钥 A 去签名公钥 L。得到一份数据包含了公钥 L 以及其签名,把这份数据称为证书。
  4. 在开发时,编译完一个 APP 后,用本地的私钥 L 对这个 APP 进行签名,同时把第三步得到的证书一起打包进 APP 里,安装到手机上。
  5. 在安装时,iOS 系统取得证书,通过系统内置的公钥 A,去验证证书的数字签名是否正确。
  6. 验证证书后确保了公钥 L 是苹果认证过的,再用公钥 L 去验证 APP 的签名,这里就间接验证了这个 APP 安装行为是否经过苹果官方允许。(这里只验证安装行为,不验证APP 是否被改动,因为开发阶段 APP 内容总是不断变化的,苹果不需要管。)

加点东西

上述流程只解决了上面第一个需求,也就是需要经过苹果允许才可以安装,还未解决第二个避免被滥用的问题。怎么解决呢?苹果再加了两个限制,一是限制在苹果后台注册过的设备才可以安装,二是限制签名只能针对某一个具体的 APP。

怎么加的?在上述第三步,苹果用私钥 A 签名我们本地公钥 L 时,实际上除了签名公钥 L,还可以加上无限多数据,这些数据都可以保证是经过苹果官方认证的,不会有被篡改的可能。

sign3

可以想到把 允许安装的设备 ID 列表 和 App对应的 AppID 等数据,都在第三步这里跟公钥L一起组成证书,再用苹果私钥 A 对这个证书签名。在最后第 5 步验证时就可以拿到设备 ID 列表,判断当前设备是否符合要求。根据数字签名的原理,只要数字签名通过验证,第 5 步这里的设备 IDs / AppID / 公钥 L 就都是经过苹果认证的,无法被修改,苹果就可以限制可安装的设备和 APP,避免滥用。

最终流程

到这里这个证书已经变得很复杂了,有很多额外信息,实际上除了 设备 ID / AppID,还有其他信息也需要在这里用苹果签名,像这个 APP 里 iCloud / push / 后台运行 等权限苹果都想控制,苹果把这些权限开关统一称为 Entitlements,它也需要通过签名去授权。

实际上一个“证书”本来就有规定的格式规范,上面我们把各种额外信息塞入证书里是不合适的,于是苹果另外搞了个东西,叫 Provisioning Profile,一个 Provisioning Profile 里就包含了证书以及上述提到的所有额外信息,以及所有信息的签名。

所以整个流程稍微变一下,就变成这样了:

sign4

因为步骤有小变动,这里我们不辞啰嗦重新再列一遍整个流程:

  1. 在你的 Mac 开发机器生成一对公私钥,这里称为公钥L,私钥L。L:Local
  2. 苹果自己有固定的一对公私钥,跟上面 AppStore 例子一样,私钥在苹果后台,公钥在每个 iOS 设备上。这里称为公钥A,私钥A。A:Apple
  3. 把公钥 L 传到苹果后台,用苹果后台里的私钥 A 去签名公钥 L。得到一份数据包含了公钥 L 以及其签名,把这份数据称为证书。
  4. 在苹果后台申请 AppID,配置好设备 ID 列表和 APP 可使用的权限,再加上第③步的证书,组成的数据用私钥 A 签名,把数据和签名一起组成一个 Provisioning Profile 文件,下载到本地 Mac 开发机。
  5. 在开发时,编译完一个 APP 后,用本地的私钥 L 对这个 APP 进行签名,同时把第④步得到的 Provisioning Profile 文件打包进 APP 里,文件名为 embedded.mobileprovision,把 APP 安装到手机上。
  6. 在安装时,iOS 系统取得证书,通过系统内置的公钥 A,去验证 embedded.mobileprovision 的数字签名是否正确,里面的证书签名也会再验一遍。
  7. 确保了 embedded.mobileprovision 里的数据都是苹果授权以后,就可以取出里面的数据,做各种验证,包括用公钥 L 验证APP签名,验证设备 ID 是否在 ID 列表上,AppID 是否对应得上,权限开关是否跟 APP 里的 Entitlements 对应等。

开发者证书从签名到认证最终苹果采用的流程大致是这样,还有一些细节像证书有效期/证书类型等就不细说了。

概念和操作

上面的步骤对应到我们平常具体的操作和概念是这样的:

  1. 第 1 步对应的是 keychain 里的 “从证书颁发机构请求证书”,这里就本地生成了一对公私钥,保存的 CertificateSigningRequest 就是公钥,私钥保存在本地电脑里。
  2. 第 2 步苹果处理,不用管。
  3. 第 3 步对应把 CertificateSigningRequest 传到苹果后台生成证书,并下载到本地。这时本地有两个证书,一个是第 1 步生成的,一个是这里下载回来的,keychain 会把这两个证书关联起来,因为他们公私钥是对应的,在XCode选择下载回来的证书时,实际上会找到 keychain 里对应的私钥去签名。这里私钥只有生成它的这台 Mac 有,如果别的 Mac 也要编译签名这个 App 怎么办?答案是把私钥导出给其他 Mac 用,在 keychain 里导出私钥,就会存成 .p12 文件,其他 Mac 打开后就导入了这个私钥。
  4. 第 4 步都是在苹果网站上操作,配置 AppID / 权限 / 设备等,最后下载 Provisioning Profile 文件。
  5. 第 5 步 XCode 会通过第 3 步下载回来的证书(存着公钥),在本地找到对应的私钥(第一步生成的),用本地私钥去签名 App,并把 Provisioning Profile 文件命名为 embedded.mobileprovision 一起打包进去。这里对 App 的签名数据保存分两部分,Mach-O 可执行文件会把签名直接写入这个文件里,其他资源文件则会保存在 _CodeSignature 目录下。

第 6 – 7 步的打包和验证都是 Xcode 和 iOS 系统自动做的事。

这里再总结一下这些概念:

  1. 证书 :内容是公钥或私钥,由其他机构对其签名组成的数据包。
  2. Entitlements :包含了 App 权限开关列表。
  3. CertificateSigningRequest :本地公钥。
  4. p12 :本地私钥,可以导入到其他电脑。
  5. Provisioning Profile :包含了 证书 / Entitlements 等数据,并由苹果后台私钥签名的数据包。

其他发布方式

前面以开发包为例子说了签名和验证的流程,另外两种方式 In-House 企业签名和 AD-Hoc 流程也是差不多的,只是企业签名不限制安装的设备数,另外需要用户在 iOS 系统设置上手动点击信任这个企业才能通过验证。

而 AppStore 的签名验证方式有些不一样,前面我们说到最简单的签名方式,苹果在后台直接用私钥签名 App 就可以了,实际上苹果确实是这样做的,如果去下载一个 AppStore 的安装包,会发现它里面是没有 embedded.mobileprovision 文件的,也就是它安装和启动的流程是不依赖这个文件,验证流程也就跟上述几种类型不一样了。

据猜测,因为上传到 AppStore 的包苹果会重新对内容加密,原来的本地私钥签名就没有用了,需要重新签名,从 AppStore 下载的包苹果也并不打算控制它的有效期,不需要内置一个 embedded.mobileprovision 去做校验,直接在苹果用后台的私钥重新签名,iOS 安装时用本地公钥验证 App 签名就可以了。

那为什么发布 AppStore 的包还是要跟开发版一样搞各种证书和 Provisioning Profile?猜测因为苹果想做统一管理,Provisioning Profile 里包含一些权限控制,AppID 的检验等,苹果不想在上传 AppStore 包时重新用另一种协议做一遍这些验证,就不如统一把这部分放在 Provisioning Profile 里,上传 AppStore 时只要用同样的流程验证这个 Provisioning Profile 是否合法就可以了。

所以 App 上传到 AppStore 后,就跟你的 证书 / Provisioning Profile 都没有关系了,无论他们是否过期或被废除,都不会影响 AppStore 上的安装包。

到这里 iOS 签名机制的原理和主流程大致说完了,希望能对理解苹果签名和排查日常签名问题有所帮助。

P.S.一些疑问

最后这里再提一下我关于签名流程的一些的疑问。

企业证书

企业证书签名因为限制少,在国内被广泛用于测试和盗版,fir.im / 蒲公英等测试平台都是通过企业证书分发,国内一些市场像 PP 助手,爱思助手,一部分安装手段也是通过企业证书重签名。通过企业证书签名安装的 App,启动时都会验证证书的有效期,并且不定期请求苹果服务器看证书是否被吊销,若已过期或被吊销,就会无法启动 App。对于这种助手的盗版安装手段,苹果想打击只能一个个吊销企业证书,并没有太好的办法。

这里我的疑问是,苹果做了那么多签名和验证机制去限制在 iOS 安装 App,为什么又要出这样一个限制很少的方式让盗版钻空子呢?若真的是企业用途不适合上 AppStore,也完全可以在 AppStore 开辟一个小的私密版块,还是通过 AppStore 去安装,就不会有这个问题了。

AppStore 加密

另一个问题是我们把 App 传上 AppStore 后,苹果会对 App 进行加密,导致 App 体积增大不少,这个加密实际上是没卵用的,只是让破解的人要多做一个步骤,运行 App 去内存 dump 出可执行文件而已,无论怎样加密,都可以用这种方式拿出加密前的可执行文件。所以为什么要做这样的加密呢?想不到有什么好处。

本地私钥

我们看到前面说的签名流程很绕很复杂,经常出现各种问题,像有 Provisioning Profile 文件但证书又不对,本地有公钥证书没对应私钥等情况,不理解原理的情况下会被绕晕,我的疑问是,这里为什么不能简化呢?还是以开发证书为例,为什么一定要用本地 Mac 生成的私钥去签名?苹果要的只是本地签名,私钥不一定是要本地生成的,苹果也可以自己生成一对公私钥给我们,放在 Provisioning Profile 里,我们用里面的私钥去加密就行了,这样就不会有 CertificateSigningRequest 和 p12 的概念,跟本地 keychain 没有关系,不需要关心证书,只要有 Provisioning Profile 就能签名,流程会减少,易用性会提高很多,同时苹果想要的控制一点都不会少,也没有什么安全问题,为什么不这样设计呢?

能想到的一个原因是 Provisioning Profile 在非 AppStore 安装时会打包进安装包,第三方拿到这个 Provisioning Profile 文件就能直接用起来给他自己的 App 签名了。但这种问题也挺好解决,只需要打包时去掉文件里的私钥就行了,所以仍不明白为什么这样设计。

]]>
iOS 签名机制挺复杂,各种证书,Provisioning Profile,entitlements,CertificateSigningRequest,p12,AppID,概念一堆,也很容易出错,本文尝试从原理出发,一步步推出为什么会有这么多概念,希望能有助于理解 iOS App 签名的原理和流程。

目的

先来看看苹果的签名机制是为了做什么。在 iOS 出来之前,在主流操作系统(Mac/Windows/Linux)上开发和运行软件是不需要签名的,软件随便从哪里下载都能运行,导致平台对第三方软件难以控制,盗版流行。苹果希望解决这样的问题,在 iOS 平台对第三方 APP 有绝对的控制权,一定要保证每一个安装到 iOS 上的 APP 都是经过苹果官方允许的,怎样保证呢?就是通过签名机制。

非对称加密

通常我们说的签名就是数字签名,它是基于非对称加密算法实现的。对称加密是通过同一份密钥加密和解密数据,而非对称加密则有两份密钥,分别是公钥和私钥,用公钥加密的数据,要用私钥才能解密,用私钥加密的数据,要用公钥才能解密。

简单说一下常用的非对称加密算法 RSA 的数学原理,理解简单的数学原理,就可以理解非对称加密是怎么做到的,为什么会是安全的:

  1. 选两个质数 p 和 q,相乘得出一个大整数n,例如 p = 61,q = 53,n = pq = 3233
  2. 选 1-n 间的随便一个质数e,例如 e = 17
  3. 经过一系列数学公式,算出一个数字 d,满足:
    a.通过 n 和 e 这两个数据一组数据进行数学运算后,可以通过 n 和 d 去反解运算,反过来也可以。
    b.如果只知道 n 和 e,要推导出 d,需要知道 p 和 q,也就是要需要把 n 因数分解。

上述的 (n,e) 这两个数据在一起就是公钥,(n,d) 这两个数据就是私钥,满足用私钥加密,公钥解密,或反过来公钥加密,私钥解密,也满足在只暴露公钥 (只知道 n 和 e)的情况下,要推导出私钥 (n,d),需要把大整数 n 因数分解。目前因数分解只能靠暴力穷举,而 n 数字越大,越难以用穷举计算出因数 p 和 q,也就越安全,当 n 大到二进制 1024 位或 2048 位时,以目前技术要破解几乎不可能,所以非常安全。

若对数字 d 是怎样计算出来的感兴趣,可以详读这两篇文章:RSA 算法原理(一)(二)

数字签名

现在知道了有非对称加密这东西,那数字签名是怎么回事呢?

数字签名的作用是我对某一份数据打个标记,表示我认可了这份数据(签了个名),然后我发送给其他人,其他人可以知道这份数据是经过我认证的,数据没有被篡改过。

有了上述非对称加密算法,就可以实现这个需求:

sign0

  1. 首先用一种算法,算出原始数据的摘要。需满足 a.若原始数据有任何变化,计算出来的摘要值都会变化。 b.摘要要够短。这里最常用的算法是MD5。
  2. 生成一份非对称加密的公钥和私钥,私钥我自己拿着,公钥公布出去。
  3. 对一份数据,算出摘要后,用私钥加密这个摘要,得到一份加密后的数据,称为原始数据的签名。把它跟原始数据一起发送给用户。
  4. 用户收到数据和签名后,用公钥解密得到摘要。同时用户用同样的算法计算原始数据的摘要,对比这里计算出来的摘要和用公钥解密签名得到的摘要是否相等,若相等则表示这份数据中途没有被篡改过,因为如果篡改过,摘要会变化。

之所以要有第一步计算摘要,是因为非对称加密的原理限制可加密的内容不能太大(不能大于上述 n 的位数,也就是一般不能大于 1024 位 / 2048 位),于是若要对任意大的数据签名,就需要改成对它的特征值签名,效果是一样的。

好了,有了非对称加密的基础,知道了数字签名是什么,怎样可以保证一份数据是经过某个地方认证的,来看看怎样通过数字签名的机制保证每一个安装到 iOS 上的 APP 都是经过苹果认证允许的。

最简单的签名

要实现这个需求很简单,最直接的方式,苹果官方生成一对公私钥,在 iOS 里内置一个公钥,私钥由苹果后台保存,我们传 App 上 AppStore 时,苹果后台用私钥对 APP 数据进行签名,iOS 系统下载这个 APP 后,用公钥验证这个签名,若签名正确,这个 APP 肯定是由苹果后台认证的,并且没有被修改过,也就达到了苹果的需求:保证安装的每一个 APP 都是经过苹果官方允许的。

sign1

如果我们 iOS 设备安装 APP 只有从 AppStore 下载这一种方式的话,这件事就结束了,没有任何复杂的东西,只有一个数字签名,非常简单地解决问题。

但实际上因为除了从 AppStore 下载,我们还可以有三种方式安装一个 App:

  1. 开发 App 时可以直接把开发中的应用安装进手机进行调试。
  2. In-House 企业内部分发,可以直接安装企业证书签名后的 APP。
  3. AD-Hoc 相当于企业分发的限制版,限制安装设备数量,较少用。

苹果要对用这三种方式安装的 App 进行控制,就有了新的需求,无法像上面这样简单了。

新的需求

我们先来看第一个,开发时安装APP,它有两个个需求:

  1. 安装包不需要传到苹果服务器,可以直接安装到手机上。如果你编译一个 APP 到手机前要先传到苹果服务器签名,这显然是不能接受的。
  2. 苹果必须对这里的安装有控制权,包括
    a. 经过苹果允许才可以这样安装。
    b. 不能被滥用导致非开发app也能被安装。

为了实现这些需求,iOS 签名的复杂度也就开始增加了。

苹果这里给出的方案是使用了双层签名,会比较绕,流程大概是这样的:

sign2

  1. 在你的 Mac 开发机器生成一对公私钥,这里称为公钥L私钥L 。L:Local
  2. 苹果自己有固定的一对公私钥,跟上面 AppStore 例子一样,私钥在苹果后台,公钥在每个 iOS 设备上。这里称为公钥A私钥A 。A:Apple
  3. 把公钥 L 传到苹果后台,用苹果后台里的私钥 A 去签名公钥 L。得到一份数据包含了公钥 L 以及其签名,把这份数据称为证书。
  4. 在开发时,编译完一个 APP 后,用本地的私钥 L 对这个 APP 进行签名,同时把第三步得到的证书一起打包进 APP 里,安装到手机上。
  5. 在安装时,iOS 系统取得证书,通过系统内置的公钥 A,去验证证书的数字签名是否正确。
  6. 验证证书后确保了公钥 L 是苹果认证过的,再用公钥 L 去验证 APP 的签名,这里就间接验证了这个 APP 安装行为是否经过苹果官方允许。(这里只验证安装行为,不验证APP 是否被改动,因为开发阶段 APP 内容总是不断变化的,苹果不需要管。)

加点东西

上述流程只解决了上面第一个需求,也就是需要经过苹果允许才可以安装,还未解决第二个避免被滥用的问题。怎么解决呢?苹果再加了两个限制,一是限制在苹果后台注册过的设备才可以安装,二是限制签名只能针对某一个具体的 APP。

怎么加的?在上述第三步,苹果用私钥 A 签名我们本地公钥 L 时,实际上除了签名公钥 L,还可以加上无限多数据,这些数据都可以保证是经过苹果官方认证的,不会有被篡改的可能。

sign3

可以想到把 允许安装的设备 ID 列表 和 App对应的 AppID 等数据,都在第三步这里跟公钥L一起组成证书,再用苹果私钥 A 对这个证书签名。在最后第 5 步验证时就可以拿到设备 ID 列表,判断当前设备是否符合要求。根据数字签名的原理,只要数字签名通过验证,第 5 步这里的设备 IDs / AppID / 公钥 L 就都是经过苹果认证的,无法被修改,苹果就可以限制可安装的设备和 APP,避免滥用。

最终流程

到这里这个证书已经变得很复杂了,有很多额外信息,实际上除了 设备 ID / AppID,还有其他信息也需要在这里用苹果签名,像这个 APP 里 iCloud / push / 后台运行 等权限苹果都想控制,苹果把这些权限开关统一称为 Entitlements,它也需要通过签名去授权。

实际上一个“证书”本来就有规定的格式规范,上面我们把各种额外信息塞入证书里是不合适的,于是苹果另外搞了个东西,叫 Provisioning Profile,一个 Provisioning Profile 里就包含了证书以及上述提到的所有额外信息,以及所有信息的签名。

所以整个流程稍微变一下,就变成这样了:

sign4

因为步骤有小变动,这里我们不辞啰嗦重新再列一遍整个流程:

  1. 在你的 Mac 开发机器生成一对公私钥,这里称为公钥L,私钥L。L:Local
  2. 苹果自己有固定的一对公私钥,跟上面 AppStore 例子一样,私钥在苹果后台,公钥在每个 iOS 设备上。这里称为公钥A,私钥A。A:Apple
  3. 把公钥 L 传到苹果后台,用苹果后台里的私钥 A 去签名公钥 L。得到一份数据包含了公钥 L 以及其签名,把这份数据称为证书。
  4. 在苹果后台申请 AppID,配置好设备 ID 列表和 APP 可使用的权限,再加上第③步的证书,组成的数据用私钥 A 签名,把数据和签名一起组成一个 Provisioning Profile 文件,下载到本地 Mac 开发机。
  5. 在开发时,编译完一个 APP 后,用本地的私钥 L 对这个 APP 进行签名,同时把第④步得到的 Provisioning Profile 文件打包进 APP 里,文件名为 embedded.mobileprovision,把 APP 安装到手机上。
  6. 在安装时,iOS 系统取得证书,通过系统内置的公钥 A,去验证 embedded.mobileprovision 的数字签名是否正确,里面的证书签名也会再验一遍。
  7. 确保了 embedded.mobileprovision 里的数据都是苹果授权以后,就可以取出里面的数据,做各种验证,包括用公钥 L 验证APP签名,验证设备 ID 是否在 ID 列表上,AppID 是否对应得上,权限开关是否跟 APP 里的 Entitlements 对应等。

开发者证书从签名到认证最终苹果采用的流程大致是这样,还有一些细节像证书有效期/证书类型等就不细说了。

概念和操作

上面的步骤对应到我们平常具体的操作和概念是这样的:

  1. 第 1 步对应的是 keychain 里的 “从证书颁发机构请求证书”,这里就本地生成了一对公私钥,保存的 CertificateSigningRequest 就是公钥,私钥保存在本地电脑里。
  2. 第 2 步苹果处理,不用管。
  3. 第 3 步对应把 CertificateSigningRequest 传到苹果后台生成证书,并下载到本地。这时本地有两个证书,一个是第 1 步生成的,一个是这里下载回来的,keychain 会把这两个证书关联起来,因为他们公私钥是对应的,在XCode选择下载回来的证书时,实际上会找到 keychain 里对应的私钥去签名。这里私钥只有生成它的这台 Mac 有,如果别的 Mac 也要编译签名这个 App 怎么办?答案是把私钥导出给其他 Mac 用,在 keychain 里导出私钥,就会存成 .p12 文件,其他 Mac 打开后就导入了这个私钥。
  4. 第 4 步都是在苹果网站上操作,配置 AppID / 权限 / 设备等,最后下载 Provisioning Profile 文件。
  5. 第 5 步 XCode 会通过第 3 步下载回来的证书(存着公钥),在本地找到对应的私钥(第一步生成的),用本地私钥去签名 App,并把 Provisioning Profile 文件命名为 embedded.mobileprovision 一起打包进去。这里对 App 的签名数据保存分两部分,Mach-O 可执行文件会把签名直接写入这个文件里,其他资源文件则会保存在 _CodeSignature 目录下。

第 6 – 7 步的打包和验证都是 Xcode 和 iOS 系统自动做的事。

这里再总结一下这些概念:

  1. 证书 :内容是公钥或私钥,由其他机构对其签名组成的数据包。
  2. Entitlements :包含了 App 权限开关列表。
  3. CertificateSigningRequest :本地公钥。
  4. p12 :本地私钥,可以导入到其他电脑。
  5. Provisioning Profile :包含了 证书 / Entitlements 等数据,并由苹果后台私钥签名的数据包。

其他发布方式

前面以开发包为例子说了签名和验证的流程,另外两种方式 In-House 企业签名和 AD-Hoc 流程也是差不多的,只是企业签名不限制安装的设备数,另外需要用户在 iOS 系统设置上手动点击信任这个企业才能通过验证。

而 AppStore 的签名验证方式有些不一样,前面我们说到最简单的签名方式,苹果在后台直接用私钥签名 App 就可以了,实际上苹果确实是这样做的,如果去下载一个 AppStore 的安装包,会发现它里面是没有 embedded.mobileprovision 文件的,也就是它安装和启动的流程是不依赖这个文件,验证流程也就跟上述几种类型不一样了。

据猜测,因为上传到 AppStore 的包苹果会重新对内容加密,原来的本地私钥签名就没有用了,需要重新签名,从 AppStore 下载的包苹果也并不打算控制它的有效期,不需要内置一个 embedded.mobileprovision 去做校验,直接在苹果用后台的私钥重新签名,iOS 安装时用本地公钥验证 App 签名就可以了。

那为什么发布 AppStore 的包还是要跟开发版一样搞各种证书和 Provisioning Profile?猜测因为苹果想做统一管理,Provisioning Profile 里包含一些权限控制,AppID 的检验等,苹果不想在上传 AppStore 包时重新用另一种协议做一遍这些验证,就不如统一把这部分放在 Provisioning Profile 里,上传 AppStore 时只要用同样的流程验证这个 Provisioning Profile 是否合法就可以了。

所以 App 上传到 AppStore 后,就跟你的 证书 / Provisioning Profile 都没有关系了,无论他们是否过期或被废除,都不会影响 AppStore 上的安装包。

到这里 iOS 签名机制的原理和主流程大致说完了,希望能对理解苹果签名和排查日常签名问题有所帮助。

P.S.一些疑问

最后这里再提一下我关于签名流程的一些的疑问。

企业证书

企业证书签名因为限制少,在国内被广泛用于测试和盗版,fir.im / 蒲公英等测试平台都是通过企业证书分发,国内一些市场像 PP 助手,爱思助手,一部分安装手段也是通过企业证书重签名。通过企业证书签名安装的 App,启动时都会验证证书的有效期,并且不定期请求苹果服务器看证书是否被吊销,若已过期或被吊销,就会无法启动 App。对于这种助手的盗版安装手段,苹果想打击只能一个个吊销企业证书,并没有太好的办法。

这里我的疑问是,苹果做了那么多签名和验证机制去限制在 iOS 安装 App,为什么又要出这样一个限制很少的方式让盗版钻空子呢?若真的是企业用途不适合上 AppStore,也完全可以在 AppStore 开辟一个小的私密版块,还是通过 AppStore 去安装,就不会有这个问题了。

AppStore 加密

另一个问题是我们把 App 传上 AppStore 后,苹果会对 App 进行加密,导致 App 体积增大不少,这个加密实际上是没卵用的,只是让破解的人要多做一个步骤,运行 App 去内存 dump 出可执行文件而已,无论怎样加密,都可以用这种方式拿出加密前的可执行文件。所以为什么要做这样的加密呢?想不到有什么好处。

本地私钥

我们看到前面说的签名流程很绕很复杂,经常出现各种问题,像有 Provisioning Profile 文件但证书又不对,本地有公钥证书没对应私钥等情况,不理解原理的情况下会被绕晕,我的疑问是,这里为什么不能简化呢?还是以开发证书为例,为什么一定要用本地 Mac 生成的私钥去签名?苹果要的只是本地签名,私钥不一定是要本地生成的,苹果也可以自己生成一对公私钥给我们,放在 Provisioning Profile 里,我们用里面的私钥去加密就行了,这样就不会有 CertificateSigningRequest 和 p12 的概念,跟本地 keychain 没有关系,不需要关心证书,只要有 Provisioning Profile 就能签名,流程会减少,易用性会提高很多,同时苹果想要的控制一点都不会少,也没有什么安全问题,为什么不这样设计呢?

能想到的一个原因是 Provisioning Profile 在非 AppStore 安装时会打包进安装包,第三方拿到这个 Provisioning Profile 文件就能直接用起来给他自己的 App 签名了。但这种问题也挺好解决,只需要打包时去掉文件里的私钥就行了,所以仍不明白为什么这样设计。

]]>
0
<![CDATA[Pointfree 编程风格指南]]> http://www.udpwork.com/item/16173.html http://www.udpwork.com/item/16173.html#reviews Mon, 13 Mar 2017 06:56:10 +0800 阮一峰 http://www.udpwork.com/item/16173.html 本文要回答一个很重要的问题:函数式编程有什么用?

目前,主流的编程语言都不是函数式的,已经能够满足需求。为何还要学函数式编程呢,只为了多理解一些新奇的概念?

一个网友说:

"函数式编程有什么优势呢?"

"我感觉,这种写法可能会令人头痛吧。"

很长一段时间,我根本不知道从何入手,如何将它用于实际项目?直到有一天,我学到了 Pointfree 这个概念,顿时豁然开朗,原来应该这样用!

我现在觉得,Pointfree 就是如何使用函数式编程的答案。

一、程序的本质

为了理解 Pointfree,请大家先想一想,什么是程序?

上图是一个编程任务,左侧是数据输入(input),中间是一系列的运算步骤,对数据进行加工,右侧是最后的数据输出(output)。一个或多个这样的任务,就组成了程序。

输入和输出(统称为 I/O)与键盘、屏幕、文件、数据库等相关,这些跟本文无关。这里的关键是,中间的运算部分不能有 I/O 操作,应该是纯运算,即通过纯粹的数学运算来求值。 否则,就应该拆分出另一个任务。

I/O 操作往往有现成命令,大多数时候,编程主要就是写中间的那部分运算逻辑。现在,主流写法是过程式编程和面向对象编程,但是我觉得,最合适纯运算的是函数式编程。

二、函数的拆分与合成

上面那张图中,运算过程可以用一个函数fn表示。

fn的类型如下。

fn :: a -> b

上面的式子表示,函数fn的输入是数据a,输出是数据b。

如果运算比较复杂,通常需要将fn拆分成多个函数。

f1、f2、f3的类型如下。

f1 :: a -> m
f2 :: m -> n
f3 :: n -> b

上面的式子中,输入的数据还是a,输出的数据还是b,但是多了两个中间值m和n。

我们可以把整个运算过程,想象成一根水管(pipe),数据从这头进去,那头出来。

函数的拆分,无非就是将一根水管拆成了三根。

进去的数据还是a,出来的数据还是b。fn与f1、f2、f3的关系如下。

fn = R.pipe(f1, f2, f3);

上面代码中,我用到了Ramda函数库的pipe方法,将三个函数合成为一个。Ramda 是一个非常有用的库,后面的例子都会使用它,如果你还不了解,可以先读一下教程

三、Pointfree 的概念

fn = R.pipe(f1, f2, f3);

这个公式说明,如果先定义f1、f2、f3,就可以算出fn。整个过程,根本不需要知道a或b。

也就是说,我们完全可以把数据处理的过程,定义成一种与参数无关的合成运算。不需要用到代表数据的那个参数,只要把一些简单的运算步骤合成在一起即可。

这就叫做 Pointfree:不使用所要处理的值,只合成运算过程。 中文可以译作"无值"风格。

请看下面的例子。

var addOne = x => x + 1;
var square = x => x * x;

上面是两个简单函数addOne和square。

把它们合成一个运算。

var addOneThenSquare = R.pipe(addOne, square);

addOneThenSquare(2) //  9

上面代码中,addOneThenSquare是一个合成函数。定义它的时候,根本不需要提到要处理的值,这就是 Pointfree。

四、Pointfree 的本质

Pointfree 的本质就是使用一些通用的函数,组合出各种复杂运算。上层运算不要直接操作数据,而是通过底层函数去处理。这就要求,将一些常用的操作封装成函数。

比如,读取对象的role属性,不要直接写成obj.role,而是要把这个操作封装成函数。

var prop = (p, obj) => obj[p];
var propRole = R.curry(prop)('role');

上面代码中,prop函数封装了读取操作。它需要两个参数p(属性名)和obj(对象)。这时,要把数据obj要放在最后一个参数,这是为了方便柯里化。函数propRole则是指定读取role属性,下面是它的用法(查看完整代码)。

var isWorker = s => s === 'worker';
var getWorkers = R.filter(R.pipe(propRole, isWorker));

var data = [
  {name: '张三', role: 'worker'},
  {name: '李四', role: 'worker'},
  {name: '王五', role: 'manager'},
];
getWorkers(data)
// [
//   {"name": "张三", "role": "worker"},
//   {"name": "李四", "role": "worker"}
// ]

上面代码中,data是传入的值,getWorkers是处理这个值的函数。定义getWorkers的时候,完全没有提到data,这就是 Pointfree。

简单说,Pointfree 就是运算过程抽象化,处理一个值,但是不提到这个值。这样做有很多好处,它能够让代码更清晰和简练,更符合语义,更容易复用,测试也变得轻而易举。

五、Pointfree 的示例一

下面,我们来看一个示例。

var str = 'Lorem ipsum dolor sit amet consectetur adipiscing elit';

上面是一个字符串,请问其中最长的单词有多少个字符?

我们先定义一些基本运算。

// 以空格分割单词
var splitBySpace = s => s.split(' ');

// 每个单词的长度
var getLength = w => w.length;

// 词的数组转换成长度的数组
var getLengthArr = arr => R.map(getLength, arr); 

// 返回较大的数字
var getBiggerNumber = (a, b) => a > b ? a : b;

// 返回最大的一个数字
var findBiggestNumber = 
  arr => R.reduce(getBiggerNumber, 0, arr);

然后,把基本运算合成为一个函数(查看完整代码)。

var getLongestWordLength = R.pipe(
  splitBySpace,
  getLengthArr,
  findBiggestNumber
);

getLongestWordLength(str) // 11

可以看到,整个运算由三个步骤构成,每个步骤都有语义化的名称,非常的清晰。这就是 Pointfree 风格的优势。

Ramda 提供了很多现成的方法,可以直接使用这些方法,省得自己定义一些常用函数(查看完整代码)。

// 上面代码的另一种写法
var getLongestWordLength = R.pipe(
  R.split(' '),
  R.map(R.length),
  R.reduce(R.max, 0)
);

六、Pointfree 示例二

最后,看一个实战的例子,拷贝自 Scott Sauyet 的文章《Favoring Curry》。那篇文章能帮助你深入理解柯里化,强烈推荐阅读。

下面是一段服务器返回的 JSON 数据。

现在要求是,找到用户 Scott 的所有未完成任务,并按到期日期升序排列。

过程式编程的代码如下(查看完整代码)。

上面代码不易读,出错的可能性很大。

现在使用 Pointfree 风格改写(查看完整代码)。

var getIncompleteTaskSummaries = function(membername) {
  return fetchData()
    .then(R.prop('tasks'))
    .then(R.filter(R.propEq('username', membername)))
    .then(R.reject(R.propEq('complete', true)))
    .then(R.map(R.pick(['id', 'dueDate', 'title', 'priority'])))
    .then(R.sortBy(R.prop('dueDate')));
};

上面代码已经清晰很多了。

另一种写法是,把各个then里面的函数合成起来(查看完整代码)。

// 提取 tasks 属性
var SelectTasks = R.prop('tasks');

// 过滤出指定的用户
var filterMember = member => R.filter(
  R.propEq('username', member)
);

// 排除已经完成的任务
var excludeCompletedTasks = R.reject(R.propEq('complete', true));

// 选取指定属性
var selectFields = R.map(
  R.pick(['id', 'dueDate', 'title', 'priority'])
);

// 按照到期日期排序
var sortByDueDate = R.sortBy(R.prop('dueDate'));

// 合成函数
var getIncompleteTaskSummaries = function(membername) {
  return fetchData().then(
    R.pipe(
      SelectTasks,
      filterMember(membername),
      excludeCompletedTasks,
      selectFields,
      sortByDueDate,
    )
  );
};

上面的代码跟过程式的写法一比较,孰优孰劣一目了然。

七、参考链接

(完)

文档信息

]]>
本文要回答一个很重要的问题:函数式编程有什么用?

目前,主流的编程语言都不是函数式的,已经能够满足需求。为何还要学函数式编程呢,只为了多理解一些新奇的概念?

一个网友说:

"函数式编程有什么优势呢?"

"我感觉,这种写法可能会令人头痛吧。"

很长一段时间,我根本不知道从何入手,如何将它用于实际项目?直到有一天,我学到了 Pointfree 这个概念,顿时豁然开朗,原来应该这样用!

我现在觉得,Pointfree 就是如何使用函数式编程的答案。

一、程序的本质

为了理解 Pointfree,请大家先想一想,什么是程序?

上图是一个编程任务,左侧是数据输入(input),中间是一系列的运算步骤,对数据进行加工,右侧是最后的数据输出(output)。一个或多个这样的任务,就组成了程序。

输入和输出(统称为 I/O)与键盘、屏幕、文件、数据库等相关,这些跟本文无关。这里的关键是,中间的运算部分不能有 I/O 操作,应该是纯运算,即通过纯粹的数学运算来求值。 否则,就应该拆分出另一个任务。

I/O 操作往往有现成命令,大多数时候,编程主要就是写中间的那部分运算逻辑。现在,主流写法是过程式编程和面向对象编程,但是我觉得,最合适纯运算的是函数式编程。

二、函数的拆分与合成

上面那张图中,运算过程可以用一个函数fn表示。

fn的类型如下。

fn :: a -> b

上面的式子表示,函数fn的输入是数据a,输出是数据b。

如果运算比较复杂,通常需要将fn拆分成多个函数。

f1、f2、f3的类型如下。

f1 :: a -> m
f2 :: m -> n
f3 :: n -> b

上面的式子中,输入的数据还是a,输出的数据还是b,但是多了两个中间值m和n。

我们可以把整个运算过程,想象成一根水管(pipe),数据从这头进去,那头出来。

函数的拆分,无非就是将一根水管拆成了三根。

进去的数据还是a,出来的数据还是b。fn与f1、f2、f3的关系如下。

fn = R.pipe(f1, f2, f3);

上面代码中,我用到了Ramda函数库的pipe方法,将三个函数合成为一个。Ramda 是一个非常有用的库,后面的例子都会使用它,如果你还不了解,可以先读一下教程

三、Pointfree 的概念

fn = R.pipe(f1, f2, f3);

这个公式说明,如果先定义f1、f2、f3,就可以算出fn。整个过程,根本不需要知道a或b。

也就是说,我们完全可以把数据处理的过程,定义成一种与参数无关的合成运算。不需要用到代表数据的那个参数,只要把一些简单的运算步骤合成在一起即可。

这就叫做 Pointfree:不使用所要处理的值,只合成运算过程。 中文可以译作"无值"风格。

请看下面的例子。

var addOne = x => x + 1;
var square = x => x * x;

上面是两个简单函数addOne和square。

把它们合成一个运算。

var addOneThenSquare = R.pipe(addOne, square);

addOneThenSquare(2) //  9

上面代码中,addOneThenSquare是一个合成函数。定义它的时候,根本不需要提到要处理的值,这就是 Pointfree。

四、Pointfree 的本质

Pointfree 的本质就是使用一些通用的函数,组合出各种复杂运算。上层运算不要直接操作数据,而是通过底层函数去处理。这就要求,将一些常用的操作封装成函数。

比如,读取对象的role属性,不要直接写成obj.role,而是要把这个操作封装成函数。

var prop = (p, obj) => obj[p];
var propRole = R.curry(prop)('role');

上面代码中,prop函数封装了读取操作。它需要两个参数p(属性名)和obj(对象)。这时,要把数据obj要放在最后一个参数,这是为了方便柯里化。函数propRole则是指定读取role属性,下面是它的用法(查看完整代码)。

var isWorker = s => s === 'worker';
var getWorkers = R.filter(R.pipe(propRole, isWorker));

var data = [
  {name: '张三', role: 'worker'},
  {name: '李四', role: 'worker'},
  {name: '王五', role: 'manager'},
];
getWorkers(data)
// [
//   {"name": "张三", "role": "worker"},
//   {"name": "李四", "role": "worker"}
// ]

上面代码中,data是传入的值,getWorkers是处理这个值的函数。定义getWorkers的时候,完全没有提到data,这就是 Pointfree。

简单说,Pointfree 就是运算过程抽象化,处理一个值,但是不提到这个值。这样做有很多好处,它能够让代码更清晰和简练,更符合语义,更容易复用,测试也变得轻而易举。

五、Pointfree 的示例一

下面,我们来看一个示例。

var str = 'Lorem ipsum dolor sit amet consectetur adipiscing elit';

上面是一个字符串,请问其中最长的单词有多少个字符?

我们先定义一些基本运算。

// 以空格分割单词
var splitBySpace = s => s.split(' ');

// 每个单词的长度
var getLength = w => w.length;

// 词的数组转换成长度的数组
var getLengthArr = arr => R.map(getLength, arr); 

// 返回较大的数字
var getBiggerNumber = (a, b) => a > b ? a : b;

// 返回最大的一个数字
var findBiggestNumber = 
  arr => R.reduce(getBiggerNumber, 0, arr);

然后,把基本运算合成为一个函数(查看完整代码)。

var getLongestWordLength = R.pipe(
  splitBySpace,
  getLengthArr,
  findBiggestNumber
);

getLongestWordLength(str) // 11

可以看到,整个运算由三个步骤构成,每个步骤都有语义化的名称,非常的清晰。这就是 Pointfree 风格的优势。

Ramda 提供了很多现成的方法,可以直接使用这些方法,省得自己定义一些常用函数(查看完整代码)。

// 上面代码的另一种写法
var getLongestWordLength = R.pipe(
  R.split(' '),
  R.map(R.length),
  R.reduce(R.max, 0)
);

六、Pointfree 示例二

最后,看一个实战的例子,拷贝自 Scott Sauyet 的文章《Favoring Curry》。那篇文章能帮助你深入理解柯里化,强烈推荐阅读。

下面是一段服务器返回的 JSON 数据。

现在要求是,找到用户 Scott 的所有未完成任务,并按到期日期升序排列。

过程式编程的代码如下(查看完整代码)。

上面代码不易读,出错的可能性很大。

现在使用 Pointfree 风格改写(查看完整代码)。

var getIncompleteTaskSummaries = function(membername) {
  return fetchData()
    .then(R.prop('tasks'))
    .then(R.filter(R.propEq('username', membername)))
    .then(R.reject(R.propEq('complete', true)))
    .then(R.map(R.pick(['id', 'dueDate', 'title', 'priority'])))
    .then(R.sortBy(R.prop('dueDate')));
};

上面代码已经清晰很多了。

另一种写法是,把各个then里面的函数合成起来(查看完整代码)。

// 提取 tasks 属性
var SelectTasks = R.prop('tasks');

// 过滤出指定的用户
var filterMember = member => R.filter(
  R.propEq('username', member)
);

// 排除已经完成的任务
var excludeCompletedTasks = R.reject(R.propEq('complete', true));

// 选取指定属性
var selectFields = R.map(
  R.pick(['id', 'dueDate', 'title', 'priority'])
);

// 按照到期日期排序
var sortByDueDate = R.sortBy(R.prop('dueDate'));

// 合成函数
var getIncompleteTaskSummaries = function(membername) {
  return fetchData().then(
    R.pipe(
      SelectTasks,
      filterMember(membername),
      excludeCompletedTasks,
      selectFields,
      sortByDueDate,
    )
  );
};

上面的代码跟过程式的写法一比较,孰优孰劣一目了然。

七、参考链接

(完)

文档信息

]]>
0
<![CDATA[一个前端项目,到底要集成多少库和工具]]> http://www.udpwork.com/item/16172.html http://www.udpwork.com/item/16172.html#reviews Sun, 12 Mar 2017 04:52:23 +0800 四火 http://www.udpwork.com/item/16172.html 一个前端项目,到底要集成多少库和工具最近忙于一些新做的项目,由于新入手,就想着往最佳实践去靠,也寻找一些可以借鉴的模板。其中前端的部分,有很成型的模板可以借鉴。大幅度减少了自己调查和集成的工作量。但是仔细看看,发现这里头的概念太多了,各种开源的库和工具,有人说“前端玩的是广度”是有道理的。

这个新项目并不算特别复杂,大致的技术是基于React+Redux的,但是大体上集成完毕以后,完成了几个demo的代码之后,粗粗地过了一遍,除了传统意义上的HTML+CSS+JavaScript(遵循ECMAScript 6的标准)三大件,居然涉及到了那么多技术,把自己吓了一跳:

  • React: an open-source declarative JavaScript library for building user interfaces.
  • Redux: a predictable state container for JavaScript apps. It helps you write applications that behave consistently, run in different environments (client, server, and native), and are easy to test.
  • Webpack: constructs two separate dependency graphs and emits bundle (compressed) files.
  • Gulp: a task/build runner for development, who can compile sass files, uglify and compress js files and much more.
  • Karma: a tool that enables the running of source code (i.e. JavaScript) against real browsers via the CLI.
  • NPM: Node Package Manager, who provides two main functionalities: Online repositories for node.js packages/modules which are searchable on search.nodejs.org. Command line utility to install Node.js packages, do version management and dependency management of Node.js packages.
  • ESLint: a tool for identifying and reporting on patterns found in ECMAScript/JavaScript code, with the goal of making code more consistent and avoiding bugs.
  • PhantomJS: a headless WebKit scriptable with a JavaScript API, or in simple terms, PhantomJS is a web browser without a graphical user interface.
  • Jasmine: Jasmine is a behavior-driven development framework for testing JavaScript code.
  • Material-UI: A Set of React Components that ImplementGoogle’s Material Design.
  • Oboe.js: an open source Javascript library for loading JSON using streaming, combining the convenience of DOM with the speed and fluidity of SAX.
  • Babel: a configurable transpiler, a compiler which translates from Javascript to Javascript unlike a compiler which translates high level application code into lower level code or binaries.

还有一些次要或者简单一些的就不列出了。在package dependency的配置文件中,我数了一下这些大大小小的依赖库、框架和工具,差不多有三十项。在这里我不想展开叙述每一项到底是用来做什么的,以及怎样集成到一起的。我倒是想说说杂七杂八的感受:

虽然写了好些年前端代码了,但这里面超过一半的技术以前并未深入使用过,因此这个项目让我觉得获益匪浅。我大概花了五个工作日时间把这些没接触过的和接触过但尚且夹生的技术,挨个摸了一遍,完全摸清摸透在那么短时间内是不可能的,但是至少从概念上、意义上,以及怎样使用上心中有谱,并且了解了一些最佳实践的方式。

前端的技术确实如百花齐放,发展速度太恐怖了,但是总感觉缺少头绪,除了那些好些年不怎么办变化的基础,需要有一些意在最佳实践的开源项目来梳理梳理,把这些东西像IDE整合一大堆插件一样整合起来,天下代码一大抄,这会给很多项目开头的工作减少很多成本。我想起几年前用的Grails,就干了类似的事情,但它并非是着力于前端的,因此还是很不一样的。

在接触软件以后,我的学习范畴一直是软硬通吃,前后兼修,一直到现在也是。从论坛门户网站,分布式服务到大数据处理的代码都写过,还写过满是业务的存储过程,赶鸭子上架的手机端代码,甚至切过图。但是要打磨更深刻的专业技术能力,日后应当更专注于专精某一领域。之前我讲过,所谓的全栈,大多数情况下并没有明显优势,除非一些特殊的团队角色,或者创业。

在没有一定深度和一定领域内的广度的情况下,所谓的“精通”和“掌握”都只能是笑笑而已。就好比去考察一些号称“精通JavaScript”的工程师,那么多开源库只是用过JQuery而已。遇到这种情况其实没有必要再深入问下去了。前端的团队更需要专业的人才。

很多人都知道,一个前端工程师在国内往往受到一定程度的轻视。其实不光在国内,国外也不见得有多少改善。比如在Amazon,一个WDE(Web Development Engineer)的发展渠道,就是远不如一个传统意义上的SDE通畅,Principal也少得多。事实上,有人觉得这些东西玩得再转,也不得不基于这小小的浏览器而已。后来我们发现这话完全不对,再后来我们发现以往那些给工程师分类的界限越来越模糊。现在NodeJS已经满世界跑了,就算不用NodeJS来掌管服务端,也不得不接受和使用基于它的一些工具。因此我相信这个不公平的现象会逐渐好转,虽说这个过程看起来会很漫长。

最后,工作了超过八年,如今的我依然觉得,一个项目组吸引我的最大因素在技术层面。似乎所有人都在谈impact,但是那些有趣的新技术,哪怕有时只是一些小技术和小点子,都能令我在半夜的时候想到了笑出声来。因为一个项目,写一写新鲜的代码,用一些新鲜的方式,学一些新鲜的技术,真是工作中莫大的享受。

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接《四火的唠叨》

分享到:
]]>
一个前端项目,到底要集成多少库和工具最近忙于一些新做的项目,由于新入手,就想着往最佳实践去靠,也寻找一些可以借鉴的模板。其中前端的部分,有很成型的模板可以借鉴。大幅度减少了自己调查和集成的工作量。但是仔细看看,发现这里头的概念太多了,各种开源的库和工具,有人说“前端玩的是广度”是有道理的。

这个新项目并不算特别复杂,大致的技术是基于React+Redux的,但是大体上集成完毕以后,完成了几个demo的代码之后,粗粗地过了一遍,除了传统意义上的HTML+CSS+JavaScript(遵循ECMAScript 6的标准)三大件,居然涉及到了那么多技术,把自己吓了一跳:

  • React: an open-source declarative JavaScript library for building user interfaces.
  • Redux: a predictable state container for JavaScript apps. It helps you write applications that behave consistently, run in different environments (client, server, and native), and are easy to test.
  • Webpack: constructs two separate dependency graphs and emits bundle (compressed) files.
  • Gulp: a task/build runner for development, who can compile sass files, uglify and compress js files and much more.
  • Karma: a tool that enables the running of source code (i.e. JavaScript) against real browsers via the CLI.
  • NPM: Node Package Manager, who provides two main functionalities: Online repositories for node.js packages/modules which are searchable on search.nodejs.org. Command line utility to install Node.js packages, do version management and dependency management of Node.js packages.
  • ESLint: a tool for identifying and reporting on patterns found in ECMAScript/JavaScript code, with the goal of making code more consistent and avoiding bugs.
  • PhantomJS: a headless WebKit scriptable with a JavaScript API, or in simple terms, PhantomJS is a web browser without a graphical user interface.
  • Jasmine: Jasmine is a behavior-driven development framework for testing JavaScript code.
  • Material-UI: A Set of React Components that ImplementGoogle’s Material Design.
  • Oboe.js: an open source Javascript library for loading JSON using streaming, combining the convenience of DOM with the speed and fluidity of SAX.
  • Babel: a configurable transpiler, a compiler which translates from Javascript to Javascript unlike a compiler which translates high level application code into lower level code or binaries.

还有一些次要或者简单一些的就不列出了。在package dependency的配置文件中,我数了一下这些大大小小的依赖库、框架和工具,差不多有三十项。在这里我不想展开叙述每一项到底是用来做什么的,以及怎样集成到一起的。我倒是想说说杂七杂八的感受:

虽然写了好些年前端代码了,但这里面超过一半的技术以前并未深入使用过,因此这个项目让我觉得获益匪浅。我大概花了五个工作日时间把这些没接触过的和接触过但尚且夹生的技术,挨个摸了一遍,完全摸清摸透在那么短时间内是不可能的,但是至少从概念上、意义上,以及怎样使用上心中有谱,并且了解了一些最佳实践的方式。

前端的技术确实如百花齐放,发展速度太恐怖了,但是总感觉缺少头绪,除了那些好些年不怎么办变化的基础,需要有一些意在最佳实践的开源项目来梳理梳理,把这些东西像IDE整合一大堆插件一样整合起来,天下代码一大抄,这会给很多项目开头的工作减少很多成本。我想起几年前用的Grails,就干了类似的事情,但它并非是着力于前端的,因此还是很不一样的。

在接触软件以后,我的学习范畴一直是软硬通吃,前后兼修,一直到现在也是。从论坛门户网站,分布式服务到大数据处理的代码都写过,还写过满是业务的存储过程,赶鸭子上架的手机端代码,甚至切过图。但是要打磨更深刻的专业技术能力,日后应当更专注于专精某一领域。之前我讲过,所谓的全栈,大多数情况下并没有明显优势,除非一些特殊的团队角色,或者创业。

在没有一定深度和一定领域内的广度的情况下,所谓的“精通”和“掌握”都只能是笑笑而已。就好比去考察一些号称“精通JavaScript”的工程师,那么多开源库只是用过JQuery而已。遇到这种情况其实没有必要再深入问下去了。前端的团队更需要专业的人才。

很多人都知道,一个前端工程师在国内往往受到一定程度的轻视。其实不光在国内,国外也不见得有多少改善。比如在Amazon,一个WDE(Web Development Engineer)的发展渠道,就是远不如一个传统意义上的SDE通畅,Principal也少得多。事实上,有人觉得这些东西玩得再转,也不得不基于这小小的浏览器而已。后来我们发现这话完全不对,再后来我们发现以往那些给工程师分类的界限越来越模糊。现在NodeJS已经满世界跑了,就算不用NodeJS来掌管服务端,也不得不接受和使用基于它的一些工具。因此我相信这个不公平的现象会逐渐好转,虽说这个过程看起来会很漫长。

最后,工作了超过八年,如今的我依然觉得,一个项目组吸引我的最大因素在技术层面。似乎所有人都在谈impact,但是那些有趣的新技术,哪怕有时只是一些小技术和小点子,都能令我在半夜的时候想到了笑出声来。因为一个项目,写一写新鲜的代码,用一些新鲜的方式,学一些新鲜的技术,真是工作中莫大的享受。

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接《四火的唠叨》

分享到:
]]>
0
<![CDATA[轻舟已过万重山——真正的技术派公司是怎么联调、测试和发布的? - 旁观者]]> http://www.udpwork.com/item/16171.html http://www.udpwork.com/item/16171.html#reviews Sat, 11 Mar 2017 09:58:00 +0800 旁观者 http://www.udpwork.com/item/16171.html 【摘要】说句狠话:没有趁手的利器,生产效率打完对折再打对折,勿谓言之不预也。阅读全文

]]>
【摘要】说句狠话:没有趁手的利器,生产效率打完对折再打对折,勿谓言之不预也。阅读全文

]]>
0
<![CDATA[编写地道的Go代码]]> http://www.udpwork.com/item/16139.html http://www.udpwork.com/item/16139.html#reviews Fri, 10 Mar 2017 10:36:26 +0800 鸟窝 http://www.udpwork.com/item/16139.html 在阅读本文之前,我先推荐你阅读官方的Effective Go文档,或者是中文翻译版:高效Go编程,它提供了很多编写标准而高效的Go代码指导,本文不会再重复介绍这些内容。

最地道的Go代码就是Go的标准库的代码,你有空的时候可以多看看Google的工程师是如何实现的。

本文仅作为一个参考,如果你有好的建议和意见,欢迎添加评论。

注释

可以通过/* …… */或者// ……增加注释,//之后应该加一个空格。

如果你想在每个文件中的头部加上注释,需要在版权注释和 Package前面加一个空行,否则版权注释会作为Package的注释。

12345678910111213
// Copyright 2009 The Go Authors. All rights reserved.// Use of this source code is governed by a BSD-style// license that can be found in the LICENSE file./*Package net provides a portable interface for network I/O, includingTCP/IP, UDP, domain name resolution, and Unix domain sockets.......*/package net......

注释应该用一个完整的句子,注释的第一个单词应该是要注释的指示符,以便在godoc中容易查找。

注释应该以一个句点.结束。

声明slice

声明空的slice应该使用下面的格式:

1
var t []string

而不是这种格式:

1
t := []string{}

前者声明了一个nilslice而后者是一个长度为0的非nil的slice。

关于字符串大小写

错误字符串不应该大写。
应该写成:

1
fmt.Errorf("failed to write data")

而不是写成:

1
fmt.Errorf("Failed to write data")

这是因为这些字符串可能和其它字符串相连接,组合后的字符串如果中间有大写字母开头的单词很突兀,除非这些首字母大写单词是固定使用的单词。

缩写词必须保持一致,比如都大写URL或者小写url。比如HTTP、ID等。
例如sendOAuth或者oauthSend。

常量一般声明为MaxLength,而不是以下划线分隔MAX_LENGTH或者MAXLENGTH。

也就是Go语言一般使用MixedCaps或者mixedCaps命名的方式区分包含多个单词的名称。

处理error而不是panic或者忽略

为了编写强壮的代码,不用使用_忽略错误,而是要处理每一个错误,尽管代码写起来可能有些繁琐。

尽量不要使用panic。

一些名称

有些单词可能有多种写法,在项目中应该保持一致,比如Golang采用的写法:

1234
// marshaling// unmarshaling// canceling// cancelation

而不是

1234
// marshalling// unmarshalling// cancelling// cancellation

包名应该用单数的形式,比如util、model,而不是utils、models。

Receiver 的名称应该缩写,一般使用一个或者两个字符作为Receiver的名称,如

123
func (f foo) method() {	...}

如果方法中没有使用receiver,还可以省略receiver name,这样更清晰的表明方法中没有使用它:

123
func (foo) method() {	...}

package级的Error变量

通常会把自定义的Error放在package级别中,统一进行维护:

123456789
var (	ErrCacheMiss = errors.New("memcache: cache miss")	ErrCASConflict = errors.New("memcache: compare-and-swap conflict")	ErrNotStored = errors.New("memcache: item not stored")	ErrServerError = errors.New("memcache: server error")	ErrNoStats = errors.New("memcache: no statistics available")	ErrMalformedKey = errors.New("malformed: key is too long or contains invalid characters")	ErrNoServers = errors.New("memcache: no servers configured or available"))

并且变量以Err开头。

空字符串检查

不要使用下面的方式检查空字符串:

123
if len(s) == 0 {	...}

而是使用下面的方式

123
if s == "" {	...}

下面的方法更是语法不对:

123
if s == nil || s == "" {	...}

非空slice检查

不要使用下面的方式检查空的slice:

123
if s != nil && len(s) > 0 {    ...}

直接比较长度即可:

123
if len(s) > 0 {    ...}

同样的道理也适用map和channel。

省略不必要的变量

比如

1
var whitespaceRegex, _ = regexp.Compile("\\s+")

可以简写为

1
var whitespaceRegex = regexp.MustCompile(`\s+`)

有时候你看到的一些第三方的类提供了类似的方法:

12
func Foo(...) (...,error)func MustFoo(...) (...)

MustFoo一般提供了一个不带error返回的类型。

直接使用bool值

对于bool类型的变量var b bool,直接使用它作为判断条件,而不是使用它和true/false进行比较

12345678
if b {    ...}if !b {    ...}

而不是

1234567
if b == true {    ...}if b == false {    ...}

byte/string slice相等性比较

不要使用

12345
   var s1 []byte   var s2 []byte   ...bytes.Compare(s1, s2) == 0bytes.Compare(s1, s2) != 0

而是:

12345
   var s1 []byte   var s2 []byte   ...bytes.Equal(s1, s2) == 0bytes.Equal(s1, s2) != 0

检查是否包含子字符串

不要使用strings.IndexRune(s1, 'x') > -1及其类似的方法IndexAny、Index检查字符串包含,
而是使用strings.ContainsRune、strings.ContainsAny、strings.Contains来检查。

使用类型转换而不是struct字面值

对于两个类型:

123456789
type t1 struct {	a int	b int}type t2 struct {	a int	b int}

可以使用类型转换将类型t1的变量转换成类型t2的变量,而不是像下面的代码进行转换

12
v1 := t1{1, 2}_ = t2{v1.a, v1.b}

应该使用类型转换,因为这两个struct底层的数据结构是一致的。

1
_ = t2(v1)

复制slice

不要使用下面的复制slice的方式:

12345678
var b1, b2 []byte	for i, v := range b1 { 		b2[i] = v	}	for i := range b1 { 		b2[i] = b1[i]	}

而是使用内建的copy函数:

1
copy(b2, b1)

不要在for中使用多此一举的true

不要这样:

12
for true {}

而是要这样:

12
for {}

尽量缩短if

下面的代码:

12345
   x := trueif x {	return true}return false

可以用return x代替。

同样下面的代码也可以使用return err代替:

12345678
func fn1() error {	var err error	if err != nil {		return err	}	return nil}
1234567891011
func fn1() bool{    ...    b := fn()	if b {		...         return true	} else {        return false    }}

应该写成:

123456789101112
func fn1() bool{    ...    b := fn()    if !b {        return false    }		...     return true	}

也就是减少if的分支/缩进。

append slice

不要这样:

1234
   var a, b []intfor _, v := range a {	b = append(b, v)}

而是要这样

12
var a, b []intb = append(b, a...)

简化range

123456
   var m map[string]int   for _ = range m { }for _, _ = range m {}

可以简化为

123
for range m {}

对slice和channel也适用。

正则表达式中使用raw字符串避免转义字符

在使用正则表达式时,不要:

12
regexp.MustCompile("\\.") regexp.Compile("\\.")

而是直接使用raw字符串,可以避免大量的\出现:

12
regexp.MustCompile(`\.`) regexp.Compile(`\.`)

简化只包含单个case的select

123
select {	case <-ch:}

直接写成<-ch即可。send也一样。

123456
   for { 	select {	case x := <-ch:		_ = x	}}

直接改成for-range即可。

这种简化只适用包含单个case的情况。

slice的索引

有时可以忽略slice的第一个索引或者第二个索引:

123
var s []int_ = s[:len(s)]_ = s[0:len(s)]

可以写成s[:]

使用time.Since

下面的代码经常会用到:

1
_ = time.Now().Sub(t1)

可以简写为:

1
_ = time.Since(t1)

使用strings.TrimPrefix/strings.TrimSuffix 掐头去尾

不要自己判断字符串是否以XXX开头或者结尾,然后自己再去掉XXX,而是使用现成的strings.TrimPrefix/strings.TrimSuffix。

123456
   var s1 = "a string value"   var s2 = "a "   var s3 stringif strings.HasPrefix(s1, s2) { 	s3 = s1[len(s2):]}

可以简化为

123
var s1 = "a string value"var s2 = "a "var s3 = strings.TrimPrefix(s1, s2)

使用工具检查你的代码

以上的很多优化规则都可以通过工具检查,下面列出了一些有用的工具:

  1. go fmt
  2. go vet
  3. gosimple
  4. keyify
  5. staticcheck
  6. unused
  7. golint
  8. misspell
  9. goimports
  10. errcheck
  11. aligncheck
  12. structcheck
  13. varcheck

参考文档

  1. https://golang.org/doc/effective_go.html
  2. https://github.com/golang/go/wiki/CodeReviewComments
  3. https://dmitri.shuralyov.com/idiomatic-go
  4. https://talks.golang.org/2014/readability.slide#1
  5. https://github.com/dominikh/go-tools/tree/master/simple
  6. https://github.com/dominikh/go-tools/tree/master/cmd/structlayout-optimize
  7. https://go-zh.org
  8. https://docs.google.com/presentation/d/1OT-dMNbiwOPeaivQOldok2hUUNMTvSC0GJ67JohLt5U/pub?start=false&loop=false&delayms=3000&slide=id.g18b1f95882_1_135
  9. https://github.com/d-smith/go-training/blob/master/idiomatic-go.md
  10. https://github.com/opennota/check
  11. http://golang-sizeof.tips
  12. https://github.com/mibk/dupl
]]>
在阅读本文之前,我先推荐你阅读官方的Effective Go文档,或者是中文翻译版:高效Go编程,它提供了很多编写标准而高效的Go代码指导,本文不会再重复介绍这些内容。

最地道的Go代码就是Go的标准库的代码,你有空的时候可以多看看Google的工程师是如何实现的。

本文仅作为一个参考,如果你有好的建议和意见,欢迎添加评论。

注释

可以通过/* …… */或者// ……增加注释,//之后应该加一个空格。

如果你想在每个文件中的头部加上注释,需要在版权注释和 Package前面加一个空行,否则版权注释会作为Package的注释。

12345678910111213
// Copyright 2009 The Go Authors. All rights reserved.// Use of this source code is governed by a BSD-style// license that can be found in the LICENSE file./*Package net provides a portable interface for network I/O, includingTCP/IP, UDP, domain name resolution, and Unix domain sockets.......*/package net......

注释应该用一个完整的句子,注释的第一个单词应该是要注释的指示符,以便在godoc中容易查找。

注释应该以一个句点.结束。

声明slice

声明空的slice应该使用下面的格式:

1
var t []string

而不是这种格式:

1
t := []string{}

前者声明了一个nilslice而后者是一个长度为0的非nil的slice。

关于字符串大小写

错误字符串不应该大写。
应该写成:

1
fmt.Errorf("failed to write data")

而不是写成:

1
fmt.Errorf("Failed to write data")

这是因为这些字符串可能和其它字符串相连接,组合后的字符串如果中间有大写字母开头的单词很突兀,除非这些首字母大写单词是固定使用的单词。

缩写词必须保持一致,比如都大写URL或者小写url。比如HTTP、ID等。
例如sendOAuth或者oauthSend。

常量一般声明为MaxLength,而不是以下划线分隔MAX_LENGTH或者MAXLENGTH。

也就是Go语言一般使用MixedCaps或者mixedCaps命名的方式区分包含多个单词的名称。

处理error而不是panic或者忽略

为了编写强壮的代码,不用使用_忽略错误,而是要处理每一个错误,尽管代码写起来可能有些繁琐。

尽量不要使用panic。

一些名称

有些单词可能有多种写法,在项目中应该保持一致,比如Golang采用的写法:

1234
// marshaling// unmarshaling// canceling// cancelation

而不是

1234
// marshalling// unmarshalling// cancelling// cancellation

包名应该用单数的形式,比如util、model,而不是utils、models。

Receiver 的名称应该缩写,一般使用一个或者两个字符作为Receiver的名称,如

123
func (f foo) method() {	...}

如果方法中没有使用receiver,还可以省略receiver name,这样更清晰的表明方法中没有使用它:

123
func (foo) method() {	...}

package级的Error变量

通常会把自定义的Error放在package级别中,统一进行维护:

123456789
var (	ErrCacheMiss = errors.New("memcache: cache miss")	ErrCASConflict = errors.New("memcache: compare-and-swap conflict")	ErrNotStored = errors.New("memcache: item not stored")	ErrServerError = errors.New("memcache: server error")	ErrNoStats = errors.New("memcache: no statistics available")	ErrMalformedKey = errors.New("malformed: key is too long or contains invalid characters")	ErrNoServers = errors.New("memcache: no servers configured or available"))

并且变量以Err开头。

空字符串检查

不要使用下面的方式检查空字符串:

123
if len(s) == 0 {	...}

而是使用下面的方式

123
if s == "" {	...}

下面的方法更是语法不对:

123
if s == nil || s == "" {	...}

非空slice检查

不要使用下面的方式检查空的slice:

123
if s != nil && len(s) > 0 {    ...}

直接比较长度即可:

123
if len(s) > 0 {    ...}

同样的道理也适用map和channel。

省略不必要的变量

比如

1
var whitespaceRegex, _ = regexp.Compile("\\s+")

可以简写为

1
var whitespaceRegex = regexp.MustCompile(`\s+`)

有时候你看到的一些第三方的类提供了类似的方法:

12
func Foo(...) (...,error)func MustFoo(...) (...)

MustFoo一般提供了一个不带error返回的类型。

直接使用bool值

对于bool类型的变量var b bool,直接使用它作为判断条件,而不是使用它和true/false进行比较

12345678
if b {    ...}if !b {    ...}

而不是

1234567
if b == true {    ...}if b == false {    ...}

byte/string slice相等性比较

不要使用

12345
   var s1 []byte   var s2 []byte   ...bytes.Compare(s1, s2) == 0bytes.Compare(s1, s2) != 0

而是:

12345
   var s1 []byte   var s2 []byte   ...bytes.Equal(s1, s2) == 0bytes.Equal(s1, s2) != 0

检查是否包含子字符串

不要使用strings.IndexRune(s1, 'x') > -1及其类似的方法IndexAny、Index检查字符串包含,
而是使用strings.ContainsRune、strings.ContainsAny、strings.Contains来检查。

使用类型转换而不是struct字面值

对于两个类型:

123456789
type t1 struct {	a int	b int}type t2 struct {	a int	b int}

可以使用类型转换将类型t1的变量转换成类型t2的变量,而不是像下面的代码进行转换

12
v1 := t1{1, 2}_ = t2{v1.a, v1.b}

应该使用类型转换,因为这两个struct底层的数据结构是一致的。

1
_ = t2(v1)

复制slice

不要使用下面的复制slice的方式:

12345678
var b1, b2 []byte	for i, v := range b1 { 		b2[i] = v	}	for i := range b1 { 		b2[i] = b1[i]	}

而是使用内建的copy函数:

1
copy(b2, b1)

不要在for中使用多此一举的true

不要这样:

12
for true {}

而是要这样:

12
for {}

尽量缩短if

下面的代码:

12345
   x := trueif x {	return true}return false

可以用return x代替。

同样下面的代码也可以使用return err代替:

12345678
func fn1() error {	var err error	if err != nil {		return err	}	return nil}
1234567891011
func fn1() bool{    ...    b := fn()	if b {		...         return true	} else {        return false    }}

应该写成:

123456789101112
func fn1() bool{    ...    b := fn()    if !b {        return false    }		...     return true	}

也就是减少if的分支/缩进。

append slice

不要这样:

1234
   var a, b []intfor _, v := range a {	b = append(b, v)}

而是要这样

12
var a, b []intb = append(b, a...)

简化range

123456
   var m map[string]int   for _ = range m { }for _, _ = range m {}

可以简化为

123
for range m {}

对slice和channel也适用。

正则表达式中使用raw字符串避免转义字符

在使用正则表达式时,不要:

12
regexp.MustCompile("\\.") regexp.Compile("\\.")

而是直接使用raw字符串,可以避免大量的\出现:

12
regexp.MustCompile(`\.`) regexp.Compile(`\.`)

简化只包含单个case的select

123
select {	case <-ch:}

直接写成<-ch即可。send也一样。

123456
   for { 	select {	case x := <-ch:		_ = x	}}

直接改成for-range即可。

这种简化只适用包含单个case的情况。

slice的索引

有时可以忽略slice的第一个索引或者第二个索引:

123
var s []int_ = s[:len(s)]_ = s[0:len(s)]

可以写成s[:]

使用time.Since

下面的代码经常会用到:

1
_ = time.Now().Sub(t1)

可以简写为:

1
_ = time.Since(t1)

使用strings.TrimPrefix/strings.TrimSuffix 掐头去尾

不要自己判断字符串是否以XXX开头或者结尾,然后自己再去掉XXX,而是使用现成的strings.TrimPrefix/strings.TrimSuffix。

123456
   var s1 = "a string value"   var s2 = "a "   var s3 stringif strings.HasPrefix(s1, s2) { 	s3 = s1[len(s2):]}

可以简化为

123
var s1 = "a string value"var s2 = "a "var s3 = strings.TrimPrefix(s1, s2)

使用工具检查你的代码

以上的很多优化规则都可以通过工具检查,下面列出了一些有用的工具:

  1. go fmt
  2. go vet
  3. gosimple
  4. keyify
  5. staticcheck
  6. unused
  7. golint
  8. misspell
  9. goimports
  10. errcheck
  11. aligncheck
  12. structcheck
  13. varcheck

参考文档

  1. https://golang.org/doc/effective_go.html
  2. https://github.com/golang/go/wiki/CodeReviewComments
  3. https://dmitri.shuralyov.com/idiomatic-go
  4. https://talks.golang.org/2014/readability.slide#1
  5. https://github.com/dominikh/go-tools/tree/master/simple
  6. https://github.com/dominikh/go-tools/tree/master/cmd/structlayout-optimize
  7. https://go-zh.org
  8. https://docs.google.com/presentation/d/1OT-dMNbiwOPeaivQOldok2hUUNMTvSC0GJ67JohLt5U/pub?start=false&loop=false&delayms=3000&slide=id.g18b1f95882_1_135
  9. https://github.com/d-smith/go-training/blob/master/idiomatic-go.md
  10. https://github.com/opennota/check
  11. http://golang-sizeof.tips
  12. https://github.com/mibk/dupl
]]>
0