142016
 

用过PHP的人都知道,PHP处理JSON数据那是相当方便,json_encode和json_decode两个函数搞定一切。那么在Go中该怎么处理JSON呢?

一、encoding/json标准库

学习 json 库应该先了解 Go 中的 struct tag、reflect等知识。

1、概述

json包实现了json对象的编解码,参见RFC 4627。Json对象和go类型的映射关系请参见Marshal和Unmarshal函数的文档。
参见”JSON and Go”获取本包的一个介绍:http://golang.org/doc/articles/json_and_go.html 这是官方的一篇博文。

2、核心函数和类型

对于函数和类型,我们关注经常使用的。

1)Marshal 和 Unmarshal

这两个是最常使用的函数,也就是 json 对象的编解码。这两个函数的文档很长,详细解释了 Go 类型和 json 对象的映射关系等。映射关系整理如下:

bool, for JSON booleans
float64, for JSON numbers
string, for JSON strings
[]interface{}, for JSON arrays
map[string]interface{}, for JSON objects
nil for JSON null

详细的编码解码规则,文档上解释的很详细,这里说几个关键点:
①默认情况下,按照上面提到的映射进行解析;
②如果对象实现了 json.Marshaler/Unmarshaler 接口且不是 nil 指针,则调用对应的方法进行编解码;如果没有实现该接口,但实现了 encoding.TextMarshaler/TextUnmarshaler 接口,则调用该接口的相应方法进行编解码;
③struct 中通过 “json” tag 来控制相关编解码,后面通过示例说明;
④struct 的匿名字段,默认展开;可以通过指定 tag 来使其不展开;
⑤如果存在匿名字段,如果同级别有相同字段名,不会冲突,具体处理规则文档有说明;
⑥在解码时到struct时,会忽略多余或不存在的字段(包括不导出的),而不会报错;

另外注意,传递给 Unmarshal 的第二个参数必须是指针。

使用示例:

package main

import (
    "encoding/json"
    "fmt"
)

func main() {
    type Book struct {
        Name  string
        Price float64 // `json:"price,string"`
    }
    var person = struct {
        Name string
        Age  int
        Book
    }{
        Name: "polaris",
        Age:  30,
        Book: Book{
            Price: 3.4,
            Name:  "Go语言",
        },
    }

    buf, _ := json.Marshal(person)
    fmt.Println(string(buf))

    // Output:{"Name":"polaris","Age":30,"Price":3.4}
    // Book 中的 Name 被忽略了
}

如果不希望内嵌类型展开,只需加上 tag:

var person = struct {
        Name string
        Age  int
        Book `json:"Book"`
}

有时候,比如之前是 PHP(弱类型语言) 写的,Age 的值很可能是 “Age”:”30” 这种形式,现在改为用 Go 实现,为了兼容;或者返回给客户端 Price 这样的浮点值,可能会涉及精度问题,客户端只是单纯的展示,返回浮点值的字符串即可。针对这样的情况,只需要加上这样的 tag:`json:”,string” 即可。这里逗号后面的“string”是 tag option。

如果想忽略某个字段,加上`json:”-”`;如果在值为空时忽略,加上
omitempty option,如:`json:”,omitempty”`

在解码时,优先匹配 struct 导出字段的 tag,之后是 Field,最后是 Field 的各种大小写不明感的形式,如 Name,能匹配 NAME/NAme等等。

2)MarshalIndent 函数

该函数的功能和 Marshal一致,只是格式化 json,方便人工阅读。如上面例子使用该函数,MarshalIndent(person, “”, “\t”) 输出如下:

{
    "Name": "polaris",
    "Age": 30,
    "Price": 3.4
}

3)Encoder 和 Decoder

有时候,我们可能从 Request 之类的输入流中直接读取 json 进行解析或将编码的 json 直接输出,为了方便,标准库为我们提供了 Decoder 和 Encoder 类型。它们分别通过一个 io.Reader 和 io.Writer 实例化,并从中读取数据或写数据。

通过阅读源码可以发现,Encoder.Encode/Decoder.Decode 和 Marshal/Unmarshal 实现大体是一样;有一些不同点:Decoder 有一个方法 UseNumber,它的作用:默认情况下,json 的 number 会映射为 Go 中的 float64,有时候,这会有些问题,比如:

b := []byte(`{"Name":"polaris","Age":30,"Money":20.3}`)

var person = make(map[string]interface{})
err := json.Unmarshal(b, &person)
if err != nil {
    log.Fatalln("json unmarshal error:", err)
}

age := person["Age"]
log.Println(age.(int))

我们希望 age 是 int,结果 panic 了:interface conversion: interface is float64, not int.

我们改为 Decoder.Decode(用上 UseNumber) 试试:

b := []byte(`{"Name":"polaris","Age":30,"Money":20.3}`)

var person = make(map[string]interface{})

decoder := json.NewDecoder(bytes.NewReader(b))
decoder.UseNumber()
err := decoder.Decode(&person)
if err != nil {
    log.Fatalln("json unmarshal error:", err)
}

age := person["Age"]
log.Println(age.(json.Number).Int64())

我们使用了 json.Number 类型。

4)RawMessage 类型

该类型的定义是 type RawMessage []byte,可见保存的是原始的 json 对象,它实现了 Marshaler 和 Unmarshaler 接口,能够延迟对 json 进行解码。使用示例可以参考 http://docs.studygolang.com/pkg/encoding/json/#RawMessage上的例子。

5)其他函数或类型不常用(如错误类型等),在此不赘述,可以直接查阅官方文档。

二、实际应用中的问题

当客户端和服务器通讯使用 json 这种数据格式时,我们一方面会解码客户端的 json 数据,另一方面,需要对数据进行 json 编码,发送给客户端。

一般的,服务器发送给客户端的 json 数据,是通过 struct、[]struct 或 map[string]interface{} 等编码得到。这里除了上文说到的,可能需要对数值类型使用 string tag options 之外,对于 time.Time 类型(实现了Marshaler 接口),默认编码得到的时间格式是:RFC3339即2006-01-02T15:04:05Z07:00,很多时候客户端可能不希望得到这样的时间,他们更多时候只是需要一个可读的时间字符串,如 2006-01-02 15:04:05。对此,我们可以定义自己的类型 type OftenTime time:

func (self OftenTime) MarshalJSON() ([]byte, error) {
    t := time.Time(self)
    if y := t.Year(); y < 0 || y >= 10000 {
        return nil, errors.New("Time.MarshalJSON: year outside of range [0,9999]")
    }
    return []byte(t.Format(`"2006-01-02 15:04:05"`)), nil
}

func (this *OftenTime) UnmarshalJSON(data []byte) (err error) {
    t := time.Time(*this)
    return t.UnmarshalJSON(data)
}

另外,有一个坑,json 对象的 key 必须是字符串,所以 map[int]interface{} 在编码时会报错,错误是 json.UnsupportedTypeError.

对于接收客户端数据,进行 json 解码,遇到的问题可能比较多,特别是同时接收多种语言的数据,比如 PHP、Java 等。比如 b := []byte(`{“Name”:”polaris”,”Age”:30,”Money”:20.3}`),PHP 传递过来的可能是:b := []byte(`{“Name”:”polaris”,”Age”:”30″,”Money”:”20.3″}`),在使用 struct 接收数据时,对于 Age,如果是 int,我们可以直接定义为 int 类型,但如果是string,可以通过 string tag options 接收;但如果Age有时是 int, 有时是 string,就会出问题。最理想的情况,当然是不希望出现这种情况,但有一点,程序要保证出现这种情况时,不能 panic。

针对这种情况,有一种办法,是定义为 json.Number 类型。不过这样有两个问题:1)使用时不方便,需要调用 json.Number 的方法,而且它的方法返回值还得判断错误;2)编码 json.Number 时,是数值,如果客户端需要字符串,就不行了。

在实际应用中,我就遇到了上面的问题,于是,自己写了一个 json 解析,能支持自动类型转换。代码开源在 github:https://github.com/polaris1119/jsonutils

三、性能问题

很明显,json 的编解码,使用了 Go 的反射功能,所以,性能自然不是太好,正因为如此,有了 ffjson、easyjson 之类的开源库(在 github 上),它们的原理是通过 go generate 根据 struct 生成相应的代码,避免反射。如果你对性能要求比较高,但又不想使用msgpack/pb/thrift 之类的,那么可以考虑使用 ffjson/easyjson 来优化性能。

282013
 

在golang标准库中,有那么一类包,它们用于处理go项目目录结构、源码、语法、基本操作等。一般程序中可能用不到这些包,但在go工具链源码中用到了,之所以学习这些标准库,是为了更好的看go工具链的源码。首先我们来看收集go包信息的库:go/build

一、build包概述

该包文档中首先介绍了Go Path。如果对该部分还不清楚,可以看下文档的说明;或者官方其他文档;或者看 Go项目的目录结构。

如果你看过go源码,应该见到过类似这样的包注释:+build ignore。这是编译约束条件(Build Constraints),可以理解为条件编译。关于这部分的更多内容,稍后详细介绍。

二、类型和函数

1、ToolDir变量

var ToolDir = filepath.Join(runtime.GOROOT(), "pkg/tool/"+runtime.GOOS+"_"+runtime.GOARCH)

该变量的值是go工具链的路径。6g/6l之类的工具,就在这个路径下

2、ArchChar函数

获得架构的字符表示。在之前的文章中介绍过。比如:x86 32bit用8表示;amd64用6表示等。该函数通过传入goarch,获得对应的架构字符。如:build.ArchChar(runtime.GOARCH)

3、IsLocalImport函数

判断是否为“本地导入“,类似”.”, “..”, “./foo”或者”../foo”。正式项目,一般不建议用本地导入,使用本地导入很多人会说找不到包。

4、Context类型

该类型为构建(build)指定上下文环境。比如:当前操作系统、架构、Go根目录、GOPATH等

该类型除了提供变量成员,还提供了函数成员,如果函数成员是nil,则会使用其他库提供的函数。

5、Package类型

描述Go包

三、主要类型的方法(包括实例化)

1、Context的实例化和方法

var Default Context = defaultContext()

这是默认实现,go build工具使用的就是这个默认实现。Default会使用GOARCH、GOOS、GOROOT和GOPATH环境变量,如果没设置,则使用安装runtime中的值。

1)Import方法

func (ctxt *Context) Import(path string, srcDir string, mode ImportMode) (*Package, error)

导入一个包,返回Package类型指针。path参数跟在代码中import path的path一样。srcDir是源码所在路径;而ImportMode类型,build包中提供了两种:FindOnly和AllowBinary。AllowBinary可以在包源码不存在的时候,编译好的包对象文件直接被引用。

之前,在论坛(关于go的代码组织)中讨论过这样一个问题:go build 的时候,如果依赖的包源码不存在,编译不成功,有一个解决办法是通过go tool 6g这种方式编译。现在,在你知道了AllowBinary参数之后,应该可以通过修改go工具源码来解决这个问题。在src/cmd/go目录中的pkg.go中225有这样的代码:

// TODO: After Go 1, decide when to pass build.AllowBinary here.
// See issue 3268 for mistakes to avoid.
bp, err := buildContext.Import(path, srcDir, 0)

通过查看Import的源码,可以知道包安装的细节,比如安装到哪里。

当目录不包含源码,如果出错,则返回NoGoError错误。

另外,build包提供了Import方法的一个简便方法,即:Import函数,默认调用Default的Import方法

2)ImportDir方法

内部实现:return ctxt.Import(“.”, dir, mode)

3)SrcDirs方法

列出GOROOT和GOPATH中的源码目录。比如,我没有设置GOPATH,执行结果如下:
fmt.Println(build.Default.SrcDirs()) // [ c:\Go\src\pkg]

2、Package的实例化和方法

Context的Import和ImportDir都会返回Package实例(*Package),当然,也可以直接实例化。

Package提供了一个方法,判断一个Package是否是命令,也就是是否是main包

func (p *Package) IsCommand() bool

3、关于Context和Package的字段

由于这两种类型字段很多,包文档中每个字段都有注释,在此不一一解释。

四、构建约束(build constraints)

或者叫条件编译(编译条件)

1、使用说明

在go源码中(src/pkg或src/cmd)搜索+build,发现有不少文件的开头有这样的注释
+build xxx

构建约束是一行以+build开始的注释。在+build之后列出了一些条件,在这个条件成立的时,该文件应该包含在包中(也就是应该被编译进包文件),约束可以出现在任何源文件中,也就是不限于go源文件。不过,这些条件必须在文件最顶部(正是代码的前面,也就是说,+build之前可以有其他注释),在+build注释之后,应该有一个空行(这是为了和package doc区分开)。

语法规则:

1)只允许是字母数字或_

2)多个条件之间,空格表示OR;逗号表示AND;叹号(!)表示NOT

3)一个文件可以有多个+build,它们之间的关系是AND。如:
// +build linux darwin
// +build 386
等价于
// +build (linux OR darwin) AND 386

4)预定义了一些条件:
runtime.GOOS、runtime.GOARCH、compiler(gc或gccgo)、cgo、context.BuildTags中的其他单词

5)如果一个文件名(不含后缀),以 *_GOOS, *_GOARCH, 或 *_GOOS_GOARCH结尾,它们隐式包含了 构建约束

6)当不想编译某个文件时,可以加上// +build ignore。这里的ignore可以是其他单词,只是ignore更能让人知道什么意思

更多详细信息,可以查看go/build/build.go文件中shouldBuild和match方法。

2、应用实例

除了*_GOOS这种预定义的应用,我们看一个实际的应用。

比如,项目中需要在测试环境输出Debug信息,一般通过一个变量(或常量)来控制是测试环境还是生产环境,比如:if DEBUG {},这样在生产环境每次也会进行这样的判断。在golang-nuts邮件列表中有人问过这样的问题,貌似没有讨论出更好的方法(想要跟C中条件编译一样的效果)。下面我们采用Build constraints来实现。

1)文件列表:main.go logger_debug.go logger_product.go
2)在main.go中简单的调用Debug()方法。
3)在logger_product.go中的Debug()是空实现,但是在文件开始加上// + build !debug
4)在logger_debug.go中的Debug()是需要输出的调试信息,同时在文件开始加上// + build debug

这样,在测试环境编译的时传递-tags参数:go build/install -tags “debug” logger。生产环境:go build/install logger就行了。

对于生产环境,不传递-tags时,为什么会编译logger_product.go呢?因为在go/build/build.go中的match方法中有这么一句:

if strings.HasPrefix(name, "!") { // negation
	return len(name) > 1 && !ctxt.match(name[1:])
}

也就是说,只要有!(不能只是!),tag不在BuildTags中时,总是会编译。

完整源码在github

五、使用实例

对go/build的使用实例,后续文章在介绍go build的时候会介绍。

232013
 

在写命令行程序(工具、server)时,对命令参数进行解析是常见的需求。各种语言一般都会提供解析命令行参数的方法或库,以方便程序员使用。如果命令行参数纯粹自己写代码解析,对于比较复杂的,还是挺费劲的。在go标准库中提供了一个包:flag,方便进行命令行解析。

注:区分几个概念
1)命令行参数(或参数):是指运行程序提供的参数
2)已定义命令行参数:是指程序中通过flag.Xxx等这种形式定义了的参数
3)非flag(non-flag)命令行参数(或保留的命令行参数):后文解释

一、flag包概述

flag包实现了命令行参数的解析。

定义flags有两种方式:

1)flag.Xxx(),其中Xxx可以是Int、String等;返回一个相应类型的指针,如:

var ip = flag.Int("flagname", 1234, "help message for flagname")

2)flag.XxxVar(),将flag绑定到一个变量上,如:

var flagvar int
flag.IntVar(&amp;flagvar, "flagname", 1234, "help message for flagname")

另外,还可以创建自定义flag,只要实现flag.Value接口即可(要求receiver是指针),这时候可以通过如下方式定义该flag:

flag.Var(&amp;flagVal, "name", "help message for flagname")

注意到,这种方式是没有提供默认值的,所以默认值就是类型的零值。

在所有的flag定义完成之后,可以通过调用flag.Parse()进行解析。

命令行flag的语法有如下三种形式:
-flag // 只支持bool类型
-flag=x
-flag x // 只支持非bool类型

其中第三种形式只能用于非bool类型的flag,原因是:如果支持,那么对于这样的命令 cmd -x *,如果有一个文件名字是:0或false等,则命令的愿意会改变(之所以这样,是因为bool类型支持-flag这种形式,如果bool类型不支持-flag这种形式,则bool类型可以和其他类型一样处理。也正因为这样,Parse()中,对bool类型进行了特殊处理)。默认的,提供了-flag,则对应的值为true,否则为flag.Bool/BoolVar中指定的默认值;如果希望显示设置为false则使用-flag=false。

int类型可以是十进制、十六进制、八进制甚至是负数;bool类型可以是1, 0, t, f, true, false, TRUE, FALSE, True, False。Duration可以接受任何time.ParseDuration能解析的类型

二、类型和函数

在看类型和函数之前,先看一下变量
ErrHelp:该错误类型用于当命令行指定了-help参数但没有定义时。
Usage:这是一个函数,用户输出所有定义了的命令行参数和帮助信息(usage message)。一般,当命令行参数解析出错时,该函数会被调用。我们可以指定自己的Usage函数,即:flag.Usage = func(){}

1、函数

go标准库中,经常这么做:

定义了一个类型,提供了很多方法;为了方便使用,会实例化一个该类型的实例(通用),这样便可以直接使用该实例调用方法。比如:encoding/base64中提供了StdEncoding和URLEncoding实例,使用时:base64.StdEncoding.Encode()

在flag包中,进行了进一步封装:将FlagSet的方法都重新定义了一遍,也就是提供了一序列函数,而函数中只是简单的调用已经实例化好了的FlagSet实例:commandLine 的方法,这样commandLine实例便不需要export。这样,使用者是这么调用:flag.Parse()而不是flag.commandLine.Parse()

这里不详细介绍各个函数,在类型方法中介绍。

2、类型(数据结构)

1)ErrorHandling

type ErrorHandling int

该类型定义了在参数解析出错时错误处理方式定义了三个该类型的常量:

const (
	ContinueOnError ErrorHandling = iota
	ExitOnError
	PanicOnError
)

三个常量在源码的FlagSet方法parseOne()中使用了。

2)Flag

// A Flag represents the state of a flag.
type Flag struct {
	Name     string // name as it appears on command line
	Usage    string // help message
	Value    Value  // value as set
	DefValue string // default value (as text); for usage message
}

Flag类型代表一个flag的状态。

比如:autogo -f abc.txt,代码flag.String(“f”, “a.txt”, “usage”),则该Flag实例(可以通过flag.Lookup(“f”)获得)相应的值为:f, usage, abc.txt, a.txt。

3)FlagSet

// A FlagSet represents a set of defined flags.
type FlagSet struct {
	// Usage is the function called when an error occurs while parsing flags.
	// The field is a function (not a method) that may be changed to point to
	// a custom error handler.
	Usage func()

	name string // FlagSet的名字。commandLine给的是os.Args[0]
	parsed bool // 是否执行过Parse()
	actual map[string]*Flag // 存放实际传递了的参数(即命令行参数)
	formal map[string]*Flag // 存放所有已定义命令行参数
	args []string // arguments after flags // 存放非flag(non-flag)参数
	exitOnError bool // does the program exit if there's an error?
	errorHandling ErrorHandling // 当解析出错时,处理错误的方式
	output io.Writer // nil means stderr; use out() accessor
}

4)Value接口

// Value is the interface to the dynamic value stored in a flag.
// (The default value is represented as a string.)
type Value interface {
	String() string
	Set(string) error
}

所有参数类型需要实现Value接口,flag包中,为int、float、bool等实现了该接口。借助该接口,我们可以自定义flag

三、主要类型的方法(包括类型实例化)

flag包中主要是FlagSet类型。

1、实例化方式

NewFlagSet()用于实例化FlagSet。预定义的FlagSet实例commandLine的定义方式:

// The default set of command-line flags, parsed from os.Args.
var commandLine = NewFlagSet(os.Args[0], ExitOnError)

可见,默认的FlagSet实例在解析出错时会提出程序。

由于FlagSet中的字段没有export,其他方式获得FlagSet实例后,比如:FlagSet{}或new(FlagSet),应该调用Init()方法,初始化name和errorHandling。

2、定义flag参数方法

这一序列的方法都有两种形式,在一开始已经说了两种方式的区别。这些方法用于定义某一类型的flag参数。

3、解析参数(Parse)

func (f *FlagSet) Parse(arguments []string) error

从参数列表中解析定义的flag。参数arguments不包括命令名,即应该是os.Args[1:]。事实上,flag.Parse()函数就是这么做的:

// Parse parses the command-line flags from os.Args[1:].  Must be called
// after all flags are defined and before flags are accessed by the program.
func Parse() {
	// Ignore errors; commandLine is set for ExitOnError.
	commandLine.Parse(os.Args[1:])
}

该方法应该在flag参数定义后而具体参数值被访问前调用。

如果提供了-help参数(命令中给了)但没有定义(代码中),该方法返回ErrHelp错误。默认的commandLine,在Parse出错时会退出(ExitOnError)

为了更深入的理解,我们看一下Parse(arguments []string)的源码:

func (f *FlagSet) Parse(arguments []string) error {
	f.parsed = true
	f.args = arguments
	for {
		seen, err := f.parseOne()
		if seen {
			continue
		}
		if err == nil {
			break
		}
		switch f.errorHandling {
		case ContinueOnError:
			return err
		case ExitOnError:
			os.Exit(2)
		case PanicOnError:
			panic(err)
		}
	}
	return nil
}

真正解析参数的方法是非导出方法parseOne。

结合parseOne方法,我们来解释non-flag以及包文档中的这句话:

Flag parsing stops just before the first non-flag argument (“-” is a non-flag argument) or after the terminator “–”.

我们需要了解解析什么时候停止

根据Parse()中for循环终止的条件(不考虑解析出错),我们知道,当parseOne返回false, nil时,Parse解析终止。正常解析完成我们不考虑。看一下parseOne的源码发现,有两处会返回false, nil。

1)第一个non-flag参数

s := f.args[0]
if len(s) == 0 || s[0] != '-' || len(s) == 1 {
	return false, nil
}

也就是,当遇到单独的一个”-”或不是”-”开始时,会停止解析。比如:

./autogo – -f或./autogo build -f

这两种情况,-f都不会被正确解析。像该例子中的”-”或build(以及之后的参数),我们称之为non-flag参数

2)两个连续的”–”

if s[1] == '-' {
	num_minuses++
	if len(s) == 2 { // "--" terminates the flags
		f.args = f.args[1:]
		return false, nil
	}
}

也就是,当遇到联系的两个”-”时,解析停止。

说明:这里说的”-”和”–”,位置和”-f”这种的一样。也就是说,下面这种情况并不是这里说的:

./autogo -f –

这里的”–”会被当成是f的值

parseOne方法中接下来是处理-flag=x,然后是-flag(bool类型)(这里对bool进行了特殊处理),接着是-flag x这种形式,最后,将解析成功的Flag实例存入FlagSet的actual map中。

另外,在parseOne中有这么一句:

f.args = f.args[1:]

也就是说,没执行成功一次parseOne,f.args会少一个。所以,FlagSet中的args最后留下来的就是所有non-flag参数。

4、Arg(i int)和Args()、NArg()、NFlag()

Arg(i int)和Args()这两个方法就是获取non-flag参数的;NArg()获得non-flag个数;NFlag()获得FlagSet中actual长度(即被设置了的参数个数)。

5、Visit/VisitAll

这两个函数分别用户访问FlatSet的actual和formal中的Flag,而具体的访问方式由调用者决定。

6、PrintDefaults()

打印所有已定义参数的默认值(调用VisitAll),默认输出到标准错误,除非指定了FlagSet的output(通过SetOutput()设置)

7、Set(name, value string)

设置某个flag的值(通过Flag的name)

四、总结

使用建议:虽然上面讲了那么多,一般来说,我们只简单的定义flag,然后parse,就如同开始的例子一样。

如果项目需要复杂或更高级的命令行解析方式,可以试试goptions

如果想要像go工具那样的多命令(子命令)处理方式,可以试试command

272013
 

一、AES简介

密码学中的高级加密标准(Advanced Encryption Standard,AES),又称Rijndael加密法,这个标准用来替代原先的DES。AES加密数据块分组长度必须为128bit,密钥长度可以是128bit、192bit、256bit中的任意一个。
AES也是对称加密算法。关于该算法的更多信息可以参考 http://baike.baidu.com/view/2310288.htm

二、Go AES加密解密

学会了DES加密后,AES加密相当简单。除了第一步,将crypto/des包换为crypto/aes外,其他几乎一样。当然,需要注意的是密钥长度和iv的长度。

DES中blocksize是8byte,AES中则是16byte(128bit)。

AES包中,使用函数func NewCipher(key []byte) (cipher.Block, error),和DES一样(包不一样)

由于详细讲解了DES之后,Goes实现AES加解密十分简单,这里只给出关键代码,不做详细解释

func AesEncrypt(origData, key []byte) ([]byte, error) {
     block, err := aes.NewCipher(key)
     if err != nil {
          return nil, err
     }
     blockSize := block.BlockSize()
     origData = PKCS5Padding(origData, blockSize)
     // origData = ZeroPadding(origData, block.BlockSize())
     blockMode := cipher.NewCBCEncrypter(block, key[:blockSize])
     crypted := make([]byte, len(origData))
     // 根据CryptBlocks方法的说明,如下方式初始化crypted也可以
     // crypted := origData
     blockMode.CryptBlocks(crypted, origData)
     return crypted, nil
}

func AesDecrypt(crypted, key []byte) ([]byte, error) {
     block, err := aes.NewCipher(key)
     if err != nil {
          return nil, err
     }
     blockSize := block.BlockSize()
     blockMode := cipher.NewCBCDecrypter(block, key[:blockSize])
     origData := make([]byte, len(crypted))
     // origData := crypted
     blockMode.CryptBlocks(origData, crypted)
     origData = PKCS5UnPadding(origData)
     // origData = ZeroUnPadding(origData)
     return origData, nil
}

对比上篇DES加密的代码,几乎一样。

三、和其他语言交互:加解密

和DES一样,实现了PHP、Java版本。具体代码在github上

232013
 

接着RSA加密解密,我们继续来看看DES的加密解密

一、DES简介

DES(Data Encryption Standard)是对称加密算法,也就是加密和解密用相同的密钥。其入口参数有三个:key、data、mode。key为加密解密使用的密钥,data为加密解密的数据,mode为其工作模式。当模式为加密模式时,明文按照64位进行分组,形成明文组,key用于对数据加密,当模式为解密模式时,key用于对数据解密。实际运用中,密钥只用到了64位中的56位,这样才具有高的安全性。DES 的常见变体是三重 DES,使用 168 位的密钥对资料进行三次加密的一种机制;它通常(但非始终)提供极其强大的安全性。如果三个 56 位的子元素都相同,则三重 DES 向后兼容 DES。

DES加密,涉及到加密模式和填充方式,所以,和其他语言加解密时,应该约定好加密模式和填充方式。(模式定义了Cipher如何应用加密算法。改变模式可以容许一个块加密程序变为流加密程序。)

关于分组加密:分组密码每次加密一个数据分组,这个分组的位数可以是随意的,一般选择64或者128位。另一方面,流加密程序每次可以加密或解密一个字节的数据,这就使它比流加密的应用程序更为有用。

在用DES加密解密时,经常会涉及到一个概念:块(block,也叫分组),模式(比如cbc),初始向量(iv),填充方式(padding,包括none,用’\0′填充,pkcs5padding或pkcs7padding)。多语言加密解密交互时,需要确定好这些。比如这么定:

采用3DES、CBC模式、pkcs5padding,初始向量用key充当;另外,对于zero padding,还得约定好,对于数据长度刚好是block size的整数倍时,是否需要额外填充。

二、Go DES加密解密

1、crypto/des包

Go中crypto/des包实现了 Data Encryption Standard (DES) and the Triple Data Encryption Algorithm (TDEA)。查看该包文档,发现相当简单:
定义了DES块大小(8bytes),定义了一个KeySizeError。另外定义了两个我们需要特别关注的函数,即

func NewCipher(key []byte) (cipher.Block, error)
func NewTripleDESCipher(key []byte) (cipher.Block, error)

他们都是用来获得一个cipher.Block。从名字可以很容易知道,DES使用NewCipher,3DES使用NewTripleDESCipher。参数都是密钥(key)

2、crypto/cipher包

那么,cipher这个包是干嘛用的呢?它实现了标准的块加密模式。我们看一下cipher.Block

type Block interface {
    // BlockSize returns the cipher's block size.
    BlockSize() int

    // Encrypt encrypts the first block in src into dst.
    // Dst and src may point at the same memory.
    Encrypt(dst, src []byte)

    // Decrypt decrypts the first block in src into dst.
    // Dst and src may point at the same memory.
    Decrypt(dst, src []byte)
}

这是一个接口

对称加密,按块方式,我们经常见到CBC、ECB之类的,这些是加密模式。可以参考:DES加密模式详解 http://linux.bokee.com/6956594.html
Go中定义了一个接口BlockMode代表各种模式

type BlockMode interface {
    // BlockSize returns the mode's block size.
    BlockSize() int

    // CryptBlocks encrypts or decrypts a number of blocks. The length of
    // src must be a multiple of the block size. Dst and src may point to
    // the same memory.
    CryptBlocks(dst, src []byte)
}

该包还提供了获取BlockMode实例的两个方法

func NewCBCDecrypter(b Block, iv []byte) BlockMode
func NewCBCEncrypter(b Block, iv []byte) BlockMode

即一个CBC加密,一个CBC解密

对于按流方式加密的,定义了一个接口:

type Stream interface {
    // XORKeyStream XORs each byte in the given slice with a byte from the
    // cipher's key stream. Dst and src may point to the same memory.
    XORKeyStream(dst, src []byte)
}

同样也提供了获取实现该接口的实例

这里,我们只讨论CBC模式

3、加密解密

1)DES
DES加密代码如下:

func DesEncrypt(origData, key []byte) ([]byte, error) {
     block, err := des.NewCipher(key)
     if err != nil {
          return nil, err
     }
     origData = PKCS5Padding(origData, block.BlockSize())
     // origData = ZeroPadding(origData, block.BlockSize())
     blockMode := cipher.NewCBCEncrypter(block, key)
     crypted := make([]byte, len(origData))
      // 根据CryptBlocks方法的说明,如下方式初始化crypted也可以
     // crypted := origData
     blockMode.CryptBlocks(crypted, origData)
     return crypted, nil
}

以上代码使用DES加密(des.NewCipher),加密模式为CBC(cipher.NewCBCEncrypter(block, key)),填充方式PKCS5Padding,该函数的代码如下:

func PKCS5Padding(ciphertext []byte, blockSize int) []byte {
     padding := blockSize - len(ciphertext)%blockSize
     padtext := bytes.Repeat([]byte{byte(padding)}, padding)
     return append(ciphertext, padtext...)
}

可见,数据长度刚好是block size的整数倍时,也进行了填充,如果不进行填充,unpadding会搞不定。
另外,为了方便,初始向量直接使用key充当了(实际项目中,最好别这么做)

DES解密代码如下:

func DesDecrypt(crypted, key []byte) ([]byte, error) {
     block, err := des.NewCipher(key)
     if err != nil {
          return nil, err
     }
     blockMode := cipher.NewCBCDecrypter(block, key)
     origData := make([]byte, len(crypted))
     // origData := crypted
     blockMode.CryptBlocks(origData, crypted)
     origData = PKCS5UnPadding(origData)
     // origData = ZeroUnPadding(origData)
     return origData, nil
}

可见,解密无非是调用cipher.NewCBCDecrypter,最后unpadding,其他跟加密几乎一样。相应的PKCS5UnPadding:

func PKCS5UnPadding(origData []byte) []byte {
	length := len(origData)
	// 去掉最后一个字节 unpadding 次
	unpadding := int(origData[length-1])
	return origData[:(length - unpadding)]
}

2)、3DES

加密代码:

// 3DES加密
func TripleDesEncrypt(origData, key []byte) ([]byte, error) {
     block, err := des.NewTripleDESCipher(key)
     if err != nil {
          return nil, err
     }
     origData = PKCS5Padding(origData, block.BlockSize())
     // origData = ZeroPadding(origData, block.BlockSize())
     blockMode := cipher.NewCBCEncrypter(block, key[:8])
     crypted := make([]byte, len(origData))
     blockMode.CryptBlocks(crypted, origData)
     return crypted, nil
}

对比DES,发现只是换了NewTripleDESCipher。不过,需要注意的是,密钥长度必须24byte,否则直接返回错误。关于这一点,PHP中却不是这样的,只要是8byte以上就行;而Java中,要求必须是24byte以上,内部会取前24byte(相当于就是24byte)。

另外,初始化向量长度是8byte(目前各个语言都是如此,不是8byte会有问题)。然而,如果你用的Go是1.0.3(或以下),iv可以不等于8byte。其实,在cipher.NewCBCEncrypter方法中有注释:
The length of iv must be the same as the Block’s block size.
可是代码中的实现却没有做判断。不过,go tips中修正了这个问题,如果iv不等于block size(des为8),则直接panic。所以,对于加解密,一定要测试,保证iv等于block size,否则可能会panic:

func NewCBCDecrypter(b Block, iv []byte) BlockMode {
     if len(iv) != b.BlockSize() {
          panic("cipher.NewCBCDecrypter: IV length must equal block size")
     }
     return (*cbcDecrypter)(newCBC(b, iv))
}

此处之所有用panic而不是返回error,个人猜测,是由于目前发布的版本,该方法没有返回error,修改方法签名会导致兼容性问题,因此用panic了。

解密代码:

// 3DES解密
func TripleDesDecrypt(crypted, key []byte) ([]byte, error) {
     block, err := des.NewTripleDESCipher(key)
     if err != nil {
          return nil, err
     }
     blockMode := cipher.NewCBCDecrypter(block, key[:8])
     origData := make([]byte, len(crypted))
     // origData := crypted
     blockMode.CryptBlocks(origData, crypted)
     origData = PKCS5UnPadding(origData)
     // origData = ZeroUnPadding(origData)
     return origData, nil
}

三、和其他语言交互:加解密

这次,我写了PHP、Java的版本,具体代码放在github上。这里说明一下,Java中,默认模式是ECB,且没有用”\0″填充的情况,只有NoPadding和PKCS5Padding;而PHP中(mcrypt扩展),默认填充方式是”\0″,而且,当数据长度刚好是block size的整数倍时,默认不会填充”\0″,这样,如果数据刚好是block size的整数倍且结尾字符是”\0″,会有问题。

综上,跨语言加密解密,应该使用PKCS5Padding填充。

完整代码 myblog_article_code

182013
 

《Go加密解密之RSA》中,说到了RSA密钥的生成问题,例子中的密钥,是通过openssl生成的。其实,通过那篇文章,可以很容易的反向,用Go生成openssl那样的密钥保存在文件中。该番外篇就是做这事。

一、加解密流程

首先回顾一下上篇文章加解密流程:

1、读取密钥(可以写死在一个变量中保存,也可以从一个外部文件读取)
2、通过encoding/pem中的Decode函数解析到block类型中
3、通过crypto/x509中相应的Parse方法得到密钥(即crypto/rsa包中的PrivateKey和PublicKey)

根据这个流程,我们可以很容易的反过来生成密钥,保存到文件中。

二、生成密钥并编码保存到文件中

首先,我们需要生成密钥,在crypto/rsa包中有一个函数:

func GenerateKey(random io.Reader, bits int) (priv *PrivateKey, err error)

该函数中,random可以直接传crypto/rand中的rand.Reader,而bits是密钥长度。

这样得到了一个PrivateKey类型的指针。我们看一下PrivateKey的定义:

type PrivateKey struct {
    PublicKey            // public part.
    D         *big.Int   // private exponent
    Primes    []*big.Int // prime factors of N, has >= 2 elements.

    // Precomputed contains precomputed values that speed up private
    // operations, if available.
    Precomputed PrecomputedValues
}

可见,该类型中嵌入了PublicKey这个类型。而PublicKey类型的定义如下:

type PublicKey struct {
    N *big.Int // modulus
    E int      // public exponent
}

这些是RSA算法规定的。

接下来就是找到x509和pem中对应的方法处理。关键代码如下:

func GenRsaKey(bits int) error {
	// 生成私钥文件
	privateKey, err := rsa.GenerateKey(rand.Reader, bits)
	if err != nil {
		return err
	}
	derStream := x509.MarshalPKCS1PrivateKey(privateKey)
	block := &pem.Block{
		Type:  "RSA PRIVATE KEY",
		Bytes: derStream,
	}
	file, err := os.Create("private.pem")
	if err != nil {
		return err
	}
	err = pem.Encode(file, block)
	if err != nil {
		return err
	}
	// 生成公钥文件
	publicKey := &privateKey.PublicKey
	derPkix, err := x509.MarshalPKIXPublicKey(publicKey)
	if err != nil {
		return err
	}
	block = &pem.Block{
		Type:  "PUBLIC KEY",
		Bytes: derPkix,
	}
	file, err = os.Create("public.pem")
	if err != nil {
		return err
	}
	err = pem.Encode(file, block)
	if err != nil {
		return err
	}
	return nil
}

以上代码将公钥和私钥分别写入public.pem和private.pem中了。
生成这两个文件后,可以用上篇文章中的方法验证一下正确性。

完整示例代码在github上。myblog_article_code  rsa/rsa_gen_key.go这个文件

注:rsa中还有另外一个方法生成密钥

func GenerateMultiPrimeKey(random io.Reader, nprimes int, bits int) (priv *PrivateKey, err error)

有兴趣的可以研究一下。

182013
 

安全总是很重要的,各个语言对于通用的加密算法都会有实现。前段时间,用Go实现了RSA和DES的加密解密,在这分享一下。(对于RSA和DES加密算法本身,请查阅相关资料)

在PHP中,很多功能经常是一个函数解决;而Go中的却不是。本文会通过PHP加密,Go解密;Go加密,PHP解密来学习Go的RSA和DES相关的API。

该文讨论Go RSA加密解密。所有操作在linux下完成。

一、概要

这是一个非对称加密算法,一般通过公钥加密,私钥解密。

在加解密过程中,使用openssl生产密钥。执行如下操作:

1)创建私钥
openssl genrsa -out private.pem 1024 //密钥长度,1024觉得不够安全的话可以用2048,但是代价也相应增大
2)创建公钥
openssl rsa -in private.pem -pubout -out public.pem

这样便生产了密钥。

一般地,各个语言也会提供API,用于生成密钥。在Go中,可以查看encoding/pem包和crypto/x509包。具体怎么产生,可查看《GO加密解密RSA番外篇:生成RSA密钥》

加密解密这块,涉及到很多标准,个人建议需要的时候临时学习一下。

二、Go RSA加密解密

1、rsa加解密,必然会去查crypto/ras这个包

Package rsa implements RSA encryption as specified in PKCS#1.

这是该包的说明:实现RSA加密技术,基于PKCS#1规范。

对于什么是PKCS#1,可以查阅相关资料。PKCS(公钥密码标准),而#1就是RSA的标准。可以查看:PKCS系列简介

从该包中函数的名称,可以看到有两对加解密的函数。

EncryptOAEP和DecryptOAEP
EncryptPKCS1v15和DecryptPKCS1v15

这称作加密方案,详细可以查看,PKCS #1 v2.1 RSA 算法标准

可见,当与其他语言交互时,需要确定好使用哪种方案。

PublicKey和PrivateKey两个类型分别代表公钥和私钥,关于这两个类型中成员该怎么设置,这涉及到RSA加密算法,本文中,这两个类型的实例通过解析文章开头生成的密钥得到。

2、解析密钥得到PublicKey和PrivateKey的实例

这个过程,我也是花了好些时间(主要对各种加密的各种东东不熟):怎么将openssl生成的密钥文件解析到公钥和私钥实例呢?

在encoding/pem包中,看到了—–BEGIN Type—–这样的字样,这正好和openssl生成的密钥形式差不多,那就试试。

在该包中,一个block代表的是PEM编码的结构,关于PEM,请查阅相关资料。我们要解析密钥,当然用Decode方法:

func Decode(data []byte) (p *Block, rest []byte)

这样便得到了一个Block的实例(指针)。

解析来看crypto/x509。为什么是x509呢?这又涉及到一堆概念。先不管这些,我也是看encoding和crypto这两个包的子包摸索出来的。
在x509包中,有一个函数:

func ParsePKIXPublicKey(derBytes []byte) (pub interface{}, err error)

从该函数的说明:ParsePKIXPublicKey parses a DER encoded public key. These values are typically found in PEM blocks with “BEGIN PUBLIC KEY”。可见这就是解析PublicKey的。另外,这里说到了PEM,可以上面的encoding/pem对了。(PKIX是啥东东,查看这里

而解析私钥的,有好几个方法,从上面的介绍,我们知道,RSA是PKCS#1,刚好有一个方法:

func ParsePKCS1PrivateKey(der []byte) (key *rsa.PrivateKey, err error)

返回的就是rsa.PrivateKey。

3、解密解密实现

通过上面的介绍,Go中RSA的解密解密实现就不难了。代码如下:

// 加密
func RsaEncrypt(origData []byte) ([]byte, error) {
	block, _ := pem.Decode(publicKey)
	if block == nil {
		return nil, errors.New("public key error")
	}
	pubInterface, err := x509.ParsePKIXPublicKey(block.Bytes)
	if err != nil {
		return nil, err
	}
	pub := pubInterface.(*rsa.PublicKey)
	return rsa.EncryptPKCS1v15(rand.Reader, pub, origData)
}

// 解密
func RsaDecrypt(ciphertext []byte) ([]byte, error) {
	block, _ := pem.Decode(privateKey)
	if block == nil {
		return nil, errors.New("private key error!")
	}
	priv, err := x509.ParsePKCS1PrivateKey(block.Bytes)
	if err != nil {
		return nil, err
	}
	return rsa.DecryptPKCS1v15(rand.Reader, priv, ciphertext)
}

其中,publicKey和privateKey是openssl生成的密钥,我生成的如下:

// 公钥和私钥可以从文件中读取
var privateKey = []byte(`
-----BEGIN RSA PRIVATE KEY-----
MIICXQIBAAKBgQDZsfv1qscqYdy4vY+P4e3cAtmvppXQcRvrF1cB4drkv0haU24Y
7m5qYtT52Kr539RdbKKdLAM6s20lWy7+5C0DgacdwYWd/7PeCELyEipZJL07Vro7
Ate8Bfjya+wltGK9+XNUIHiumUKULW4KDx21+1NLAUeJ6PeW+DAkmJWF6QIDAQAB
AoGBAJlNxenTQj6OfCl9FMR2jlMJjtMrtQT9InQEE7m3m7bLHeC+MCJOhmNVBjaM
ZpthDORdxIZ6oCuOf6Z2+Dl35lntGFh5J7S34UP2BWzF1IyyQfySCNexGNHKT1G1
XKQtHmtc2gWWthEg+S6ciIyw2IGrrP2Rke81vYHExPrexf0hAkEA9Izb0MiYsMCB
/jemLJB0Lb3Y/B8xjGjQFFBQT7bmwBVjvZWZVpnMnXi9sWGdgUpxsCuAIROXjZ40
IRZ2C9EouwJBAOPjPvV8Sgw4vaseOqlJvSq/C/pIFx6RVznDGlc8bRg7SgTPpjHG
4G+M3mVgpCX1a/EU1mB+fhiJ2LAZ/pTtY6sCQGaW9NwIWu3DRIVGCSMm0mYh/3X9
DAcwLSJoctiODQ1Fq9rreDE5QfpJnaJdJfsIJNtX1F+L3YceeBXtW0Ynz2MCQBI8
9KP274Is5FkWkUFNKnuKUK4WKOuEXEO+LpR+vIhs7k6WQ8nGDd4/mujoJBr5mkrw
DPwqA3N5TMNDQVGv8gMCQQCaKGJgWYgvo3/milFfImbp+m7/Y3vCptarldXrYQWO
AQjxwc71ZGBFDITYvdgJM1MTqc8xQek1FXn1vfpy2c6O
-----END RSA PRIVATE KEY-----
`)

var publicKey = []byte(`
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDZsfv1qscqYdy4vY+P4e3cAtmv
ppXQcRvrF1cB4drkv0haU24Y7m5qYtT52Kr539RdbKKdLAM6s20lWy7+5C0Dgacd
wYWd/7PeCELyEipZJL07Vro7Ate8Bfjya+wltGK9+XNUIHiumUKULW4KDx21+1NL
AUeJ6PeW+DAkmJWF6QIDAQAB
-----END PUBLIC KEY-----
`)

4、使用例子

package main

import (
	"fmt"
)

func main() {
	data, err := RsaEncrypt([]byte("polaris@studygolang.com"))
	if err != nil {
		panic(err)
	}
	origData, err := RsaDecrypt(data)
	if err != nil {
		panic(err)
	}
	fmt.Println(string(origData))
}

该例子是加密完polaris@studygolang.com后立马解密

三、跨语言加解密

语言内部正常,还得看看和其他语言是否一致,即:其他语言加密,Go语言得正确解密;Go语言加密,其他语言正确解密

1、PHP RSA加解密

这里,我选择PHP,使用的是openssl扩展。PHP中加解密很简单,如下两个方法(这里只考虑用公钥加密,私钥解密):

bool openssl_public_encrypt ( string $data , string &$crypted , mixed $key [, int $padding = OPENSSL_PKCS1_PADDING ] )
bool openssl_private_decrypt ( string $data , string &$decrypted , mixed $key [, int $padding = OPENSSL_PKCS1_PADDING ] )

最后一个参数是加密方案(补齐方式)。由于Go中使用的是PKCS1而不是OAEP,所以,使用默认值即可。

PHP代码如下:

$privateKey = '-----BEGIN RSA PRIVATE KEY-----
MIICXQIBAAKBgQDZsfv1qscqYdy4vY+P4e3cAtmvppXQcRvrF1cB4drkv0haU24Y
7m5qYtT52Kr539RdbKKdLAM6s20lWy7+5C0DgacdwYWd/7PeCELyEipZJL07Vro7
Ate8Bfjya+wltGK9+XNUIHiumUKULW4KDx21+1NLAUeJ6PeW+DAkmJWF6QIDAQAB
AoGBAJlNxenTQj6OfCl9FMR2jlMJjtMrtQT9InQEE7m3m7bLHeC+MCJOhmNVBjaM
ZpthDORdxIZ6oCuOf6Z2+Dl35lntGFh5J7S34UP2BWzF1IyyQfySCNexGNHKT1G1
XKQtHmtc2gWWthEg+S6ciIyw2IGrrP2Rke81vYHExPrexf0hAkEA9Izb0MiYsMCB
/jemLJB0Lb3Y/B8xjGjQFFBQT7bmwBVjvZWZVpnMnXi9sWGdgUpxsCuAIROXjZ40
IRZ2C9EouwJBAOPjPvV8Sgw4vaseOqlJvSq/C/pIFx6RVznDGlc8bRg7SgTPpjHG
4G+M3mVgpCX1a/EU1mB+fhiJ2LAZ/pTtY6sCQGaW9NwIWu3DRIVGCSMm0mYh/3X9
DAcwLSJoctiODQ1Fq9rreDE5QfpJnaJdJfsIJNtX1F+L3YceeBXtW0Ynz2MCQBI8
9KP274Is5FkWkUFNKnuKUK4WKOuEXEO+LpR+vIhs7k6WQ8nGDd4/mujoJBr5mkrw
DPwqA3N5TMNDQVGv8gMCQQCaKGJgWYgvo3/milFfImbp+m7/Y3vCptarldXrYQWO
AQjxwc71ZGBFDITYvdgJM1MTqc8xQek1FXn1vfpy2c6O
-----END RSA PRIVATE KEY-----';

$publicKey = '-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDZsfv1qscqYdy4vY+P4e3cAtmv
ppXQcRvrF1cB4drkv0haU24Y7m5qYtT52Kr539RdbKKdLAM6s20lWy7+5C0Dgacd
wYWd/7PeCELyEipZJL07Vro7Ate8Bfjya+wltGK9+XNUIHiumUKULW4KDx21+1NL
AUeJ6PeW+DAkmJWF6QIDAQAB
-----END PUBLIC KEY-----';

function rsaEncrypt($data)
{
    global $publicKey;
    openssl_public_encrypt($data, $crypted, $publicKey);
    return $crypted;
}

function rsaDecrypt($data)
{
    global $privateKey;
    openssl_private_decrypt($data, $decrypted, $privateKey);
    return $decrypted;
}

function main()
{
    $crypted = rsaEncrypt("polaris@studygolang.com");
    $decrypted = rsaDecrypt($crypted);
    echo "encrypt and decrypt:" . $decrypted;
}

main();

这里也是用PHP加解密polaris@studygolang.com

2、Go和PHP一起工作

这里要注意的一点是,由于加密后是字节流,直接输出查看会乱码,因此,为了便于语言直接加解密,这里将加密之后的数据进行base64编码。

完整代码放在了github上
https://github.com/polaris1119/myblog_article_code/tree/master/rsa

3、使用

示例中,php和Go版本都支持-d参数传入加密好的字符串,将其解密;不传时,会输出加密好并base64编码的串,可用于其他语言解密。

十二 142012
 

在JSON还未像现在这么广泛使用时,XML的使用相当广泛。XML作为一种数据交换和信息传递的格式,使用还是很广泛的,现在很多开放平台接口,基本会支持XML格式。Go语言中提供了处理XML的标准库。下面我们一起来学习它。

一、encoding/xml包概述

该包实现了一个简单的XML 1.0 解析器(支持XML命名空间)

二、类型和函数

在看类型和函数之前,先看一下变量和常量
Header常量:由于Marshal生成的xml并不会生成XML标准头部,所以,定义了一个标准头常量
HTMLAutoClose变量:一些应该自动闭合的HTML标签。很明显,这是用来处理html的。这样的标签如:br、hr等
HTMLEntity变量:标准HTML字符实体的映射转换。(实体名=>实体编号)
可见,两个变量都是跟HTML相关的,之后会用到。

1、函数

func Escape(w io.Writer, s []byte)
将s中包含的特殊字符转换为实体,然后写入w中。如<转为&lt;

func Marshal(v interface{}) ([]byte, error)
func Unmarshal(data []byte, v interface{}) error
上面两个函数的文档(注释)很长。从文档知道,Marshal是将v代表的数据转为XML格式(生成XML);而Unmarshal刚好相反,是解析XML,同时将解析的结果保存在v中。

func MarshalIndent(v interface{}, prefix, indent string) ([]byte, error)
这个函数和Marshal的不同是,每个XML元素会增加前缀和缩进

这三个函数的详细说明和使用在后面介绍。

2、类型(数据结构)

1)跟表示XML相关的

①type Name struct {
Space, Local string
}
Local表示本地名字,Space表示命名空间前缀(命名空间标示符),比如元素,具有本地名称polaris和命名空间前缀studygolang
该类型没有提供任何方法,主要用途一般是在XML根元素上定义一个该类型,变量名必须为XMLName,类型就为:xml.Name

②Attr(属性)、CharData(字符数据)、Comment(注释)、ProcInst(处理指令)等。这些类型都是XML标准定义的表示,一般不会用到这些。有兴趣可以查看XML标准对照着学习。

2)跟XML解析相关的

①Decoder :代表一个XML解析器,解析器假定输入是UTF-8编码

有如下字段:
Strict bool 是否允许文档错误,比如元素没关闭。在NewDecoder时,该值默认为true,这是XML的基本要求。然而,你可以自己设置为false,配合下面的字段,可以解析HTML。

AutoClose []string 当Strict是false时,在AutoClose中的元素将会自动关闭,比如:<input type=”text” />将会变为:<input type=”text” />,而不管后面有没有。当解析HTML时,AutoClose = HTMLAutoClose,如本文开始提到的,HTMLAutoClose变量定义了一些HTML中应该自动关闭的标签。

Entity map[string]string 要于HTML字符实体替换。当解析HTML时,Entity = HTMLEntity。

CharsetReader func(charset string, input io.Reader) (io.Reader, error) 这是一个函数字段,提供了一种功能,将XML中非UTF-8字符集转换为UTF-8字符集。参数charset是原字符集;input是文档来源。

还有其他非导出字段,一般不用关心,

②Encoder:输出XML数据。该类型没有提供导出的字段。

三、主要类型的方法(包括类型实例化)

在xml包中,Decoder和Encoder是两个主要的数据结构,分别解析XML和生成XML。

1、Decoder实例化和方法

func NewDecoder(r io.Reader) *Decoder
这是实例化一个Decoder,参数io.Reader,这是一个接口,它指示了要解析的XML数据源来自哪里。具体怎么使用,可以查看Unmarshal函数的实现:NewDecoder(bytes.NewBuffer(data)).Decode(v)。这里之所以使用bytes.NewBuffer,是因为bytes.Buffer实现了io.ByteReader接口。如果没有传入的参数没有实现该接口,NewDecoder内部会将其转换为:bufio.Reader。之所以这么做,是为了高效的读取(Get efficient byte at a time reader)

func (d *Decoder) Decode(v interface{}) error
该方法类似 xml.Unmarshal,只不过数据来源于decoder流,也就是实例化时的io.Reader流。其实,从上面实例化中知道,Unmarshal函数内部调用的就是Decode进行XML解析的。

func (d *Decoder) DecodeElement(v interface{}, start *StartElement) error
在Decode内部,调用的是这个方法,return d.DecodeElement(v, nil),只是start *StartElement是nil,表示从头开始解析。当只想解析某个元素之后的内容时,可以调用这个方法。

func (d *Decoder) RawToken() (Token, error)
func (d *Decoder) Token() (t Token, err error)
都是从流中获得下一个Token,RawToken不会验证开始元素和结束元素;而Token会验证(嵌套、关闭是否正确)。

func (d *Decoder) Skip() error
读Token直到找到搭配最近的开始元素为止。它可以用来跳过内部嵌套结构。找到返回nil,否则返回错误。

2、Encoder实例化和方法

func NewEncoder(w io.Writer) *Encoder
这是实例化一个Encoder,参数io.Writer是一个接口。它指示了将生成的XML输出到哪去。

func (enc *Encoder) Encode(v interface{}) error
encode v表示的数据为XML,保存到流中。Marshal的内部实现:NewEncoder(&b).Encode(v),其中,b是var b bytes.Buffer。

四、解析XML

一般的,解析XML只需要使用func Unmarshal(data []byte, v interface{}) error方法就可以。该方法接受XML数据流,和一个v,这个v是一个interface{},也就是可以“任何类型”。然而,v实际上是有要求的,它是存储解析后的XML的,要求v必须是指针,指向的类型只能是:struct、slice或string。
注意,XML解析和后面的输出XML,用的都是反射,因而struct中的字段必须可导出(首字母大写)。

1、先看一个简单的例子

package main

import (
    "encoding/xml"
    "strings"
    "fmt"
)

func main() {
    var t xml.Token
    var err error

    input := `<Person><FirstName>Xu</FirstName><LastName>Xinhua</LastName></Person>`
    inputReader := strings.NewReader(input)

    // 从文件读取,如可以如下:
    // content, err := ioutil.ReadFile("studygolang.xml")
    // decoder := xml.NewDecoder(bytes.NewBuffer(content))

    decoder := xml.NewDecoder(inputReader)
    for t, err = decoder.Token(); err == nil; t, err = decoder.Token() {
        switch token := t.(type) {
        // 处理元素开始(标签)
        case xml.StartElement:
            name := token.Name.Local
            fmt.Printf("Token name: %s\n", name)
            for _, attr := range token.Attr {
                attrName := attr.Name.Local
                attrValue := attr.Value
                fmt.Printf("An attribute is: %s %s\n", attrName, attrValue)
            }
        // 处理元素结束(标签)
        case xml.EndElement:
            fmt.Printf("Token of '%s' end\n", token.Name.Local)
        // 处理字符数据(这里就是元素的文本)
        case xml.CharData:
            content := string([]byte(token))
            fmt.Printf("This is the content: %v\n", content)
        default:
            // ...
        }
    }
}

程序输出:
Token name: Person
Token name: FirstName
This is the content: Xu
Token of ‘FirstName’ end
Token name: LastName
This is the content: Xinhua
Token of ‘LastName’ end
Token of ‘Person’ end

说明:
这里没有直接从文件中读取XML,而是讲XML定义在Go源码文件中,当然,从文件中读是一样的。注意,如果此处XML格式化,即多了换行和缩进,则输出结果会不一样,因为空格属于内容。
由于这里没有涉及到将XML保存到Go的某种数据类型中(比如struct),所以,xml中元素的大小写是无所谓的,不管大小写解析代码都一样处理。
这个例子可以看到解析XML后,内部结构是个什么样子,xml包中定义的各种元素代表什么。

2、解析到struct

这部分是xml的核心,一步步来。先看简单的。xml文件内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<Persons>
    <Person>
        <Name>polaris</Name>
        <Age>28</Age>
        <Career>无业游民</Career>
        <Interests>
            <Interest>编程</Interest>
            <Interest>下棋</Interest>
        </Interests>
    </Person>
    <Person>
        <Name>studygolang</Name>
        <Age>27</Age>
        <Career>码农</Career>
        <Interests>
            <Interest>编程</Interest>
            <Interest>下棋</Interest>
        </Interests>
    </Person>
</Persons>

注意,所有标签首字母大写,没有属性,只有元素

Go解析代码如下:

package main
import (
    "encoding/xml"
    "log"
    "io/ioutil"
)
type Result struct {
    Person []Person
}
type Person struct {
    Name string
    Age int
    Career string
    Interests Interests
}
type Interests struct {
    Interest []string
}
func main() {
    content, err := ioutil.ReadFile("studygolang.xml")
    if err != nil {
        log.Fatal(err)
    }
    var result Result
    err = xml.Unmarshal(content, &result)
    if err != nil {
        log.Fatal(err)
    }
    log.Println(result)
}

最后输出:
{[{polaris 28 无业游民 [{[编程 下棋]}]} {studygolang 27 码农 [{[编程 下棋]}]}]}

注:这里的解析没有用到任何tag。元素首字母的大小写不能错。

简单分析一下这段代码,主要看数据结构:
Result保存最后的结果,相当于跟元素Persons,它包含了多个Person子元素,所以,咱们定义为Person []Person
而Person结构,对应Person这个元素,它包含了Name、Age、Career和Interests这些子元素,前三个只是普通的string,而Interests又包含了子元素,因而定义一个Interests类型
Interests结构对应Interests元素,包含了多个Interest元素,Interest只是一个普通的string,由于是多个,因而是一个string slice
可见,XML元素到Go中的struct只需要按XML的树状定义就可以。

我们改一下,将Name和Age改为Person的属性,Go代码不变,这个时候发现,Name和Age没有解析成功。这个时候就需要用到tag了。
我们只需要修改Person结构的定义:

type Person struct {
    Name string `xml:",attr"`
    Age int `xml:",attr"`
    Career string
    Interests Interests
}

这个时候可以解析成功了。很明显,xml包会解析字段中的tag,如果tag为:`xml:”,attr”`,表示该字段是该元素的属性。注意,解析xml,要求tag必须以xml:开头,后面的必须在双引号中。这是根据反射包中StructTag类型的func (tag StructTag) Get(key string) string方法解析的,代码中固定写死了:Get(“xml”),这样来获得”,attr”这一部分。
那么”,attr”前面的逗号是啥意思?我们知道,一般xml中元素和属性等都会用小写,而我们上面的例子中,用的都是大写,如果改为小写,解析不成功(个人感觉,xml库应该支持xml元素小写时,将其首字母变为大写,当然,可能设计者更希望的是通过tag来实现xml解析)。我们可以试着将xml改为这样:

<?xml version="1.0" encoding="UTF-8"?>
<Persons>
    <Person name="polaris" age="28">
        <Career>无业游民</Career>
        <Interests>
            <Interest>编程</Interest>
            <Interest>下棋</Interest>
        </Interests>
    </Person>
    <Person Name="studygolang" Age="27">
        <Career>码农</Career>
        <Interests>
            <Interest>编程</Interest>
            <Interest>下棋</Interest>
        </Interests>
    </Person>
</Persons>

注意到,第一个Person元素中的name和age首字母小写,而第二个保持不变,而这时候我们的解析代码如下:

package main
import (
    "encoding/xml"
    "log"
    "io/ioutil"
)
type Result struct {
    Person []Person
}
type Person struct {
    Name string `xml:",attr"`
    Age int `xml:",attr"`
    Career string
    Interests Interests
}
type Interests struct {
    Interest []string
}
func main() {
    content, err := ioutil.ReadFile("studygolang.xml")
    if err != nil {
        log.Fatal(err)
    }
    var result Result
    err = xml.Unmarshal(content, &result)
    if err != nil {
        log.Fatal(err)
    }
    log.Println(result)
}

结果:
{[{ 0 无业游民 {[编程 下棋]}} {studygolang 27 码农 {[编程 下棋]}}]}

可见,第一个Person没解析成功,第二个成功了。这个时候可以引出为什么”,attr”前面多个逗号了。

我们将Person结构改为如下:

type Person struct {
    Name string `xml:"name,attr"`
    Age int `xml:"age,attr"`
    Career string
    Interests Interests
}

这个时候执行结果为:
{[{polaris 28 无业游民 {[编程 下棋]}} { 0 码农 {[编程 下棋]}}]}

第一个Person解析成功了,但第二个Person没解析成功。你可以试着将tag中name和age首字母改为大写试试。
由此得出:tag的优先级高于Field,而且tag中也是区分大小写的。为了统一,xml中元素应该全部用小写。
注意:name、逗号和attr直接不能有空格

到现在,将xml中所有元素都改为小写,然后修改Go代码,正确解析该xml应该没问题了。

type Result struct {
    Person []Person `xml:"person"`
}
type Person struct {
    Name string `xml:"name,attr"`
    Age int `xml:"age,attr"`
    Career string `xml:"career"`
    Interests Interests `xml:"interests"`
}
type Interests struct {
    Interest []string `xml:"interest"`
}

现在,是时候总结一下XML到Go中struct的转换规则了。(标准库encoding/xml文档有详细的说明)

1)如果struct的一个字段是string或者[]byte类型且它的tag含有”,innerxml”,Unmarshal会将此字段所对应的元素内所有内嵌的原始xml累加到此字段上。

2)如果struct中有一个叫做XMLName且类型为xml.Name的字段,Unmarshal会保存对应的元素的名字到该字段。比如,上面的例子,在Person结构中加上该字段XMLName xml.Name,则结果会是:{[{{ person} polaris 28 无业游民 {[编程 下棋]}} {{ person} studygolang 27 码农 {[编程 下棋]}}]}。可见,该字段是用来映射XML元素的,在生成XML时比较有用。注意,XMLName和类型xml.Name必须是这样,不能改为XmlName。

3)如果XMLName字段有tag,且tag的形式是:”name”或”namespace-URL name”,则相应的XML元素必须是这个名字(命名空间可选),否则Unmarshal会返回错误。可以通过在上面的例子中,修改Person的XMLName xml.Name `xml:”myperson”`试试,会报错:expected element typebut have

4)如果某个XML元素有一个属性,它的名字和struct中某个字段匹配(大小写都得匹配),并且该字段的tag包含”,attr”,或者元素的名字显示的被写在了tag中(”name,attr”),这时,Unmarshal会将该属性赋值给该字段。如上面的Name和Age

5)如果XML元素包含字符数据(character data),那么,字符数据会被累加到struct中第一个有tag为”,chardata”的字段。struct字段的类型可以是string或[]byte。如果没有这样的字段,字符数据会被丢弃。如上面的Interests可以再定义一个类型Interest:
type Interest struct {
Inter string `xml:”,chardata”`
}
Interests 中相应的改为:Interest []Interest
当然这个例子中这种方式有些啰嗦。

6)如果某个XML元素包含一条或者多条注释,那么这些注释将被累加到第一个含有”,comments” tag的字段上,这个字段的类型可以是[]byte或string,如果没有这样的字段存在,那么注释将会被丢弃。

7)如果某个XML元素的子元素的名字和 “a”或 “a>b>c”这种格式的tag的前缀匹配,Unmarshal会沿着XML结构向下寻找这样名字的元素,然后将最里面的元素映射到struct的字段上。以”>”开始的tag和字段后面跟上”>”是等价的。从这知道,上面例子中关于Interests的解析可以更简单,即不需要Interest结构类型

8)如果某XML元素的子元素的名字和某个struct的XMLName字段的tag匹配,且该struct的字段没有定义以上规则的tag,Unmarshal会映射该子元素到该struct的字段上。

9)如果某个XML元素的子元素的名字和一个没有任何tag的字段匹配,则Unmarshal会映射这个子元素到那个字段上。比如最开始没有使用tag的例子,使用的就是这条规则。

10)如果某个XML元素的子元素根据上面的规则都没有匹配到任何字段,然而,该struct有字段带有”,any”的tag,则Unmarshal会映射该子元素到该字段上。

11)一个非指针的匿名struct字段会被这样处理:该字段的值是外部struct的一部分

12)如果一个struct字段的tag定义为”-”,则Unmarshal不会给它赋值

这些规则,有些没有解释怎么使用,这里用标准库中的一个例子说明。(见我加的注释)

type Email struct {
    Where string `xml:"where,attr"`
    Addr  string
}
type Address struct {
    City, State string
}
type Result struct {
    XMLName xml.Name `xml:"Person"`     // 一般建议根元素加上此字段
    Name    string   `xml:"FullName"`
    Phone   string
    Email   []Email
    Groups  []string `xml:"Group>Value"`     // 规则 7,可见字段名可以随意
    Address                                  // 规则11
}
v := Result{Name: "none", Phone: "none"}
data := `
    <Person>
        <FullName>Grace R. Emlin</FullName>
        <Company>Example Inc.</Company>
        <Email where="home">
            <Addr>gre@example.com</Addr>
        </Email>
        <Email where='work'>
            <Addr>gre@work.com</Addr>
        </Email>
        <Group>
            <Value>Friends</Value>
            <Value>Squash</Value>
        </Group>
        <City>Hanga Roa</City>
        <State>Easter Island</State>
    </Person>
`
err := xml.Unmarshal([]byte(data), &v)
if err != nil {
    fmt.Printf("error: %v", err)
    return
}
fmt.Printf("XMLName: %#v\n", v.XMLName)
fmt.Printf("Name: %q\n", v.Name)
fmt.Printf("Phone: %q\n", v.Phone)
fmt.Printf("Email: %v\n", v.Email)
fmt.Printf("Groups: %v\n", v.Groups)
fmt.Printf("Address: %v\n", v.Address)

3、完整的例子

package main
import (
    "encoding/xml"
    "log"
    "io/ioutil"
)
type Result struct {
    XMLName xml.Name `xml:"persons"`
    Persons []Person `xml:"person"`
}
type Person struct {
    Name string `xml:"name,attr"`
    Age int `xml:"age,attr"`
    Career string `xml:"career"`
    Interests []string `xml:"interests>interest"`
}
func main() {
    content, err := ioutil.ReadFile("studygolang.xml")
    if err != nil {
        log.Fatal(err)
    }
    var result Result
    err = xml.Unmarshal(content, &result)
    if err != nil {
        log.Fatal(err)
    }
    log.Println(result)
    log.Println(result.Persons[0].Name)
}

studygolang.xml:

<?xml version="1.0" encoding="UTF-8"?>
<persons>
    <person name="polaris" age="28">
        <career>无业游民</career>
        <interests>
            <interest>编程</interest>
            <interest>下棋</interest>
        </interests>
    </person>
    <person name="studygolang" age="27">
        <career>码农</career>
        <interests>
            <interest>编程</interest>
            <interest>下棋</interest>
        </interests>
    </person>
</persons>
十一 302012
 

一、数据结构

Template:代表一个解析过的模板

二、获取模板(Template)实例

三种方式:

1、template.New(name string) *Template

其实内部就简单的return &Template{name:name};
name标示模板(模板的名字)

2、template.ParseFiles(filenames …string) (*Template, error)

从一个或多个文件中读取要解析的文本生成模板类,并以第一个文件名作为模板名
注:文件可以是绝对路径或相对路径

3、tempplate.ParseGlob(pattern string) (*Template, error)

内部调用filepath.Glob来解析pattern对应的文件(正则匹配文件路径,可能会多个),
然后调用template.ParseFiles()

注:以上三种方式,第一种只是得到了一个Template的实例,而第二、三种已经执行了Parse()

三、模板库使用基本步骤

1、获得template.Template实例:tpl
2、如果是第一种方式获取的,则需要执行tpl.Parse或tpl.ParseFiles或tpl.ParseGlob;
否则,第一步已经做了Parse
3、调用tpl.Execute,应用数据,并输出

template中提供了一个Must函数,用于方便处理错误,因为Parse出错不应该在运行时才暴露,所以,Must中会将panic,如果error!=nil
所以一般这么用:
tpl := template.Must(template.New(“test”).Parse(“text”))

未完待续。。。

十一 292012
 

学习一门语言,熟悉语言语法、规范等之后,应该学习语言的标准库。在Python中,会有一些函数来探究包的内容。在Go中,更多的是通过查看Go标准库文档来学习。

不仅要知其然,更要知其所以然。

实际写代码中,肯定需要用到很多标准库中的包,在学习阶段,可以在需要用某个包时,彻底学习这个包,掌握它。标准库中每个包的文档是学习包最好的资料,一定要仔细看明白。

Go包具体该怎么学了?以下是我自己的学习方法,仅供参考(以time包为例)

1、看文档中的Overview,整体上对该包有一个了解

从这知道,该包用于处理和显示日期和时间。日期都是公历。

2、看Index,关注函数和类型(函数是指不属于某种类型的func),不需要关注类型的方法(Variable和Constants需要大概关注)

1)定义了一些常量,表示时间格式
2)定义了Duration、Location、Month、ParseError、Ticker、Time、Timer和Weekday等类型
从名字可以很容易的知道这些类型所代表的含义
3)有After、Sleep和Tick三个全局函数

其实全局函数还有其他的,但是其他全局函数在文档中放在了某种类型的下面,表示该全局函数会生成一个该类型的实例,这个可以当做类型的方法来研究

这一步,我们可以不关心这些类型该怎么使用,我们关心全局函数怎么用
①func After(d Duration) 在【Index】视图 点击该函数,跳转到该函数的说明处。
函数注释已经说的很清楚:指定的时间过去之后,将当前发送到chanel中返回,和NewTimer(d).C功能相同。另外,还提供了使用示例。
②func Sleep(d Duration)
很明显,暂停当前goroutine一段时间
③func Tick(d Duration) 这是包设计中 设计函数 常用的方式:方便包的使用者,直接使用Tiker类型,而不需要另外实例化再调用其方法。Tick方法适用于那些需要使用Tiker,但是永远不需要停止的场景。

以上三个函数文档都提供了对应的例子

3、关注类型及其方法 (以Duration为例)

1)看类型的定义(结构)

type Duration int64
类型注释说,Duration表示两个时间的间隔,单位是纳秒(nanosecond)
该类型预定义了一些常量,这样方便类型转换、计算以及阅读。另外还提供了一些示例。

2)实例化方式

由于Duration是int64的别名,整型字面值常量可以直接赋值给Duration。对于整型类型的变量,则需要类型转换
比如,Sleep函数接收Duration类型的参数。如果想Sleep 2秒,这样写:Sleep(2 * time.Second)或者Sleep(2e9)。或者 i := 2; Sleep(time.Duration(i) * time.Second)

另外,ParseDuration可以将字符串转为Duration;Since()也可以获取Duration的实例

3)提供的方法

分别转为时、分、纳秒、秒。除了纳秒,其他都是float64。
这里说一下String()这个方法:
△如果某种类型定义了String()方法,那么,fmt包格式化时,直接传入类型,内部会调用类型的String()方法进行格式化,类似于Java中toString方法

4、自己写几个例子练练手

比如,想要格式化输出当前时间,格式为:2012-11-27 15:10:00
time.Now().Format(“2006-01-02 15:04:05″)

注意,2006这样的必须固定,不能为2007什么的。(不知道为什么这么设计)

设计了一个包实现类似PHP中date()和strtotime()的功能

https://github.com/polaris1119/times

5、学习第三方包(几种生成文档的方式)

1)将包放入$GOROOT/src/pkg/ 目录下,godoc 时会生成该包的文档(当做标准库包了)
2)设定GOPATH,将第三方包放在GOPATH中
3)使用godoc命令时,通过path指定第三方包的路径
4)直接通过godoc命令在命令行看起文档(不直观)
5)http://go.pkgdoc.org/在这个网站搜索包,有的话,会提供文档(这个网站会搜索github上的资源)