Teroy’s Blog


  • 首页

  • 归档

GORM时区配置

发表于 2018-12-01 |

近期有同事反馈,测试环境的数据库存储的datetime与北京时间有差异,看到这个问题我的第一反应可能是时区配置有问题。首先检查了一下mysql服务器的时区配置,配置的确实是北京时间,所以就排除了mysql服务器的问题,接下来再检查了一下业务服务器的时区配置,发现配置的UTC时间,由此可以初步确定是业务服务器的问题。通常遇到这个问题最快的解决方案是将服务器的时区改为北京时间,然而仔细思考一番,假如真的有服务器时区配置不一致的问题,难道就只能通过事后发现的修复方案,没有预防方案?
我们使用golang作为后端语言,数据库操作层面使用了gorm框架,查阅了gorm相关技术文档,发现gorm是支持时区配置的,那么这个gorm时区配置是否真的可以作为一个预防方案?实践出真知,接下来将通过一些样例代码来验证这个问题。

准备

(1) 将本地环境的时区调为UTC
(2) 使用mysql数据库,建立一张测试表

1
2
3
CREATE TABLE test(
d datetime NOT NULL
);

样例代码:入库时间使用本地时区

1
2
3
4
5
6
7
8
9
10
conStr := "root:123456@tcp(192.168.3.93:33061)/zxd?charset=utf8mb4&parseTime=true&loc=Local"
db, err := gorm.Open("mysql", conStr)
if err != nil {
log.Fatalf("%v", err)
}
db.LogMode(true)
db = db.Exec("INSERT INTO test (d) VALUES (?)", time.Now())
if db.Error != nil {
log.Fatalf("%v", db.Error)
}

gorm配置的时区为使用本地时区,通过查询数据库表可以观察得到,生成的时间为本地当前时间。

样例代码:使用配置时区

1
2
3
4
5
6
7
8
9
10
conStr := "root:123456@tcp(192.168.3.93:33061)/zxd?charset=utf8mb4&parseTime=true&loc=Asia%2fShanghai"
db, err := gorm.Open("mysql", conStr)
if err != nil {
log.Fatalf("%v", err)
}
db.LogMode(true)
db = db.Exec("INSERT INTO test (d) VALUES (?)", time.Now())
if db.Error != nil {
log.Fatalf("%v", db.Error)
}

注意 loc=Asia%2fShanghai,gorm配置链接字符串要求对Loc做UrlEncode处理,这里配置成固定的北京时间,通过查询数据库可以验证入库的时间正确性。
翻看gorm的源码,可以看到,拼接sql的时候,gorm使用配置的时区对time.Time类型的变量做了一次转换,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
case time.Time:
paramTypes[i+i] = byte(fieldTypeString)
paramTypes[i+i+1] = 0x00

var a [64]byte
var b = a[:0]

if v.IsZero() {
b = append(b, "0000-00-00"...)
} else {
b = v.In(mc.cfg.Loc).AppendFormat(b, timeFormat)
}

paramValues = appendLengthEncodedInteger(paramValues,
uint64(len(b)),
)
paramValues = append(paramValues, b...)

这里需要注意一个细节,就是配置的时区只对time.Time类型有效,假如我们使用了time.Now().Format()进行格式化当前时间,则会使用本地时间,因为作为一个sting占位符,gorm并不会做任何处理。

golang代码实践指南

发表于 2018-07-27 |

本篇文章是基于链接的精简翻译,作为Effective Go的补充,主要罗列了一系列golang语言的编程实践规范,对于开发者而言具有很好的参考价值。

Gofmt

gofmt是一款golang的代码格式化组件,推荐在保存代码前运行gofmt来自动格式化代码,这样子能让团队项目有近乎一致的代码风格,从而提高代码的可读性。

Comment Sentences

文档注释代码必须是一条完整的语句,以句号结尾,以被描述的内容(方法名、类型名、变量名、常量名、包名)开头,例如:

1
2
3
4
5
// Request represents a request to run a command.
type Request struct { ...

// Encode writes the JSON encoding of req to w.
func Encode(w io.Writer, req *Request) { ...

Contexts

context.Context可以用来携带信息,通常用来传递请求周期变量。如果有需要用到它,要将其变成方法的第一个参数(推荐命名为ctx),而不是将ctx变成变量存储在一个结构体类型中。

Declaring Empty Slices

当定义一个空的切片的时候,通常有两种方式:

1
2
3
4
5
// 方式1
var t []string

// 方式2
t := []string{}

方式1中的t为nil,而方式2中的t则是一个非nil的零切片,在功能上两者是没有区别的(len(t)、cap(t)、append(t, “”)),但是在序列化成json的时候就有区别,方式1的t会为null,方式2的t会为[],要特别注意这个点。

Crypto Rand

当需要生成随机数的时候,不要使用包 math/rand ,因为其计算得到的是伪随机数,推荐使用包 crypto/rand ,示例用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import (
"crypto/rand"
"fmt"
"log"
)

func Key() string {
buf := make([]byte, 16)
_, err := rand.Read(buf)
if err != nil {
log.Fatalf(err) // out of randomness, should never happen
}
return fmt.Sprintf("%x", buf)
}

Doc Comments

包中所有对外可用的变量、方法、结构体、常量都应该有注释,一些重要的不对外暴露的变量、方法、结构体也要声明注释。

Don’t Panic

不要使用panic来处理错误,应该使用error返回。

Error Strings

错误描述不要使用大写字母开头(除非是专有名词或缩写),不要用任何标点符号结尾,这样在记录log的时候可以组成一条完整的语句。

Goroutine Lifetimes

当大量启用协程的时候,应该尽量让代码保持简单,使得协程的生命周期很清晰。

Handle Errors

当函数返回error的时候,不要使用_吞掉error,每个error都应该被处理。

Imports

当引入的包出现命名有重复时候,优先对本项目的包起别名;引入的包要分组,组之间留一空行,golang的标准包默认要放在开头位置,例如:

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

import (
"fmt"
"hash/adler32"
"os"

"github.com/foo/bar"
"rsc.io/goversion/version"
)

In-Band Errors

1
2
3
4
5
// (不推荐做法) Lookup returns the value for key or "" if there is no mapping for key.
func Lookup(key string) string

// (推荐做法) Lookup returns the value for key or ok=false if there is no mapping for key.
func Lookup(key string) (value string, ok bool)

Initialisms

对于名词缩写词,命名的时候要使用全部大写或者全部小写,例如”URL”作为命名的一部分的时候,应命名为”urlPony或者URLPony”,而不是”UrlPony”。

Named Result Parameters

1
2
3
// Location returns f's latitude and longitude.
// Negative values mean south and west, respectively.
func (f *Foo) Location() (lat, long float64, err error)

适当地给多返回参数命名,可以提高代码的可读性。

Package Comments

包名的注释通常注释在go文件的首行,注释中间不留空行。如果对”package main”注释,通常使用编程生成的二进制文件的名称开头,例如seedgen二进制文件的注释:

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
// Binary seedgen ...
package main

or

// Command seedgen ...
package main

or

// Program seedgen ...
package main

or

// The seedgen command ...
package main

or

// The seedgen program ...
package main

or

// Seedgen ..
package main

Package Names

对于包内的变量命名,要尽量精简,无需要带包的名称前缀,例如包chubby:

1
2
3
4
5
6
7
package chubby

// (不推荐)
var ChubbyFile *os.File

// (推荐)
var File *os.File

对于包的命名则要避免使用太泛的名字,例如common、util、api、types、interfaces等。

Pass Values

当函数内部无需使用到参数的指针类型,同时传入的参数不是一个大结构体的时候,尽量不要使用指针类型参数传递。

Receiver Names

1
2
3
4
5
6
7
type UserService struct{}

// (不推荐) 使用me、this、self作为receiver名称
func (me *UserService) AddUser() {}

// (推荐) 直接使用结构体的首字母简写
func (u *UserService) AddUser() {}

Receiver Types

receiver该使用值类型还是指针类型的一些指导原则:
(1) 当receiver是map或者func或者chan的时候,使用值类型;
(2) 当receiver是slice且方法内部无需reslice的时候,使用值类型;
(3) 当方法内部需要改动receiver的内容的时候,使用指针类型;
(4) 当receiver是结构体,同时结构体中包含有sync.Mutex等同步变量的时候,使用指针类型,从而避免同步变量被复制而失效的问题;
(5) 当receiver是大数组或者大结构体的时候,使用指针类型,可以提高性能;
(6) 当然,出现意见分歧的时候,那就使用指针类型。

dep使用指南

发表于 2018-05-01 |

什么是dep

dep是一款golang的包管理工具,通过将工程中相关依赖的包加载到工程目录下的vendor文件夹的形式,实现包的版本管理。

为什么要使用dep

要解释这个问题,首先要解释为什么要出现vendor目录。在没有vendor目录之前,golang在查找工程中引入的第三方包的时候,会到环境上配置的$GOPATH目录下查找对应目录的包,意味着我们在使用一个包的时候,必须先用 go get 命名下载到$GOPATH中,但是我们知道第三方包会有版本的概念,假如A工程引用了P包的v1.0.0版本,B工程引用了P包的v2.0.0版本,但是每个$GOPATH下面对应的P包只能存放一份,所以每次需要编译不同的AB不同的工程的时候,需要到$GOPATH手工切换第三方包的版本,整个流程就会变得很繁琐。

为了解决这个问题,golang在go1.5版本引入了vendor目录的概念,golang在查找工程中引入的第三方包的时候会优先到当前工程中的vendor目录中查找包,我们只需要将需要用到的包按目录规则存放即可,这样包的版本问题就迎刃而解了。

而dep就是用来做vendor包管理的工具,提供了初始化vendor包和更新vendor包的命令,以及对vendor中的第三方包的约束配置,能帮助开发者更好地管理工程中的包依赖。

如何安装dep

官方文档 提供了详细的安装指南,这里也提供一种我个人比较推荐的做法:
首先先将$GOPATH/bin目录加入到机器的$PATH中,然后使用 go get 命令下载dep包即可,命令执行如下:

1
go get -v github.com/golang/dep/cmd/dep

如何使用dep在工程中初始化依赖

安装上一步安装好dep后,即可开始使用dep命令了。对于一个之前没引入vendor包的项目来说,需要执行一下的命名做初始化:

1
2
3
// -v表示详细打印整个初始化过程,由于很多golang相关的包都需要翻墙下载,网络会存在
// 不稳定的情况,所以最好是带上这个指令打印详细输出信息
dep init -v

执行命令成功后,我们会看到工程目录下除了多了vendor目录外,还会生成两个文件:Gopkg.toml和Gopkg.lock,Gopkg.toml文件是用来做包的约束说明(例如指定某个依赖包的版本),Gopkg.lock文件则是描述了当前vendor目录下所有包的版本信息。

关于Gopkg.toml的配置说明

Gopkg.toml提供了丰富的包约束规则配置,具体介绍如下:
(1) require/ignored 配置

1
2
3
// 示例
require = ["github.com/garyburd/redigo", "github.com/bradfitz/gomemcache"]
ignored = ["github.com/go-ini/ini"]

当包路径出现在require节点中,则表示不管当前工程中有没有用到这个包都将这个包加入到vendor中,这个特性在有些场景下很会用,比如做一些开发脚手架的时候,某些包在外来有极大可能会被引用,则先把它们加入到require节点中,避免未来用到的时候再执行一次包引入;

出现在ignored中的包路径,则表示不管当前工程有没有用到这个包都将不把这个包加入到vendor中,这个特性的用途主要在当出现间接引用的时候,比如A->B->C,如果确定C在当前的工程中没被执行到,则可以考虑将C不加入到vendor包,减少编译生成的文件大小(不过这个做法其实并不常见,建议谨慎使用)。

(2) prune 配置

1
2
3
4
// 示例
[prune]
go-tests = true
unused-packages = true

prune的中文意思是精简的意思,默认生成的Gopkg.toml文件通常会带上示例的配置,go-tests = true 表示vendor目录将不包含包中_test结尾的go文件,unused-packages = true 表示没有被用到的包将不包含在vendor目录中(例如有外部包结构如下:github.com/teroy/stark/a和github.com/teroy/stark/b,假如工程中只引入了a包,则b包不会出现在vendor目录中)。

注意如果要改变精简条件,无需将false赋值给对应项的配置,而是直接将对应项的配置在配置文件中移除。

(3) constraint 配置

1
2
3
4
5
// 示例
[[constraint]]
name = "github.com/teroy/common"
branch = "master"
source = "git@github.com:teroy/common.git"

constraint用来配置当前工程的直接依赖包约束。constraint提供了较多的配置项,具体罗列如下:

1
2
3
4
5
6
7
8
9
[[constraint]]
// 包名(节必须配置)
name = "github.com/teroy/common"
// branch、version和revision 只允许配置一个(必须配置)
branch = "master" // 表示当前包的获取分支
version = "1.0.0" // 表示获取包的版本,"1.0.0"表示获取范围为>="1.0.0", <"2.0.0",参考:(https://golang.github.io/dep/docs/Gopkg.toml.html#version)
revision = "12766cc1f36d22b11887b42740d06ff966534e0a" // 表示指定具体的git的commit SHA1 通常情况下不使用,只有在确定version没有适合的版本的情况下使用
// 源地址(可选配置)
source = "git@github.com:teroy/common.git" // 默认使用http的形式,也可以配置ssh的形式

(4) ovverride 配置

1
2
3
4
5
// 示例
[[override]]
name = "github.com/teroy/common"
branch = "master"
source = "git@github.com:teroy/common.git"

override与constraint相同的配置约束,唯一不同的是override使用来配置当前工程的间接依赖包约束。

dep ensure 命令说明

dep ensure -v 执行的过程中,会做以下两个检查:检查当前工程所需要的包是否和.lock文件中指定的包一致以及检查.toml文件中的约束条件是否和.lock文件中指定的包一致。假如出现了不一致的情况,则dep会执行工程的所有依赖包的重新加载,所以这个命令适合于工程中引入了新的包(不存在于.lock文件中配置的包)的时候执行,或者修改了.toml配置的时候执行。注意只要.lock文件指定的包的版本是在.toml约束范围内,不管.toml文件如何变动,执行这条命令都不会更新包到约束范围内的最新版本。

那如果我们要更新某个包到约束范围内的最新版本要怎么做呢?答案就是执行 dep ensure -v -update 具体的包,当然也可以直接执行 dep ensure -v -update 更新所有的包,但通常这会比较费时,一般也不推荐这种方式。

ZipArchive扩展遇到的问题

发表于 2017-05-02 |

近期已经遇到了两个关于php扩展zip压缩的问题,而且问题都属于比较难以排查的,因为都跟环境有关系,所以这里总结记录一下。

问题1:php版本问题

1
2
3
4
5
$zip = new \ZipArchive;
$res = $zip->open($destination, \ZIPARCHIVE::OVERWRITE);
if ($res == true) {
// todo
}

在php5.5下,以上代码是可以正常运行,当$destination这个文件路径不存在的时候会创建,如果存在则重新生成并覆盖。我们线上的部署环境是用php5.5,我本机使用php5.6,然而这段代码在php5.6下运行就会报错,当$destination不存在时候它无法生成新文件!
stackoverflow一番之后,顺利找到问题的根源,php5.6已经不再支持这个特性,为了解决这个问题必须使用下面的代码:

1
2
3
4
5
6
$zip = new \ZipArchive;
// 同时指定\ZIPARCHIVE::CREATE 解决php5.6的不兼容问题
$res = $zip->open($destination, \ZIPARCHIVE::CREATE | \ZIPARCHIVE::OVERWRITE);
if ($res == true) {
// todo
}

问题2:zip扩展版本问题

1
2
3
4
5
6
7
$zip = new \ZipArchive;
$res = $zip->open($destination, \ZIPARCHIVE::CREATE | \ZIPARCHIVE::OVERWRITE);
if ($res === true) {
$fileName = iconv("UTF-8", "GBK//IGNORE", $fileName);
$zip->addFile($exportFilePath, $fileName);
$zip->close();
}

这里可以看到fileName变量做了一次编码转换,因为当文件名称为中文的时候,压缩包解压出来的文件名会变为乱码,所以这里做了一次特殊转码。
然而,同样的代码到了另外一台服务器上运行,会发现压缩包下载下来双击打开里面的文件会被隐藏了!一步一步调试代码后,发现只要移除那一句对fileName变量的编码转换,压缩就会变得正常了,中文也不会出现乱码,但是有些环境不转码又会出现乱码,这让人非常怀疑是扩展版本问题。执行php –ri zip命令查看扩展的版本,确实也发现了两个环境的扩展版本不一样,乱码的环境zip版本是1.11.0,而另一个环境是1.13.0,这样就基本定位到问题了,1.11.0版本的zip扩展对中文的文件名称支持地不好,在1.13.0版本就修复了这个问题,然而如果代码中对文件名称做了特殊转码,在1.13.0版本又会引发新的问题,压缩包打开不显示文件。
最终的解决方案是升级所有环境的zip扩展到1.13.0版本,然后移除掉对file转码的代码。

linux目录文件介绍

发表于 2017-04-05 |
目录说明
/home用户主文件夹,当创建一个普通用户的时候,一般都会生成一个同名的文件夹在home目录下。
/rootroot用户的主文件夹。
/etc配置文件目录。注意:/etc/init.d为service服务的脚本存放路径。
/bin系统的可执行文件存放目录,该文件下的命令可被所有用户使用,这个目录一般我们是不需要去动它。
/sbin这个目录下的命令root用户可以用来设置系统环境,一般我们也不需要去动它。
/usrusr并非user的简写,而是Unix Software Resource,所有系统默认的软件都会放置到/usr下,有些类似于windows系统的C:\Windows\和C:\Program files\的结合体。
/opt第三方软件包安装目录。我们用yum安装的包可能会安装到该目录下,注意与/usr/local目录区分,/usr/local通常是用来作为下载源码编译安装的目录。
/proc这个目录属于虚拟文件系统,它的所有数据都是放在内存中,本身不占用任何硬盘资源,用来存储进程相关的信息。eg:/proc/进程id/status 查看某个进程的相关信息
/tmp临时文件夹,所有的用户都有rwx权限,linux会定期自动清理tmp下面的文件,所以这个目录一般用来存放一些不重要的临时文件。
/var主要存储变动频繁数据的目录。eg:/var/cache 应用程序的缓存目录;/var/log 日记目录;/var/run 应用程序的pid存放目录

composer使用实践

发表于 2017-03-11 |

composer作为php领域最为流行的包依赖管理器,基本也成为每个phper的必备技能之一,composer官方也提供了很好的中文学习资料,整体而言也是很容易上手的。自己在使用composer的过程中也遇到了概念不清晰和实践方面的问题,所以用这篇文章来记录总结一下。

问题1:该不该将vendor文件夹包裹在源代码管理器(git、svn等)中?

vendor文件夹通常都会比较大,不将其放在源代码管理器中,可以减少仓库的体积和历史提交差异记录,加快代码签入签出的速度,所以一般不将vendor包放在仓库中。但是需要注意一些问题,因为有时候composer安装的过程中需要执行一些检验token的操作,在持续集成的工具中要预留一些解决方案。

问题2:composer提供了install和update命令来维护vendor包,那么这两个命令有什么区别呢?

与composer明确相关的有三个东西:composer.json文件、composer.lock文件和vendor文件夹。后两者都由composer自行管理,所以与开发者密切相关的就是composer.json文件。
要回答这个问题,前提就是要区分应用场景。假如一个项目之前完全没用过composer,现在刚刚创建好了composer.json文件,那么用install和update命令都可以完成同样的工作:生成包源文件集合vendor和composer.lock锁文件。两个命名之间没有本质区别。
假如项目已经使用了composer,现在修改了composer.json文件,想修改包的版本或者增加第三方包的引入,那么必须使用update命令,因为install命令会根据composer.lock来生成包源文件,它是不会去识别composer.json的改动的。
上述的场景中,用update命令就可以满足了,那么install命令应该在什么场景下使用呢?答案就是在生产环境中。我们可以在开发环境中维护好composer.lock文件,并将.lock文件提交到代码仓库中,到了生产环境就只用install命令,根据.lock文件生成vendor包,就不会造成生产环境贸然更新了某个包与开发环境不一致的问题。

问题3:composer的包版本号提供了多种约定方式,那么在项目中最好用哪种方式呢?

composer官网明确介绍了包的约定方式:

  • 确切的版本号: 1.0.2
  • 范围: >=1.0 大于1.0的版本
  • 通配符: 1.0.* 1.0下的子版本
  • 赋值运算符: ~1.2 相当于范围>=1.2,<2.0

我们最常用的使用场景应该是在require下面包裹一个第三方组件进来,这时候我认为最好的方式是使用明确版本号的方式。但是为什么呢?理由很简单:保持第三方包的稳定。第三方包基本都会作版本更新,使用其他的约定方式则会存在一个问题:当前匹配的版本会变更。举个例子,假如我们用通配符的方式引入了monolog包(require {“monolog/monolog”:”*”}),现在项目中用的是1.0版本,隔了一阵子,monolog发布了2.0版本,而且与1.0版本的api有了明显的不兼容,我们在项目中执行了composer update命令,则会自动将版本更新为2.0版本,组件api改变了,原来的地方有可能就会出现报错。
至于如何知道第三方包的版本,我们可以直接在packagist搜索得到,可以自行选择引入的版本。

问题4:composer执行安装命令后一直停留在等待界面?

这里有些小技巧可以改进这个问题。首先就是命令的后面加上-vvv,就可以打印出具体的debug信息,实时地知道现在的运行情况;很多时候等待过久是因为遇到不翻墙下不了包的问题,这时可以通过设置包的源为中国镜像,具体可以参考这里。

yii框架遇到的数组引用循环问题

发表于 2017-02-25 |

目前所在的公司用的yii版本是2.0.3,最近上线了一个新功能后,有个地方突然出现了问题,出错的地方的代码简化如下:

1
2
3
4
5
6
$userIds = ['1', '2'];
foreach ($userIds as &$userId) {

}
$count = \common\entities\User::find()->where(['user_id' => $userIds])->count();
// var_dump($userIds);

这里是用了yii的orm查询了数据,但是最后发现$userIds数组的最后一个元素被串改了,导致接下来的逻辑就出现了错误。在我把foreach循环中的引用&去掉后,$userIds就没有被修改覆盖了,所以基本可以断定yii框架中应该是有地方改动了$userIds,同时也跟foreach循环使用&有关。调试进去yii的框架代码中,追踪到这个方法会引起变量的改动:

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
public function buildInCondition($operator, $operands, &$params)
{
if (!isset($operands[0], $operands[1])) {
throw new Exception("Operator '$operator' requires two operands.");
}

list($column, $values) = $operands;

if ($values === [] || $column === []) {
return $operator === 'IN' ? '0=1' : '';
}

if ($values instanceof Query) {
return $this->buildSubqueryInCondition($operator, $column, $values, $params);
}

$values = (array) $values;

if (count($column) > 1) {
return $this->buildCompositeInCondition($operator, $column, $values, $params);
}

if (is_array($column)) {
$column = reset($column);
}
foreach ($values as $i => $value) {
if (is_array($value)) {
$value = isset($value[$column]) ? $value[$column] : null;
}
if ($value === null) {
$values[$i] = 'NULL';
} elseif ($value instanceof Expression) {
$values[$i] = $value->expression;
foreach ($value->params as $n => $v) {
$params[$n] = $v;
}
} else {
$phName = self::PARAM_PREFIX . count($params);
$params[$phName] = $value;
$values[$i] = $phName;
}
}
if (strpos($column, '(') === false) {
$column = $this->db->quoteColumnName($column);
}

if (count($values) > 1) {
return "$column $operator (" . implode(', ', $values) . ')';
} else {
$operator = $operator === 'IN' ? '=' : '<>';
return $column . $operator . reset($values);
}
}

为了进一步验证问题,写了一段简化代码去模拟,最终顺利找到了问题所在:

1
2
3
4
5
6
7
8
9
10
11
12
function test($data) {
foreach ($data as $k => $v) {
$data[$k] = $k;
}
}

$data = ['1', '2'];
foreach ($data as &$item) {

}
test($data);
var_dump($data); // 输出:['1', 1]

查阅了官方文档中对foreach的使用描述,里面有谈到一个使用警告:数组最后一个元素的 $value 引用在 foreach 循环之后仍会保留,建议使用 unset() 来将其销毁。所以在使用foreach引用&变量的时候,最好能在循环后unset变量,避免不必要的问题。
yii框架开发组应该也意识到了这个问题,经过测试,在2.0.10版中这个问题已经被修复了。

php项目根据环境读取配置的方案

发表于 2017-01-06 |

通常我们会将不同环境的配置使用不同名称的文件区分开来,比如开发环境使用dev.config,线上环境使用prod.conf,来尽可能地减少混淆出错的可能性,但是我们的代码是同一套,这样就会带来一个问题:如何根据当前的部署环境读取合适的配置?针对这个问题,根据一些实战经验,这里谈一下自己总结的三种方案。

方案一:将环境配置文件从版本控制系统中忽略

我们可以在项目中定义一个文件env.php,并在env.php定义具体的环境变量,同时将这个env.php排除在版本控制器之外(使用git的话就使用gitignore配置排除),最后在项目的入口文件中引入这个env.php,这样每个环境就可以自行在env.php中配置。

1
2
3
// env.php文件
<?php
defined('PHP_ENV') or define('PHP_ENV', 'dev');

这个方案很灵活,但带来的一个问题是要管理的env.php可能会很多,因为一台机器上可能同时部署了好几个项目,每个项目都有一个env.php文件需要维护。

方案二:在web服务器中配置环境变量

php可以使用getenv()函数拿到环境变量,使用apache或者nginx可以在配置文件中定义这个环境变量,来看一个nginx配置环境变量的例子:

1
fastcgi_param  PHP_ENV dev;

这个方案不需要在代码中定义环境变量,但还是存在维护多个配置的问题,同时如果是运行控制台的php命令,不通过web服务器,则拿不到对应的环境变量。

方案三:定义linux服务器的环境变量

getenv()函数可以拿到linux配置的环境变量,通常可以在linux的启动文件/etc/profile中加入如下的命令配置:

1
export PHP_ENV='dev'

运行web项目,还需要在配置中加入环境变量的读取,这里以nginx+php-fpm为例,需要在php-fpm.conf增加如下的配置:

1
env[PHP_ENV] = $PHP_ENV //读取linux系统的环境变量

最后将. /etc/profile加入到php的启动脚本中去,就大功告成了。
这个方案应该是上述方案中可维护性最高的,因为配置的地方最少,但是这个方案也存在一个问题:不够灵活,假如我们的服务器资源吃紧,需要在同一台机器上面既部署开发环境的东西,也部署测试环境的东西,这个方案就不能采用。

小结

当然,上述三种方案并非是单选题,也可以组合起来用,例如方案二和方案三,当我们在web服务器和linux机器定义了相同的环境变量,则在web项目中会以web服务器的为基准,这样子组合起来使用,将可以应对更多的使用场景。

php7杂谈

发表于 2016-12-17 |

虽然php7在2015年未的时候已经正式发布了,但自己目前所经历过的两家公司都是用着5.x版本,不过,php7注定是未来的主流,最近自己也是开始关注这块,所以写下这篇博客来记录一些自己的理解和思考。

升级到php7

php7虽然做了大量的改动过,但同时也做了很多向下兼容的工作,所以升级到php7是没有太大问题的,当然其中也包括一些不兼容的点,具体可以参考这篇文章。我总体看了一遍,有些不兼容的改动基本是很“冷门”的,可能这个关于foreach的修改要特别注意一下:

1
2
3
4
5
// foreach的修改
$arr = [1, 2, 3];
foreach($arr as &$i) {
var_dump(current($i)); // php7输出1 1 1 php7之前输出2 3 false
}

php7的foreach遍历将不再改变数组的内部指针,所以如果以前的代码有在foreach中使用current得到遍历的下一个元素的,在升级之前需要重构掉代码。

php7新特性

关于php7新特性的介绍,可以参考官网的说明文档。总体而言,这次php7的升级重点并不是在语言特性上,所以语法糖并不多,相对而言也没有太多亮点的地方。

标量类型声明和返回类型声明

1
2
3
4
5
6
7
// 新增参数类型支持int、float、string和bool
function (int $i) {
}
// 返回类型支持
function (): array {
return [];
}

php是弱类型语言,弱类型语言向来被人诟病的其中一点是缺少“解析前检查”。函数的传参没有类型约束,意味着传入什么东西都可以被接受,直到了运行期问题才会被抛出,这很让人无能为力。
php7无疑强化了关于类型的输入和输出,语言也渐渐地严谨化和与强类型挂钩,这样带来的好处就是可以让问题暴露在解析前,让开发者能更早地知道代码的错误之处。我个人认为严谨化和强类型是未来语言发展的重要方向,宽松化和容错性高注定是要立下墓碑,所以php7的这个特性还是很切合时代的发展的。

null合并运算符

1
2
3
4
// php7之前的写法
$a = isset($_POST['a']) ? $_POST['a'] : 0;
// php7的写法
$a = $_POST['a'] ?? 0;

这个语法糖应该是php7所有新语法中最实用的。之前的代码确实存在太多isset加三目表达式的写法,新语法要节省多少敲击键盘的次数啊,“人生苦短,敲少代码”把。

组合运算符<=>

1
2
3
echo 1 <=> 1; // 0
echo 1 <=> 2; // -1
echo 2 <=> 1; // 1

这次推出了新的运算符<=>,现在我能想到的一个应用场景就是在数组排序中使用:

1
2
3
4
5
6
7
8
// php7之前的写法
usort($arr, function ($x, $y) {
return $x - $y;
});
// php7的写法
usort($arr, function ($x, $y) {
return $x <=> $y;
});

php7之前的写法基本也能满足,新特性并没有带来很好的体验提升,个人认为这个特性或许没有那么“必须”。

匿名类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
interface Logger {
public function log(string $msg);
}

class Application {
private $logger;

public function getLogger(): Logger {
return $this->logger;
}

public function setLogger(Logger $logger) {
$this->logger = $logger;
}
}

$app = new Application;
$app->setLogger(new class implements Logger {
public function log(string $msg) {
echo $msg;
}
});

通过new class来实例化一个匿名类,适用于替代一些“用后即焚”的完整类定义。当我看到这个的时候,第一反映是这个特性与单元测试的结合。我们知道单元测试强调快速运行,假如方法中存在调用服务类的地方,需要将服务类接口化并mock一个空实现,传统的实现就要定义一个空实现的类,而有了匿名类,就可以节省这个类,同时还可以支持不同的个性化需求。

通过define定义常量为数组类型

1
2
3
4
5
6
// php7的实现
define('ORDER', ['unpay' => 1, 'pay' => 2]);
echo ORDER['unpay'];
// php7之前的替代实现
define('ORDER_UNPAY', 1);
define('ORDER_PAY', 2);

php7之前的常量定义只能未int、float、string和bool值,现在就多了一个数组,可以将关联的元素集中化,以前的替代实现就需要数组的每个元素都定义为一个常量,现在就会方便一些。

使用use导入一组类

1
2
3
4
5
// php7之前的写法
use common\helper\A;
use common\helper\B as Be;
// php7的写法
use common\helper\{A, B as Be};

这个语法糖简化了导入同一个命名空间下类的写法,但我认为还是比较麻烦,需要指定具体的类,而不是像java那样直接导入命名空间,在代码中用到的类自动识别到命名空间中,不知未来php是否会往这个方向上靠。

php关于error_reporting的配置

发表于 2016-12-15 |

今天将代码上线到测试环境的,突然报错了,但在我本地自测的时候一直是没问题的,真让人感到奇怪,这段出错的代码大概写了如下的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class BaseTest
{
public function test()
{
return "test";
}
}

class Test extends BaseTest
{
public static function getName()
{
return parent::test();
}
}

在测试环境上调用BaseTest::getName()方法的时候,就会报一个Strict standards的错误。这里应该是自己的一个疏忽,在static方法中是不应该用parent的,但是居然在开发环境不报错!Strict standards应该是指严格标准的错误报告,第一反应就是去看本地php.ini的配置,发现自己本地的php.ini配置的error_reporting是E_ALL & ~E_DEPRECATED & ~E_STRICT,原来是这里将严格标准的错误报告给过滤了,结果出现了测试环境的错误。在看一下php.ini的描述,里面提到开发环境应该将error_reporting配置为E_ALL级别,而生产环境配置为E_ALL & ~E_DEPRECATED & ~E_STRICT,自己的开发环境装上php后就默认是去严格标准格式的,这个地方在配置开发环境的时候以后要必须注意了。

12
Teroy

Teroy

15 日志
10 标签
GitHub 邮箱
© 2018 Teroy