开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

go利用(*interface{})(nil)传递参数类型

发表于 2017-12-01

原创文章转载请注明出处

最近阅读Martini的源码,读到了inject这部分,inject在Martini的代码中无处不在,是Martini框架的绝对核心。

先看看injector类型的声明:

1
2
3
4
elm复制代码type injector struct {       
values map[reflect.Type]reflect.Value
parent Injector
}

撇开 parent不看,values是一个映射表,用于保存注入的参数,它是一个用reflect.Type当键、reflect.Value为值的map。

parent Injector又是什么鬼?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
reasonml复制代码// Injector represents an interface for mapping and injecting dependencies into structs
// and function arguments.
type Injector interface {
Applicator
Invoker
TypeMapper
// SetParent sets the parent of the injector. If the injector cannot find a
// dependency in its Type map it will check its parent before returning an
// error.
SetParent(Injector)
}

// Applicator represents an interface for mapping dependencies to a struct.
type Applicator interface {
// Maps dependencies in the Type map to each field in the struct
// that is tagged with 'inject'. Returns an error if the injection
// fails.
Apply(interface{}) error
}

// Invoker represents an interface for calling functions via reflection.
type Invoker interface {
// Invoke attempts to call the interface{} provided as a function,
// providing dependencies for function arguments based on Type. Returns
// a slice of reflect.Value representing the returned values of the function.
// Returns an error if the injection fails.
Invoke(interface{}) ([]reflect.Value, error)
}

// TypeMapper represents an interface for mapping interface{} values based on type.
type TypeMapper interface {
// Maps the interface{} value based on its immediate type from reflect.TypeOf.
Map(interface{}) TypeMapper
// Maps the interface{} value based on the pointer of an Interface provided.
// This is really only useful for mapping a value as an interface, as interfaces
// cannot at this time be referenced directly without a pointer.
MapTo(interface{}, interface{}) TypeMapper
// Provides a possibility to directly insert a mapping based on type and value.
// This makes it possible to directly map type arguments not possible to instantiate
// with reflect like unidirectional channels.
Set(reflect.Type, reflect.Value) TypeMapper
// Returns the Value that is mapped to the current type. Returns a zeroed Value if
// the Type has not been mapped.
Get(reflect.Type) reflect.Value
}

Injector是注入接口声明的组合,我们先关注TypeMapper这个接口,从源码可以得知Map和MapTo是用来映射数据类型和数据到values map[reflect.Type]reflect.Value的方法。

Map方法相对来说比较简单,利用反射获取对象的type。

1
2
3
4
stylus复制代码func (i *injector) Map(val interface{}) TypeMapper {
i.values[reflect.TypeOf(val)] = reflect.ValueOf(val)
return i
}

现在我们先假设参数中有多个string时,values map[reflect.Type]reflect.Value这个map只会保存最后一个string的映射,那我们该如何处理才能完整的保存所有的string参数呢?

考虑interface类型在底层的实现(type,data),inject库实现了一个从interface指针中获取类型的函数InterfaceOf,而MapTo则利用InterfaceOf来获取传入的数据类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
stylus复制代码func InterfaceOf(value interface{}) reflect.Type {       
t := reflect.TypeOf(value)

for t.Kind() == reflect.Ptr {
t = t.Elem()
}

if t.Kind() != reflect.Interface {
panic("Called inject.InterfaceOf with a value that is not a pointer to an interface. (*MyInterface)(nil)")
}
return t
}

func (i *injector) MapTo(val interface{}, ifacePtr interface{}) TypeMapper {
i.values[InterfaceOf(ifacePtr)] = reflect.ValueOf(val)
return i
}

简直是神来之笔,再找个别人的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
go复制代码package main

import (
"fmt"
"github.com/codegangsta/inject"
)

type SpecialString interface{}

func main() {
fmt.Println(inject.InterfaceOf((*interface{})(nil)))
fmt.Println(inject.InterfaceOf((*SpecialString)(nil)))
}

输出

1
2
haxe复制代码interface {}
main.SpecialString

看到了吗?指向接口的空指针,虽然data是nil,但是我们只要它的type。分步解释一下:

1
2
3
4
5
6
7
8
9
stylus复制代码//以(*SpecialString)(nil)为例
t := reflect.TypeOf(value) //t是*main.SpecialString,t.Kind()是ptr,t.Elem()是main.SpecialString
for t.Kind() == reflect.Ptr { //循环判断,也许是指向指针的指针
t = t.Elem() //Elem returns a type's element type.
}
if t.Kind() != reflect.Interface {
... //如果不是Interface类型,报panic
}
return t //返回(*SpecialString)(nil)的元素原始类型

interface{}是什么,在go里面interface{}就是万能的Any。inject利用了(*interface{})(nil)携带数据类型的特点,只用一个空指针就搞定了数据类型的传输,而且扩展了同类型数据的绑定。

让我们到martini.go去看看这个注入是怎么用的吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
stylus复制代码// Martini represents the top level web application. inject.Injector methods can be invoked to map services on a global level.
type Martini struct {
inject.Injector
handlers []Handler
action Handler
logger *log.Logger
}

// New creates a bare bones Martini instance. Use this method if you want to have full control over the middleware that is used.
func New() *Martini {
m := &Martini{Injector: inject.New(), action: func() {}, logger: log.New(os.Stdout, "[martini] ", 0)}
m.Map(m.logger)
m.Map(defaultReturnHandler())
return m
}

func (m *Martini) createContext(res http.ResponseWriter, req *http.Request) *context {
c := &context{inject.New(), m.handlers, m.action, NewResponseWriter(res), 0}
c.SetParent(m)
c.MapTo(c, (*Context)(nil))
c.MapTo(c.rw, (*http.ResponseWriter)(nil))
c.Map(req)
return c
}

自定义的Martini结构体包含了inject.Injector接口,所以可以很方便的注入logger。后续Invoke中间件的时候,自然就可以通过Injector的Get方法获取logger对象。context则使用了MapTo方法注入了Context和http.ResponseWriter这两个接口类型。

那么Invoke的时候又是如何调用函数并且注入参数的呢?请移步《Invoke如何动态传参》

我是咕咕鸡,一个还在不停学习的全栈工程师。热爱生活,喜欢跑步,家庭是我不断向前进步的动力。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Spring Boot在链家网技术实践

发表于 2017-12-01

内容来源:2017年5月6日,链家网架构师程天亮在“Spring Cloud中国社区技术沙龙-北京站”进行《Spring Boot在链家网实践》演讲分享。IT大咖说作为独家视频合作方,经主办方和讲者审阅授权发布。

阅读字数:2133 | 5分钟阅读

嘉宾PPT及演讲视频:t.cn/Rp022Hh摘要

SpringBoot是由Pivotal团队提供的全新框架,其设计目的是用来简化新Spring应用的初始搭建以及开发过程。该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置。通过这种方式,Boot致力于在蓬勃发展的快速应用开发领域成为领导者。

Spring Boot实践

Spring Boot是什么?

SpringBoot是基于spring4以上生态的一个框架,是一个优化并简化Spring开发的框架,也是一个微服务的入门级框架。

SpringBoot主要是简化开发,也改变了传统Web的部署方式,将一些容器内置,使得我们的部署和运行都比较方便,通过一个jar包就可以运行。它还提供了自动配置的功能,让开发更简单。Spring Boot自带进程内部的监控,以及JMX的封装,这样我们就可以把进程内部的信息接到监控平台上去,能够掌握更多程序进程内部的情况。因为Spring Boot已经内置了Web容器,就不再需要打WAR包了。

链家网的平台策略技术主要是与平台数据驱动和数据挖掘等方向相关,所以基本都是一些服务。

如图可见,在最上层有API网关,用来连接下面的服务,提供非java系列的API转换以及API的路由分发等等。对于java相关的我们提供了服务的SDK。

中间一层主要是业务上的一些基础服务平台。有RDC作为服务的提供者,主要是以dubbo形式。在这过程中我们做了配置和注册中心。在整个过程中我们有进程内部的监控和服务监控。由于我们做的是基础平台,还要做一些灰度发布、流量实验,所以我们会有一个流量实验平台。这些平台最初是基于Spring,现在我们把它改成在Spring Boot的基础上重做这些服务。

最下面是数据实施流和实时计算流。

链家网平台策略部发展遇到的问题

服务配置复杂。基础服务多,服务的资源配置复杂。传统方式管理服务复杂。

服务之间调用复杂。检索服务、用户中心服务等,其之间的调用复杂,依赖多,需要微服务化。

服务监控难度大。服务比较多,机器部署复杂,服务存活监控、业务是否正常监控尤为重要。

服务化测试问题。服务依赖性比较大,测试一个小的功能,周边服务也需要启动。

SpringBoot把凌乱的配置集中化。传统Spring是采用XML的方式进行配置,容器和业务上的配置很难分离。使用Spring Boot之后,就能把这两块分开,把容器的注入通过Spring4的Java Config方式进行配置。

和业务相关的业务配置可以通过yml、properties等方式进行管理。

做到这里还只是第一步,把容器相关的管理和业务的配置分离。但是这远远不够,我们仍然很难维护数据库的资源,以及数据库的用户名密码和它的链接等都是暴露出来的。这对于安全性来说是一个很大的隐患。所以我们接下来就要把这些通用的、异变的、保密性的资源集中到服务器的Config Center。其余一些基本不变的业务相关的配置放在本地。

于是就形成了上图中的方式。我们有一个资源配置或注册中心,检索平台、数据挖掘平台和用户中心都是依赖于注册中心和一些少量的本地配置文件。

通过这种方式简化了配置,在ID编码上也有比较整洁简单的接入,以及对一些重点资源的安全进行统一管理,从而大大提升了安全性。

解决单进程多应用低吞吐量

单应用单处理,使用Spring Boot fat jar模式,将tomcat等容器内嵌。

每个tomcat只管理一个应用,提高应用的吞吐量。

服务的管理更加清晰化,打包工程更加流程化。

服务内嵌tomcat,测试更加容易控制进程,更容易实现自动化测试。

传统方法是通过xml、Spring各种依赖、SDK依赖等等非常繁杂,版本控制、版本依赖都容易出现问题,导致SSO或权限管理很复杂。我们通过扩展Spring把它的配置信息放到配置中心,只需要一个配置中心的地址就可以。再加上一些本地不太变化的配置,最后再做一个SSO/User-starter。使我们的集成用户中心和权限系统变得非常容易,为我们链家网所有系统需要的用户相关的接入减少很多成本,以及它的调试成本都会很低。

我们所有的服务之前都是用dubbo或者native自己写的一些RPC,在这过程中我们通过Spring Boot对dubbo也进行了一次改造。

关于dubbo的配置解析,在原有的SpringXML的配置解析基础上,添加了Spring Config Parser以及Spring Boot Start。在传输协议上加了restful和Restful两个协议。改造后在写服务的时候只需要一个注册中心配置+注解就可以了。

微服务治理

SpringBoot也是一个轻量级的微服务入口,我们进行了一些微服务治理,当然现在还不是很健全。

我们主要做的是一些API的服务网关、dubbo的原生admin,在模块上增加了调用链追踪、查询和诊断,以及在Spring Boot Actutor的基础上增加了服务内部的监控。还整合了jolokia,丰富JMX监控和控制。

困难与挑战

线上运维问题

以前单Tomcat很容易维护,因为它是一个很标准的容器。Op可以通过它进行一些控制、配置和管理。

现在经过Spring Boot改造后变成每个应用自己就带有Tomcat,这样必然将Tomcat的配置下发到应用程序。如果没有一个编程规范和配置规范的话,会导致每个应用程序的配置不一定合格,还会产生一些安全风险。

在这种情况下就需要做一些编码规范以及配置规范,把Tomcat相关的信息再次剥离,到配置中心交给Op去运行。

之前有标准的启停脚本,通过Spring Boot改造后的启动脚本会随意化,导致启停脚本五花八门难以维护。所以我们对脚本进行了规范化,自动生成脚本从而达到统一。

编码上的问题

代码阅读难度加大:代码的入口更加隐蔽,不如web.xml和spring xml逻辑清晰。要求更高的编程规范。

学习成本加大:新的技术都有学习成本。Spring Boot入门简单,用好或扩展需要更深的学习或源码学习。

入门级微服务框架:Spring Boot主要还是对Spring的再封装,解决Spring易用。Spring Boot做微服务,还需要对周边进行丰富。

今天的分享到此结束,谢谢大家!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Python3 CookBook 字符串和文本

发表于 2017-12-01

欢迎关注我的微信公众号 AlwaysBeta,更多精彩内容等你来。

以下测试代码全部基于 Python3。

字符串操作在程序中的出现频率相当高,包括分割,替换,拼接等等,这篇文章介绍五个最常遇到的问题,希望给你带来一些思考。

1、使用多个界定符分割字符串

分割字符串属于字符串最基本的操作了,直接用 split() 即可。

1
2
3
4
复制代码In [88]: a = 'a,b,c,c'

In [89]: a.split(',')
Out[89]: ['a', 'b', 'c', 'c']

如果一个字符串包含多个不同的分隔符呢?当然可以用 split() 进行多次分割,但这显然比较麻烦,这时候用 re.split() 是更好的解决办法。

1
2
3
4
5
6
复制代码In [90]: a = 'a,b c:d,e'

In [91]: import re

In [92]: re.split(r'[,| |:]', a)
Out[92]: ['a', 'b', 'c', 'd', 'e']

re.split() 函数的第一个参数可以根据你的需要,写出符合要求的正则表达式。

2、字符串开头或结尾匹配

解决这个问题可以有很多种方法。

第一可以使用切片操作,截取出字符串和要求字符串进行比较;第二可以使用 re.march() 函数,通过写正则表达式,检查匹配结果来达到比较目的。

这里介绍两个更简单的函数 startswith() 和 endswith()。

1
2
3
4
5
6
7
复制代码In [93]: a = 'hello.py'

In [94]: a.endswith('.py')
Out[94]: True

In [95]: a.startswith('h')
Out[95]: True

需要注意的是,如果要一次比较多个字符串,所传入的参数必须是元组,否则会报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码In [96]: names = ['a.txt', 'b.py', 'c', 'd.py']

In [97]: [name for name in names if name.endswith('.py')]
Out[97]: ['b.py', 'd.py']

In [98]: [name for name in names if name.endswith('.py', '.txt')]
-----------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-98-900fc5940b5c> in <module>()
> 1 [name for name in names if name.endswith('.py', '.txt')]

<ipython-input-98-900fc5940b5c> in <listcomp>(.0)
> 1 [name for name in names if name.endswith('.py', '.txt')]

TypeError: slice indices must be integers or None or have an __index__ method

In [99]: [name for name in names if name.endswith(('.py', '.txt'))]
Out[99]: ['a.txt', 'b.py', 'd.py']

再来看另一种情况,我们用 Linux 系统 shell 命令时,很喜欢用通配符,比如 ls *.py 来查看文件夹下所有 Python 文件。

在程序中也有两个函数,fnmatch() 和 fnmatchcase() 来支持这种通配符的操作方式。

1
2
3
4
复制代码In [100]: from fnmatch import fnmatch, fnmatchcase

In [101]: fnmatch('a.py', '*.py')
Out[101]: True

由于不同的操作系统,对于模式大小写匹配规则是不一样的,所以 fnmatchcase() 函数的作用就是完全按照模式大小写来匹配。

3、字符串搜索和替换

对于简单的模式,直接使用 replace() 函数即可。

1
2
3
4
复制代码In [102]: a = 'hello world'

In [103]: a.replace('hello', 'go')
Out[103]: 'go world'

如果是复杂的模式,可以使用 re.sub() 函数。比如你想把 ’11/30/2017’ 转换成 ’2017-11-30‘。

1
2
3
4
复制代码In [104]: a = 'time is 11/30/2017'

In [106]: re.sub(r'(\d+)/(\d+)/(\d+)', r'\3-\1-\2', a)
Out[106]: 'time is 2017-11-30'

sub() 函数第一个参数表示原字符串匹配模式,第二个参数是希望转换成的模式,反斜杠 3 为前面获取的组号。

其实这种解决方法归根结底还是考验对正则表达式理解程度,如果对正则表达式很熟悉的话,解决这类问题会很轻松。

我刚开始工作时用的语言是 Perl,这个语言对文本处理相当强悍,用的最多的就是正则表达式,写起来也很方便,当时写过很多很复杂的正则。后来开始写 Python 之后,感觉写正则好麻烦,很不习惯。

4、最短匹配和多行匹配模式

这两个操作所要注意的就两个方面。

第一,当使用 .* 进行匹配时,所采用的模式为贪婪匹配,如果想要结果为最短匹配,需要写成非贪婪匹配:.*?。

第二,点号是不能匹配换行符的,所以在进行多行匹配时,要写成:[.|\n]。

5、字符串格式换

首先说说字符串拼接,如果简单拼接两个字符串,加号便可以轻松搞定。

1
2
3
4
5
6
复制代码In [110]: a = 'hello'

In [111]: b = 'world'

In [112]: a + ' ' + b
Out[112]: 'hello world'

如果拼接一个列表中的字符串呢?可以用 join() 函数。

1
2
3
4
复制代码In [113]: a = ['I', 'have', 'a', 'dream']

In [114]: ' '.join(a)
Out[114]: 'I have a dream'

再来看看这类替换操作:

1
2
3
4
复制代码In [115]: a = '{name} is {age}'

In [116]: a.format(name='Tom', age=18)
Out[116]: 'Tom is 18'

其实 format() 函数还有很多功能,比如打印的时候就常用它来格式化,有兴趣可以进行更多了解。

未完待续。。。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【译】Go语言之声明语法

发表于 2017-12-01

作者:Rob Pike,原文链接:Go’s Declaration Syntax

以下是译文:


前言

Go 的初学者可能会有这样的疑问:为什么 Go 的声明语法与传统的其他 C 家族编程语言不太一样?在这篇文章中我们会比较这两种不同的方式,并且也会解释为什么。

C 变量

首先,让我们说说 C 中的语法。C 使用了一种不寻常的巧妙的方法来实现声明语法。我们不是用什么特殊的语法来描述类型,而是写一个表达式,这个表达式包含两个部分:被声明的变量和变量的类型。

1
复制代码int x;

上面这行代码声明了一个类型为 int 的变量 x。一般来说,为了弄清楚如何编写新变量的类型,可以先写一个含基本类型变量的表达式,然后将基本类型放在左边,将表达式放在右边。

因此,下面的声明:

1
2
复制代码int *p;
int a[3];

描述的是 p 是一个指向 int 类型的指针,因为 ‘*p’ 的类型为 int。而 a 是一个 int 类型的数组,因为 ‘a[3]’ (这里请忽略下标的值 3,它只是说明数组的大小)的类型是 int。

那函数呢?在最开始的时候,C 的函数声明是将 参数的类型写在括号外面的,像这样:

1
2
3
4
复制代码int main(argc, argv)
int argc;
char *argv[];
{ /* ... */ }

再一次,我们可以看到 main 是一个函数,因为表达式 main(argc, argv) 返回了一个 int 类型的值。现在大家比较习惯写成这样:

1
复制代码int main(int argc, char *argv[]) { /* ... */ }

但是基本的结构还是一样的。

对于简单的类型来说这种巧妙的语法思想是能很好工作的,但是一旦类型变得复杂就会令人感到困惑了。非常经典的一个例子就是声明一个函数指针。遵循着规则,你得到了下面的这种写法:

1
复制代码int (*fp)(int a, int b);

fp 是一个指向函数的指针,因为如果你写一个表达式 (*fp)(a, b) 你会调用函数并得到一个 int 类型的值。那如果 fp 的其中一个入参它本身也是一个函数呢?

1
复制代码int (*fp)(int (*ff)(int x, int y), int b)

这就变得开始难以阅读了。

当然,我们可以在声明一个函数的时候去掉参数名,那么 main 函数可以声明成:

1
复制代码int main(int, char *[])

让我们回想一下,argv 是这样声明的,

1
复制代码char *agrv[]

通过把变量名放在中间来声明类似 char *[] 这样类型的时候其实是令人困惑的。

然后我们再来看看如果我们将入参变量名去掉的情况下 fp 函数的声明是怎么样的:

1
复制代码int (*fp)(int (*)(int, int), int)

无论将变量名放在内部的哪里都不那么清晰明了。对于第一个入参:

1
复制代码int (*)(int, int)

我想这不太容易能一眼看出是在声明一个指向函数的指针。再进一步,如果我们的返回值也是一个函数指针呢?

1
复制代码int (*(*fp)(int (*)(int, int), int))(int, int)

这根本就看不清声明出来的 fp 到底是个啥玩意。。。

你自己也可以构造出更多这类详细的例子,但是这些都说明了 C 的声明语法可能引入的一些困难。

不过还有一点需要提出。因为类型和声明的语法是相同的,所以解析中间类型的表达式是很困难的。这就是为什么 C 的类型转换总是用括号括起来:

1
复制代码(int)M_PI

Go 语法

非 C 家族的编程语言通常使用不同的声明类型的语法:变量名通常放在前面,然后紧跟着一个冒号。因此我们上面的例子就变成了这样:

1
2
3
复制代码x: int
p: pointer to int
a: array[3] of int

这些声明是明确的,如果从左往右读你会发现也是详细的。Go 语言从中得到了启发,但为了简洁起见,删除了冒号和一些关键字:

1
2
3
复制代码x int
p *int
a [3]int

这个例子中 [3]int 与如何在表达式中使用 a 这两者似乎没有直接的对应。(后面一小节中我们会讲到指针的。)你可以通过单独的语法来获得清晰的结果。

现在让我们考虑下函数。让我们把这个声明写成 Go 的形式,尽管在 Go 中真正的 main 函数是没有入参的:

1
复制代码func main(argc int, argv []string) int

表面上这和 C 语言并没什么不同,除了将字符数组改成了字符串形式。但是从左往右读起来却很顺畅:

函数 main 需要传入一个整型和字符串切片并且返回一个整型。(译者注:直到译者看到这篇文章,译者才发现原来这么写读起来竟这么顺畅。。。)

即便舍去变量名还是很明确——因为对于类型声明上没有位置的变化,所以也没有什么困惑。

1
复制代码func main(int, []string) int

这种从左到右的风格有一个优点:就算类型变得越来越复杂,这种方式还是表现得很得当。

举个声明函数变量的例子(类似在 C 语言中的函数指针):

1
复制代码f func(func(int, int) int, int) int

或者如果 f 返回的也是一个函数(译者注:边写边读你会再次惊讶于这丝滑般的顺畅感。。。):

1
复制代码f func(func(int, int) int, int) func(int, int) int

从左到右依然读起来很顺畅,并且当变量名被声明的时候也很明显。

类型和表达式的语法的不同点使得在 Go 中编写和调用闭包是那么的简单:

1
复制代码sum := func(a, b int) int { return a + b } (3, 4)

指针

指针这家伙总是表现得“与众不同”一点。观察下数组和切片,举个例子,Go 的类型语法将方括号放在类型的左边,但是赋值表达式语法却是将其放在表达式的右边:

1
2
复制代码var a []int
x = a[1]

为了让大家有一种熟悉的感觉,Go 的指针同样延续 C 语言中的 * 符号,但是我们不能简单的将指针类型也反转一下。所以指针使用方式如下:

1
2
复制代码var p *int
x = *p

我们不能简单粗暴地改成这样:

1
2
复制代码var p *int
x = p*

因为后缀 会与乘法的 相混淆。那或许我们可以使用 ^,举个例子:

1
2
复制代码var p ^int
x = p^

但同样的这个符号也已经有其他含义了,类型和表达式在前缀后缀的问题上总是在许多方面使事情复杂化。举个例子,

1
复制代码[]int("hi")

这是一种写法,但一旦以 * 打头就必须用括号将其包住:

1
复制代码(*int)(nil)

如果我们愿意放弃 * 作为指针语法,那么这些括号就不是必要的了。(译者注:但还能有更好的指针语法吗。。。)

所以 Go 的指针语法与熟悉的 C 语言是类似的,但这个关联也意味着我们不得不使用括号来消除语法中的类型和表达式之间的差异。

总体而言,我们相信 Go 的类型语法比 C 的要更容易理解,尤其是当事情变得复杂的时候。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

快速实现业务需求:不重复扣费

发表于 2017-11-30

docstore是一种创新的文档数据库,帮助开发者更快地完成复杂的业务需求。这个例子里,我们来看这样的一个需求:

  • 根据单号进行扣费
  • 对于重复的单号,不重复扣费

实现代码在这里:v2pro/quokka

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
复制代码docstore.Entity("Account", `
struct Doc {
1: i64 amount
2: string account_type
}
`).Command("create", `
function handle(doc, req) {
doc.amount = req.amount;
doc.account_type = req.account_type;
return {};
}
`, `
struct Request {
1: i64 amount
2: string account_type
}
struct Response {
}
`).Command("charge", `
function handle(doc, req) {
if (doc.account_type == 'vip') {
if (doc.amount - req.charge < -10) {
throw 'vip account can not below -10';
}
} else {
if (doc.amount - req.charge < 0) {
throw 'normal account can not below 0';
}
}
doc.amount -= req.charge;
return {remaining_amount: doc.amount};
}
`, `
struct Request {
1: i64 charge
}
struct Response {
1: i64 remaining_amount
}
`)

struct Doc 定义的是文档自身的格式,用的是thrift IDL定义语言。然后定义了两个操作,create(开户)和charge(扣费)。每个操作都定义自己的Request和Response的格式。实际的业务逻辑是用javascript写的。

调用这个docstore的代码是这样的:

1
2
3
4
5
6
7
8
9
10
11
复制代码func Test_charge_idempotence(t *testing.T) {
should := require.New(t)
execAndExpectSuccess(t, "http://127.0.0.1:9865/docstore/Account/create",
"EntityId", "123", "CommandRequest", runtime.NewObject("amount", 100, "account_type", "vip"))
resp := execAndExpectSuccess(t, "http://127.0.0.1:9865/docstore/Account/charge",
"EntityId", "123", "CommandId", "xcvf", "CommandRequest", runtime.NewObject("charge", 10))
should.Equal(90, jsoniter.Get(resp, "data", "remaining_amount").ToInt())
resp = execAndExpectSuccess(t, "http://127.0.0.1:9865/docstore/Account/charge",
"EntityId", "123", "CommandId", "xcvf", "CommandRequest", runtime.NewObject("charge", 10))
should.Equal(90, jsoniter.Get(resp, "data", "remaining_amount").ToInt())
}

实际也就是执行 HTTP POST,去调用 javascript 定义的 handler。

通过这个及其简单的例子,我们可以看到docstore的一些优点:

  • 业务逻辑不用考虑并发,底层框架保证了所有的command对于一个entity来说是串行执行的。
  • 只需要写javascript的handler,以及定义schema。然后自动就有http的数据接口。避免了crud数据服务的重复开发。
  • 框架支持了CommandId,只要CommandId相同,保证了不会重复被执行,而且返回之前的返回值。也就是原生支持了幂等性。

参见:创新的主存储方案

参见:我们需要什么样的数据库

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

设计模式之建造者模式

发表于 2017-11-30
  1. 名词解释

将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。

比如一台电脑包括主机、显示器、键盘等外设,这些部件组成了完整的一台电脑。如何将这些部件组装成一台完整的电脑并返回给用户,这是建造者模式需要解决的问题。建造者模式(builder)又称为生成器模式,从名词就可以看出,它是一种较为复杂、使用频率也相对较低的创建型模式。建造者模式为客户端返回的不是一个简单的产品,而是一个由多个部件组成的复杂产品。

  1. 建造者模式UML图

builder

builder模式

上图中包含了建造者模式的四个主要角色:

  • Builder(抽象建造者):它为创建一个产品Product对象的各个部件指定抽象方法,在该接口中一般声明两类方法,一类方法是buildPartX(),它们用于创建复杂对象的各个部件;另一类方法是getResult(),它们用于返回复杂对象。Builder既可以是抽象类,也可以是接口。
  • ConcreteBuilder(具体建造者):它实现了Builder抽象方法,实现各个部件的具体构造和装配方法,定义并明确它所创建的复杂对象,也可以提供一个方法返回创建好的复杂产品对象。依赖于Product。
  • Product(产品角色):它是被构建的复杂对象,包含多个组成部件,具体建造者创建该产品的内部表示并定义它的装配过程。
  • Director(指挥者):指挥者又称为导演类,它负责安排复杂对象的建造次序,指挥者与抽象建造者之间存在关联关系,可以在其construct()建造方法中调用建造者对象的部件构造与装配方法,完成复杂对象的建造。客户端一般只需要与指挥者进行交互,在客户端确定具体建造者的类型,并实例化具体建造者对象(也可以通过配置文件和反射机制),然后通过指挥者类的构造函数或者Setter方法将该对象传入指挥者类中。
  1. 建造者模式实现

下面代码基于Java实现的一个建造者模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
复制代码@data
class Product {
private String name;
private String type;
public void showProduct(){
System.out.println("名称:"+name);
System.out.println("型号:"+type);
}
}
//抽象类
abstract class Builder {
public abstract void buildPart(String arg1, String arg2);
public abstract Product getProduct();
}
//具体建造者
class ConcreteBuilder extends Builder {
private Product product = new Product();

public Product getResult() {
return product;
}

public void buildPart(String arg1, String arg2) {
product.setName(arg1);
product.setType(arg2);
}
}
// 指挥者
public class Director {
private Builder builder;

public Director(Builder builder) {
this.builder=builder;
}

public void setBuilder(Builder builder) {
this.builder=builer;
}
public Product construct(){
builder.buildPart("lenvono","Y470");
return builder.getResult();
}
}
//客户端调用
public class Client {
public static void main(String[] args){
Builder builder = new ConcreteBuilder();
Director director = new Director(builder);
Product product = director.construct();
product1.showProduct();
}
}

四个角色都包含在上面的实现中,客户端进行调用,对于客户端而言,只需关心具体的建造者即可。在指挥者类中可以注入一个抽象建造者类型的对象,其核心在于提供了一个建造方法construct(),在该方法中调用了builder对象的构造部件的方法,最后返回一个产品对象。

  1. 与工厂模式区别

建造者模式的优点是封装性好,且易于扩展。上面提到,对于客户端而言,只需关心具体的建造者即可。使用建造者模式可以优先的封装变化,product和builder比较稳定,主要的业务逻辑封装在控制类中对整体可取得比较好的稳定性。如需扩展,只需要加一个新的建造者,对之前代码没有影响。

与工厂模式相比,建造者模式一般用来创建更为复杂的对象,因为对象的创建过程更为复杂,因此将对象的创建过程独立出来组成一个新的类——指挥者类。也就是说,工厂模式是将对象的全部创建过程封装在工厂类中,由工厂类向客户端提供最终的产品;而建造者模式中,建造者类一般只提供产品类中各个组件的建造,而将具体建造过程交付给指挥者类。由指挥者类负责将各个组件按照特定的规则组建为产品,然后将组建好的产品交付给客户端。

  1. 总结

本文讲解了设计模式中的建造者模式,大的分类属于创建型模式。首先介绍了建造者模式的概念;然后给出了建造者模式的类图,并对其中涉及到的四个角色进行了解释;又给出了基于Java实现的代码;最后简单说了下其优点,与工厂模式的区别。建造者模式主要适用于创建一些复杂的对象,这些对象的内部组成构件间的建造顺序是稳定的,但是对象的内部组成构件面临着复杂的变化。


参考

  1. 《java与模式》阎宏

  2. 23种设计模式(4):建造者模式

    aoho wechat 欢迎您扫一扫上面的微信公众号,aoho求索,订阅我的博客! 坚持原创技术分享,您的支持将鼓励我继续创作!

    赏
    aoho WeChat Pay
    微信打赏

    aoho Alipay
    支付宝打赏

  • 本文作者: aoho
  • 本文链接: blueskykong.com/2017/02/26/…
  • 版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 许可协议。转载请注明出处!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

设计模式之建造者模式

发表于 2017-11-30
  1. 名词解释

将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。

比如一台电脑包括主机、显示器、键盘等外设,这些部件组成了完整的一台电脑。如何将这些部件组装成一台完整的电脑并返回给用户,这是建造者模式需要解决的问题。建造者模式(builder)又称为生成器模式,从名词就可以看出,它是一种较为复杂、使用频率也相对较低的创建型模式。建造者模式为客户端返回的不是一个简单的产品,而是一个由多个部件组成的复杂产品。

  1. 建造者模式UML图

builder

builder

上图中包含了建造者模式的四个主要角色:

  • Builder(抽象建造者):它为创建一个产品Product对象的各个部件指定抽象方法,在该接口中一般声明两类方法,一类方法是buildPartX(),它们用于创建复杂对象的各个部件;另一类方法是getResult(),它们用于返回复杂对象。Builder既可以是抽象类,也可以是接口。
  • ConcreteBuilder(具体建造者):它实现了Builder抽象方法,实现各个部件的具体构造和装配方法,定义并明确它所创建的复杂对象,也可以提供一个方法返回创建好的复杂产品对象。依赖于Product。
  • Product(产品角色):它是被构建的复杂对象,包含多个组成部件,具体建造者创建该产品的内部表示并定义它的装配过程。
  • Director(指挥者):指挥者又称为导演类,它负责安排复杂对象的建造次序,指挥者与抽象建造者之间存在关联关系,可以在其construct()建造方法中调用建造者对象的部件构造与装配方法,完成复杂对象的建造。客户端一般只需要与指挥者进行交互,在客户端确定具体建造者的类型,并实例化具体建造者对象(也可以通过配置文件和反射机制),然后通过指挥者类的构造函数或者Setter方法将该对象传入指挥者类中。
  1. 建造者模式实现

下面代码基于Java实现的一个建造者模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
复制代码@data
class Product {
private String name;
private String type;
public void showProduct(){
System.out.println("名称:"+name);
System.out.println("型号:"+type);
}
}

//抽象类
abstract class Builder {
public abstract void buildPart(String arg1, String arg2);
public abstract Product getProduct();
}

//具体建造者
class ConcreteBuilder extends Builder {
private Product product = new Product();

public Product getResult() {
return product;
}

public void buildPart(String arg1, String arg2) {
product.setName(arg1);
product.setType(arg2);
}
}

// 指挥者
public class Director {
private Builder builder;

public Director(Builder builder) {
this.builder=builder;
}

public void setBuilder(Builder builder) {
this.builder=builer;
}
public Product construct(){
builder.buildPart("lenvono","Y470");
return builder.getResult();
}
}
//客户端调用
public class Client {
public static void main(String[] args){
Builder builder = new ConcreteBuilder();
Director director = new Director(builder);
Product product = director.construct();
product1.showProduct();
}
}

四个角色都包含在上面的实现中,客户端进行调用,对于客户端而言,只需关心具体的建造者即可。在指挥者类中可以注入一个抽象建造者类型的对象,其核心在于提供了一个建造方法construct(),在该方法中调用了builder对象的构造部件的方法,最后返回一个产品对象。

  1. 与工厂模式区别

建造者模式的优点是封装性好,且易于扩展。上面提到,对于客户端而言,只需关心具体的建造者即可。使用建造者模式可以优先的封装变化,product和builder比较稳定,主要的业务逻辑封装在控制类中对整体可取得比较好的稳定性。如需扩展,只需要加一个新的建造者,对之前代码没有影响。

与工厂模式相比,建造者模式一般用来创建更为复杂的对象,因为对象的创建过程更为复杂,因此将对象的创建过程独立出来组成一个新的类——指挥者类。也就是说,工厂模式是将对象的全部创建过程封装在工厂类中,由工厂类向客户端提供最终的产品;而建造者模式中,建造者类一般只提供产品类中各个组件的建造,而将具体建造过程交付给指挥者类。由指挥者类负责将各个组件按照特定的规则组建为产品,然后将组建好的产品交付给客户端。

  1. 总结

本文讲解了设计模式中的建造者模式,大的分类属于创建型模式。首先介绍了建造者模式的概念;然后给出了建造者模式的类图,并对其中涉及到的四个角色进行了解释;又给出了基于Java实现的代码;最后简单说了下其优点,与工厂模式的区别。建造者模式主要适用于创建一些复杂的对象,这些对象的内部组成构件间的建造顺序是稳定的,但是对象的内部组成构件面临着复杂的变化。

订阅最新文章,欢迎关注我的公众号

微信公众号

微信公众号


参考

  1. 《java与模式》阎宏
  2. 23种设计模式(4):建造者模式

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

从0到1优雅的实现PHP多进程管理 业务场景 目的 分析 建

发表于 2017-11-30
1
2
3
4
5
6
7
8
复制代码                       _        
| |
_ __ __ _ _ __ _ _| |_ ___
| '_ \ / _` | '__| | | | __/ _ \
| | | | (_| | | | |_| | || (_) |
|_| |_|\__,_|_| \__,_|\__\___/ .TIGERB.cn

An object-oriented multi process manager for PHP

业务场景

在我们实际的业务场景中(PHP技术栈),我们可能需要定时或者近乎实时的执行一些业务逻辑,简单的我们可以使用unix系统自带的crontab实现定时任务,但是对于一些实时性要求比较高的业务就不适用了,所以我们就需要一个常驻内存的任务管理工具,为了保证实时性,一方面我们让它一直执行任务(适当的睡眠,保证cpu不被100%占用),另一方面我们实现多进程保证并发的执行任务。

目的

综上所述,我的目标就是:实现基于php-cli模式实现的master-worker多进程管理工具。其次,“我有这样一个目标,我是怎样一步步去分析、规划和实现的”,这是本文的宗旨。

备注:下文中,父进程统称为master,子进程统称为worker。

分析

我们把这一个大目标拆成多个小目标去逐个实现,如下:

  • 多进程
    • 目的:一个master fork多个worker
    • 现象:所有worker的ppid父进程ID为当前master的pid
  • master控制worker
    • 目的:master通知worker,worker接收来自master的消息
  • master接收信号
    • 目的:master接收并自定义处理来自终端的信号

多进程

PHP fork进程的方法 pcntl_fork, 这个大家应该有所了解,如果不知道的简单google/bing一下应该很容易找到这个函数。接着FTM, 我们看看pcntl_fork这个函数的使用方式大致如下:

1
2
3
4
复制代码$pid = pcntl_fork(); // pcntl_fork 的返回值是一个int值
// 如果$pid=-1 fork进程失败
// 如果$pid=0 当前的上下文环境为worker
// 如果$pid>0 当前的上下文环境为master,这个pid就是fork的worker的pid

接着看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码$pid = pcntl_fork();	
switch ($pid) {
case -1:
// fatal error 致命错误 所有进程crash掉
break;

case 0:
// worker context
exit; // 这里exit掉,避免worker继续执行下面的代码而造成一些问题
break;

default:
// master context
pcntl_wait($status); // pcntl_wait会阻塞,例如直到一个子进程exit
// 或者 pcntl_waitpid($pid, $status, WNOHANG); // WNOHANG:即使没有子进程exit,也会立即返回
break;
}

我们看到master有调用pcntl_wait或者pcntl_waitpid函数,为什么呢?首先我们在这里得提到两个概念,如下:

  • 孤儿进程:父进程挂了,子进程被pid=1的init进程接管(wait/waitpid),直到子进程自身生命周期结束被系统回收资源和父进程采取相关的回收操作
  • 僵尸进程:子进程exit退出,父进程没有通过wait/waitpid获取子进程状态,子进程占用的进程号等描述资源符还存在,产生危害:例如进程号是有限的,无法释放进程号导致未来可能无进程号可用

所以,pcntl_wait或者pcntl_waitpid的目的就是防止worker成为僵尸进程(zombie process)。

除此之外我们还需要把我们的master挂起和worker挂起,我使用的的是while循环,然后usleep(200000)防止CPU被100%占用。

最后我们通过下图(1-1)来简单的总结和描述这个多进程实现的过程:

master控制worker

上面实现了多进程和多进程的常驻内存,那master如何去管理worker呢?答案:多进程通信。话不多说google/bing一下,以下我列举几种方式:

  • 命名管道: 感兴趣
  • 队列: 个人感觉和业务中使用redis做消息队列思路应该一致
  • 共享内存: 违背“不要通过共享内存来通信,要通过通信来实现共享”原则
  • 信号: 承载信息量少
  • 套接字: 不熟悉

所以我选择了“命名管道”的方式。我设计的通信流程大致如下:

  • step 1: 创建worker管道
  • step 2: master写消息到worker管道
  • step 3: worker读消息从worker管道

接着还是逐个击破,当然话不多说还是google/bing一下。posix_mkfifo创建命名管道、fopen打开文件(管道以文件形式存在)、fread读取管道、fclose关闭管道就呼啸而出,哈哈,这样我们就能很容易的实现我们上面的思路的了。接着说说我在这里遇到的问题:fopen阻塞了,导致业务代码无法循环执行,一想不对啊,平常fopen普通文件不存在阻塞行为,这时候二话不说FTM搜fopen,crtl+f页面搜“block”,重点来了:

fopen() will block if the file to be opened is a fifo. This is true whether it’s opened in “r” or “w” mode. (See man 7 fifo: this is the correct, default behaviour; although Linux supports non-blocking fopen() of a fifo, PHP doesn’t).

翻译下,大概意思就是“当使用fopen的r或者w模式打开一个fifo的文件,就会一直阻塞;尽管linux支持非阻塞的打开fifo,但是php不支持。”,得不到解决方案,不支持,感觉要放弃,一想这种场景应该不会不支持吧,再去看看posix_mkfifo,结果喜出望外:

1
2
3
4
5
6
复制代码<?php
$fh=fopen($fifo, "r+"); // ensures at least one writer (us) so will be non-blocking
stream_set_blocking($fh, false); // prevent fread / fwrite blocking
?>

The "r+" allows fopen to return immediately regardless of external writer channel.

结论使用“r+”,同时我们又知道了使用stream_set_blocking防止紧接着的fread阻塞。接着我们用下图(1-2)来简单的总结和描述这个master-worker通信的方式。

master接收信号

最后我们需要解决的问题就是master怎么接受来自client的信号,google/bing结论:

1
复制代码master接收信号 -> pcntl_signal注册对应信号的handler方法 -> pcntl_signal_dispatch() 派发信号到handler

如下图(1-3)所示,

其他

接着我们只要实现不同信号下master&worker的策略,例如worker的重启等。这里需要注意的就是,当master接受到重启的信号后,worker不要立即exit,而是等到worker的业务逻辑执行完成了之后exit。具体的方式就是:

1
复制代码master接收reload信号 -> master把reload信号写worker管道 -> worker读取到reload信号 -> worker添加重启标志位 -> worker执行完业务逻辑后且检测到重启的标志位后exit

建模

上面梳理完我们的实现方式后,接着我们就开始码代码了。码代码之前进行简单的建模,如下:

进程管理类Manager

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码- attributes
+ master: master对象
+ workers: worker进程对象池
+ waitSignalProcessPool: 等待信号的worker池
+ startNum: 启动进程数量
+ userPasswd: linux用户密码
+ pipeDir: 管道存放路径
+ signalSupport: 支持的信号
+ hangupLoopMicrotime: 挂起间隔睡眠时间
- method
+ welcome: 欢迎于
+ configure: 初始化配置
+ fork: forkworker方法
+ execFork: 执行forkworker方法
+ defineSigHandler: 定义信号handler
+ registerSigHandler: 注册信号handler
+ hangup: 挂起主进程

进程抽象类Process

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码- attributes
+ type: 进程类型 master/worker
+ pid: 进程ID
+ pipeName: 管道名称
+ pipeMode: 管道模式
+ pipeDir: 管道存放路径
+ pipeNamePrefix: 管道名称前缀
+ pipePath: 管道生成路径
+ readPipeType: 读取管道数据的字节数
+ workerExitFlag: 进程退出标志位
+ signal: 当前接受到的信号
+ hangupLoopMicrotime: 挂起间隔睡眠时间
- method
+ hangup: 挂起进程(抽象方法)
+ pipeMake: 创建管道
+ pipeWrite: 写管道
+ pipeRead: 读管道
+ clearPipe: 清理管道文件
+ stop: 进程exit

master实体类MasterProcess

1
2
3
4
复制代码- attributes
+
- method
+ hangup: 挂起进程

worker实体类MasterProcess

1
2
3
4
复制代码- attributes
+
- method
+ dispatchSig: 定义worker信号处理方式

最后我们需要做的就是优雅的填充我们的代码了。

最后

项目地址 github.com/TIGERB/naru…

个人知识还有很多不足,如果有写的不对的地方,希望大家及时指正。

THX~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

在 IBM Cloud 上自动测试并部署 PHP 应用程序

发表于 2017-11-30

在 IBM Cloud 上自动测试并部署 PHP 应用程序

使用集成了 GitHub 的 IBM Cloud Continuous Delivery 工具链和 Travis CI,自动将您的最新代码库部署到 IBM Cloud 环境

回想我最初作为开发人员时,测试应用程序通常涉及到将应用程序代码文件压缩成一个 ZIP 归档文件,或者将它们放在一个共享的网络文件服务器上,并将它们传输到测试环境,解压或复制到正确的位置,在必要时编译它们,然后手动运行各种测试。毋须说,这是一个耗时、令人沮丧且容易出错的过程,许多人都不喜欢这一过程。

到现在,这些操作变得容易得多。借助容易获得的新持续交付工具,只要发生了更改,就可以自动且安全地从存储库拉取应用程序代码并部署到测试或生产环境。无需手动更改 ZIP 文件、网络共享或配置文件,而且很容易检查部署的当前状态或将它回滚到以前的版本。

“IBM Cloud Continuous Delivery
提供了一种安全、自动化的方式来一致且无错误地构建、测试并部署您的 PHP 应用程序。
”

IBM Cloud® Continuous Delivery 服务很有用,因为借助该服务,您可以进行相应设置,让 GitHub 存储库中每个新提交或成功的拉取请求都能自动构建、测试您修改的应用程序代码,并将其部署到 IBM Cloud。让我展示一下如何做。

获取 GitHub 上的样本应用程序

您需要做的准备工作

本文的目的是向您 — 一位希望向托管在 IBM Cloud 上的 PHP 应用程序添加持续交付功能的 PHP 开发人员 — 介绍 IBM Cloud Continuous Delivery 服务,以及它可以添加到 PHP 应用程序中的功能。我将使用一个基本 PHP 应用程序(包括单元测试)和一个简单的工具链将应用程序从 GitHub 移动到 IBM Cloud。在此场景中,目标 IBM Cloud 部署将始终反映 PHP 应用程序开发分支的已通过测试的最新版。

本文中列出的基本流程如下:

  1. 每次在 GitHub 源存储库中创建一个拉取请求,Travis CI 都会使用 PHPUnit 自动测试源代码,以确保所有单元测试都会通过。
  2. 如果单元测试通过,拉取请求会手动或自动合并到存储库的开发分支中。这次合并会自动触发使用 IBM Cloud Continuous Delivery 工具链,在 IBM Cloud 上实现对该应用程序的一次新部署。
  3. 如果单元测试失败,则不会合并拉取请求,IBM Cloud 上的当前部署不会受到影响。

这个简单流程应能让您的 IBM Cloud 部署始终拥有最新的开发分支。了解此流程后,您应该能顺利调整它来满足更复杂的需求。

请注意,本文中描述的方法主要用于确保您的测试和开发环境能够始终反映正在开发的应用程序代码分支中的最新更改。尽管您也可以使用这里描述的方法来自动部署到应用程序的生产环境,但如果没有事先考虑所涉及的内在风险(以及随后采取适当的控制措施来管理这些风险),不希望您这么做。

以下是您需要做的准备工作:

  • 一个 IBM Cloud 帐户(在此处注册)
  • 一个 GitHub 帐户(在此处注册)和一个包含一些 PHPUnit 单元测试的 PHP 应用程序
  • 基本熟悉 PHP 和 PHPUnit
  • 一个包含 Apache 的本地或托管 PHP 开发环境(支持 mod_rewrite 和 .htaccess 文件)
  • Composer(PHP 依赖项管理器)
  • Cloud Foundry
    命令行工具
  • Git 命令行工具(或其他任何 Git 客户端)
  • 一个文本编辑器或 IDE

1
创建 PHP 应用程序


要执行本文中的步骤,您的本地开发环境中需要有一个包含 PHPUnit 单元测试的 PHP 应用程序;下面给出了创建这样一个应用程序的操作说明。如果您的本地环境或 GitHub 存储库中已经有一个满足这些要求的 PHP 应用程序,那么您可以跳过这一步。如果没有,可以按照这些操作说明进行操作,或克隆 GitHub 上的示例应用程序源代码。

首先来了解一下这个 Composer 配置文件,应该将它保存到 $APP_ROOT/composer.json($APP_ROOT 是您的项目目录)中。处理器类和名称空间尚未存在,但您很快将创建它们。

1
2
3
4
5
6
7
8
9
10
11
12
13
swift复制代码{
    "require": {
          "slim/slim": "^3.8",
          "slim/php-view": "^2.2",
          "phpunit/phpunit": "^5.7",
          "ext-mbstring": "*"
    },
    "autoload": {
        "psr-4": {
            "App\\Processor\\": "src/app/processors"    
        }
    }
}

使用 Composer 通过以下命令安装配置文件

1
cmake复制代码shell> php composer.phar install

接下来,设置应用程序的主要控制脚本。此脚本会加载 Slim 框架并初始化 Slim 应用程序。它还包含对应用程序的每个路径的回调,每个回调定义了在该路径与某个传入请求匹配时要执行的代码。在 $APP_ROOT/public/index.php 上创建一个包含以下内容的脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
php复制代码<?php
use \Psr\Http\Message\ServerRequestInterface as Request;
use \Psr\Http\Message\ResponseInterface as Response;
 
// autoload files
require '../vendor/autoload.php';
 
// configure Slim application instance
// initialize application
$app = new \Slim\App();
 
// initialize dependency injection container
$container = $app->getContainer();
 
// configure processor in DI container
$container['processor'] = function ($container) {
  return new \App\Processor\MyProcessor();
};
 
// "hello world" controller
$app->get('/hello[/{name}]', function (Request $request, Response $response) {
    $name = $request->getAttribute('name');
    if ($name) {
      $processedName = $this->processor->process($name);
      $response->getBody()->write("Hello, $processedName!");
    } else {
      $response->getBody()->write("Sorry, we haven't been introduced yet.");   
    }
    return $response;
});
 
$app->run();

此代码是 Slim 框架的官方“Hello
world”脚本
的变体。它会设置应用程序路径 /hello/$NAME,并在您浏览到此路径并提供一个名称时,通过一条欢迎消息来问候您。如果没有提供名称,则会显示一条礼貌性的错误。

该版本的脚本向 Slim 依赖注入容器添加了一个处理器类,以便对输入名称执行一些基本的字符串处理。包含这个处理器类,只是为了向 PHPUnit 单元测试添加一些有价值的内容,它看起来类似于(将它保存到 $APP_ROOT/src/app/processors/MyProcessor.php):

1
2
3
4
5
6
7
8
9
php复制代码<?php
namespace App\Processor;
class MyProcessor
{
  public function process($string)
  {
    return ucfirst(strtolower($string));
  }
}

最后,将推荐的
Apache 重写规则
添加到 $APP_ROOT/public/.htaccess:

1
2
3
4
5
6
apache复制代码<IfModule mod_rewrite.c>
  RewriteEngine On
  RewriteCond %{REQUEST_FILENAME} !-f
  RewriteCond %{REQUEST_FILENAME} !-d
  RewriteRule ^ index.php [QSA,L]
</IfModule>

通过访问 http://localhost/hello/james(根据需要更新此 URL,以反映您的本地 Apache 环境)来确定应用程序工作正常,您会看到类似的输出:

图 1. 示例 PHP 应用程序

示例 PHP 应用程序2
创建 PHPUnit 测试套件和 GitHub 存储库


现在,在 $APP_ROOT/tests/AppTest.php 中添加一些供您的应用程序使用的真实和虚拟的 PHPUnit 测试:

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
php复制代码<?php
use PHPUnit\Framework\TestCase;
class AppTest extends TestCase
{
    public function testTrue()
    {
        $value = true;
        $this->assertTrue($value);
    }
 
    public function testEmpty()
    {
        $arr = [];
        $this->assertEmpty($arr);
    }
     
    public function testProcessor()
    {
        $processor = new \App\Processor\MyProcessor();
        $this->assertEquals('John', $processor->process('john'));
        $this->assertEquals('John', $processor->process('JOHN'));
    }
 
}
?>

在 $APP_ROOT/phpunit.xml 上创建一个包含以下内容的最小化 PHPUnit 配置文件:

1
2
3
4
5
6
7
8
9
10
11
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
 
<phpunit bootstrap="vendor/autoload.php" colors="true" convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="true" stopOnFailure="false" syntaxCheck="false">
 
    <testsuites>
        <testsuite name="Test suite">
            <directory>./tests/</directory>
        </testsuite>
    </testsuites>
 
</phpunit>

运行 PHPUnit 并确保您的所有测试都通过:

图 2. PHPUnit 测试运行器

PHPUnit 测试运行器
最后,通过 GitHub 网站创建一个存储库,使用类似下面代码清单中所示的命令将您的代码推送到该存储库。 创建两个分支:master 用于生产代码库,dev-master 用于开发代码库。

1
2
3
4
5
6
7
livecodeserver复制代码shell> git init
shell> git add .
shell> git commit -a
shell> git remote add origin https://github.com/$GITHUB-USERNAME/$GITHUB-REPOSITORY-NAME.git
shell> git push origin master
shell> git checkout -b dev-master
shell> git push origin dev-master

记下 GitHub 存储库 URL,因为下一步需要使用它。

图 3. GitHub 存储库 URL

GitHub 存储库 URL3
部署到 IBM Cloud


现在是时候将应用程序从您的本地开发环境部署到 IBM Cloud 了。如果您因为 GitHub 中已经有一个 PHP 应用程序存储库而跳过了前两步,请在继续后面的步骤之前,将您的存储库克隆到本地开发环境。

创建应用程序清单文件,请记得通过附加一个随机字符串(比如您姓名的首字母)使用唯一的主机和应用程序名称。

1
2
3
4
5
6
7
8
yaml复制代码---
applications:
- name: myapp-dev-[initials]
memory: 256M
instances: 1
host: myapp-dev-[initials]
buildpack: https://github.com/cloudfoundry/php-buildpack.git
stack: cflinuxfs2

还必须配置该 buildpack 来使用该应用程序的 public/ 目录作为 Web 服务器目录。创建一个包含以下内容的 $APP_ROOT/.bp-config/options.json 文件:

1
2
3
4
5
6
json复制代码{
    "WEB_SERVER": "httpd",
    "COMPOSER_VENDOR_DIR": "vendor",
    "WEBDIR": "public",
    "PHP_VERSION": "{PHP_56_LATEST}"
}

现在,继续将应用程序推送到 IBM Cloud:

1
2
3
stata复制代码shell> cf api https://api.ng.bluemix.net
shell> cf login
shell> cf push

您现在应该能在 http://myapp-dev-[initials].mybluemix.net/hello/james 上浏览该应用程序,并查看您在第 1 步(您在这一步创建了 PHP 应用程序)结束后看到的欢迎消息。如果没有看到该消息,可以使用调试日志查明是哪里出了错。

4
创建 IBM Cloud Continuous Delivery 工具链


从 IBM Cloud 控制台内,单击左上角的菜单图标,然后选择 Services -> DevOps 子菜单。在服务细节页面上,单击 Create a Toolchain 并选择 Simple Cloud Foundry 工具链。

图 4. 创建工具链

创建工具链
点击查看大图

这个工具链模板已预先配置为使用 GitHub 和 Delivery Pipeline 服务。在工具链配置页面上,为工具链输入一个名称。选择 GitHub 图标并授权 IBM Cloud 访问您的 GitHub 帐户。完成这个一次性授权流程后,选择一个现有存储库并输入来自第 2 步(您在这一步创建了 GitHub 存储库)的 GitHub 源存储库 URL。

图 5. 存储库集成

存储库集成
点击查看大图

单击 Create 创建工具链。现在,将使用一些预先设置的默认值对工具链和 Delivery Pipeline 服务进行初始化,并向您的 GitHub 存储库添加一个 webhook,以便自动向 Delivery Pipeline 服务告知对存储库的更改。您应该在 IBM Cloud 仪表板中的工具链概述页面中看到类似图 6 的信息。

图 6. 工具链概述

工具链概述
单击 Delivery Pipeline 图标,您应该看到该管道已配置了两个阶段:构建和部署。

图 7. 工具链阶段

工具链阶段
尽管这些阶段对大部分设置都使用了合理的默认值,但您仍需要按照以下步骤检查它们一次:

  1. 在构建阶段面板中,单击 Configure Stage 图标。在 Input 选项卡中,确认 GitHub 存储库细节是正确的,而且 Branch 字段被设置为 dev-master。
    图 8. 构建阶段配置

构建阶段配置
2. 在部署阶段面板中,单击 Configure Stage 图标。在 Jobs 选项卡中,确认部署细节(地区、组织、空间和应用程序名称)是正确的。请注意,应用程序名称将被存储库中的应用程序清单文件中的设置覆盖。

图 9. 部署阶段配置

部署阶段配置
点击查看大图

保存对这两个阶段执行的更改。

测试该工具链,方法是向 GitHub 存储库的 dev-master 分支提交一个细微更新 — 例如,将欢迎消息从“Hello…”更改为“Aloha…”。如果工具链已正确配置,IBM Cloud Delivery Pipeline 服务应该会检测到更改,从而会按顺序触发构建和部署阶段。在构建应用程序的新版本并将它部署到 IBM Cloud 时,您应该能够实时看到更改。

图 10. 工具链实战

工具链实战
如果发生错误,可以使用每个阶段中的 View logs and history 链接获取包含错误细节的日志。

图 11. 工具链日志

工具链日志
此刻,您的工具链已正常运行,向指定 GitHub 存储库和分支的每次提交都会触发在 IBM Cloud 上实现应用程序代码的一次新部署。但是您的工作还未完成!

5
配置 Travis CI 来构建和测试拉取请求


如果您在一个分布式团队工作,可能您不会直接提交到一个主要存储库分支。大部分时候,您将会创建手动或自动进行评审、测试和合并的拉取请求。

这时就需要使用 Travis CI。Travis CI 是一个可用于所有 GitHub 存储库的持续集成引擎,可以通过配置它,为每个新拉取请求构建和运行测试(包括 PHPUnit 测试),并将构建结果标记为通过或失败。然后,评审者将拉取请求手动合并到存储库中,或者可以配置 Travis CI,通过自定义脚本自动执行此任务。

您肯定能看出我在何处实现此功能。您可以执行设置,使通过拉取请求向您的应用程序建议的更改可由 Travis CI 自动测试,然后合并到您的 dev-master 分支中。请记住,此分支已由 Delivery Pipeline 服务监控,所以每次成功合并都会导致第 4 步中配置的工具链自动将应用程序的新版本部署到 IBM Cloud。

首先使用您的 GitHub 帐户凭证登录到 Travis CI 网站,授予 Travis CI 访问您的 GitHub 帐户的权限。

图 12. Travis CI-GitHub 集成

Travis CI-GitHub 集成
从您的 Travis CI 概况页面,为您的 GitHub 存储库打开 Travis CI 引擎。为存储库启用 Travis CI 后,在 Travis CI 界面中单击该存储库的 Settings 图标来配置构建设置。

图 13. Travis CI 引擎激活

Travis CI 引擎激活
在 General 部分,确保 Build pull request
updates
已开启。

图 14. Travis CI 配置

Travis CI 配置
在 $APP_ROOT/.travis.yml 上创建一个包含以下内容的最小化 Travis CI 配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
nestedtext复制代码language: php
 
php:
  - 5.6
     
install:
  - composer install --no-interaction
 
branches:
  only:
    - "dev-master"
     
script:
  - vendor/bin/phpunit

简单讲,此配置文件告诉 Travis CI 仅构建 dev-master 分支,并使用 vendor/bin/phpunit 脚本来测试构建结果。如果成功,Travis CI 会在通过测试时标记该构建,以便可以手动合并它。

在 GitHub 存储库的 dev-master 分支中创建一个新的拉取请求来测试配置。Travis CI 应该注册该拉取请求,并构建和测试它。您将能够从 Travis CI 概况页面跟踪构建的进度。

图 15. Travis CI 实战

Travis CI 实战
如果对构建进行编码并通过测试,可将它合并到您的 GitHub 存储库的 dev-master 分支中。

图 16. GitHub 合并操作

GitHub 合并操作
IBM Continuous Delivery 工具链现在将接管工作,并将修改后的代码库部署到 IBM Cloud,就像我在第 4 步(您在这一步创建了 IBM Cloud Continuous Delivery 工具链)中描述的那样。

如果您没有耐心,不想等待手动合并成功的构建结果,或者如果您有源源不断的拉取请求,可以让 Travis CI 自动将成功的构建结果合并到存储库中。这种无人值守、自动化的合并具有一定风险,所以您应该只考虑对您的开发分支这么做。

Travis CI 不包含任何内置的自动合并功能,所以您需要一个自定义脚本来完成此工作。有关自动合并脚本的示例,请参阅本文末尾的链接。您也可以创建自己的脚本来完成此任务和其他相关任务,比如发布通知或更新项目公告板。

创建自定义脚本后,请记住,您必须通过在 $APP_ROOT/.travis.yml 文件中添加一个与下面的示例类似的代码块,告诉 Travis CI 在构建成功时调用该脚本:

1
2
3
stata复制代码after_success: 
  - chmod +x ./do-merge.sh
  - ./do-merge.sh

结束语

在本文中,我演示了如何使用 IBM Cloud Continuous Delivery 服务让您的 IBM Cloud 应用程序部署与您的 GitHub 代码存储库保持同步。通过提供现成的工具链模板,该模板被配置为可以与GitHub即开即用,IBM Cloud Continuous Delivery 提供了一种安全的自动化方式来一致且无误地构建、测试并部署您的 PHP 应用程序。

IBM Cloud 提供了一些已准备好的工具集成,所以您也可以将您的工具链与其他流行工具相结合,比如使用 Slack 提供通知或使用 JIRA 跟踪问题。您可以自行尝试并查看结果!

致谢

感谢 Chris Down 提供了他的 Travis 自动合并脚本,这是在撰写本文时使用的脚本的修改版。

相关主题

  • Slim 文档
  • 针对 Apache 的 Slim 配置
  • Composer 文档
  • GitHub 文档
  • Travis CI 文档
  • 分支自动合并脚本示例(来自 Chris Down)
  • 另一个分支自动合并脚本示例(来自 Brian Broll)
  • 拉取请求的自动合并脚本示例
  • 在 IBM Cloud 上调试 PHP 错误

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Mysql 8 新特性 window functions 有

发表于 2017-11-30
  1. 问题

Mysql 8.0.2 中新增加了一个主要功能 - 窗口函数 window function

这个功能具体是解决什么问题?下面先看一个SQL查询的场景,看一下平时我们是怎么做的,然后再看一下如何使用窗口函数来更方便的解决

(1)准备测试表和数据

建一个简单的电影信息表,字段有:

  • ID
  • release_year(发行年份)
  • category_id(所属分类ID)
  • rating(评分)
1
2
3
4
5
6
复制代码CREATE TABLE films (
 id int(11),
 release_year int(11),
 category_id int(11),
 rating decimal(3,2)
)

插入测试数据

1
2
3
4
5
6
7
复制代码insert into films2 values
(1,2015,1,8.00),
(2,2015,2,8.50),
(3,2015,3,9.00),
(4,2016,2,8.20),
(5,2016,1,8.40),
(6,2017,2,7.00);

整体形式如下

(2)查询需求

查询每一年中的平均评分,要求每条记录后面都显示当年的平均评分

例如 2015 年,有3条记录,8.00, 8.50, 9.00 的平均分是 8.5,2016年有2条记录,平均分是 8.3,2017年有1条记录,平均分为 7.00

最终结果的形式如下:

我们可以使用子查询来计算各年的平均分,然后使用 join 把结果连接回去

1
2
3
4
5
6
7
8
9
10
11
复制代码SELECT
 f.id, f.release_year,
 f.rating, years.year_avg
FROM films f
LEFT JOIN (
 SELECT f.release_year,
       AVG(rating) AS year_avg
 FROM films f
 GROUP BY f.release_year
) years
ON f.release_year = years.release_year

是不是有点复杂,下面看下窗口函数的处理方式

  1. window functions 的解决方案

什么是 window functions

window functions 是对一组数据进行计算,与使用 group by 时不同,不会进行单行的结果输出,而是与每条记录相关联

语法示例:

1
2
3
复制代码SELECT
function_name OVER ( window_definition )
FROM (...)

window_definition 是定义要计算的记录集合,就像是一个小窗口,在整体数据集合上显示出一部分

function_name 指定了对于窗口中的数据集合执行什么计算

回头看下上面的那个查询,需要计算每部电影所在年份的所有电影平均评分,下面使用窗口函数来处理

1
2
3
4
5
6
复制代码SELECT
 f.id, f.release_year,
 f.category_id, f.rating,
 AVG(rating) OVER
 (PARTITION BY release_year) AS year_avg
FROM films f

window_definition 部分使用了 PARTITION BY 从句,它告诉数据库把结果数据集合分割成更小的部分,把 release_year 相同的放在一起,函数 AVG(rating) 会对每个窗口数据进行计算,然后把结果放到每一行中

查询示例1

计算每部电影在所属年份中的评分排行位置

查询语句

1
2
3
4
5
6
复制代码SELECT
f.id, f.release_year,
f.category_id, f.rating,
RANK() OVER (PARTITION BY release_year
       ORDER BY rating DESC) AS year_rank
FROM films f

window_definition 部分使用 PARTITION BY 来根据 release_year 划分窗口,使用 ORDER BY 在窗口内排序

RANK() 函数可以返回一行数据在本窗口中的位置

查询结果

查询示例2

查看每部电影在总排行榜中的位置

查询语句

1
2
3
4
5
6
复制代码SELECT
f.id, f.release_year,
f.category_id, f.rating,
RANK() OVER (ORDER BY rating DESC)
       AS general_rank
FROM films f order by id

主语句中的 order by 保证了整个数据集的排序

window_definition 中没有使用 PARTITION BY,那么就是把整个结果集合当做一个窗口,ORDER BY 对窗口中的数据根据 rating 做降序排序,把得分最高的放前面

RANK() 函数取得每条记录在窗口中的位置

查询结果

  1. 小结

窗口函数是 Mysql 8.0.2 中的高级特性,可以方便的执行聚合计算,而不用对结果集进行实际的聚合,大大增加了灵活性、可读性,更便于维护

有兴趣的同学可以提前学习下,可以使用 Mysql 8.0.2 的 Docker 镜像,很方便

参考资料:

  • mysqlserverteam.com/mysql-8-0-2…
  • https://dev.mysql.com/doc/refman/8.0/en/window-functions-usage.html

原 文:性能与架构

作 者:杜亦舒

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…923924925…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%