diff --git a/1.1.md b/1.1.md index 11b7cd9d5393eed52719489a15bdd99f08b23271..95f8b8b18e7ca09bbefee77d32021cf8e7ba7485 100644 --- a/1.1.md +++ b/1.1.md @@ -130,8 +130,5 @@ homebrew是Mac系统下面目前使用最多的管理软件的工具,目前已 * 上一节: [Go环境配置](<1.md>) * 下一节: [GOPATH 与工作空间](<1.2.md>) -## LastModified - * $Id$ - [downlink]: http://code.google.com/p/go/downloads/list "Go安装包下载" [hg]: http://mercurial.selenic.com/downloads/ "Mercurial下载" diff --git a/1.2.md b/1.2.md index 7dd28c80ccf870a6c08cc288ac9be9d2eca69781..1f5db4c77f5f42ac9180bd517b02fd63733c94c0 100644 --- a/1.2.md +++ b/1.2.md @@ -1,162 +1,159 @@ -# 1.2 GOPATH与工作空间 - -## GOPATH设置 - go 命令依赖一个重要的环境变量:$GOPATH1 - - *(注:这个不是Go安装目录。下面以笔者的工作目录为说明,请替换自己机器上的工作目录。)* - - 在类似 Unix 环境大概这样设置: - - export GOPATH=/home/apple/mygo - - Windows 设置如下,新建一个环境变量名称叫做GOPATH: - - GOPATH=c:\mygo - -GOPATH允许多个目录,当有多个目录时,请注意分隔符,多个GOPATH的时候Windows是分号,Linux系统是冒号,当有多个GOPATH时,默认会将go get的内容放在第一个目录下 - - -以上 $GOPATH 目录约定有三个子目录: - -- src 存放源代码(比如:.go .c .h .s等) -- pkg 编译后生成的文件(比如:.a) -- bin 编译后生成的可执行文件(为了方便,可以把此目录加入到 $PATH 变量中) - -以后我所有的例子都是以mygo作为我的gopath目录 - -## 应用目录结构 -建立包和目录:$GOPATH/src/mymath/sqrt.go(包名:"mymath") - -以后自己新建应用或者一个代码包都是在src目录下新建一个文件夹,文件夹名称代码包名称,当然也允许多级目录,例如在src下面新建了目录$GOPATH/src/github.com/astaxie/beedb 那么这个包名称就是“github.com/astaxie/beedb” - -执行如下代码 - - cd $GOPATH/src - mkdir mymath - -新建文件sqrt.go,内容如下 - - // $GOPATH/src/mymath/sqrt.go源码如下: - package mymath - - func Sqrt(x float64) float64 { - z := 0.0 - for i := 0; i < 1000; i++ { - z -= (z*z - x) / (2 * x) - } - return z - } - -这样我的应用包目录和代码已经新建完毕,注意:package的名称必须和目录名保持一致 - -## 编译应用 -上面我们已经建立了自己的应用包,如何进行编译安装呢?有两种方式可以进行安装 - -1、只要进入对应的应用包目录,然后执行`go install`,就可以安装了 - -2、在任意的目录执行如下代码`go install mymath` - -安装完之后,我们可以进入如下目录 - - cd $GOPATH/pkg/${GOOS}_${GOARCH} - //可以看到如下文件 - mymath.a - -这个.a文件是应用包,相当于一个函数库一样,那么我们如何进行调用呢? - -接下来我们新建一个应用程序来调用 - -新建应用包mathapp - - cd $GOPATH/src - mkdir mathapp - vim main.go - -// `$GOPATH/src/mathapp/main.go`源码: - - package main - - import ( - "mymath" - "fmt" - ) - - func main() { - fmt.Printf("Hello, world. Sqrt(2) = %v\n", mymath.Sqrt(2)) - } - -如何编译程序呢?进入该应用目录,然后执行`go build`,那么在该目录下面会生成一个mathapp的可执行文件 - - ./mathapp - -输出如下内容 - - Hello, world. Sqrt(2) = 1.414213562373095 - -如何安装该应用,进入该目录执行`go install`,那么在$GOPATH/bin/下增加了一个可执行文件mathapp,这样可以在命令行输入如下命令就可以执行 - - mathapp - -也是输出如下内容 - - Hello, world. Sqrt(2) = 1.414213562373095 - -## 获取远程包 - go语言有一个获取远程包的工具就是`go get`,目前go get支持多数开源社区(例如:github、googlecode、bitbucket、Launchpad) - - go get github.com/astaxie/beedb - -通过这个命令可以获取相应的源码,对应的开源平台采用不同的源码控制工具,例如github采用git、googlecode采用hg,所以要想获取这些源码,必须先安装相应的源码控制工具 - -通过上面获取的代码在我们本地的源码相应的代码结构如下 - - $GOPATH - src - |--github.com - |-astaxie - |-beedb - pkg - |--相应平台 - |-github.com - |--astaxie - |beedb.a - -go get本质上可以理解为首先第一步是通过源码工具clone代码到src下面,然后执行`go install` - -在代码中如何使用远程包,很简单的就是和使用本地包一样,只要在开头import相应的路径就可以 - - import "github.com/astaxie/beedb" - -## 程序的整体结构 -通过上面建立的我本地的mygo的目录结构如下所示 - - bin/ - mathapp - pkg/ - 平台名/ 如:darwin_amd64、linux_amd64 - mymath.a - github.com/ - astaxie/ - beedb.a - src/ - mathapp - main.go - mymath/ - sqrt.go - github.com/ - astaxie/ - beedb/ - beedb.go - util.go - -从上面的结构我们可以很清晰的看到,bin目录下面存的是编译之后可执行的文件,pkg下面存放的是函数包,src下面保存的是应用源代码 - - - - - -[1] Windows系统中环境变量的形式为`%GOPATH%`,本书主要使用Unix形式,Windows用户请自行替换。 -## links - * [目录]() - * 上一节: [GO安装](<1.1.md>) - * 下一节: [GO 命令](<1.3.md>) - -## LastModified - * $Id$ +# 1.2 GOPATH与工作空间 + +## GOPATH设置 + go 命令依赖一个重要的环境变量:$GOPATH1 + + *(注:这个不是Go安装目录。下面以笔者的工作目录为说明,请替换自己机器上的工作目录。)* + + 在类似 Unix 环境大概这样设置: + + export GOPATH=/home/apple/mygo + + Windows 设置如下,新建一个环境变量名称叫做GOPATH: + + GOPATH=c:\mygo + +GOPATH允许多个目录,当有多个目录时,请注意分隔符,多个GOPATH的时候Windows是分号,Linux系统是冒号,当有多个GOPATH时,默认会将go get的内容放在第一个目录下 + + +以上 $GOPATH 目录约定有三个子目录: + +- src 存放源代码(比如:.go .c .h .s等) +- pkg 编译后生成的文件(比如:.a) +- bin 编译后生成的可执行文件(为了方便,可以把此目录加入到 $PATH 变量中) + +以后我所有的例子都是以mygo作为我的gopath目录 + +## 应用目录结构 +建立包和目录:$GOPATH/src/mymath/sqrt.go(包名:"mymath") + +以后自己新建应用或者一个代码包都是在src目录下新建一个文件夹,文件夹名称代码包名称,当然也允许多级目录,例如在src下面新建了目录$GOPATH/src/github.com/astaxie/beedb 那么这个包名称就是“github.com/astaxie/beedb” + +执行如下代码 + + cd $GOPATH/src + mkdir mymath + +新建文件sqrt.go,内容如下 + + // $GOPATH/src/mymath/sqrt.go源码如下: + package mymath + + func Sqrt(x float64) float64 { + z := 0.0 + for i := 0; i < 1000; i++ { + z -= (z*z - x) / (2 * x) + } + return z + } + +这样我的应用包目录和代码已经新建完毕,注意:package的名称必须和目录名保持一致 + +## 编译应用 +上面我们已经建立了自己的应用包,如何进行编译安装呢?有两种方式可以进行安装 + +1、只要进入对应的应用包目录,然后执行`go install`,就可以安装了 + +2、在任意的目录执行如下代码`go install mymath` + +安装完之后,我们可以进入如下目录 + + cd $GOPATH/pkg/${GOOS}_${GOARCH} + //可以看到如下文件 + mymath.a + +这个.a文件是应用包,相当于一个函数库一样,那么我们如何进行调用呢? + +接下来我们新建一个应用程序来调用 + +新建应用包mathapp + + cd $GOPATH/src + mkdir mathapp + vim main.go + +// `$GOPATH/src/mathapp/main.go`源码: + + package main + + import ( + "mymath" + "fmt" + ) + + func main() { + fmt.Printf("Hello, world. Sqrt(2) = %v\n", mymath.Sqrt(2)) + } + +如何编译程序呢?进入该应用目录,然后执行`go build`,那么在该目录下面会生成一个mathapp的可执行文件 + + ./mathapp + +输出如下内容 + + Hello, world. Sqrt(2) = 1.414213562373095 + +如何安装该应用,进入该目录执行`go install`,那么在$GOPATH/bin/下增加了一个可执行文件mathapp,这样可以在命令行输入如下命令就可以执行 + + mathapp + +也是输出如下内容 + + Hello, world. Sqrt(2) = 1.414213562373095 + +## 获取远程包 + go语言有一个获取远程包的工具就是`go get`,目前go get支持多数开源社区(例如:github、googlecode、bitbucket、Launchpad) + + go get github.com/astaxie/beedb + +通过这个命令可以获取相应的源码,对应的开源平台采用不同的源码控制工具,例如github采用git、googlecode采用hg,所以要想获取这些源码,必须先安装相应的源码控制工具 + +通过上面获取的代码在我们本地的源码相应的代码结构如下 + + $GOPATH + src + |--github.com + |-astaxie + |-beedb + pkg + |--相应平台 + |-github.com + |--astaxie + |beedb.a + +go get本质上可以理解为首先第一步是通过源码工具clone代码到src下面,然后执行`go install` + +在代码中如何使用远程包,很简单的就是和使用本地包一样,只要在开头import相应的路径就可以 + + import "github.com/astaxie/beedb" + +## 程序的整体结构 +通过上面建立的我本地的mygo的目录结构如下所示 + + bin/ + mathapp + pkg/ + 平台名/ 如:darwin_amd64、linux_amd64 + mymath.a + github.com/ + astaxie/ + beedb.a + src/ + mathapp + main.go + mymath/ + sqrt.go + github.com/ + astaxie/ + beedb/ + beedb.go + util.go + +从上面的结构我们可以很清晰的看到,bin目录下面存的是编译之后可执行的文件,pkg下面存放的是函数包,src下面保存的是应用源代码 + + - - - +[1] Windows系统中环境变量的形式为`%GOPATH%`,本书主要使用Unix形式,Windows用户请自行替换。 +## links + * [目录]() + * 上一节: [GO安装](<1.1.md>) + * 下一节: [GO 命令](<1.3.md>) diff --git a/1.3.md b/1.3.md index e997a5d50aaf2d80da3ecfc4df4a055eddada6a2..cf51e55c8b45a0afe992993875e1b80099775ca8 100644 --- a/1.3.md +++ b/1.3.md @@ -1,111 +1,108 @@ -# 1.3 Go 命令 - -## Go 命令 - - Go语言自带有一套完整的命令操作工具,你可以通过在命令行中执行`go`来查看它们: - - ![](images/1.3.go.png?raw=true) - - 这些命令对于我们平时编写的代码非常有用,接下来就让我们了解一些常用的命令。 - -## go build - - 这个命令主要用于测试编译。在包的编译过程中,若有必要,会同时编译与之相关联的包。 - - - 如果是普通包,就像我们在1.2节中编写的`mymath`包那样,当你执行`go build`之后,它不会产生任何文件。如果你需要在`$GOPATH/pkg`下生成相应的文件,那就得执行`go install`了。 - - - 如果是`main`包,当你执行`go build`之后,它就会在当前目录下生成一个可执行文件。如果你需要在`$GOPATH/bin`下生成相应的文件,同样需要执行`go install`。 - - - 如果某个项目文件夹下有多个文件,而你只想编译某个文件,就可在`go build`之后加上文件名,例如`go build a.go`;`go build`命令默认会编译当前目录下的所有go文件。 - - - 你也可以指定编译输出的文件名。例如1.2节中的`mathapp`应用,我们可以指定`go build -o astaxie.exe`,默认情况是你的package名,就是你的文件夹名称。 - - (注:实际上,package名在[Go语言规范](https://golang.org/ref/spec)中指代码中“package”后使用的名称,此名称可以与文件夹名不同。默认生成的可执行文件名是文件夹名。) - - - go build会忽略目录下以“_”或“.”开头的go文件。 - - - 如果你的源代码针对不同的操作系统需要不同的处理,那么你可以根据不同的操作系统后缀来命名文件。例如有一个读取数组的程序,它对于不同的操作系统可能有如下几个源文件: - - array_linux.go - array_darwin.go - array_windows.go - array_freebsd.go - - `go build`的时候会选择性地编译以系统名结尾的文件(linux、darwin、windows、freebsd)。例如Linux系统下面编译只会选择array_linux.go文件,其它系统命名后缀文件全部忽略。 - -## go clean - - 这个命令是用来移除当前源码包里面编译的文件的。这些文件包括 - - _obj/ 旧的object目录,由Makefiles遗留 - _test/ 旧的test目录,由Makefiles遗留 - _testmain.go 旧的gotest文件,由Makefiles遗留 - test.out 旧的test记录,由Makefiles遗留 - build.out 旧的test记录,由Makefiles遗留 - *.[568ao] object文件,由Makefiles遗留 - - DIR(.exe) 由go build产生 - DIR.test(.exe) 由go test -c产生 - MAINFILE(.exe) 由go build MAINFILE.go产生 - - 我一般都是利用这个命令进行清除编译文件,然后github递交源码,在本机测试的时候这些编译文件都是和系统相关的,但是对于源码管理来说没必要 - -## go fmt - - 有过C/C++经验的读者会知道,一些人经常为代码采取K&R风格还是ANSI风格而争论不休。在go中,代码则有标准的风格。由于之前已经有的一些习惯或其它的原因我们常将代码写成ANSI风格或者其它更合适自己的格式,这将为人们在阅读别人的代码时添加不必要的负担,所以go强制了代码格式(比如左大括号必须放在行尾),不按照此格式的代码将不能编译通过,为了减少浪费在排版上的时间,go工具集中提供了一个`go fmt`命令 它可以帮你格式化你写好的代码文件,使你写代码的时候不需要关心格式,你只需要在写完之后执行`go fmt <文件名>.go`,你的代码就被修改成了标准格式,但是我平常很少用到这个命令,因为开发工具里面一般都带了保存时候自动格式化功能,这个功能其实在底层就是调用了`go fmt`。接下来的一节我将讲述两个工具,这两个工具都自带了保存文件时自动化`go fmt`功能。 - -## go get - - 这个命令是用来动态获取远程代码包的,目前支持的有BitBucket、GitHub、Google Code和Launchpad。这个命令在内部实际上分成了两步操作:第一步是下载源码包,第二步是执行`go install`。下载源码包的go工具会自动根据不同的域名调用不同的源码工具,对应关系如下: - - BitBucket (Mercurial Git) - GitHub (Git) - Google Code Project Hosting (Git, Mercurial, Subversion) - Launchpad (Bazaar) - - 所以为了`go get` 能正常工作,你必须确保安装了合适的源码管理工具,并同时把这些命令加入你的PATH中。其实`go get`支持自定义域名的功能,具体参见`go help remote`。 - -## go install - - 这个命令在内部实际上分成了两步操作:第一步是`go build`,第二步会把编译好的东西move到`$GOPATH/pkg`或者`$GOPATH/bin`。 - -## go test - - 执行这个命令,会自动读取源码目录下面名为`*_test.go`的文件,生成并运行测试用的可执行文件。输出的信息类似 - - ok archive/tar 0.011s - FAIL archive/zip 0.022s - ok compress/gzip 0.033s - ... - - 默认的情况下,不需要任何的参数,它会自动把你源码包下面所有test文件测试完毕,当然你也可以带上参数,详情请参考`go help testflag` - -## go doc - - 很多人说go不需要任何的第三方文档,例如chm手册之类的(其实我已经做了一个了,[chm手册](https://github.com/astaxie/godoc)),因为它内部就有一个很强大的文档工具。 - - 如何查看相应package的文档呢? - 例如builtin包,那么执行`go doc builtin` - 如果是http包,那么执行`go doc net/http` - 查看某一个包里面的函数,那么执行`godoc fmt Printf` - 也可以查看相应的代码,执行`godoc -src fmt Printf` - - 通过命令在命令行执行 godoc -http=:端口号 比如`godoc -http=:8080`。然后在浏览器中打开`127.0.0.1:8080`,你将会看到一个golang.org的本地copy版本,通过它你可以查询pkg文档等其它内容。如果你设置了GOPATH,在pkg分类下,不但会列出标准包的文档,还会列出你本地`GOPATH`中所有项目的相关文档,这对于经常被墙的用户来说是一个不错的选择。 - -## 其它命令 - - go还提供了其它很多的工具,例如下面的这些工具 - - go fix 用来修复以前老版本的代码到新版本,例如go1之前老版本的代码转化到go1 - go version 查看go当前的版本 - go env 查看当前go的环境变量 - go list 列出当前全部安装的package - go run 编译并运行Go程序 - -## links - * [目录]() - * 上一节: [GOPATH与工作空间](<1.2.md>) - * 下一节: [Go开发工具](<1.4.md>) - -## LastModified - * $Id$ +# 1.3 Go 命令 + +## Go 命令 + + Go语言自带有一套完整的命令操作工具,你可以通过在命令行中执行`go`来查看它们: + + ![](images/1.3.go.png?raw=true) + + 这些命令对于我们平时编写的代码非常有用,接下来就让我们了解一些常用的命令。 + +## go build + + 这个命令主要用于测试编译。在包的编译过程中,若有必要,会同时编译与之相关联的包。 + + - 如果是普通包,就像我们在1.2节中编写的`mymath`包那样,当你执行`go build`之后,它不会产生任何文件。如果你需要在`$GOPATH/pkg`下生成相应的文件,那就得执行`go install`了。 + + - 如果是`main`包,当你执行`go build`之后,它就会在当前目录下生成一个可执行文件。如果你需要在`$GOPATH/bin`下生成相应的文件,同样需要执行`go install`。 + + - 如果某个项目文件夹下有多个文件,而你只想编译某个文件,就可在`go build`之后加上文件名,例如`go build a.go`;`go build`命令默认会编译当前目录下的所有go文件。 + + - 你也可以指定编译输出的文件名。例如1.2节中的`mathapp`应用,我们可以指定`go build -o astaxie.exe`,默认情况是你的package名,就是你的文件夹名称。 + + (注:实际上,package名在[Go语言规范](https://golang.org/ref/spec)中指代码中“package”后使用的名称,此名称可以与文件夹名不同。默认生成的可执行文件名是文件夹名。) + + - go build会忽略目录下以“_”或“.”开头的go文件。 + + - 如果你的源代码针对不同的操作系统需要不同的处理,那么你可以根据不同的操作系统后缀来命名文件。例如有一个读取数组的程序,它对于不同的操作系统可能有如下几个源文件: + + array_linux.go + array_darwin.go + array_windows.go + array_freebsd.go + + `go build`的时候会选择性地编译以系统名结尾的文件(linux、darwin、windows、freebsd)。例如Linux系统下面编译只会选择array_linux.go文件,其它系统命名后缀文件全部忽略。 + +## go clean + + 这个命令是用来移除当前源码包里面编译的文件的。这些文件包括 + + _obj/ 旧的object目录,由Makefiles遗留 + _test/ 旧的test目录,由Makefiles遗留 + _testmain.go 旧的gotest文件,由Makefiles遗留 + test.out 旧的test记录,由Makefiles遗留 + build.out 旧的test记录,由Makefiles遗留 + *.[568ao] object文件,由Makefiles遗留 + + DIR(.exe) 由go build产生 + DIR.test(.exe) 由go test -c产生 + MAINFILE(.exe) 由go build MAINFILE.go产生 + + 我一般都是利用这个命令进行清除编译文件,然后github递交源码,在本机测试的时候这些编译文件都是和系统相关的,但是对于源码管理来说没必要 + +## go fmt + + 有过C/C++经验的读者会知道,一些人经常为代码采取K&R风格还是ANSI风格而争论不休。在go中,代码则有标准的风格。由于之前已经有的一些习惯或其它的原因我们常将代码写成ANSI风格或者其它更合适自己的格式,这将为人们在阅读别人的代码时添加不必要的负担,所以go强制了代码格式(比如左大括号必须放在行尾),不按照此格式的代码将不能编译通过,为了减少浪费在排版上的时间,go工具集中提供了一个`go fmt`命令 它可以帮你格式化你写好的代码文件,使你写代码的时候不需要关心格式,你只需要在写完之后执行`go fmt <文件名>.go`,你的代码就被修改成了标准格式,但是我平常很少用到这个命令,因为开发工具里面一般都带了保存时候自动格式化功能,这个功能其实在底层就是调用了`go fmt`。接下来的一节我将讲述两个工具,这两个工具都自带了保存文件时自动化`go fmt`功能。 + +## go get + + 这个命令是用来动态获取远程代码包的,目前支持的有BitBucket、GitHub、Google Code和Launchpad。这个命令在内部实际上分成了两步操作:第一步是下载源码包,第二步是执行`go install`。下载源码包的go工具会自动根据不同的域名调用不同的源码工具,对应关系如下: + + BitBucket (Mercurial Git) + GitHub (Git) + Google Code Project Hosting (Git, Mercurial, Subversion) + Launchpad (Bazaar) + + 所以为了`go get` 能正常工作,你必须确保安装了合适的源码管理工具,并同时把这些命令加入你的PATH中。其实`go get`支持自定义域名的功能,具体参见`go help remote`。 + +## go install + + 这个命令在内部实际上分成了两步操作:第一步是`go build`,第二步会把编译好的东西move到`$GOPATH/pkg`或者`$GOPATH/bin`。 + +## go test + + 执行这个命令,会自动读取源码目录下面名为`*_test.go`的文件,生成并运行测试用的可执行文件。输出的信息类似 + + ok archive/tar 0.011s + FAIL archive/zip 0.022s + ok compress/gzip 0.033s + ... + + 默认的情况下,不需要任何的参数,它会自动把你源码包下面所有test文件测试完毕,当然你也可以带上参数,详情请参考`go help testflag` + +## go doc + + 很多人说go不需要任何的第三方文档,例如chm手册之类的(其实我已经做了一个了,[chm手册](https://github.com/astaxie/godoc)),因为它内部就有一个很强大的文档工具。 + + 如何查看相应package的文档呢? + 例如builtin包,那么执行`go doc builtin` + 如果是http包,那么执行`go doc net/http` + 查看某一个包里面的函数,那么执行`godoc fmt Printf` + 也可以查看相应的代码,执行`godoc -src fmt Printf` + + 通过命令在命令行执行 godoc -http=:端口号 比如`godoc -http=:8080`。然后在浏览器中打开`127.0.0.1:8080`,你将会看到一个golang.org的本地copy版本,通过它你可以查询pkg文档等其它内容。如果你设置了GOPATH,在pkg分类下,不但会列出标准包的文档,还会列出你本地`GOPATH`中所有项目的相关文档,这对于经常被墙的用户来说是一个不错的选择。 + +## 其它命令 + + go还提供了其它很多的工具,例如下面的这些工具 + + go fix 用来修复以前老版本的代码到新版本,例如go1之前老版本的代码转化到go1 + go version 查看go当前的版本 + go env 查看当前go的环境变量 + go list 列出当前全部安装的package + go run 编译并运行Go程序 + +## links + * [目录]() + * 上一节: [GOPATH与工作空间](<1.2.md>) + * 下一节: [Go开发工具](<1.4.md>) diff --git a/1.4.md b/1.4.md index cd91b8911444422417150e092c2cfdc55fc5adbd..a94521d4165e2c81da02f661ef4017507acf0f53 100644 --- a/1.4.md +++ b/1.4.md @@ -1,304 +1,301 @@ -# 1.4 Go开发工具 - -本节我将介绍几个开发工具,它们都具有自动化提示,自动化fmt功能。因为它们都是跨平台的,所以安装步骤之类的都是通用的。 - -## LiteIDE - - LiteIDE是一款专门为Go语言开发的集成开发环境(IDE),由visualfc编写。支持项目管理、集成构建、GDB调试、语法高亮、自动补全、大纲显示等功能。下载地址: [http://code.google.com/p/golangide/downloads/list](http://code.google.com/p/golangide/downloads/list),根据自己的系统下载相应的发行版本。Windows和Ubuntu系统可直接打开bin下面的liteide;Mac则需通过LaunchPad打开LiteIDE.app。 - - ![](images/1.4.liteide.png?raw=true) - - LiteIDE配置需要按照前面几个小节配置了相应的go和`$GOPATH`(LiteIDE中也可以图形化配置LiteIDE专用的GOPATH)。LiteIDE当前的编译环境可以通过编译工具栏上的环境配置来切换,如切换32位和64位,针对自己的系统,可能需要修改相应的LiteEnv环境变量,如64位版本,LiteIDE => 查看 => 选项 => LiteEnv => Win64.env => GOROOT=c:\go-w64 为你的`$GOROOT`,不然会无法使用`build`命令。 - - 配置好LiteIDE后,可以打开或拖动任何目录到LiteIDE中作为项目,LiteIDE的编译是针对当前编辑文档所属目录来执行相应的go命令,所以编译时要先打开相应的Go文件。LiteIDE仿IDEA界面,支持项目浏览、文件系统,Package浏览、Golang文档检索、类视图、大纲显示等多个工具窗口的切换。 - - 代码补全需要安装gocode,目前LiteIDE的自动化提示只支持本文件中函数的提示,还没有做到整个项目中函数的提示。 - - -## Sublime Text - - 这里将介绍Sublime Text 2(以下简称Sublime)+GoSublime+gocode+MarGo的组合,那么为什么选择这个组合呢? - - - 自动化提示代码,如下图所示 - ![](images/1.4.sublime1.png?raw=true) - - - 保存的时候自动格式化代码,让您编写的代码更加美观,符合Go的标准。 - - 支持项目管理 - ![](images/1.4.sublime2.png?raw=true) - - 支持语法高亮 - - Sublime Text 2可免费使用,只是保存次数达到一定数量之后就会提示是否购买,点击取消继续用,和正式注册版本没有任何区别。 - - 接下来就开始讲如何安装,下载[Sublime](http://www.sublimetext.com/) - - 根据自己相应的系统下载相应的版本,然后打开Sublime,对于不熟悉Sublime的同学可以先看一下这篇文章[Sublime Text 2 入门及技巧](http://lucifr.com/139225/sublime-text-2-tricks-and-tips/) - - 1. 打开之后安装 Package Control:Ctrl+` 打开命令行,执行如下代码: - - import urllib2,os; pf='Package Control.sublime-package'; ipp=sublime.installed_packages_path(); os.makedirs(ipp) if not os.path.exists(ipp) else None; urllib2.install_opener(urllib2.build_opener(urllib2.ProxyHandler())); open(os.path.join(ipp,pf),'wb').write(urllib2.urlopen('http://sublime.wbond.net/'+pf.replace(' ','%20')).read()); print 'Please restart Sublime Text to finish installation' - - 这个时候重启一下Sublime,可以发现在在菜单栏多了一个如下的栏目,说明Package Control已经安装成功了。 - - ![](images/1.4.sublime3.png?raw=true) - - 2. 接下来安装gocode和MarGo。 - 打开终端运行如下代码(需要git) - - go get -u github.com/nsf/gocode - go get -u github.com/DisposaBoy/MarGo - - 这个时候我们会发现在`$GOPATH/bin`下面多了两个可执行文件,gocode和MarGo,这两个文件会在GoSublime加载时自动启动。 - - 3. 安装完之后就可以安装Sublime的插件了。需安装GoSublime、SidebarEnhancements和Go Build,安装插件之后记得重启Sublime生效,Ctrl+Shift+p打开Package Controll 输入`pcip`(即“Package Control: Install Package”的缩写)。 - - 这个时候看左下角显示正在读取包数据,完成之后出现如下界面 - - ![](images/1.4.sublime4.png?raw=true) - - 这个时候输入GoSublime,按确定就开始安装了。同理应用于SidebarEnhancements和Go Build。 - - 4. 验证是否安装成功,你可以打开Sublime,打开main.go,看看语法是不是高亮了,输入`import`是不是自动化提示了,`import "fmt"`之后,输入`fmt.`是不是自动化提示有函数了。 - - 如果已经出现这个提示,那说明你已经安装完成了,并且完成了自动提示。 - - 如果没有出现这样的提示,一般就是你的`$PATH`没有配置正确。你可以打开终端,输入gocode,是不是能够正确运行,如果不行就说明`$PATH`没有配置正确。 - - -## Vim -Vim是从vi发展出来的一个文本编辑器, 代码补全、编译及错误跳转等方便编程的功能特别丰富,在程序员中被广泛使用。 - - ![](images/1.4.vim.png?raw=true) - - 1. 配置vim高亮显示 - - cp -r $GOROOT/misc/vim/* ~/.vim/ - - 2. 在~/.vimrc文件中增加语法高亮显示 - - filetype plugin indent on - syntax on - - 3. 安装[Gocode](https://github.com/nsf/gocode/) - - go get -u github.com/nsf/gocode - - gocode默认安装到`$GOPATH/bin`下面,需要把`$GOPATH/bin`路径设置到系统`$PATH`里面。 - - 4. 配置[Gocode](https://github.com/nsf/gocode/) - - ~ cd $GOPATH/src/github.com/nsf/gocode/vim - ~ ./update.bash - ~ gocode set propose-builtins true - propose-builtins true - ~ gocode set lib-path "/home/border/gocode/pkg/linux_amd64" - lib-path "/home/border/gocode/pkg/linux_amd64" - ~ gocode set - propose-builtins true - lib-path "/home/border/gocode/pkg/linux_amd64" - - >gocode set里面的两个参数的含意说明: - > - >propose-builtins:是否自动提示Go的内置函数、类型和常量,默认为false,不提示。 - > - >lib-path:默认情况下,gocode只会搜索**$GOPATH/pkg/$GOOS_$GOARCH **和 **$GOROOT/pkg/$GOOS_$GOARCH**目录下的包,当然这个设置就是可以设置我们额外的lib能访问的路径 - - - - 5. 恭喜你,安装完成,你现在可以使用`:e main.go`体验一下开发Go的乐趣。 - - -## Emacs -Emacs传说中的神器,她不仅仅是一个编辑器,它是一个整合环境,或可称它为集成开发环境,这些功能如让使用者置身于全功能的操作系统中。 - - ![](images/1.4.emacs.png?raw=true) - - 1. 配置Emacs高亮显示 - - cp $GOROOT/misc/emacs/* ~/.emacs.d/ - - 2. 安装[Gocode](https://github.com/nsf/gocode/) - - go get -u github.com/nsf/gocode - - gocode默认安装到`$GOPATH/bin`里面下面,需要把`$GOPATH/bin`路径设置到系统`$PATH`里面。 - - 3. 配置[Gocode](https://github.com/nsf/gocode/) - - - ~ cd $GOPATH/src/github.com/nsf/gocode/emacs - ~ cp go-autocomplete.el ~/.emacs.d/ - ~ gocode set propose-builtins true - propose-builtins true - ~ gocode set lib-path "/home/border/gocode/pkg/linux_amd64" // 换为你自己的路径 - lib-path "/home/border/gocode/pkg/linux_amd64" - ~ gocode set - propose-builtins true - lib-path "/home/border/gocode/pkg/linux_amd64" - - 4. 需要安装 [Auto Completion](http://www.emacswiki.org/emacs/AutoComplete) - - 下载AutoComplete并解压 - - ~ make install DIR=$HOME/.emacs.d/auto-complete - - 配置~/.emacs文件 - - ;;auto-complete - (require 'auto-complete-config) - (add-to-list 'ac-dictionary-directories "~/.emacs.d/auto-complete/ac-dict") - (ac-config-default) - (local-set-key (kbd "M-/") 'semantic-complete-analyze-inline) - (local-set-key "." 'semantic-complete-self-insert) - (local-set-key ">" 'semantic-complete-self-insert) - - 详细信息参考: http://www.emacswiki.org/emacs/AutoComplete - - 5. 配置.emacs - - ;; golang mode - (require 'go-mode-load) - (require 'go-autocomplete) - ;; speedbar - ;; (speedbar 1) - (speedbar-add-supported-extension ".go") - (add-hook - 'go-mode-hook - '(lambda () - ;; gocode - (auto-complete-mode 1) - (setq ac-sources '(ac-source-go)) - ;; Imenu & Speedbar - (setq imenu-generic-expression - '(("type" "^type *\\([^ \t\n\r\f]*\\)" 1) - ("func" "^func *\\(.*\\) {" 1))) - (imenu-add-to-menubar "Index") - ;; Outline mode - (make-local-variable 'outline-regexp) - (setq outline-regexp "//\\.\\|//[^\r\n\f][^\r\n\f]\\|pack\\|func\\|impo\\|cons\\|var.\\|type\\|\t\t*....") - (outline-minor-mode 1) - (local-set-key "\M-a" 'outline-previous-visible-heading) - (local-set-key "\M-e" 'outline-next-visible-heading) - ;; Menu bar - (require 'easymenu) - (defconst go-hooked-menu - '("Go tools" - ["Go run buffer" go t] - ["Go reformat buffer" go-fmt-buffer t] - ["Go check buffer" go-fix-buffer t])) - (easy-menu-define - go-added-menu - (current-local-map) - "Go tools" - go-hooked-menu) - - ;; Other - (setq show-trailing-whitespace t) - )) - ;; helper function - (defun go () - "run current buffer" - (interactive) - (compile (concat "go run " (buffer-file-name)))) - - ;; helper function - (defun go-fmt-buffer () - "run gofmt on current buffer" - (interactive) - (if buffer-read-only - (progn - (ding) - (message "Buffer is read only")) - (let ((p (line-number-at-pos)) - (filename (buffer-file-name)) - (old-max-mini-window-height max-mini-window-height)) - (show-all) - (if (get-buffer "*Go Reformat Errors*") - (progn - (delete-windows-on "*Go Reformat Errors*") - (kill-buffer "*Go Reformat Errors*"))) - (setq max-mini-window-height 1) - (if (= 0 (shell-command-on-region (point-min) (point-max) "gofmt" "*Go Reformat Output*" nil "*Go Reformat Errors*" t)) - (progn - (erase-buffer) - (insert-buffer-substring "*Go Reformat Output*") - (goto-char (point-min)) - (forward-line (1- p))) - (with-current-buffer "*Go Reformat Errors*" - (progn - (goto-char (point-min)) - (while (re-search-forward "" nil t) - (replace-match filename)) - (goto-char (point-min)) - (compilation-mode)))) - (setq max-mini-window-height old-max-mini-window-height) - (delete-windows-on "*Go Reformat Output*") - (kill-buffer "*Go Reformat Output*")))) - ;; helper function - (defun go-fix-buffer () - "run gofix on current buffer" - (interactive) - (show-all) - (shell-command-on-region (point-min) (point-max) "go tool fix -diff")) - - 6. 恭喜你,你现在可以体验在神器中开发Go的乐趣。默认speedbar是关闭的,如果打开需要把 ;; (speedbar 1) 前面的注释去掉,或者也可以通过 *M-x speedbar* 手动开启。 - -## Eclipse -Eclipse也是非常常用的开发利器,以下介绍如何使用Eclipse来编写Go程序。 - - ![](images/1.4.eclipse1.png?raw=true) - - 1.首先下载并安装好[Eclipse](http://www.eclipse.org/) - - 2.下载[goeclipse](https://code.google.com/p/goclipse/)插件 - - http://code.google.com/p/goclipse/wiki/InstallationInstructions - - 3.下载gocode,用于go的代码补全提示 - -gocode的github地址: - - https://github.com/nsf/gocode - -在windows下要安装git,通常用[msysgit](https://code.google.com/p/msysgit/) - -再在cmd下安装: - - go get -u github.com/nsf/gocode - -也可以下载代码,直接用go build来编译,会生成gocode.exe - - 4.下载[MinGW](http://sourceforge.net/projects/mingw/files/MinGW/)并按要求装好 - - 5.配置插件 - - Windows->Reference->Go - - (1).配置Go的编译器 - - ![](images/1.4.eclipse2.png?raw=true) - - (2).配置Gocode(可选,代码补全),设置Gocode路径为之前生成的gocode.exe文件 - - ![](images/1.4.eclipse3.png?raw=true) - - (3).配置GDB(可选,做调试用),设置GDB路径为MingW安装目录下的gdb.exe文件 - - ![](images/1.4.eclipse4.png?raw=true) - - 6.测试是否成功 - -新建一个go工程,再建立一个hello.go。如下图: - - ![](images/1.4.eclipse5.png?raw=true) - -调试如下(要在console中用输入命令来调试): - - ![](images/1.4.eclipse6.png?raw=true) - - -## links - * [目录]() - * 上一节: [Go 命令](<1.3.md>) - * 下一节: [总结](<1.5.md>) - -## LastModified - * $Id$ +# 1.4 Go开发工具 + +本节我将介绍几个开发工具,它们都具有自动化提示,自动化fmt功能。因为它们都是跨平台的,所以安装步骤之类的都是通用的。 + +## LiteIDE + + LiteIDE是一款专门为Go语言开发的集成开发环境(IDE),由visualfc编写。支持项目管理、集成构建、GDB调试、语法高亮、自动补全、大纲显示等功能。下载地址: [http://code.google.com/p/golangide/downloads/list](http://code.google.com/p/golangide/downloads/list),根据自己的系统下载相应的发行版本。Windows和Ubuntu系统可直接打开bin下面的liteide;Mac则需通过LaunchPad打开LiteIDE.app。 + + ![](images/1.4.liteide.png?raw=true) + + LiteIDE配置需要按照前面几个小节配置了相应的go和`$GOPATH`(LiteIDE中也可以图形化配置LiteIDE专用的GOPATH)。LiteIDE当前的编译环境可以通过编译工具栏上的环境配置来切换,如切换32位和64位,针对自己的系统,可能需要修改相应的LiteEnv环境变量,如64位版本,LiteIDE => 查看 => 选项 => LiteEnv => Win64.env => GOROOT=c:\go-w64 为你的`$GOROOT`,不然会无法使用`build`命令。 + + 配置好LiteIDE后,可以打开或拖动任何目录到LiteIDE中作为项目,LiteIDE的编译是针对当前编辑文档所属目录来执行相应的go命令,所以编译时要先打开相应的Go文件。LiteIDE仿IDEA界面,支持项目浏览、文件系统,Package浏览、Golang文档检索、类视图、大纲显示等多个工具窗口的切换。 + + 代码补全需要安装gocode,目前LiteIDE的自动化提示只支持本文件中函数的提示,还没有做到整个项目中函数的提示。 + + +## Sublime Text + + 这里将介绍Sublime Text 2(以下简称Sublime)+GoSublime+gocode+MarGo的组合,那么为什么选择这个组合呢? + + - 自动化提示代码,如下图所示 + ![](images/1.4.sublime1.png?raw=true) + + - 保存的时候自动格式化代码,让您编写的代码更加美观,符合Go的标准。 + - 支持项目管理 + ![](images/1.4.sublime2.png?raw=true) + - 支持语法高亮 + - Sublime Text 2可免费使用,只是保存次数达到一定数量之后就会提示是否购买,点击取消继续用,和正式注册版本没有任何区别。 + + 接下来就开始讲如何安装,下载[Sublime](http://www.sublimetext.com/) + + 根据自己相应的系统下载相应的版本,然后打开Sublime,对于不熟悉Sublime的同学可以先看一下这篇文章[Sublime Text 2 入门及技巧](http://lucifr.com/139225/sublime-text-2-tricks-and-tips/) + + 1. 打开之后安装 Package Control:Ctrl+` 打开命令行,执行如下代码: + + import urllib2,os; pf='Package Control.sublime-package'; ipp=sublime.installed_packages_path(); os.makedirs(ipp) if not os.path.exists(ipp) else None; urllib2.install_opener(urllib2.build_opener(urllib2.ProxyHandler())); open(os.path.join(ipp,pf),'wb').write(urllib2.urlopen('http://sublime.wbond.net/'+pf.replace(' ','%20')).read()); print 'Please restart Sublime Text to finish installation' + + 这个时候重启一下Sublime,可以发现在在菜单栏多了一个如下的栏目,说明Package Control已经安装成功了。 + + ![](images/1.4.sublime3.png?raw=true) + + 2. 接下来安装gocode和MarGo。 + 打开终端运行如下代码(需要git) + + go get -u github.com/nsf/gocode + go get -u github.com/DisposaBoy/MarGo + + 这个时候我们会发现在`$GOPATH/bin`下面多了两个可执行文件,gocode和MarGo,这两个文件会在GoSublime加载时自动启动。 + + 3. 安装完之后就可以安装Sublime的插件了。需安装GoSublime、SidebarEnhancements和Go Build,安装插件之后记得重启Sublime生效,Ctrl+Shift+p打开Package Controll 输入`pcip`(即“Package Control: Install Package”的缩写)。 + + 这个时候看左下角显示正在读取包数据,完成之后出现如下界面 + + ![](images/1.4.sublime4.png?raw=true) + + 这个时候输入GoSublime,按确定就开始安装了。同理应用于SidebarEnhancements和Go Build。 + + 4. 验证是否安装成功,你可以打开Sublime,打开main.go,看看语法是不是高亮了,输入`import`是不是自动化提示了,`import "fmt"`之后,输入`fmt.`是不是自动化提示有函数了。 + + 如果已经出现这个提示,那说明你已经安装完成了,并且完成了自动提示。 + + 如果没有出现这样的提示,一般就是你的`$PATH`没有配置正确。你可以打开终端,输入gocode,是不是能够正确运行,如果不行就说明`$PATH`没有配置正确。 + + +## Vim +Vim是从vi发展出来的一个文本编辑器, 代码补全、编译及错误跳转等方便编程的功能特别丰富,在程序员中被广泛使用。 + + ![](images/1.4.vim.png?raw=true) + + 1. 配置vim高亮显示 + + cp -r $GOROOT/misc/vim/* ~/.vim/ + + 2. 在~/.vimrc文件中增加语法高亮显示 + + filetype plugin indent on + syntax on + + 3. 安装[Gocode](https://github.com/nsf/gocode/) + + go get -u github.com/nsf/gocode + + gocode默认安装到`$GOPATH/bin`下面,需要把`$GOPATH/bin`路径设置到系统`$PATH`里面。 + + 4. 配置[Gocode](https://github.com/nsf/gocode/) + + ~ cd $GOPATH/src/github.com/nsf/gocode/vim + ~ ./update.bash + ~ gocode set propose-builtins true + propose-builtins true + ~ gocode set lib-path "/home/border/gocode/pkg/linux_amd64" + lib-path "/home/border/gocode/pkg/linux_amd64" + ~ gocode set + propose-builtins true + lib-path "/home/border/gocode/pkg/linux_amd64" + +>gocode set里面的两个参数的含意说明: +> +>propose-builtins:是否自动提示Go的内置函数、类型和常量,默认为false,不提示。 +> +>lib-path:默认情况下,gocode只会搜索**$GOPATH/pkg/$GOOS_$GOARCH** 和 **$GOROOT/pkg/$GOOS_$GOARCH**目录下的包,当然这个设置就是可以设置我们额外的lib能访问的路径 + + + + 5. 恭喜你,安装完成,你现在可以使用`:e main.go`体验一下开发Go的乐趣。 + + +## Emacs +Emacs传说中的神器,她不仅仅是一个编辑器,它是一个整合环境,或可称它为集成开发环境,这些功能如让使用者置身于全功能的操作系统中。 + + ![](images/1.4.emacs.png?raw=true) + + 1. 配置Emacs高亮显示 + + cp $GOROOT/misc/emacs/* ~/.emacs.d/ + + 2. 安装[Gocode](https://github.com/nsf/gocode/) + + go get -u github.com/nsf/gocode + + gocode默认安装到`$GOPATH/bin`里面下面,需要把`$GOPATH/bin`路径设置到系统`$PATH`里面。 + + 3. 配置[Gocode](https://github.com/nsf/gocode/) + + + ~ cd $GOPATH/src/github.com/nsf/gocode/emacs + ~ cp go-autocomplete.el ~/.emacs.d/ + ~ gocode set propose-builtins true + propose-builtins true + ~ gocode set lib-path "/home/border/gocode/pkg/linux_amd64" // 换为你自己的路径 + lib-path "/home/border/gocode/pkg/linux_amd64" + ~ gocode set + propose-builtins true + lib-path "/home/border/gocode/pkg/linux_amd64" + + 4. 需要安装 [Auto Completion](http://www.emacswiki.org/emacs/AutoComplete) + + 下载AutoComplete并解压 + + ~ make install DIR=$HOME/.emacs.d/auto-complete + + 配置~/.emacs文件 + + ;;auto-complete + (require 'auto-complete-config) + (add-to-list 'ac-dictionary-directories "~/.emacs.d/auto-complete/ac-dict") + (ac-config-default) + (local-set-key (kbd "M-/") 'semantic-complete-analyze-inline) + (local-set-key "." 'semantic-complete-self-insert) + (local-set-key ">" 'semantic-complete-self-insert) + + 详细信息参考: http://www.emacswiki.org/emacs/AutoComplete + + 5. 配置.emacs + + ;; golang mode + (require 'go-mode-load) + (require 'go-autocomplete) + ;; speedbar + ;; (speedbar 1) + (speedbar-add-supported-extension ".go") + (add-hook + 'go-mode-hook + '(lambda () + ;; gocode + (auto-complete-mode 1) + (setq ac-sources '(ac-source-go)) + ;; Imenu & Speedbar + (setq imenu-generic-expression + '(("type" "^type *\\([^ \t\n\r\f]*\\)" 1) + ("func" "^func *\\(.*\\) {" 1))) + (imenu-add-to-menubar "Index") + ;; Outline mode + (make-local-variable 'outline-regexp) + (setq outline-regexp "//\\.\\|//[^\r\n\f][^\r\n\f]\\|pack\\|func\\|impo\\|cons\\|var.\\|type\\|\t\t*....") + (outline-minor-mode 1) + (local-set-key "\M-a" 'outline-previous-visible-heading) + (local-set-key "\M-e" 'outline-next-visible-heading) + ;; Menu bar + (require 'easymenu) + (defconst go-hooked-menu + '("Go tools" + ["Go run buffer" go t] + ["Go reformat buffer" go-fmt-buffer t] + ["Go check buffer" go-fix-buffer t])) + (easy-menu-define + go-added-menu + (current-local-map) + "Go tools" + go-hooked-menu) + + ;; Other + (setq show-trailing-whitespace t) + )) + ;; helper function + (defun go () + "run current buffer" + (interactive) + (compile (concat "go run " (buffer-file-name)))) + + ;; helper function + (defun go-fmt-buffer () + "run gofmt on current buffer" + (interactive) + (if buffer-read-only + (progn + (ding) + (message "Buffer is read only")) + (let ((p (line-number-at-pos)) + (filename (buffer-file-name)) + (old-max-mini-window-height max-mini-window-height)) + (show-all) + (if (get-buffer "*Go Reformat Errors*") + (progn + (delete-windows-on "*Go Reformat Errors*") + (kill-buffer "*Go Reformat Errors*"))) + (setq max-mini-window-height 1) + (if (= 0 (shell-command-on-region (point-min) (point-max) "gofmt" "*Go Reformat Output*" nil "*Go Reformat Errors*" t)) + (progn + (erase-buffer) + (insert-buffer-substring "*Go Reformat Output*") + (goto-char (point-min)) + (forward-line (1- p))) + (with-current-buffer "*Go Reformat Errors*" + (progn + (goto-char (point-min)) + (while (re-search-forward "" nil t) + (replace-match filename)) + (goto-char (point-min)) + (compilation-mode)))) + (setq max-mini-window-height old-max-mini-window-height) + (delete-windows-on "*Go Reformat Output*") + (kill-buffer "*Go Reformat Output*")))) + ;; helper function + (defun go-fix-buffer () + "run gofix on current buffer" + (interactive) + (show-all) + (shell-command-on-region (point-min) (point-max) "go tool fix -diff")) + + 6. 恭喜你,你现在可以体验在神器中开发Go的乐趣。默认speedbar是关闭的,如果打开需要把 ;; (speedbar 1) 前面的注释去掉,或者也可以通过 *M-x speedbar* 手动开启。 + +## Eclipse +Eclipse也是非常常用的开发利器,以下介绍如何使用Eclipse来编写Go程序。 + + ![](images/1.4.eclipse1.png?raw=true) + + 1.首先下载并安装好[Eclipse](http://www.eclipse.org/) + + 2.下载[goeclipse](https://code.google.com/p/goclipse/)插件 + + http://code.google.com/p/goclipse/wiki/InstallationInstructions + + 3.下载gocode,用于go的代码补全提示 + +gocode的github地址: + + https://github.com/nsf/gocode + +在windows下要安装git,通常用[msysgit](https://code.google.com/p/msysgit/) + +再在cmd下安装: + + go get -u github.com/nsf/gocode + +也可以下载代码,直接用go build来编译,会生成gocode.exe + + 4.下载[MinGW](http://sourceforge.net/projects/mingw/files/MinGW/)并按要求装好 + + 5.配置插件 + + Windows->Reference->Go + + (1).配置Go的编译器 + + ![](images/1.4.eclipse2.png?raw=true) + + (2).配置Gocode(可选,代码补全),设置Gocode路径为之前生成的gocode.exe文件 + + ![](images/1.4.eclipse3.png?raw=true) + + (3).配置GDB(可选,做调试用),设置GDB路径为MingW安装目录下的gdb.exe文件 + + ![](images/1.4.eclipse4.png?raw=true) + + 6.测试是否成功 + +新建一个go工程,再建立一个hello.go。如下图: + + ![](images/1.4.eclipse5.png?raw=true) + +调试如下(要在console中用输入命令来调试): + + ![](images/1.4.eclipse6.png?raw=true) + + +## links + * [目录]() + * 上一节: [Go 命令](<1.3.md>) + * 下一节: [总结](<1.5.md>) diff --git a/1.5.md b/1.5.md index 8e0a998feeb7e2f779dd5c1e9d5193b6c79a8206..229a86ae81ba1b330ae7abbb4ab87e6f6306dcc6 100644 --- a/1.5.md +++ b/1.5.md @@ -1,11 +1,8 @@ -# 总结 - -这一章中我们主要介绍了如何安装Go,以及如何配置本地的`$GOPATH`,通过设置`$GOPATH`之后如何创建项目,项目创建之后如何编译、如何安装,接着介绍了一些Go的常用命令工具,最后介绍了Go的开发工具,希望能够通过有利的工具快速的开发Go应用。 - -## links - * [目录]() - * 上一节: [Go开发工具](<1.4.md>) - * 下一章: [go语言基础](<2.md>) - -## LastModified - * $Id$ +# 总结 + +这一章中我们主要介绍了如何安装Go,以及如何配置本地的`$GOPATH`,通过设置`$GOPATH`之后如何创建项目,项目创建之后如何编译、如何安装,接着介绍了一些Go的常用命令工具,最后介绍了Go的开发工具,希望能够通过有利的工具快速的开发Go应用。 + +## links + * [目录]() + * 上一节: [Go开发工具](<1.4.md>) + * 下一章: [go语言基础](<2.md>) diff --git a/1.md b/1.md index a3fb7537127a0fdda16ca3322daadeaef9d36a27..d13737f8d3ca13716c2fa7a18f063bfeb6f0fc1c 100644 --- a/1.md +++ b/1.md @@ -1,29 +1,26 @@ -# 1 GO环境配置 - -## 目录 - * 1. [Go安装](1.1.md) - * 2. [GOPATH与工作空间](1.2.md) - * 3. [Go命令](1.3.md) - * 4. [Go开发工具](1.4.md) - * 5. [小结](1.5.md) - -欢迎来到Go的世界,让我们开始吧! - -Go是一种新的语言,一种并发的、带垃圾回收的、快速编译的语言。它具有以下特点: - -- 它可以在一台计算机上用几秒钟的时间编译一个大型的Go程序。 -- Go为软件构造提供了一种模型,它使依赖分析更加容易,且避免了大部分C风格include文件与库的开头。 -- Go是静态类型的语言,它的类型系统没有层级。因此用户不需要在定义类型之间的关系上花费时间,这样感觉起来比典型的面向对象语言更轻量级。 -- Go完全是垃圾回收型的语言,并为并发执行与通信提供了基本的支持。 -- 按照其设计,Go打算为多核机器上系统软件的构造提供一种方法。 - -Go试图成为结合解释型编程的轻松、动态类型语言的高效以及静态类型语言的安全的编译型语言。它也打算成为现代的,支持网络与多核计算的语言。要满足这些目标,需要解决一些语言上的问题:一个富有表达能力但轻量级的类型系统,并发与垃圾回收机制,严格的依赖规范等等。这些无法通过库或工具解决好,因此Go也就应运而生了。 - -在本章中,我们将讲述Go的安装方法,以及如何配置项目信息。 - -## links - * [目录]() - * 下一节: [Go安装](<1.1.md>) - -## LastModified - * $Id$ +# 1 GO环境配置 + +## 目录 + * 1. [Go安装](1.1.md) + * 2. [GOPATH与工作空间](1.2.md) + * 3. [Go命令](1.3.md) + * 4. [Go开发工具](1.4.md) + * 5. [小结](1.5.md) + +欢迎来到Go的世界,让我们开始吧! + +Go是一种新的语言,一种并发的、带垃圾回收的、快速编译的语言。它具有以下特点: + +- 它可以在一台计算机上用几秒钟的时间编译一个大型的Go程序。 +- Go为软件构造提供了一种模型,它使依赖分析更加容易,且避免了大部分C风格include文件与库的开头。 +- Go是静态类型的语言,它的类型系统没有层级。因此用户不需要在定义类型之间的关系上花费时间,这样感觉起来比典型的面向对象语言更轻量级。 +- Go完全是垃圾回收型的语言,并为并发执行与通信提供了基本的支持。 +- 按照其设计,Go打算为多核机器上系统软件的构造提供一种方法。 + +Go试图成为结合解释型编程的轻松、动态类型语言的高效以及静态类型语言的安全的编译型语言。它也打算成为现代的,支持网络与多核计算的语言。要满足这些目标,需要解决一些语言上的问题:一个富有表达能力但轻量级的类型系统,并发与垃圾回收机制,严格的依赖规范等等。这些无法通过库或工具解决好,因此Go也就应运而生了。 + +在本章中,我们将讲述Go的安装方法,以及如何配置项目信息。 + +## links + * [目录]() + * 下一节: [Go安装](<1.1.md>) diff --git a/10.1.md b/10.1.md index 6adcaf9af4954aca8799329b52ffb42f7468c6ff..0ade1322f995cb21a2793902f3dbd96e168a63cb 100644 --- a/10.1.md +++ b/10.1.md @@ -1,87 +1,85 @@ -# 10.1 设置默认地区 -## 什么是Locale -Locale是一组描述世界上某一特定区域文本格式和语言习惯的设置的集合。locale名通常由三个部分组成:第一部分,是一个强制性的,表示语言的缩写,例如"en"表示英文或"zh"表示中文。第二部分,跟在一个下划线之后,是一个可选的国家说明符,用于区分讲同一种语言的不同国家,例如"en_US"表示美国英语,而"en_UK"表示英国英语。最后一部分,跟在一个句点之后,是可选的字符集说明符,例如"zh_CN.gb2312"表示中国使用gb2312字符集。 - -GO语言默认采用"UTF-8"编码集,所以我们实现i18n时不考虑第三部分,接下来我们都采用locale描述的前面两部分来作为i18n标准的locale名。 - - ->在Linux和Solaris系统中可以通过`locale -a`命令列举所有支持的地区名,读者可以看到这些地区名的命名规范。对于BSD等系统,没有locale命令,但是地区信息存储在/usr/share/locale中。 - -## 设置Locale -有了上面对locale的定义,那么我们就需要根据用户的信息(访问信息、个人信息、访问域名等)来设置与之相关的locale,我们可以通过如下几种方式来设置用户的locale。 - -### 通过域名设置Locale -设置Locale的办法这一就是在应用运行的时候采用域名分级的方式,例如,我们采用www.asta.com当做我们的英文站(默认站),而把域名www.asta.cn当做中文站。这样通过在应用里面设置域名和相应的locale的对应关系,就可以设置好地区。这样处理有几点好处: - -- 通过URL就可以很明显的识别 -- 用户可以通过域名很直观的知道将访问那种语言的站点 -- 在Go程序中实现非常的简单方便,通过一个map就可以实现 -- 有利于搜索引擎抓取,能够提高站点的SEO - -我们可以通过下面的代码来实现域名的对应locale: - - if r.Host == "www.asta.com" { - i18n.SetLocale("en") - } else if r.Host == "www.asta.cn" { - i18n.SetLocale("zh-CN") - } else if r.Host == "www.asta.tw" { - i18n.SetLocale("zh-TW") - } - -当然除了整域名设置地区之外,我们还可以通过子域名来设置地区,例如"en.asta.com"表示英文站点,"cn.asta.com"表示中文站点。实现代码如下所示: - - prefix := strings.Split(r.Host,".") - - if prefix[0] == "en" { - i18n.SetLocale("en") - } else if prefix[0] == "cn" { - i18n.SetLocale("zh-CN") - } else if prefix[0] == "tw" { - i18n.SetLocale("zh-TW") - } - -通过域名设置Locale有如上所示的优点,但是我们一般开发Web应用的时候不会采用这种方式,因为首先域名成本比较高,开发一个Locale就需要一个域名,而且往往统一名称的域名不一定能申请的到,其次我们不愿意为每个站点去本地化一个配置,而更多的是采用url后面带参数的方式,请看下面的介绍。 - -### 从域名参数设置Locale +# 10.1 设置默认地区 +## 什么是Locale +Locale是一组描述世界上某一特定区域文本格式和语言习惯的设置的集合。locale名通常由三个部分组成:第一部分,是一个强制性的,表示语言的缩写,例如"en"表示英文或"zh"表示中文。第二部分,跟在一个下划线之后,是一个可选的国家说明符,用于区分讲同一种语言的不同国家,例如"en_US"表示美国英语,而"en_UK"表示英国英语。最后一部分,跟在一个句点之后,是可选的字符集说明符,例如"zh_CN.gb2312"表示中国使用gb2312字符集。 + +GO语言默认采用"UTF-8"编码集,所以我们实现i18n时不考虑第三部分,接下来我们都采用locale描述的前面两部分来作为i18n标准的locale名。 + + +>在Linux和Solaris系统中可以通过`locale -a`命令列举所有支持的地区名,读者可以看到这些地区名的命名规范。对于BSD等系统,没有locale命令,但是地区信息存储在/usr/share/locale中。 + +## 设置Locale +有了上面对locale的定义,那么我们就需要根据用户的信息(访问信息、个人信息、访问域名等)来设置与之相关的locale,我们可以通过如下几种方式来设置用户的locale。 + +### 通过域名设置Locale +设置Locale的办法这一就是在应用运行的时候采用域名分级的方式,例如,我们采用www.asta.com当做我们的英文站(默认站),而把域名www.asta.cn当做中文站。这样通过在应用里面设置域名和相应的locale的对应关系,就可以设置好地区。这样处理有几点好处: + +- 通过URL就可以很明显的识别 +- 用户可以通过域名很直观的知道将访问那种语言的站点 +- 在Go程序中实现非常的简单方便,通过一个map就可以实现 +- 有利于搜索引擎抓取,能够提高站点的SEO + +我们可以通过下面的代码来实现域名的对应locale: + + if r.Host == "www.asta.com" { + i18n.SetLocale("en") + } else if r.Host == "www.asta.cn" { + i18n.SetLocale("zh-CN") + } else if r.Host == "www.asta.tw" { + i18n.SetLocale("zh-TW") + } + +当然除了整域名设置地区之外,我们还可以通过子域名来设置地区,例如"en.asta.com"表示英文站点,"cn.asta.com"表示中文站点。实现代码如下所示: + + prefix := strings.Split(r.Host,".") + + if prefix[0] == "en" { + i18n.SetLocale("en") + } else if prefix[0] == "cn" { + i18n.SetLocale("zh-CN") + } else if prefix[0] == "tw" { + i18n.SetLocale("zh-TW") + } + +通过域名设置Locale有如上所示的优点,但是我们一般开发Web应用的时候不会采用这种方式,因为首先域名成本比较高,开发一个Locale就需要一个域名,而且往往统一名称的域名不一定能申请的到,其次我们不愿意为每个站点去本地化一个配置,而更多的是采用url后面带参数的方式,请看下面的介绍。 + +### 从域名参数设置Locale 目前最常用的设置Locale的方式是在URL里面带上参数,例如www.asta.com/hello?locale=zh或者www.asta.com/zh/hello。这样我们就可以设置地区:`i18n.SetLocale(params["locale"])`。 这种设置方式几乎拥有前面讲的通过域名设置Locale的所有优点,它采用RESTful的方式,以使得我们不需要增加额外的方法来处理。但是这种方式需要在每一个的link里面增加相应的参数locale,这也许有点复杂而且有时候甚至相当的繁琐。不过我们可以写一个通用的函数url,让所有的link地址都通过这个函数来生成,然后在这个函数里面增加`locale=params["locale"]`参数来缓解一下。 也许我们希望URL地址看上去更加的RESTful一点,例如:www.asta.com/en/books(英文站点)和www.asta.com/zh/books(中文站点),这种方式的URL更加有利于SEO,而且对于用户也比较友好,能够通过URL直观的知道访问的站点。那么这样的URL地址可以通过router来获取locale(参考REST小节里面介绍的router插件实现): - mux.Get("/:locale/books", listbook) - + mux.Get("/:locale/books", listbook) + ### 从客户端设置地区 -在一些特殊的情况下,我们需要根据客户端的信息而不是通过URL来设置Locale,这些信息可能来自于客户端设置的喜好语言(浏览器中设置),用户的IP地址,用户在注册的时候填写的所在地信息等。这种方式比较适合Web为基础的应用。 - +在一些特殊的情况下,我们需要根据客户端的信息而不是通过URL来设置Locale,这些信息可能来自于客户端设置的喜好语言(浏览器中设置),用户的IP地址,用户在注册的时候填写的所在地信息等。这种方式比较适合Web为基础的应用。 + - Accept-Language - 客户端请求的时候在HTTP头信息里面有`Accept-Language`,一般的客户端都会设置该信息,下面是Go语言实现的一个简单的根据`Accept-Language`实现设置地区的代码: - - AL := r.Header.Get("Accept-Language") - if AL == "en" { - i18n.SetLocale("en") - } else if AL == "zh-CN" { - i18n.SetLocale("zh-CN") - } else if AL == "zh-TW" { - i18n.SetLocale("zh-TW") - } - 当然在实际应用中,可能需要更加严格的判断来进行设置地区 +客户端请求的时候在HTTP头信息里面有`Accept-Language`,一般的客户端都会设置该信息,下面是Go语言实现的一个简单的根据`Accept-Language`实现设置地区的代码: + + AL := r.Header.Get("Accept-Language") + if AL == "en" { + i18n.SetLocale("en") + } else if AL == "zh-CN" { + i18n.SetLocale("zh-CN") + } else if AL == "zh-TW" { + i18n.SetLocale("zh-TW") + } + +当然在实际应用中,可能需要更加严格的判断来进行设置地区 - IP地址 - 另一种根据客户端来设定地区就是用户访问的IP,我们根据相应的IP库,对应访问的IP到地区,目前全球比较常用的就是GeoIP Lite Country这个库。这种设置地区的机制非常简单,我们只需要根据IP数据库查询用户的IP然后返回国家地区,根据返回的结果设置对应的地区。 + 另一种根据客户端来设定地区就是用户访问的IP,我们根据相应的IP库,对应访问的IP到地区,目前全球比较常用的就是GeoIP Lite Country这个库。这种设置地区的机制非常简单,我们只需要根据IP数据库查询用户的IP然后返回国家地区,根据返回的结果设置对应的地区。 - 用户profile - 当然你也可以让用户根据你提供的下拉菜单或者别的什么方式的设置相应的locale,然后我们将用户输入的信息,保存到与它帐号相关的profile中,当用户再次登陆的时候把这个设置复写到locale设置中,这样就可以保证该用户每次访问都是基于自己先前设置的locale来获得页面。 + 当然你也可以让用户根据你提供的下拉菜单或者别的什么方式的设置相应的locale,然后我们将用户输入的信息,保存到与它帐号相关的profile中,当用户再次登陆的时候把这个设置复写到locale设置中,这样就可以保证该用户每次访问都是基于自己先前设置的locale来获得页面。 ## 总结 通过上面的介绍可知,设置Locale可以有很多种方式,我们应该根据需求的不同来选择不同的设置Locale的方法,以让用户能以它最熟悉的方式,获得我们提供的服务,提高应用的用户友好性。 - -## links - * [目录]() - * 上一节: [国际化和本地化](<10.md>) - * 下一节: [本地化资源](<10.2.md>) - -## LastModified - * $Id$ + +## links + * [目录]() + * 上一节: [国际化和本地化](<10.md>) + * 下一节: [本地化资源](<10.2.md>) diff --git a/10.2.md b/10.2.md index af6a67f8281c52ce642fb51d9f287cfa3bd90eb0..91375f54e1ce16f8fef5b8704aeb9b99870e086a 100644 --- a/10.2.md +++ b/10.2.md @@ -44,7 +44,7 @@ fmt.Printf(msg(lang, "how old"), 30) 上面的示例代码仅用以演示内部的实现方案,而实际数据是存储在JSON里面的,所以我们可以通过`json.Unmarshal`来为相应的map填充数据。 - + ## 本地化日期和时间 因为时区的关系,同一时刻,在不同的地区,表示是不一样的,而且因为Locale的关系,时间格式也不尽相同,例如中文环境下可能显示:`2012年10月24日 星期三 23时11分13秒 CST`,而在英文环境下可能显示:`Wed Oct 24 23:11:13 CST 2012`。这里面我们需要解决两点: @@ -55,7 +55,7 @@ $GOROOT/lib/time包中的timeinfo.zip含有locale对应的时区的定义,为 en["time_zone"]="America/Chicago" cn["time_zone"]="Asia/Shanghai" - + loc,_:=time.LoadLocation(msg(lang,"time_zone")) t:=time.Now() t = t.In(loc) @@ -65,9 +65,9 @@ $GOROOT/lib/time包中的timeinfo.zip含有locale对应的时区的定义,为 en["date_format"]="%Y-%m-%d %H:%M:%S" cn["date_format"]="%Y年%m月%d日 %H时%M分%S秒" - + fmt.Println(date(msg(lang,"date_format"),t)) - + func date(fomate string,t time.Time) string{ year, month, day = t.Date() hour, min, sec = t.Clock() @@ -84,54 +84,51 @@ $GOROOT/lib/time包中的timeinfo.zip含有locale对应的时区的定义,为 cn["money"] ="¥%d元" fmt.Println(date(msg(lang,"date_format"),100)) - + func money_format(fomate string,money int64) string{ return fmt.Sprintf(fomate,money) } - + ## 本地化视图和资源 我们可能会根据Locale的不同来展示视图,这些视图包含不同的图片、css、js等各种静态资源。那么应如何来处理这些信息呢?首先我们应按locale来组织文件信息,请看下面的文件目录安排: views - |--en //英文模板 - |--images //存储图片信息 - |--js //存储JS文件 - |--css //存储css文件 - index.tpl //用户首页 - login.tpl //登陆首页 - |--zh-CN //中文模板 - |--images - |--js - |--css - index.tpl - login.tpl - + |--en //英文模板 + |--images //存储图片信息 + |--js //存储JS文件 + |--css //存储css文件 + index.tpl //用户首页 + login.tpl //登陆首页 + |--zh-CN //中文模板 + |--images + |--js + |--css + index.tpl + login.tpl + 有了这个目录结构后我们就可以在渲染的地方这样来实现代码: - + s1, _ := template.ParseFiles("views"+lang+"index.tpl") VV.Lang=lang - s1.Execute(os.Stdout, VV) - + s1.Execute(os.Stdout, VV) + 而对于里面的index.tpl里面的资源设置如下: - //js文件 - - //css文件 - - //图片文件 - - -采用这种方式来本地化视图以及资源时,我们就可以很容易的进行扩展了。 - +// js文件 + +// css文件 + +// 图片文件 + + +采用这种方式来本地化视图以及资源时,我们就可以很容易的进行扩展了。 + ## 总结 本小节介绍了如何使用及存储本地资源,有时需要通过转换函数来实现,有时通过lang来设置,但是最终都是通过key-value的方式来存储Locale对应的数据,在需要时取出相应于Locale的信息后,如果是文本信息就直接输出,如果是时间日期或者货币,则需要先通过`fmt.Printf`或其他格式化函数来处理,而对于不同Locale的视图和资源则是最简单的,只要在路径里面增加lang就可以实现了。 - -## links - * [目录]() - * 上一节: [设置默认地区](<10.1.md>) - * 下一节: [国际化站点](<10.3.md>) - -## LastModified - * $Id$ + +## links + * [目录]() + * 上一节: [设置默认地区](<10.1.md>) + * 下一节: [国际化站点](<10.3.md>) diff --git a/10.3.md b/10.3.md index 416e80d45db0ac7bd8ac781f2c498caf9d935aaa..0d8c8a353698918619a3875a85a9d10ce737c858 100644 --- a/10.3.md +++ b/10.3.md @@ -2,30 +2,30 @@ 前面小节介绍了如何处理本地化资源,即Locale一个相应的配置文件,那么如果处理多个的本地化资源呢?而对于一些我们经常用到的例如:简单的文本翻译、时间日期、数字等如果处理呢?本小节将一一解决这些问题。 ## 管理多个本地包 在开发一个应用的时候,首先我们要决定是只支持一种语言,还是多种语言,如果要支持多种语言,我们则需要制定一个组织结构,以方便将来更多语言的添加。在此我们设计如下:Locale有关的文件放置在config/locales下,假设你要支持中文和英文,那么你需要在这个文件夹下放置en.json和zh.json。大概的内容如下所示: - + # zh.json { - "zh": { - "submit": "提交", - "create": "创建" - } + "zh": { + "submit": "提交", + "create": "创建" + } } #en.json { - "en": { - "submit": "Submit", - "create": "Create" - } + "en": { + "submit": "Submit", + "create": "Create" + } } 为了支持国际化,在此我们使用了一个国际化相关的包——go-i18n(https://github.com/astaxie/go-i18n),首先我们向go-i18n包注册config/locales这个目录,以加载所有的locale文件 - Tr:=i18n.NewLocale() + Tr:=i18n.NewLocale() Tr.LoadPath("config/locales") - + 这个包使用起来很简单,你可以通过下面的方式进行测试: fmt.Println(Tr.Translate("submit")) @@ -41,27 +41,27 @@ //加载默认配置文件,这些文件都放在go-i18n/locales下面 //文件命名zh.json、en-json、en-US.json等,可以不断的扩展支持更多的语言 - + func (il *IL) loadDefaultTranslations(dirPath string) error { dir, err := os.Open(dirPath) if err != nil { return err } defer dir.Close() - + names, err := dir.Readdirnames(-1) if err != nil { return err } - + for _, name := range names { fullPath := path.Join(dirPath, name) - + fi, err := os.Stat(fullPath) if err != nil { return err } - + if fi.IsDir() { if err := il.loadTranslations(fullPath); err != nil { return err @@ -72,13 +72,13 @@ return err } defer file.Close() - + if err := il.loadTranslation(file, locale); err != nil { return err } } } - + return nil } @@ -100,83 +100,81 @@ 1. 文本信息 - 文本信息调用`Tr.Translate`来实现相应的信息转换,mapFunc的实现如下: +文本信息调用`Tr.Translate`来实现相应的信息转换,mapFunc的实现如下: - func I18nT(args ...interface{}) string { - ok := false - var s string - if len(args) == 1 { - s, ok = args[0].(string) - } - if !ok { - s = fmt.Sprint(args...) - } - return Tr.Translate(s) + func I18nT(args ...interface{}) string { + ok := false + var s string + if len(args) == 1 { + s, ok = args[0].(string) } + if !ok { + s = fmt.Sprint(args...) + } + return Tr.Translate(s) + } - 注册函数如下: +注册函数如下: - t.Funcs(template.FuncMap{"T": I18nT}) + t.Funcs(template.FuncMap{"T": I18nT}) - 模板中使用如下: +模板中使用如下: + + {{.V.Submit | T}} - {{.V.Submit | T}} - 2. 时间日期 - 时间日期调用`Tr.Time`函数来实现相应的时间转换,mapFunc的实现如下: +时间日期调用`Tr.Time`函数来实现相应的时间转换,mapFunc的实现如下: - func I18nTimeDate(args ...interface{}) string { - ok := false - var s string - if len(args) == 1 { - s, ok = args[0].(string) - } - if !ok { - s = fmt.Sprint(args...) - } - return Tr.Time(s) + func I18nTimeDate(args ...interface{}) string { + ok := false + var s string + if len(args) == 1 { + s, ok = args[0].(string) + } + if !ok { + s = fmt.Sprint(args...) } + return Tr.Time(s) + } - 注册函数如下: +注册函数如下: - t.Funcs(template.FuncMap{"TD": I18nTimeDate}) + t.Funcs(template.FuncMap{"TD": I18nTimeDate}) - 模板中使用如下: +模板中使用如下: + + {{.V.Now | TD}} - {{.V.Now | TD}} 3. 货币信息 - 货币调用`Tr.Money`函数来实现相应的时间转换,mapFunc的实现如下: +货币调用`Tr.Money`函数来实现相应的时间转换,mapFunc的实现如下: - func I18nMoney(args ...interface{}) string { - ok := false - var s string - if len(args) == 1 { - s, ok = args[0].(string) - } - if !ok { - s = fmt.Sprint(args...) - } - return Tr.Money(s) + func I18nMoney(args ...interface{}) string { + ok := false + var s string + if len(args) == 1 { + s, ok = args[0].(string) } + if !ok { + s = fmt.Sprint(args...) + } + return Tr.Money(s) + } + +注册函数如下: - 注册函数如下: + t.Funcs(template.FuncMap{"M": I18nMoney}) - t.Funcs(template.FuncMap{"M": I18nMoney}) +模板中使用如下: - 模板中使用如下: + {{.V.Money | M}} - {{.V.Money | M}} +## 总结 +通过这小节我们知道了如何实现一个多语言包的Web应用,通过自定义语言包我们可以方便的实现多语言,而且通过配置文件能够非常方便的扩充多语言,默认情况下,go-i18n会自定加载一些公共的配置信息,例如时间、货币等,我们就可以非常方便的使用,同时为了支持在模板中使用这些函数,也实现了相应的模板函数,这样就允许我们在开发Web应用的时候直接在模板中通过pipeline的方式来操作多语言包。 -## 总结 -通过这小节我们知道了如何实现一个多语言包的Web应用,通过自定义语言包我们可以方便的实现多语言,而且通过配置文件能够非常方便的扩充多语言,默认情况下,go-i18n会自定加载一些公共的配置信息,例如时间、货币等,我们就可以非常方便的使用,同时为了支持在模板中使用这些函数,也实现了相应的模板函数,这样就允许我们在开发Web应用的时候直接在模板中通过pipeline的方式来操作多语言包。 - -## links - * [目录]() - * 上一节: [本地化资源](<10.2.md>) - * 下一节: [小结](<10.4.md>) - -## LastModified - * $Id$ +## links + * [目录]() + * 上一节: [本地化资源](<10.2.md>) + * 下一节: [小结](<10.4.md>) diff --git a/10.4.md b/10.4.md index 2af42faca4302f936b732db1aeb232f638a2707f..2869ddc636ab03df50106675423fa53d3816cb64 100644 --- a/10.4.md +++ b/10.4.md @@ -1,9 +1,6 @@ # 10.4 小结 -通过这一章的介绍,读者应该对如何操作i18n有了深入的了解,我也根据这一章介绍的内容实现了一个开源的解决方案go-i18n:https://github.com/astaxie/go-i18n 通过这个开源库我们可以很方便的实现多语言版本的Web应用,使得我们的应用能够轻松的实现国际化。如果你发现这个开源库中的错误或者那些缺失的地方,请一起参与到这个开源项目中来,让我们的这个库争取成为Go的标准库。 -## links - * [目录]() - * 上一节: [国际化站点](<10.3.md>) - * 下一节: [错误处理,故障排除和测试](<11.md>) - -## LastModified - * $Id$ +通过这一章的介绍,读者应该对如何操作i18n有了深入的了解,我也根据这一章介绍的内容实现了一个开源的解决方案go-i18n:https://github.com/astaxie/go-i18n 通过这个开源库我们可以很方便的实现多语言版本的Web应用,使得我们的应用能够轻松的实现国际化。如果你发现这个开源库中的错误或者那些缺失的地方,请一起参与到这个开源项目中来,让我们的这个库争取成为Go的标准库。 +## links + * [目录]() + * 上一节: [国际化站点](<10.3.md>) + * 下一节: [错误处理,故障排除和测试](<11.md>) diff --git a/10.md b/10.md index 93ec1c88e921156346e6af8f655d4c1aadb1e05b..9c4c398e7ff883a587298a161c01be1961f298c0 100644 --- a/10.md +++ b/10.md @@ -1,30 +1,27 @@ -# 10 国际化和本地化 -为了适应经济的全球一体化,作为开发者,我们需要开发出支持多国语言、国际化的Web应用,即同样的页面在不同的语言环境下需要显示不同的效果,也就是说应用程序在运行时能够根据请求所来自的地域与语言的不同而显示不同的用户界面。这样,当需要在应用程序中添加对新的语言的支持时,无需修改应用程序的代码,只需要增加语言包即可实现。 - -国际化与本地化(Internationalization and localization,通常用i18n和L10N表示),国际化是将针对某个地区设计的程序进行重构,以使它能够在更多地区使用,本地化是指在一个面向国际化的程序中增加对新地区的支持。 - -目前,Go语言的标准包没有提供对i18n的支持,但有一些比较简单的第三方实现,这一章我们将实现一个go-i18n库,用来支持Go语言的i18n。 - -所谓的国际化:就是根据特定的locale信息,提取与之相应的字符串或其它一些东西(比如时间和货币的格式)等等。这涉及到三个问题: - -1、如何确定locale。 - -2、如何保存与locale相关的字符串或其它信息。 - -3、如何根据locale提取字符串和其它相应的信息。 - -在第一小节里,我们将介绍如何设置正确的locale以便让访问站点的用户能够获得与其语言相应的页面。第二小节将介绍如何处理或存储字符串、货币、时间日期等与locale相关的信息,第三小节将介绍如何实现国际化站点,即如何根据不同locale返回不同合适的内容。通过这三个小节的学习,我们将获得一个完整的i18n方案。 - -## 目录 - * 1 [设置默认地区](10.1.md) - * 2 [本地化资源](10.2.md) - * 3 [国际化站点](10.3.md) - * 4 [小结](10.4.md) - -## links - * [目录]() - * 上一章: [第九章总结](<9.7.md>) - * 下一节: [设置默认地区](<10.1.md>) - -## LastModified - * $Id$ +# 10 国际化和本地化 +为了适应经济的全球一体化,作为开发者,我们需要开发出支持多国语言、国际化的Web应用,即同样的页面在不同的语言环境下需要显示不同的效果,也就是说应用程序在运行时能够根据请求所来自的地域与语言的不同而显示不同的用户界面。这样,当需要在应用程序中添加对新的语言的支持时,无需修改应用程序的代码,只需要增加语言包即可实现。 + +国际化与本地化(Internationalization and localization,通常用i18n和L10N表示),国际化是将针对某个地区设计的程序进行重构,以使它能够在更多地区使用,本地化是指在一个面向国际化的程序中增加对新地区的支持。 + +目前,Go语言的标准包没有提供对i18n的支持,但有一些比较简单的第三方实现,这一章我们将实现一个go-i18n库,用来支持Go语言的i18n。 + +所谓的国际化:就是根据特定的locale信息,提取与之相应的字符串或其它一些东西(比如时间和货币的格式)等等。这涉及到三个问题: + +1、如何确定locale。 + +2、如何保存与locale相关的字符串或其它信息。 + +3、如何根据locale提取字符串和其它相应的信息。 + +在第一小节里,我们将介绍如何设置正确的locale以便让访问站点的用户能够获得与其语言相应的页面。第二小节将介绍如何处理或存储字符串、货币、时间日期等与locale相关的信息,第三小节将介绍如何实现国际化站点,即如何根据不同locale返回不同合适的内容。通过这三个小节的学习,我们将获得一个完整的i18n方案。 + +## 目录 + * 1 [设置默认地区](10.1.md) + * 2 [本地化资源](10.2.md) + * 3 [国际化站点](10.3.md) + * 4 [小结](10.4.md) + +## links + * [目录]() + * 上一章: [第九章总结](<9.7.md>) + * 下一节: [设置默认地区](<10.1.md>) diff --git a/11.1.md b/11.1.md index b71cbc5bce3865cb68f5642e6e9d1541d551958a..0a9e98b3759e6bd16d8dcefb6c49da1849df8617 100644 --- a/11.1.md +++ b/11.1.md @@ -6,9 +6,9 @@ Go语言设计的时候主要的特点是:简洁、明白,简洁是指语法 下面这个例子通过`os.Open`打开一个文件,如果出错那么会执行`log.Fatal`打印出来错误信息: f, err := os.Open("filename.ext") - if err != nil { - log.Fatal(err) - } + if err != nil { + log.Fatal(err) + } 其实这样的error返回在Go语言的很多内置包里面有很多,我们这个小节将详细的介绍这些error是怎么设计的,以及在我们设计的Web应用如何更好的处理error。 ## Error类型 @@ -35,14 +35,14 @@ error是一个内置的类型变量,我们可以在/builtin/包下面找到相 return &errorString{text} } -下面这个例子演示了如何使用`errors.New`: +下面这个例子演示了如何使用`errors.New`: func Sqrt(f float64) (float64, error) { if f < 0 { return 0, errors.New("math: square root of negative number") } // implementation - } + } ## 自定义Error @@ -53,4 +53,4 @@ error是一个内置的类型变量,我们可以在/builtin/包下面找到相 ## links * [目录]() * 上一节: [错误处理,调试和测试](<11.md>) - * 下一节: [使用GDB调试](<11.2.md>) \ No newline at end of file + * 下一节: [使用GDB调试](<11.2.md>) diff --git a/11.md b/11.md index 79f911c3723856ceb87d8f49b24a4d6d83970b54..66f0ac09a3829bab4585ccd1518b9f9ceccddf9c 100644 --- a/11.md +++ b/11.md @@ -9,13 +9,13 @@ 长期以来,培养良好的调试、测试习惯一直是很多程序员逃避的事情,所以现在你不要再逃避了,就从你现在的项目开发,从学习Go Web开发开始养成良好的习惯。 -## 目录 - * 1 [错误处理](11.1.md) - * 2 [使用GDB调试](11.2.md) - * 3 [Go怎么写测试用例](11.3.md) - * 4 [小结](11.4.md) - -## links - * [目录]() - * 上一章: [第十章总结](<10.4.md>) +## 目录 + * 1 [错误处理](11.1.md) + * 2 [使用GDB调试](11.2.md) + * 3 [Go怎么写测试用例](11.3.md) + * 4 [小结](11.4.md) + +## links + * [目录]() + * 上一章: [第十章总结](<10.4.md>) * 下一节: [错误处理](<11.1.md>) \ No newline at end of file diff --git a/2.1.md b/2.1.md index 6b079ce521d758d135c44b5527273ea09cf153df..a503142867df99875d79cab42ab84525c4c4d22c 100644 --- a/2.1.md +++ b/2.1.md @@ -1,55 +1,52 @@ -# 2.1 你好,Go - -在开始编写应用之前,我们先从最基本的程序开始。就像你造房子之前不知道什么是地基一样,编写程序也不知道如何开始。因此,在本节中,我们要学习用最基本的语法让Go程序运行起来。 - -## 程序 - -这就像一个传统,在学习大部分语言之前,你先学会如何编写一个可以输出`hello world`的程序。 - -准备好了吗?Let's Go! - - package main - - import "fmt" - - func main() { - fmt.Printf("Hello, world or καλημ ́ρα κóσμ or こんにちは世界\n") - } - -输出如下: - - Hello, world or καλημ ́ρα κóσμ or こんにちは世界 - -## 详解 -首先我们要了解一个概念,Go程序是通过`package`来组织的 - -`package `(在我们的例子中是`package main`)这一行告诉我们当前文件属于哪个包,而包名`main`则告诉我们它是一个可独立运行的包,它在编译后会产生可执行文件。除了`main`包之外,其它的包最后都会生成`*.a`文件(也就是包文件)并放置在`$GOPATH/pkg/$GOOS_$GOARCH`中(以Mac为例就是`$GOPATH/pkg/darwin_amd64`)。 - ->每一个可独立运行的Go程序,必定包含一个`package main`,在这个`main`包中必定包含一个入口函数`main`,而这个函数既没有参数,也没有返回值。 - -为了打印`Hello, world...`,我们调用了一个函数`Printf`,这个函数来自于`fmt`包,所以我们在第三行中导入了系统级别的`fmt`包:`import "fmt"`。 - -包的概念和Python中的module相同,它们都有一些特别的好处:模块化(能够把你的程序分成多个模块)和可重用性(每个模块都能被其它应用程序反复使用)。我们在这里只是先了解一下包的概念,后面我们将会编写自己的包。 - -在第五行中,我们通过关键字`func`定义了一个`main`函数,函数体被放在`{}`(大括号)中,就像我们平时写C、C++或Java时一样。 - -大家可以看到`main`函数是没有任何的参数的,我们接下来就学习如何编写带参数的、返回0个或多个值的函数。 - -第六行,我们调用了`fmt`包里面定义的函数`Printf`。大家可以看到,这个函数是通过`.`的方式调用的,这一点和Python十分相似。 - ->前面提到过,包名和包所在的文件夹名可以是不同的,此处的``即为通过`package `声明的包名,而非文件夹名。 - -最后大家可以看到我们输出的内容里面包含了很多非ASCII码字符。实际上,Go是天生支持UTF-8的,任何字符都可以直接输出,你甚至可以用UTF-8中的任何字符作为标识符。 - - -## 结论 - -Go使用`package`(和Python的模块类似)来组织代码。`main.main()`函数(这个函数主要位于主包)是每一个独立的可运行程序的入口点。Go使用UTF-8字符串和标识符(因为UTF-8的发明者也就是Go的发明者),所以它天生就具有多语言的支持。 - -## links - * [目录]() - * 上一节: [Go语言基础](<2.md>) - * 下一节: [Go基础](<2.2.md>) - -## LastModified - * $Id$ +# 2.1 你好,Go + +在开始编写应用之前,我们先从最基本的程序开始。就像你造房子之前不知道什么是地基一样,编写程序也不知道如何开始。因此,在本节中,我们要学习用最基本的语法让Go程序运行起来。 + +## 程序 + +这就像一个传统,在学习大部分语言之前,你先学会如何编写一个可以输出`hello world`的程序。 + +准备好了吗?Let's Go! + + package main + + import "fmt" + + func main() { + fmt.Printf("Hello, world or καλημ ́ρα κóσμ or こんにちは世界\n") + } + +输出如下: + + Hello, world or καλημ ́ρα κóσμ or こんにちは世界 + +## 详解 +首先我们要了解一个概念,Go程序是通过`package`来组织的 + +`package `(在我们的例子中是`package main`)这一行告诉我们当前文件属于哪个包,而包名`main`则告诉我们它是一个可独立运行的包,它在编译后会产生可执行文件。除了`main`包之外,其它的包最后都会生成`*.a`文件(也就是包文件)并放置在`$GOPATH/pkg/$GOOS_$GOARCH`中(以Mac为例就是`$GOPATH/pkg/darwin_amd64`)。 + +>每一个可独立运行的Go程序,必定包含一个`package main`,在这个`main`包中必定包含一个入口函数`main`,而这个函数既没有参数,也没有返回值。 + +为了打印`Hello, world...`,我们调用了一个函数`Printf`,这个函数来自于`fmt`包,所以我们在第三行中导入了系统级别的`fmt`包:`import "fmt"`。 + +包的概念和Python中的module相同,它们都有一些特别的好处:模块化(能够把你的程序分成多个模块)和可重用性(每个模块都能被其它应用程序反复使用)。我们在这里只是先了解一下包的概念,后面我们将会编写自己的包。 + +在第五行中,我们通过关键字`func`定义了一个`main`函数,函数体被放在`{}`(大括号)中,就像我们平时写C、C++或Java时一样。 + +大家可以看到`main`函数是没有任何的参数的,我们接下来就学习如何编写带参数的、返回0个或多个值的函数。 + +第六行,我们调用了`fmt`包里面定义的函数`Printf`。大家可以看到,这个函数是通过`.`的方式调用的,这一点和Python十分相似。 + +>前面提到过,包名和包所在的文件夹名可以是不同的,此处的``即为通过`package `声明的包名,而非文件夹名。 + +最后大家可以看到我们输出的内容里面包含了很多非ASCII码字符。实际上,Go是天生支持UTF-8的,任何字符都可以直接输出,你甚至可以用UTF-8中的任何字符作为标识符。 + + +## 结论 + +Go使用`package`(和Python的模块类似)来组织代码。`main.main()`函数(这个函数主要位于主包)是每一个独立的可运行程序的入口点。Go使用UTF-8字符串和标识符(因为UTF-8的发明者也就是Go的发明者),所以它天生就具有多语言的支持。 + +## links + * [目录]() + * 上一节: [Go语言基础](<2.md>) + * 下一节: [Go基础](<2.2.md>) diff --git a/2.2.md b/2.2.md index 69f092976890be751682eed135e6b0e3fa7eef3b..80bab5ad13d14a429f90b95fb834dea06bcb0512 100644 --- a/2.2.md +++ b/2.2.md @@ -1,453 +1,450 @@ -# 2.2 Go基础 - -这小节我们将要介绍如何定义变量、常量、Go内置类型以及Go程序设计中的一些技巧。 - -## 定义变量 - -Go语言里面定义变量有多种方式。 - -使用`var`关键字是Go最基本的定义变量方式,与C语言不同的是Go把变量类型放在变量名后面: - - //定义一个名称为“variableName”,类型为"type"的变量 - var variableName type - -定义多个变量 - - //定义三个类型都是“type”的三个变量 - var vname1, vname2, vname3 type - -定义变量并初始化值 - - //初始化“variableName”的变量为“value”值,类型是“type” - var variableName type = value - -同时初始化多个变量 - - /* - 定义三个类型都是"type"的三个变量,并且它们分别初始化相应的值 - vname1为v1,vname2为v2,vname3为v3 - */ - var vname1, vname2, vname3 type= v1, v2, v3 - -你是不是觉得上面这样的定义有点繁琐?没关系,因为Go语言的设计者也发现了,有一种写法可以让它变得简单一点。我们可以直接忽略类型声明,那么上面的代码变成这样了: - - /* - 定义三个变量,它们分别初始化相应的值 - vname1为v1,vname2为v2,vname3为v3 - 然后Go会根据其相应值的类型来帮你初始化它们 - */ - var vname1, vname2, vname3 = v1, v2, v3 - -你觉得上面的还是有些繁琐?好吧,我也觉得。让我们继续简化: - - /* - 定义三个变量,它们分别初始化相应的值 - vname1为v1,vname2为v2,vname3为v3 - 编译器会根据初始化的值自动推导出相应的类型 - */ - vname1, vname2, vname3 := v1, v2, v3 - -现在是不是看上去非常简洁了?`:=`这个符号直接取代了`var`和`type`,这种形式叫做简短声明。不过它有一个限制,那就是它只能用在函数内部;在函数外部使用则会无法编译通过,所以一般用`var`方式来定义全局变量。 - -`_`(下划线)是个特殊的变量名,任何赋予它的值都会被丢弃。在这个例子中,我们将值`35`赋予`b`,并同时丢弃`34`: - - _, b := 34, 35 - -Go对于已声明但未使用的变量会在编译阶段报错,比如下面的代码就会产生一个错误:声明了`i`但未使用。 - - package main - - func main() { - var i int - } - -## 常量 - -所谓常量,也就是在程序编译阶段就确定下来的值,而程序在运行时则无法改变该值。在Go程序中,常量可定义为数值、布尔值或字符串等类型。 - -它的语法如下: - - const constantName = value - //如果需要,也可以明确指定常量的类型: - const Pi float32 = 3.1415926 - -下面是一些常量声明的例子: - - const Pi = 3.1415926 - const i = 10000 - const MaxThread = 10 - const prefix = "astaxie_" - - -## 内置基础类型 - -### Boolean - -在Go中,布尔值的类型为`bool`,值是`true`或`false`,默认为`false`。 - - //示例代码 - var isActive bool // 全局变量声明 - var enabled, disabled = true, false // 忽略类型的声明 - func test() { - var available bool // 一般声明 - valid := false // 简短声明 - available = true // 赋值操作 - } - - -### 数值类型 - -整数类型有无符号和带符号两种。Go同时支持`int`和`uint`,这两种类型的长度相同,但具体长度取决于不同编译器的实现。当前的gcc和gccgo编译器在32位和64位平台上都使用32位来表示`int`和`uint`,但未来在64位平台上可能增加到64位。Go里面也有直接定义好位数的类型:`rune`, `int8`, `int16`, `int32`, `int64`和`byte`, `uint8`, `uint16`, `uint32`, `uint64`。其中`rune`是`int32`的别称,`byte`是`uint8`的别称。 - ->需要注意的一点是,这些类型的变量之间不允许互相赋值或操作,不然会在编译时引起编译器报错。 -> ->如下的代码会产生错误 -> ->> var a int8 ->> var b int32 ->> c:=a + b -> ->另外,尽管int的长度是32 bit, 但int 与 int32并不可以互用。 - -浮点数的类型有`float32`和`float64`两种(没有`float`类型),默认是`float64`。 - -这就是全部吗?No!Go还支持复数。它的默认类型是`complex128`(64位实数+64位虚数)。如果需要小一些的,也有`complex64`(32位实数+32位虚数)。复数的形式为`RE + IMi`,其中`RE`是实数部分,`IM`是虚数部分,而最后的`i`是虚数单位。下面是一个使用复数的例子: - - var c complex64 = 5+5i - //output: (5+5i) - fmt.Printf("Value is: %v", c) - - -### 字符串 - -我们在上一节中讲过,Go中的字符串都是采用`UTF-8`字符集编码。字符串是用一对双引号(`""`)或反引号(`` ` `` `` ` ``)括起来定义,它的类型是`string`。 - - //示例代码 - var frenchHello string // 声明变量为字符串的一般方法 - var emptyString string = "" // 声明了一个字符串变量,初始化为空字符串 - func test() { - no, yes, maybe := "no", "yes", "maybe" // 简短声明,同时声明多个变量 - japaneseHello := "Ohaiou" // 同上 - frenchHello = "Bonjour" // 常规赋值 - } - -在Go中字符串是不可变的,例如下面的代码编译时会报错: - - var s string = "hello" - s[0] = 'c' - - -但如果真的想要修改怎么办呢?下面的代码可以实现: - - s := "hello" - c := []byte(s) // 将字符串 s 转换为 []byte 类型 - c[0] = 'c' - s2 := string(c) // 再转换回 string 类型 - fmt.Printf("%s\n", s2) - - -Go中可以使用`+`操作符来连接两个字符串: - - s := "hello," - m := " world" - a := s + m - fmt.Printf("%s\n", a) - -修改字符串也可写为: - - s := "hello" - s = "c" + s[1:] // 字符串虽不能更改,但可进行切片操作 - fmt.Printf("%s\n", s) - -如果要声明一个多行的字符串怎么办?可以通过`` ` ``来声明: - - m := `hello - world` - -`` ` `` 括起的字符串为Raw字符串,即字符串在代码中的形式就是打印时的形式,它没有字符转义,换行也将原样输出。 - -### 错误类型 -Go内置有一个`error`类型,专门用来处理错误信息,Go的`package`里面还专门有一个包`errors`来处理错误: - - err := errors.New("emit macho dwarf: elf header corrupted") - if err != nil { - fmt.Print(err) - } - -### Go数据底层的存储 - -下面这张图来源于[Russ Cox Blog](http://research.swtch.com/)中一篇介绍[Go数据结构](http://research.swtch.com/godata)的文章,大家可以看到这些基础类型底层都是分配了一块内存,然后存储了相应的值。 - -![](images/2.2.basic.png?raw=true) - -## 一些技巧 - -### 分组声明 - -在Go语言中,同时声明多个常量、变量,或者导入多个包时,可采用分组的方式进行声明。 - -例如下面的代码: - - import "fmt" - import "os" - - const i = 100 - const pi = 3.1415 - const prefix = "Go_" - - var i int - var pi float32 - var prefix string - -可以分组写成如下形式: - - import( - "fmt" - "os" - ) - - const( - i = 100 - pi = 3.1415 - prefix = "Go_" - ) - - var( - i int - pi float32 - prefix string - ) - ->除非被显式设置为其它值或`iota`,每个`const`分组的第一个常量被默认设置为它的0值,第二及后续的常量被默认设置为它前面那个常量的值,如果前面那个常量的值是`iota`,则它也被设置为`iota`。 - -### iota枚举 - -Go里面有一个关键字`iota`,这个关键字用来声明`enum`的时候采用,它默认开始值是0,每调用一次加1: - - const( - x = iota // x == 0 - y = iota // y == 1 - z = iota // z == 2 - w // 常量声明省略值时,默认和之前一个值的字面相同。这里隐式地说w = iota,因此w == 3。其实上面y和z可同样不用"= iota" - ) - - const v = iota // 每遇到一个const关键字,iota就会重置,此时v == 0 - -### Go程序设计的一些规则 -Go之所以会那么简洁,是因为它有一些默认的行为: -- 大写字母开头的变量是可导出的,也就是其它包可以读取的,是公用变量;小写字母开头的就是不可导出的,是私有变量。 -- 大写字母开头的函数也是一样,相当于`class`中的带`public`关键词的公有函数;小写字母开头的就是有`private`关键词的私有函数。 - -## array、slice、map - -### array -`array`就是数组,它的定义方式如下: - - var arr [n]type - -在`[n]type`中,`n`表示数组的长度,`type`表示存储元素的类型。对数组的操作和其它语言类似,都是通过`[]`来进行读取或赋值: - - var arr [10]int // 声明了一个int类型的数组 - arr[0] = 42 // 数组下标是从0开始的 - arr[1] = 13 // 赋值操作 - fmt.Printf("The first element is %d\n", arr[0]) // 获取数据,返回42 - fmt.Printf("The last element is %d\n", arr[9]) //返回未赋值的最后一个元素,默认返回0 - -由于长度也是数组类型的一部分,因此`[3]int`与`[4]int`是不同的类型,数组也就不能改变长度。数组之间的赋值是值的赋值,即当把一个数组作为参数传入函数的时候,传入的其实是该数组的副本,而不是它的指针。如果要使用指针,那么就需要用到后面介绍的`slice`类型了。 - -数组可以使用另一种`:=`来声明 - - a := [3]int{1, 2, 3} // 声明了一个长度为3的int数组 - - b := [10]int{1, 2, 3} // 声明了一个长度为10的int数组,其中前三个元素初始化为1、2、3,其它默认为0 - - c := [...]int{4, 5, 6} // 可以省略长度而采用`...`的方式,Go会自动根据元素个数来计算长度 - -也许你会说,我想数组里面的值还是数组,能实现吗?当然咯,Go支持嵌套数组,即多维数组。比如下面的代码就声明了一个二维数组: - - // 声明了一个二维数组,该数组以两个数组作为元素,其中每个数组中又有4个int类型的元素 - doubleArray := [2][4]int{[4]int{1, 2, 3, 4}, [4]int{5, 6, 7, 8}} - - // 如果内部的元素和外部的一样,那么上面的声明可以简化,直接忽略内部的类型 - easyArray := [2][4]int{{1, 2, 3, 4}, {5, 6, 7, 8}} - -数组的分配如下所示: - -![](images/2.2.array.png?raw=true) - - -### slice - -在很多应用场景中,数组并不能满足我们的需求。在初始定义数组时,我们并不知道需要多大的数组,因此我们就需要“动态数组”。在Go里面这种数据结构叫`slice` - -`slice`并不是真正意义上的动态数组,而是一个引用类型。`slice`总是指向一个底层`array`,`slice`的声明也可以像`array`一样,只是不需要长度。 - - // 和声明array一样,只是少了长度 - var fslice []int - -接下来我们可以声明一个`slice`,并初始化数据,如下所示: - - slice := []byte {'a', 'b', 'c', 'd'} - -`slice`可以从一个数组或一个已经存在的`slice`中再次声明。`slice`通过`array[i:j]`来获取,其中`i`是数组的开始位置,`j`是结束位置,但不包含`array[j]`,它的长度是`j-i`。 - - // 声明一个含有10个元素元素类型为byte的数组 - var ar = [10]byte {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'} - - // 声明两个含有byte的slice - var a, b []byte - - // a指向数组的第3个元素开始,并到第五个元素结束, - a = ar[2:5] - //现在a含有的元素: ar[2]、ar[3]和ar[4] - - // b是数组ar的另一个slice - b = ar[3:5] - // b的元素是:ar[3]和ar[4] - ->注意`slice`和数组在声明时的区别:声明数组时,方括号内写明了数组的长度或使用`...`自动计算长度,而声明`slice`时,方括号内没有任何字符。 - -它们的数据结构如下所示 - -![](images/2.2.slice.png?raw=true) - -slice有一些简便的操作 - - - `slice`的默认开始位置是0,`ar[:n]`等价于`ar[0:n]` - - `slice`的第二个序列默认是数组的长度,`ar[n:]`等价于`ar[n:len(ar)]` - - 如果从一个数组里面直接获取`slice`,可以这样`ar[:]`,因为默认第一个序列是0,第二个是数组的长度,即等价于`ar[0:len(ar)]` - -下面这个例子展示了更多关于`slice`的操作: - - // 声明一个数组 - var array = [10]byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'} - // 声明两个slice - var aSlice, bSlice []byte - - // 演示一些简便操作 - aSlice = array[:3] // 等价于aSlice = array[0:3] aSlice包含元素: a,b,c - aSlice = array[5:] // 等价于aSlice = array[5:9] aSlice包含元素: f,g,h,i,j - aSlice = array[:] // 等价于aSlice = array[0:9] 这样aSlice包含了全部的元素 - - // 从slice中获取slice - aSlice = array[3:7] // aSlice包含元素: d,e,f,g,len=4,cap=7 - bSlice = aSlice[1:3] // bSlice 包含aSlice[1], aSlice[2] 也就是含有: e,f - bSlice = aSlice[:3] // bSlice 包含 aSlice[0], aSlice[1], aSlice[2] 也就是含有: d,e,f - bSlice = aSlice[0:5] // 对slice的slice可以在cap范围内扩展,此时bSlice包含:d,e,f,g,h - bSlice = aSlice[:] // bSlice包含所有aSlice的元素: d,e,f,g - -`slice`是引用类型,所以当引用改变其中元素的值时,其它的所有引用都会改变该值,例如上面的`aSlice`和`bSlice`,如果修改了`aSlice`中元素的值,那么`bSlice`相对应的值也会改变。 - -从概念上面来说`slice`像一个结构体,这个结构体包含了三个元素: -- 一个指针,指向数组中`slice`指定的开始位置 -- 长度,即`slice`的长度 -- 最大长度,也就是`slice`开始位置到数组的最后位置的长度 - - Array_a := [10]byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'} - Slice_a := Array_a[2:5] - -上面代码的真正存储结构如下图所示 - -![](images/2.2.slice2.png?raw=true) - -对于`slice`有几个有用的内置函数: - -- `len` 获取`slice`的长度 -- `cap` 获取`slice`的最大容量 -- `append` 向`slice`里面追加一个或者多个元素,然后返回一个和`slice`一样类型的`slice` -- `copy` 函数`copy`从源`slice`的`src`中复制元素到目标`dst`,并且返回复制的元素的个数 - -注:`append`函数会改变`slice`所引用的数组的内容,从而影响到引用同一数组的其它`slice`。 -但当`slice`中没有剩余空间(即`(cap-len) == 0`)时,此时将动态分配新的数组空间。返回的`slice`数组指针将指向这个空间,而原数组的内容将保持不变;其它引用此数组的`slice`则不受影响。 - -### map - -`map`也就是Python中字典的概念,它的格式为`map[keyType]valueType` - -我们看下面的代码,`map`的读取和设置也类似`slice`一样,通过`key`来操作,只是`slice`的`key`只能是`int`类型,而`map`多了很多类型,可以是`int`,可以是`string`及所有完全定义了`==`与`!=`操作的类型。 - - // 声明一个key是字符串,值为int的字典,这种方式的声明需要在使用之前使用make初始化 - var numbers map[string] int - // 另一种map的声明方式 - numbers := make(map[string]int) - numbers["one"] = 1 //赋值 - numbers["ten"] = 10 //赋值 - numbers["three"] = 3 - - fmt.Println("第三个数字是: ", numbers["three"]) // 读取数据 - // 打印出来如:第三个数字是: 3 - -这个`map`就像我们平常看到的表格一样,左边列是`key`,右边列是值 - -使用map过程中需要注意的几点: -- `map`是无序的,每次打印出来的`map`都会不一样,它不能通过`index`获取,而必须通过`key`获取 -- `map`的长度是不固定的,也就是和`slice`一样,也是一种引用类型 -- 内置的`len`函数同样适用于`map`,返回`map`拥有的`key`的数量 -- `map`的值可以很方便的修改,通过`numbers["one"]=11`可以很容易的把key为`one`的字典值改为`11` - -`map`的初始化可以通过`key:val`的方式初始化值,同时`map`内置有判断是否存在`key`的方式 - -通过`delete`删除`map`的元素: - - // 初始化一个字典 - rating := map[string]float32 {"C":5, "Go":4.5, "Python":4.5, "C++":2 } - // map有两个返回值,第二个返回值,如果不存在key,那么ok为false,如果存在ok为true - csharpRating, ok := rating["C#"] - if ok { - fmt.Println("C# is in the map and its rating is ", csharpRating) - } else { - fmt.Println("We have no rating associated with C# in the map") - } - - delete(rating, "C") // 删除key为C的元素 - - -上面说过了,`map`也是一种引用类型,如果两个`map`同时指向一个底层,那么一个改变,另一个也相应的改变: - - m := make(map[string]string) - m["Hello"] = "Bonjour" - m1 := m - m1["Hello"] = "Salut" // 现在m["hello"]的值已经是Salut了 - - -### make、new操作 - -`make`用于内建类型(`map`、`slice` 和`channel`)的内存分配。`new`用于各种类型的内存分配。 - -内建函数`new`本质上说跟其它语言中的同名函数功能一样:`new(T)`分配了零值填充的`T`类型的内存空间,并且返回其地址,即一个`*T`类型的值。用Go的术语说,它返回了一个指针,指向新分配的类型`T`的零值。有一点非常重要: - ->`new`返回指针。 - -内建函数`make(T, args)`与`new(T)`有着不同的功能,make只能创建`slice`、`map`和`channel`,并且返回一个有初始值(非零)的`T`类型,而不是`*T`。本质来讲,导致这三个类型有所不同的原因是指向数据结构的引用在使用前必须被初始化。例如,一个`slice`,是一个包含指向数据(内部`array`)的指针、长度和容量的三项描述符;在这些项目被初始化之前,`slice`为`nil`。对于`slice`、`map`和`channel`来说,`make`初始化了内部的数据结构,填充适当的值。 - ->`make`返回初始化后的(非零)值。 - -下面这个图详细的解释了`new`和`make`之间的区别。 - -![](images/2.2.makenew.png?raw=true) - -关于“零值”,所指并非是空值,而是一种“变量未填充前”的默认值,通常为0。 -此处罗列 部分类型 的 “零值” - - int 0 - int8 0 - int32 0 - int64 0 - uint 0x0 - rune 0 //rune的实际类型是 int32 - byte 0x0 // byte的实际类型是 uint8 - float32 0 //长度为 4 byte - float64 0 //长度为 8 byte - bool false - string "" - - - - -## links - * [目录]() - * 上一章: [你好,Go](<2.1.md>) - * 下一节: [流程和函数](<2.3.md>) - -## LastModified - * $Id$ +# 2.2 Go基础 + +这小节我们将要介绍如何定义变量、常量、Go内置类型以及Go程序设计中的一些技巧。 + +## 定义变量 + +Go语言里面定义变量有多种方式。 + +使用`var`关键字是Go最基本的定义变量方式,与C语言不同的是Go把变量类型放在变量名后面: + + //定义一个名称为“variableName”,类型为"type"的变量 + var variableName type + +定义多个变量 + + //定义三个类型都是“type”的三个变量 + var vname1, vname2, vname3 type + +定义变量并初始化值 + + //初始化“variableName”的变量为“value”值,类型是“type” + var variableName type = value + +同时初始化多个变量 + + /* + 定义三个类型都是"type"的三个变量,并且它们分别初始化相应的值 + vname1为v1,vname2为v2,vname3为v3 + */ + var vname1, vname2, vname3 type= v1, v2, v3 + +你是不是觉得上面这样的定义有点繁琐?没关系,因为Go语言的设计者也发现了,有一种写法可以让它变得简单一点。我们可以直接忽略类型声明,那么上面的代码变成这样了: + + /* + 定义三个变量,它们分别初始化相应的值 + vname1为v1,vname2为v2,vname3为v3 + 然后Go会根据其相应值的类型来帮你初始化它们 + */ + var vname1, vname2, vname3 = v1, v2, v3 + +你觉得上面的还是有些繁琐?好吧,我也觉得。让我们继续简化: + + /* + 定义三个变量,它们分别初始化相应的值 + vname1为v1,vname2为v2,vname3为v3 + 编译器会根据初始化的值自动推导出相应的类型 + */ + vname1, vname2, vname3 := v1, v2, v3 + +现在是不是看上去非常简洁了?`:=`这个符号直接取代了`var`和`type`,这种形式叫做简短声明。不过它有一个限制,那就是它只能用在函数内部;在函数外部使用则会无法编译通过,所以一般用`var`方式来定义全局变量。 + +`_`(下划线)是个特殊的变量名,任何赋予它的值都会被丢弃。在这个例子中,我们将值`35`赋予`b`,并同时丢弃`34`: + + _, b := 34, 35 + +Go对于已声明但未使用的变量会在编译阶段报错,比如下面的代码就会产生一个错误:声明了`i`但未使用。 + + package main + + func main() { + var i int + } + +## 常量 + +所谓常量,也就是在程序编译阶段就确定下来的值,而程序在运行时则无法改变该值。在Go程序中,常量可定义为数值、布尔值或字符串等类型。 + +它的语法如下: + + const constantName = value + //如果需要,也可以明确指定常量的类型: + const Pi float32 = 3.1415926 + +下面是一些常量声明的例子: + + const Pi = 3.1415926 + const i = 10000 + const MaxThread = 10 + const prefix = "astaxie_" + + +## 内置基础类型 + +### Boolean + +在Go中,布尔值的类型为`bool`,值是`true`或`false`,默认为`false`。 + + //示例代码 + var isActive bool // 全局变量声明 + var enabled, disabled = true, false // 忽略类型的声明 + func test() { + var available bool // 一般声明 + valid := false // 简短声明 + available = true // 赋值操作 + } + + +### 数值类型 + +整数类型有无符号和带符号两种。Go同时支持`int`和`uint`,这两种类型的长度相同,但具体长度取决于不同编译器的实现。当前的gcc和gccgo编译器在32位和64位平台上都使用32位来表示`int`和`uint`,但未来在64位平台上可能增加到64位。Go里面也有直接定义好位数的类型:`rune`, `int8`, `int16`, `int32`, `int64`和`byte`, `uint8`, `uint16`, `uint32`, `uint64`。其中`rune`是`int32`的别称,`byte`是`uint8`的别称。 + +>需要注意的一点是,这些类型的变量之间不允许互相赋值或操作,不然会在编译时引起编译器报错。 +> +>如下的代码会产生错误 +> +>> var a int8 +>> var b int32 +>> c:=a + b +> +>另外,尽管int的长度是32 bit, 但int 与 int32并不可以互用。 + +浮点数的类型有`float32`和`float64`两种(没有`float`类型),默认是`float64`。 + +这就是全部吗?No!Go还支持复数。它的默认类型是`complex128`(64位实数+64位虚数)。如果需要小一些的,也有`complex64`(32位实数+32位虚数)。复数的形式为`RE + IMi`,其中`RE`是实数部分,`IM`是虚数部分,而最后的`i`是虚数单位。下面是一个使用复数的例子: + + var c complex64 = 5+5i + //output: (5+5i) + fmt.Printf("Value is: %v", c) + + +### 字符串 + +我们在上一节中讲过,Go中的字符串都是采用`UTF-8`字符集编码。字符串是用一对双引号(`""`)或反引号(`` ` `` `` ` ``)括起来定义,它的类型是`string`。 + + //示例代码 + var frenchHello string // 声明变量为字符串的一般方法 + var emptyString string = "" // 声明了一个字符串变量,初始化为空字符串 + func test() { + no, yes, maybe := "no", "yes", "maybe" // 简短声明,同时声明多个变量 + japaneseHello := "Ohaiou" // 同上 + frenchHello = "Bonjour" // 常规赋值 + } + +在Go中字符串是不可变的,例如下面的代码编译时会报错: + + var s string = "hello" + s[0] = 'c' + + +但如果真的想要修改怎么办呢?下面的代码可以实现: + + s := "hello" + c := []byte(s) // 将字符串 s 转换为 []byte 类型 + c[0] = 'c' + s2 := string(c) // 再转换回 string 类型 + fmt.Printf("%s\n", s2) + + +Go中可以使用`+`操作符来连接两个字符串: + + s := "hello," + m := " world" + a := s + m + fmt.Printf("%s\n", a) + +修改字符串也可写为: + + s := "hello" + s = "c" + s[1:] // 字符串虽不能更改,但可进行切片操作 + fmt.Printf("%s\n", s) + +如果要声明一个多行的字符串怎么办?可以通过`` ` ``来声明: + + m := `hello + world` + +`` ` `` 括起的字符串为Raw字符串,即字符串在代码中的形式就是打印时的形式,它没有字符转义,换行也将原样输出。 + +### 错误类型 +Go内置有一个`error`类型,专门用来处理错误信息,Go的`package`里面还专门有一个包`errors`来处理错误: + + err := errors.New("emit macho dwarf: elf header corrupted") + if err != nil { + fmt.Print(err) + } + +### Go数据底层的存储 + +下面这张图来源于[Russ Cox Blog](http://research.swtch.com/)中一篇介绍[Go数据结构](http://research.swtch.com/godata)的文章,大家可以看到这些基础类型底层都是分配了一块内存,然后存储了相应的值。 + +![](images/2.2.basic.png?raw=true) + +## 一些技巧 + +### 分组声明 + +在Go语言中,同时声明多个常量、变量,或者导入多个包时,可采用分组的方式进行声明。 + +例如下面的代码: + + import "fmt" + import "os" + + const i = 100 + const pi = 3.1415 + const prefix = "Go_" + + var i int + var pi float32 + var prefix string + +可以分组写成如下形式: + + import( + "fmt" + "os" + ) + + const( + i = 100 + pi = 3.1415 + prefix = "Go_" + ) + + var( + i int + pi float32 + prefix string + ) + +>除非被显式设置为其它值或`iota`,每个`const`分组的第一个常量被默认设置为它的0值,第二及后续的常量被默认设置为它前面那个常量的值,如果前面那个常量的值是`iota`,则它也被设置为`iota`。 + +### iota枚举 + +Go里面有一个关键字`iota`,这个关键字用来声明`enum`的时候采用,它默认开始值是0,每调用一次加1: + + const( + x = iota // x == 0 + y = iota // y == 1 + z = iota // z == 2 + w // 常量声明省略值时,默认和之前一个值的字面相同。这里隐式地说w = iota,因此w == 3。其实上面y和z可同样不用"= iota" + ) + + const v = iota // 每遇到一个const关键字,iota就会重置,此时v == 0 + +### Go程序设计的一些规则 +Go之所以会那么简洁,是因为它有一些默认的行为: +- 大写字母开头的变量是可导出的,也就是其它包可以读取的,是公用变量;小写字母开头的就是不可导出的,是私有变量。 +- 大写字母开头的函数也是一样,相当于`class`中的带`public`关键词的公有函数;小写字母开头的就是有`private`关键词的私有函数。 + +## array、slice、map + +### array +`array`就是数组,它的定义方式如下: + + var arr [n]type + +在`[n]type`中,`n`表示数组的长度,`type`表示存储元素的类型。对数组的操作和其它语言类似,都是通过`[]`来进行读取或赋值: + + var arr [10]int // 声明了一个int类型的数组 + arr[0] = 42 // 数组下标是从0开始的 + arr[1] = 13 // 赋值操作 + fmt.Printf("The first element is %d\n", arr[0]) // 获取数据,返回42 + fmt.Printf("The last element is %d\n", arr[9]) //返回未赋值的最后一个元素,默认返回0 + +由于长度也是数组类型的一部分,因此`[3]int`与`[4]int`是不同的类型,数组也就不能改变长度。数组之间的赋值是值的赋值,即当把一个数组作为参数传入函数的时候,传入的其实是该数组的副本,而不是它的指针。如果要使用指针,那么就需要用到后面介绍的`slice`类型了。 + +数组可以使用另一种`:=`来声明 + + a := [3]int{1, 2, 3} // 声明了一个长度为3的int数组 + + b := [10]int{1, 2, 3} // 声明了一个长度为10的int数组,其中前三个元素初始化为1、2、3,其它默认为0 + + c := [...]int{4, 5, 6} // 可以省略长度而采用`...`的方式,Go会自动根据元素个数来计算长度 + +也许你会说,我想数组里面的值还是数组,能实现吗?当然咯,Go支持嵌套数组,即多维数组。比如下面的代码就声明了一个二维数组: + + // 声明了一个二维数组,该数组以两个数组作为元素,其中每个数组中又有4个int类型的元素 + doubleArray := [2][4]int{[4]int{1, 2, 3, 4}, [4]int{5, 6, 7, 8}} + + // 如果内部的元素和外部的一样,那么上面的声明可以简化,直接忽略内部的类型 + easyArray := [2][4]int{{1, 2, 3, 4}, {5, 6, 7, 8}} + +数组的分配如下所示: + +![](images/2.2.array.png?raw=true) + + +### slice + +在很多应用场景中,数组并不能满足我们的需求。在初始定义数组时,我们并不知道需要多大的数组,因此我们就需要“动态数组”。在Go里面这种数据结构叫`slice` + +`slice`并不是真正意义上的动态数组,而是一个引用类型。`slice`总是指向一个底层`array`,`slice`的声明也可以像`array`一样,只是不需要长度。 + + // 和声明array一样,只是少了长度 + var fslice []int + +接下来我们可以声明一个`slice`,并初始化数据,如下所示: + + slice := []byte {'a', 'b', 'c', 'd'} + +`slice`可以从一个数组或一个已经存在的`slice`中再次声明。`slice`通过`array[i:j]`来获取,其中`i`是数组的开始位置,`j`是结束位置,但不包含`array[j]`,它的长度是`j-i`。 + + // 声明一个含有10个元素元素类型为byte的数组 + var ar = [10]byte {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'} + + // 声明两个含有byte的slice + var a, b []byte + + // a指向数组的第3个元素开始,并到第五个元素结束, + a = ar[2:5] + //现在a含有的元素: ar[2]、ar[3]和ar[4] + + // b是数组ar的另一个slice + b = ar[3:5] + // b的元素是:ar[3]和ar[4] + +>注意`slice`和数组在声明时的区别:声明数组时,方括号内写明了数组的长度或使用`...`自动计算长度,而声明`slice`时,方括号内没有任何字符。 + +它们的数据结构如下所示 + +![](images/2.2.slice.png?raw=true) + +slice有一些简便的操作 + + - `slice`的默认开始位置是0,`ar[:n]`等价于`ar[0:n]` + - `slice`的第二个序列默认是数组的长度,`ar[n:]`等价于`ar[n:len(ar)]` + - 如果从一个数组里面直接获取`slice`,可以这样`ar[:]`,因为默认第一个序列是0,第二个是数组的长度,即等价于`ar[0:len(ar)]` + +下面这个例子展示了更多关于`slice`的操作: + + // 声明一个数组 + var array = [10]byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'} + // 声明两个slice + var aSlice, bSlice []byte + + // 演示一些简便操作 + aSlice = array[:3] // 等价于aSlice = array[0:3] aSlice包含元素: a,b,c + aSlice = array[5:] // 等价于aSlice = array[5:9] aSlice包含元素: f,g,h,i,j + aSlice = array[:] // 等价于aSlice = array[0:9] 这样aSlice包含了全部的元素 + + // 从slice中获取slice + aSlice = array[3:7] // aSlice包含元素: d,e,f,g,len=4,cap=7 + bSlice = aSlice[1:3] // bSlice 包含aSlice[1], aSlice[2] 也就是含有: e,f + bSlice = aSlice[:3] // bSlice 包含 aSlice[0], aSlice[1], aSlice[2] 也就是含有: d,e,f + bSlice = aSlice[0:5] // 对slice的slice可以在cap范围内扩展,此时bSlice包含:d,e,f,g,h + bSlice = aSlice[:] // bSlice包含所有aSlice的元素: d,e,f,g + +`slice`是引用类型,所以当引用改变其中元素的值时,其它的所有引用都会改变该值,例如上面的`aSlice`和`bSlice`,如果修改了`aSlice`中元素的值,那么`bSlice`相对应的值也会改变。 + +从概念上面来说`slice`像一个结构体,这个结构体包含了三个元素: +- 一个指针,指向数组中`slice`指定的开始位置 +- 长度,即`slice`的长度 +- 最大长度,也就是`slice`开始位置到数组的最后位置的长度 + + Array_a := [10]byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'} + Slice_a := Array_a[2:5] + +上面代码的真正存储结构如下图所示 + +![](images/2.2.slice2.png?raw=true) + +对于`slice`有几个有用的内置函数: + +- `len` 获取`slice`的长度 +- `cap` 获取`slice`的最大容量 +- `append` 向`slice`里面追加一个或者多个元素,然后返回一个和`slice`一样类型的`slice` +- `copy` 函数`copy`从源`slice`的`src`中复制元素到目标`dst`,并且返回复制的元素的个数 + +注:`append`函数会改变`slice`所引用的数组的内容,从而影响到引用同一数组的其它`slice`。 +但当`slice`中没有剩余空间(即`(cap-len) == 0`)时,此时将动态分配新的数组空间。返回的`slice`数组指针将指向这个空间,而原数组的内容将保持不变;其它引用此数组的`slice`则不受影响。 + +### map + +`map`也就是Python中字典的概念,它的格式为`map[keyType]valueType` + +我们看下面的代码,`map`的读取和设置也类似`slice`一样,通过`key`来操作,只是`slice`的`key`只能是`int`类型,而`map`多了很多类型,可以是`int`,可以是`string`及所有完全定义了`==`与`!=`操作的类型。 + + // 声明一个key是字符串,值为int的字典,这种方式的声明需要在使用之前使用make初始化 + var numbers map[string] int + // 另一种map的声明方式 + numbers := make(map[string]int) + numbers["one"] = 1 //赋值 + numbers["ten"] = 10 //赋值 + numbers["three"] = 3 + + fmt.Println("第三个数字是: ", numbers["three"]) // 读取数据 + // 打印出来如:第三个数字是: 3 + +这个`map`就像我们平常看到的表格一样,左边列是`key`,右边列是值 + +使用map过程中需要注意的几点: +- `map`是无序的,每次打印出来的`map`都会不一样,它不能通过`index`获取,而必须通过`key`获取 +- `map`的长度是不固定的,也就是和`slice`一样,也是一种引用类型 +- 内置的`len`函数同样适用于`map`,返回`map`拥有的`key`的数量 +- `map`的值可以很方便的修改,通过`numbers["one"]=11`可以很容易的把key为`one`的字典值改为`11` + +`map`的初始化可以通过`key:val`的方式初始化值,同时`map`内置有判断是否存在`key`的方式 + +通过`delete`删除`map`的元素: + + // 初始化一个字典 + rating := map[string]float32 {"C":5, "Go":4.5, "Python":4.5, "C++":2 } + // map有两个返回值,第二个返回值,如果不存在key,那么ok为false,如果存在ok为true + csharpRating, ok := rating["C#"] + if ok { + fmt.Println("C# is in the map and its rating is ", csharpRating) + } else { + fmt.Println("We have no rating associated with C# in the map") + } + + delete(rating, "C") // 删除key为C的元素 + + +上面说过了,`map`也是一种引用类型,如果两个`map`同时指向一个底层,那么一个改变,另一个也相应的改变: + + m := make(map[string]string) + m["Hello"] = "Bonjour" + m1 := m + m1["Hello"] = "Salut" // 现在m["hello"]的值已经是Salut了 + + +### make、new操作 + +`make`用于内建类型(`map`、`slice` 和`channel`)的内存分配。`new`用于各种类型的内存分配。 + +内建函数`new`本质上说跟其它语言中的同名函数功能一样:`new(T)`分配了零值填充的`T`类型的内存空间,并且返回其地址,即一个`*T`类型的值。用Go的术语说,它返回了一个指针,指向新分配的类型`T`的零值。有一点非常重要: + +>`new`返回指针。 + +内建函数`make(T, args)`与`new(T)`有着不同的功能,make只能创建`slice`、`map`和`channel`,并且返回一个有初始值(非零)的`T`类型,而不是`*T`。本质来讲,导致这三个类型有所不同的原因是指向数据结构的引用在使用前必须被初始化。例如,一个`slice`,是一个包含指向数据(内部`array`)的指针、长度和容量的三项描述符;在这些项目被初始化之前,`slice`为`nil`。对于`slice`、`map`和`channel`来说,`make`初始化了内部的数据结构,填充适当的值。 + +>`make`返回初始化后的(非零)值。 + +下面这个图详细的解释了`new`和`make`之间的区别。 + +![](images/2.2.makenew.png?raw=true) + +关于“零值”,所指并非是空值,而是一种“变量未填充前”的默认值,通常为0。 +此处罗列 部分类型 的 “零值” + + int 0 + int8 0 + int32 0 + int64 0 + uint 0x0 + rune 0 //rune的实际类型是 int32 + byte 0x0 // byte的实际类型是 uint8 + float32 0 //长度为 4 byte + float64 0 //长度为 8 byte + bool false + string "" + + + + +## links + * [目录]() + * 上一章: [你好,Go](<2.1.md>) + * 下一节: [流程和函数](<2.3.md>) diff --git a/2.3.md b/2.3.md index 60bef660bf8db27f8cddfa6ce3efcbc0550ced28..11dc24d7c68c9fde9e41b2f06a154f4e244aa2ac 100644 --- a/2.3.md +++ b/2.3.md @@ -1,465 +1,462 @@ -# 2.3 流程和函数 -这小节我们要介绍Go里面的流程控制以及函数操作 -## 流程控制 -流程控制在编程语言中是最伟大的发明了,因为有了它,你可以通过很简单的流程描述来表达很复杂的逻辑。流程控制包含分三大类:条件判断,循环控制和无条件跳转。 -### if -`if`也许是各种编程语言中最常见的了,它的语法概括起来就是:如果满足条件就做某事,否则做另一件事。 - -Go里面`if`条件判断语句中不需要括号,如下代码所示 - - if x > 10 { - fmt.Println("x is greater than 10") - } else { - fmt.Println("x is less than 10") - } - -Go的`if`还有一个强大的地方就是条件判断语句里面允许声明一个变量,这个变量的作用域只能在该条件逻辑块内,其他地方就不起作用了,如下所示 - - // 计算获取值x,然后根据x返回的大小,判断是否大于10。 - if x := computedValue(); x > 10 { - fmt.Println("x is greater than 10") - } else { - fmt.Println("x is less than 10") - } - - //这个地方如果这样调用就编译出错了,因为x是条件里面的变量 - fmt.Println(x) - -多个条件的时候如下所示: - - if integer == 3 { - fmt.Println("The integer is equal to 3") - } else if integer < 3 { - fmt.Println("The integer is less than 3") - } else { - fmt.Println("The integer is greater than 3") - } - -### goto - -Go有`goto`语句——请明智地使用它。用`goto`跳转到必须在当前函数内定义的标签。例如假设这样一个循环: - - func myFunc() { - i := 0 - Here: //这行的第一个词,以冒号结束作为标签 - println(i) - i++ - goto Here //跳转到Here去 - } - ->标签名是大小写敏感的。 - -### for -Go里面最强大的一个控制逻辑就是`for`,它即可以用来循环读取数据,又可以当作`while`来控制逻辑,还能迭代操作。它的语法如下: - - for expression1; expression2; expression3 { - //... - } - -`expression1`、`expression2`和`expression3`都是表达式,其中`expression1`和`expression3`是变量声明或者函数调用返回值之类的,`expression2`是用来条件判断,`expression1`在循环开始之前调用,`expression3`在每轮循环结束之时调用。 - -一个例子比上面讲那么多更有用,那么我们看看下面的例子吧: - - package main - import "fmt" - - func main(){ - sum := 0; - for index:=0; index < 10 ; index++ { - sum += index - } - fmt.Println("sum is equal to ", sum) - } - // 输出:sum is equal to 45 - -有些时候需要进行多个赋值操作,由于Go里面没有`,`操作,那么可以使用平行赋值`i, j = i+1, j-1` - - -有些时候如果我们忽略`expression1`和`expression3`: - - sum := 1 - for ; sum < 1000; { - sum += sum - } - -其中`;`也可以省略,那么就变成如下的代码了,是不是似曾相识?对,这就是`while`的功能。 - - sum := 1 - for sum < 1000 { - sum += sum - } - -在循环里面有两个关键操作`break`和`continue` ,`break`操作是跳出当前循环,`continue`是跳过本次循环。当嵌套过深的时候,`break`可以配合标签使用,即跳转至标签所指定的位置,详细参考如下例子: - - for index := 10; index>0; index-- { - if index == 5{ - break // 或者continue - } - fmt.Println(index) - } - // break打印出来10、9、8、7、6 - // continue打印出来10、9、8、7、6、4、3、2、1 - -`break`和`continue`还可以跟着标号,用来跳到多重循环中的外层循环 - -`for`配合`range`可以用于读取`slice`和`map`的数据: - - for k,v:=range map { - fmt.Println("map's key:",k) - fmt.Println("map's val:",v) - } - -由于 Go 支持 “多值返回”, 而对于“声明而未被调用”的变量, 编译器会报错, 在这种情况下, 可以使用`_`来丢弃不需要的返回值 -例如 - - for _, v := range map{ - fmt.Println("map's val:", v) - } - - -### switch -有些时候你需要写很多的`if-else`来实现一些逻辑处理,这个时候代码看上去就很丑很冗长,而且也不易于以后的维护,这个时候`switch`就能很好的解决这个问题。它的语法如下 - - switch sExpr { - case expr1: - some instructions - case expr2: - some other instructions - case expr3: - some other instructions - default: - other code - } - -`sExpr`和`expr1`、`expr2`、`expr3`的类型必须一致。Go的`switch`非常灵活,表达式不必是常量或整数,执行的过程从上至下,直到找到匹配项;而如果`switch`没有表达式,它会匹配`true`。 - - i := 10 - switch i { - case 1: - fmt.Println("i is equal to 1") - case 2, 3, 4: - fmt.Println("i is equal to 2, 3 or 4") - case 10: - fmt.Println("i is equal to 10") - default: - fmt.Println("All I know is that i is an integer") - } - -在第5行中,我们把很多值聚合在了一个`case`里面,同时,Go里面`switch`默认相当于每个`case`最后带有`break`,匹配成功后不会自动向下执行其他case,而是跳出整个`switch`, 但是可以使用`fallthrough`强制执行后面的case代码。 - - integer := 6 - switch integer { - case 4: - fmt.Println("The integer was <= 4") - fallthrough - case 5: - fmt.Println("The integer was <= 5") - fallthrough - case 6: - fmt.Println("The integer was <= 6") - fallthrough - case 7: - fmt.Println("The integer was <= 7") - fallthrough - case 8: - fmt.Println("The integer was <= 8") - fallthrough - default: - fmt.Println("default case") - } - -上面的程序将输出 - - The integer was <= 6 - The integer was <= 7 - The integer was <= 8 - default case - - -## 函数 -函数是Go里面的核心设计,它通过关键字`func`来声明,它的格式如下: - - func funcName(input1 type1, input2 type2) (output1 type1, output2 type2) { - //这里是处理逻辑代码 - //返回多个值 - return value1, value2 - } - -上面的代码我们看出 - -- 关键字`func`用来声明一个函数`funcName` -- 函数可以有一个或者多个参数,每个参数后面带有类型,通过`,`分隔 -- 函数可以返回多个值 -- 上面返回值声明了两个变量`output1`和`output2`,如果你不想声明也可以,直接就两个类型 -- 如果只有一个返回值且不声明返回值变量,那么你可以省略 包括返回值 的括号 -- 如果没有返回值,那么就直接省略最后的返回信息 -- 如果有返回值, 那么必须在函数的外层添加return语句 - -下面我们来看一个实际应用函数的例子(用来计算Max值) - - package main - import "fmt" - - // 返回a、b中最大值. - func max(a, b int) int { - if a > b { - return a - } - return b - } - - func main() { - x := 3 - y := 4 - z := 5 - - max_xy := max(x, y) //调用函数max(x, y) - max_xz := max(x, z) //调用函数max(x, z) - - fmt.Printf("max(%d, %d) = %d\n", x, y, max_xy) - fmt.Printf("max(%d, %d) = %d\n", x, z, max_xz) - fmt.Printf("max(%d, %d) = %d\n", y, z, max(y,z)) // 也可在这直接调用它 - } - -上面这个里面我们可以看到`max`函数有两个参数,它们的类型都是`int`,那么第一个变量的类型可以省略(即 a,b int,而非 a int, b int),默认为离它最近的类型,同理多于2个同类型的变量或者返回值。同时我们注意到它的返回值就是一个类型,这个就是省略写法。 - -### 多个返回值 -Go语言比C更先进的特性,其中一点就是函数能够返回多个值。 - -我们直接上代码看例子 - - package main - import "fmt" - - //返回 A+B 和 A*B - func SumAndProduct(A, B int) (int, int) { - return A+B, A*B - } - - func main() { - x := 3 - y := 4 - - xPLUSy, xTIMESy := SumAndProduct(x, y) - - fmt.Printf("%d + %d = %d\n", x, y, xPLUSy) - fmt.Printf("%d * %d = %d\n", x, y, xTIMESy) - } - -上面的例子我们可以看到直接返回了两个参数,当然我们也可以命名返回参数的变量,这个例子里面只是用了两个类型,我们也可以改成如下这样的定义,然后返回的时候不用带上变量名,因为直接在函数里面初始化了。但如果你的函数是导出的(首字母大写),官方建议:最好命名返回值,因为不命名返回值,虽然使得代码更加简洁了,但是会造成生成的文档可读性差。 - - func SumAndProduct(A, B int) (add int, Multiplied int) { - add = A+B - Multiplied = A*B - return - } - -### 变参 -Go函数支持变参。接受变参的函数是有着不定数量的参数的。为了做到这点,首先需要定义函数使其接受变参: - - func myfunc(arg ...int) {} -`arg ...int`告诉Go这个函数接受不定数量的参数。注意,这些参数的类型全部是`int`。在函数体中,变量`arg`是一个`int`的`slice`: - - for _, n := range arg { - fmt.Printf("And the number is: %d\n", n) - } - -### 传值与传指针 -当我们传一个参数值到被调用函数里面时,实际上是传了这个值的一份copy,当在被调用函数中修改参数值的时候,调用函数中相应实参不会发生任何变化,因为数值变化只作用在copy上。 - -为了验证我们上面的说法,我们来看一个例子 - - package main - import "fmt" - - //简单的一个函数,实现了参数+1的操作 - func add1(a int) int { - a = a+1 // 我们改变了a的值 - return a //返回一个新值 - } - - func main() { - x := 3 - - fmt.Println("x = ", x) // 应该输出 "x = 3" - - x1 := add1(x) //调用add1(x) - - fmt.Println("x+1 = ", x1) // 应该输出"x+1 = 4" - fmt.Println("x = ", x) // 应该输出"x = 3" - } - -看到了吗?虽然我们调用了`add1`函数,并且在`add1`中执行`a = a+1`操作,但是上面例子中`x`变量的值没有发生变化 - -理由很简单:因为当我们调用`add1`的时候,`add1`接收的参数其实是`x`的copy,而不是`x`本身。 - -那你也许会问了,如果真的需要传这个`x`本身,该怎么办呢? - -这就牵扯到了所谓的指针。我们知道,变量在内存中是存放于一定地址上的,修改变量实际是修改变量地址处的内存。只有`add1`函数知道`x`变量所在的地址,才能修改`x`变量的值。所以我们需要将`x`所在地址`&x`传入函数,并将函数的参数的类型由`int`改为`*int`,即改为指针类型,才能在函数中修改`x`变量的值。此时参数仍然是按copy传递的,只是copy的是一个指针。请看下面的例子 - - package main - import "fmt" - - //简单的一个函数,实现了参数+1的操作 - func add1(a *int) int { // 请注意, - *a = *a+1 // 修改了a的值 - return *a // 返回新值 - } - - func main() { - x := 3 - - fmt.Println("x = ", x) // 应该输出 "x = 3" - - x1 := add1(&x) // 调用 add1(&x) 传x的地址 - - fmt.Println("x+1 = ", x1) // 应该输出 "x+1 = 4" - fmt.Println("x = ", x) // 应该输出 "x = 4" - } - -这样,我们就达到了修改`x`的目的。那么到底传指针有什么好处呢? - -- 传指针使得多个函数能操作同一个对象。 -- 传指针比较轻量级 (8bytes),只是传内存地址,我们可以用指针传递体积大的结构体。如果用参数值传递的话, 在每次copy上面就会花费相对较多的系统开销(内存和时间)。所以当你要传递大的结构体的时候,用指针是一个明智的选择。 -- Go语言中`string`,`slice`,`map`这三种类型的实现机制类似指针,所以可以直接传递,而不用取地址后传递指针。(注:若函数需改变`slice`的长度,则仍需要取地址传递指针) - -### defer -Go里面有一个不错的设计,就是回调函数,有点类似面向对象语言里面的析构函数,当函数执行完之后再执行。特别是当你在进行一些打开资源的操作时,遇到错误需要提前返回,在返回前你需要关闭相应的资源,不然很容易造成资源泄露等问题。如下代码所示,我们一般写打开一个资源是这样操作的: - - func ReadWrite() bool { - file.Open("file") - // 做一些工作 - if failureX { - file.Close() - return false - } - - if failureY { - file.Close() - return false - } - - file.Close() - return true - } - -我们看到上面有很多重复的代码,Go的`defer`有效解决了这个问题。使用它后,不但代码量减少了很多,而且程序变得更优雅。在`defer`后指定的函数会在函数退出前调用。 - - func ReadWrite() bool { - file.Open("file") - defer file.Close() - if failureX { - return false - } - if failureY { - return false - } - return true - } - -如果有很多调用`defer`,那么`defer`是采用后进先出模式,所以如下代码会输出`4 3 2 1 0` - - for i := 0; i < 5; i++ { - defer fmt.Printf("%d ", i) - } - -### 函数作为值、类型 - -在Go中函数也是一种变量,我们可以通过`type`来定义它,它的类型就是所有拥有相同的参数,相同的返回值的一种类型 - - type typeName func(input1 inputType1 [, input2 inputType2 [, ...]) (result1 resultType1 [, ...]) - -函数作为类型到底有什么好处呢?那就是可以把这个类型的函数当做值来传递,请看下面的例子 - - package main - import "fmt" - - type testInt func(int) bool // 声明了一个函数类型 - - func isOdd(integer int) bool { - if integer%2 == 0 { - return false - } - return true - } - - func isEven(integer int) bool { - if integer%2 == 0 { - return true - } - return false - } - - // 声明的函数类型在这个地方当做了一个参数 - - func filter(slice []int, f testInt) []int { - var result []int - for _, value := range slice { - if f(value) { - result = append(result, value) - } - } - return result - } - - func main(){ - slice := []int {1, 2, 3, 4, 5, 7} - fmt.Println("slice = ", slice) - odd := filter(slice, isOdd) // 函数当做值来传递了 - fmt.Println("Odd elements of slice are: ", odd) - even := filter(slice, isEven) // 函数当做值来传递了 - fmt.Println("Even elements of slice are: ", even) - } - -函数当做值和类型在我们写一些通用接口的时候非常有用,通过上面例子我们看到`testInt`这个类型是一个函数类型,然后两个`filter`函数的参数和返回值与`testInt`类型是一样的,但是我们可以实现很多种的逻辑,这样使得我们的程序变得非常的灵活。 - -### Panic和Recover - -Go没有像Java那样的异常机制,它不能抛出异常,而是使用了`panic`和`recover`机制。一定要记住,你应当把它作为最后的手段来使用,也就是说,你的代码中应当没有,或者很少有`panic`的东西。这是个强大的工具,请明智地使用它。那么,我们应该如何使用它呢? - -Panic ->是一个内建函数,可以中断原有的控制流程,进入一个令人恐慌的流程中。当函数`F`调用`panic`,函数F的执行被中断,但是`F`中的延迟函数会正常执行,然后F返回到调用它的地方。在调用的地方,`F`的行为就像调用了`panic`。这一过程继续向上,直到发生`panic`的`goroutine`中所有调用的函数返回,此时程序退出。恐慌可以直接调用`panic`产生。也可以由运行时错误产生,例如访问越界的数组。 - -Recover ->是一个内建的函数,可以让进入令人恐慌的流程中的`goroutine`恢复过来。`recover`仅在延迟函数中有效。在正常的执行过程中,调用`recover`会返回`nil`,并且没有其它任何效果。如果当前的`goroutine`陷入恐慌,调用`recover`可以捕获到`panic`的输入值,并且恢复正常的执行。 - -下面这个函数演示了如何在过程中使用`panic` - - var user = os.Getenv("USER") - - func init() { - if user == "" { - panic("no value for $USER") - } - } - -下面这个函数检查作为其参数的函数在执行时是否会产生`panic`: - - func throwsPanic(f func()) (b bool) { - defer func() { - if x := recover(); x != nil { - b = true - } - }() - f() //执行函数f,如果f中出现了panic,那么就可以恢复回来 - return - } - -### `main`函数和`init`函数 - -Go里面有两个保留的函数:`init`函数(能够应用于所有的`package`)和`main`函数(只能应用于`package main`)。这两个函数在定义时不能有任何的参数和返回值。虽然一个`package`里面可以写任意多个`init`函数,但这无论是对于可读性还是以后的可维护性来说,我们都强烈建议用户在一个`package`中只写一个`init`函数。 - -Go程序会自动调用`init()`和`main()`,所以你不需要在任何地方调用这两个函数。每个`package`中的`init`函数都是可选的,但`package main`就必须包含一个`main`函数。 - -程序的初始化和执行都起始于`main`包。如果`main`包还导入了其它的包,那么就会在编译时将它们依次导入。有时一个包会被多个包同时导入,那么它只会被导入一次(例如很多包可能都会用到`fmt`包,但它只会被导入一次,因为没有必要导入多次)。当一个包被导入时,如果该包还导入了其它的包,那么会先将其它包导入进来,然后再对这些包中的包级常量和变量进行初始化,接着执行`init`函数(如果有的话),依次类推。等所有被导入的包都加载完毕了,就会开始对`main`包中的包级常量和变量进行初始化,然后执行`main`包中的`init`函数(如果存在的话),最后执行`main`函数。下图详细地解释了整个执行过程: - -![](images/2.3.init.png?raw=true) - - -## links - * [目录]() - * 上一章: [Go基础](<2.2.md>) - * 下一节: [struct类型](<2.4.md>) - -## LastModified - * $Id$ +# 2.3 流程和函数 +这小节我们要介绍Go里面的流程控制以及函数操作 +## 流程控制 +流程控制在编程语言中是最伟大的发明了,因为有了它,你可以通过很简单的流程描述来表达很复杂的逻辑。流程控制包含分三大类:条件判断,循环控制和无条件跳转。 +### if +`if`也许是各种编程语言中最常见的了,它的语法概括起来就是:如果满足条件就做某事,否则做另一件事。 + +Go里面`if`条件判断语句中不需要括号,如下代码所示 + + if x > 10 { + fmt.Println("x is greater than 10") + } else { + fmt.Println("x is less than 10") + } + +Go的`if`还有一个强大的地方就是条件判断语句里面允许声明一个变量,这个变量的作用域只能在该条件逻辑块内,其他地方就不起作用了,如下所示 + + // 计算获取值x,然后根据x返回的大小,判断是否大于10。 + if x := computedValue(); x > 10 { + fmt.Println("x is greater than 10") + } else { + fmt.Println("x is less than 10") + } + + //这个地方如果这样调用就编译出错了,因为x是条件里面的变量 + fmt.Println(x) + +多个条件的时候如下所示: + + if integer == 3 { + fmt.Println("The integer is equal to 3") + } else if integer < 3 { + fmt.Println("The integer is less than 3") + } else { + fmt.Println("The integer is greater than 3") + } + +### goto + +Go有`goto`语句——请明智地使用它。用`goto`跳转到必须在当前函数内定义的标签。例如假设这样一个循环: + + func myFunc() { + i := 0 + Here: //这行的第一个词,以冒号结束作为标签 + println(i) + i++ + goto Here //跳转到Here去 + } + +>标签名是大小写敏感的。 + +### for +Go里面最强大的一个控制逻辑就是`for`,它即可以用来循环读取数据,又可以当作`while`来控制逻辑,还能迭代操作。它的语法如下: + + for expression1; expression2; expression3 { + //... + } + +`expression1`、`expression2`和`expression3`都是表达式,其中`expression1`和`expression3`是变量声明或者函数调用返回值之类的,`expression2`是用来条件判断,`expression1`在循环开始之前调用,`expression3`在每轮循环结束之时调用。 + +一个例子比上面讲那么多更有用,那么我们看看下面的例子吧: + + package main + import "fmt" + + func main(){ + sum := 0; + for index:=0; index < 10 ; index++ { + sum += index + } + fmt.Println("sum is equal to ", sum) + } + // 输出:sum is equal to 45 + +有些时候需要进行多个赋值操作,由于Go里面没有`,`操作,那么可以使用平行赋值`i, j = i+1, j-1` + + +有些时候如果我们忽略`expression1`和`expression3`: + + sum := 1 + for ; sum < 1000; { + sum += sum + } + +其中`;`也可以省略,那么就变成如下的代码了,是不是似曾相识?对,这就是`while`的功能。 + + sum := 1 + for sum < 1000 { + sum += sum + } + +在循环里面有两个关键操作`break`和`continue` ,`break`操作是跳出当前循环,`continue`是跳过本次循环。当嵌套过深的时候,`break`可以配合标签使用,即跳转至标签所指定的位置,详细参考如下例子: + + for index := 10; index>0; index-- { + if index == 5{ + break // 或者continue + } + fmt.Println(index) + } + // break打印出来10、9、8、7、6 + // continue打印出来10、9、8、7、6、4、3、2、1 + +`break`和`continue`还可以跟着标号,用来跳到多重循环中的外层循环 + +`for`配合`range`可以用于读取`slice`和`map`的数据: + + for k,v:=range map { + fmt.Println("map's key:",k) + fmt.Println("map's val:",v) + } + +由于 Go 支持 “多值返回”, 而对于“声明而未被调用”的变量, 编译器会报错, 在这种情况下, 可以使用`_`来丢弃不需要的返回值 +例如 + + for _, v := range map{ + fmt.Println("map's val:", v) + } + + +### switch +有些时候你需要写很多的`if-else`来实现一些逻辑处理,这个时候代码看上去就很丑很冗长,而且也不易于以后的维护,这个时候`switch`就能很好的解决这个问题。它的语法如下 + + switch sExpr { + case expr1: + some instructions + case expr2: + some other instructions + case expr3: + some other instructions + default: + other code + } + +`sExpr`和`expr1`、`expr2`、`expr3`的类型必须一致。Go的`switch`非常灵活,表达式不必是常量或整数,执行的过程从上至下,直到找到匹配项;而如果`switch`没有表达式,它会匹配`true`。 + + i := 10 + switch i { + case 1: + fmt.Println("i is equal to 1") + case 2, 3, 4: + fmt.Println("i is equal to 2, 3 or 4") + case 10: + fmt.Println("i is equal to 10") + default: + fmt.Println("All I know is that i is an integer") + } + +在第5行中,我们把很多值聚合在了一个`case`里面,同时,Go里面`switch`默认相当于每个`case`最后带有`break`,匹配成功后不会自动向下执行其他case,而是跳出整个`switch`, 但是可以使用`fallthrough`强制执行后面的case代码。 + + integer := 6 + switch integer { + case 4: + fmt.Println("The integer was <= 4") + fallthrough + case 5: + fmt.Println("The integer was <= 5") + fallthrough + case 6: + fmt.Println("The integer was <= 6") + fallthrough + case 7: + fmt.Println("The integer was <= 7") + fallthrough + case 8: + fmt.Println("The integer was <= 8") + fallthrough + default: + fmt.Println("default case") + } + +上面的程序将输出 + + The integer was <= 6 + The integer was <= 7 + The integer was <= 8 + default case + + +## 函数 +函数是Go里面的核心设计,它通过关键字`func`来声明,它的格式如下: + + func funcName(input1 type1, input2 type2) (output1 type1, output2 type2) { + //这里是处理逻辑代码 + //返回多个值 + return value1, value2 + } + +上面的代码我们看出 + +- 关键字`func`用来声明一个函数`funcName` +- 函数可以有一个或者多个参数,每个参数后面带有类型,通过`,`分隔 +- 函数可以返回多个值 +- 上面返回值声明了两个变量`output1`和`output2`,如果你不想声明也可以,直接就两个类型 +- 如果只有一个返回值且不声明返回值变量,那么你可以省略 包括返回值 的括号 +- 如果没有返回值,那么就直接省略最后的返回信息 +- 如果有返回值, 那么必须在函数的外层添加return语句 + +下面我们来看一个实际应用函数的例子(用来计算Max值) + + package main + import "fmt" + + // 返回a、b中最大值. + func max(a, b int) int { + if a > b { + return a + } + return b + } + + func main() { + x := 3 + y := 4 + z := 5 + + max_xy := max(x, y) //调用函数max(x, y) + max_xz := max(x, z) //调用函数max(x, z) + + fmt.Printf("max(%d, %d) = %d\n", x, y, max_xy) + fmt.Printf("max(%d, %d) = %d\n", x, z, max_xz) + fmt.Printf("max(%d, %d) = %d\n", y, z, max(y,z)) // 也可在这直接调用它 + } + +上面这个里面我们可以看到`max`函数有两个参数,它们的类型都是`int`,那么第一个变量的类型可以省略(即 a,b int,而非 a int, b int),默认为离它最近的类型,同理多于2个同类型的变量或者返回值。同时我们注意到它的返回值就是一个类型,这个就是省略写法。 + +### 多个返回值 +Go语言比C更先进的特性,其中一点就是函数能够返回多个值。 + +我们直接上代码看例子 + + package main + import "fmt" + + //返回 A+B 和 A*B + func SumAndProduct(A, B int) (int, int) { + return A+B, A*B + } + + func main() { + x := 3 + y := 4 + + xPLUSy, xTIMESy := SumAndProduct(x, y) + + fmt.Printf("%d + %d = %d\n", x, y, xPLUSy) + fmt.Printf("%d * %d = %d\n", x, y, xTIMESy) + } + +上面的例子我们可以看到直接返回了两个参数,当然我们也可以命名返回参数的变量,这个例子里面只是用了两个类型,我们也可以改成如下这样的定义,然后返回的时候不用带上变量名,因为直接在函数里面初始化了。但如果你的函数是导出的(首字母大写),官方建议:最好命名返回值,因为不命名返回值,虽然使得代码更加简洁了,但是会造成生成的文档可读性差。 + + func SumAndProduct(A, B int) (add int, Multiplied int) { + add = A+B + Multiplied = A*B + return + } + +### 变参 +Go函数支持变参。接受变参的函数是有着不定数量的参数的。为了做到这点,首先需要定义函数使其接受变参: + + func myfunc(arg ...int) {} +`arg ...int`告诉Go这个函数接受不定数量的参数。注意,这些参数的类型全部是`int`。在函数体中,变量`arg`是一个`int`的`slice`: + + for _, n := range arg { + fmt.Printf("And the number is: %d\n", n) + } + +### 传值与传指针 +当我们传一个参数值到被调用函数里面时,实际上是传了这个值的一份copy,当在被调用函数中修改参数值的时候,调用函数中相应实参不会发生任何变化,因为数值变化只作用在copy上。 + +为了验证我们上面的说法,我们来看一个例子 + + package main + import "fmt" + + //简单的一个函数,实现了参数+1的操作 + func add1(a int) int { + a = a+1 // 我们改变了a的值 + return a //返回一个新值 + } + + func main() { + x := 3 + + fmt.Println("x = ", x) // 应该输出 "x = 3" + + x1 := add1(x) //调用add1(x) + + fmt.Println("x+1 = ", x1) // 应该输出"x+1 = 4" + fmt.Println("x = ", x) // 应该输出"x = 3" + } + +看到了吗?虽然我们调用了`add1`函数,并且在`add1`中执行`a = a+1`操作,但是上面例子中`x`变量的值没有发生变化 + +理由很简单:因为当我们调用`add1`的时候,`add1`接收的参数其实是`x`的copy,而不是`x`本身。 + +那你也许会问了,如果真的需要传这个`x`本身,该怎么办呢? + +这就牵扯到了所谓的指针。我们知道,变量在内存中是存放于一定地址上的,修改变量实际是修改变量地址处的内存。只有`add1`函数知道`x`变量所在的地址,才能修改`x`变量的值。所以我们需要将`x`所在地址`&x`传入函数,并将函数的参数的类型由`int`改为`*int`,即改为指针类型,才能在函数中修改`x`变量的值。此时参数仍然是按copy传递的,只是copy的是一个指针。请看下面的例子 + + package main + import "fmt" + + //简单的一个函数,实现了参数+1的操作 + func add1(a *int) int { // 请注意, + *a = *a+1 // 修改了a的值 + return *a // 返回新值 + } + + func main() { + x := 3 + + fmt.Println("x = ", x) // 应该输出 "x = 3" + + x1 := add1(&x) // 调用 add1(&x) 传x的地址 + + fmt.Println("x+1 = ", x1) // 应该输出 "x+1 = 4" + fmt.Println("x = ", x) // 应该输出 "x = 4" + } + +这样,我们就达到了修改`x`的目的。那么到底传指针有什么好处呢? + +- 传指针使得多个函数能操作同一个对象。 +- 传指针比较轻量级 (8bytes),只是传内存地址,我们可以用指针传递体积大的结构体。如果用参数值传递的话, 在每次copy上面就会花费相对较多的系统开销(内存和时间)。所以当你要传递大的结构体的时候,用指针是一个明智的选择。 +- Go语言中`string`,`slice`,`map`这三种类型的实现机制类似指针,所以可以直接传递,而不用取地址后传递指针。(注:若函数需改变`slice`的长度,则仍需要取地址传递指针) + +### defer +Go里面有一个不错的设计,就是回调函数,有点类似面向对象语言里面的析构函数,当函数执行完之后再执行。特别是当你在进行一些打开资源的操作时,遇到错误需要提前返回,在返回前你需要关闭相应的资源,不然很容易造成资源泄露等问题。如下代码所示,我们一般写打开一个资源是这样操作的: + + func ReadWrite() bool { + file.Open("file") + // 做一些工作 + if failureX { + file.Close() + return false + } + + if failureY { + file.Close() + return false + } + + file.Close() + return true + } + +我们看到上面有很多重复的代码,Go的`defer`有效解决了这个问题。使用它后,不但代码量减少了很多,而且程序变得更优雅。在`defer`后指定的函数会在函数退出前调用。 + + func ReadWrite() bool { + file.Open("file") + defer file.Close() + if failureX { + return false + } + if failureY { + return false + } + return true + } + +如果有很多调用`defer`,那么`defer`是采用后进先出模式,所以如下代码会输出`4 3 2 1 0` + + for i := 0; i < 5; i++ { + defer fmt.Printf("%d ", i) + } + +### 函数作为值、类型 + +在Go中函数也是一种变量,我们可以通过`type`来定义它,它的类型就是所有拥有相同的参数,相同的返回值的一种类型 + + type typeName func(input1 inputType1 [, input2 inputType2 [, ...]) (result1 resultType1 [, ...]) + +函数作为类型到底有什么好处呢?那就是可以把这个类型的函数当做值来传递,请看下面的例子 + + package main + import "fmt" + + type testInt func(int) bool // 声明了一个函数类型 + + func isOdd(integer int) bool { + if integer%2 == 0 { + return false + } + return true + } + + func isEven(integer int) bool { + if integer%2 == 0 { + return true + } + return false + } + + // 声明的函数类型在这个地方当做了一个参数 + + func filter(slice []int, f testInt) []int { + var result []int + for _, value := range slice { + if f(value) { + result = append(result, value) + } + } + return result + } + + func main(){ + slice := []int {1, 2, 3, 4, 5, 7} + fmt.Println("slice = ", slice) + odd := filter(slice, isOdd) // 函数当做值来传递了 + fmt.Println("Odd elements of slice are: ", odd) + even := filter(slice, isEven) // 函数当做值来传递了 + fmt.Println("Even elements of slice are: ", even) + } + +函数当做值和类型在我们写一些通用接口的时候非常有用,通过上面例子我们看到`testInt`这个类型是一个函数类型,然后两个`filter`函数的参数和返回值与`testInt`类型是一样的,但是我们可以实现很多种的逻辑,这样使得我们的程序变得非常的灵活。 + +### Panic和Recover + +Go没有像Java那样的异常机制,它不能抛出异常,而是使用了`panic`和`recover`机制。一定要记住,你应当把它作为最后的手段来使用,也就是说,你的代码中应当没有,或者很少有`panic`的东西。这是个强大的工具,请明智地使用它。那么,我们应该如何使用它呢? + +Panic +>是一个内建函数,可以中断原有的控制流程,进入一个令人恐慌的流程中。当函数`F`调用`panic`,函数F的执行被中断,但是`F`中的延迟函数会正常执行,然后F返回到调用它的地方。在调用的地方,`F`的行为就像调用了`panic`。这一过程继续向上,直到发生`panic`的`goroutine`中所有调用的函数返回,此时程序退出。恐慌可以直接调用`panic`产生。也可以由运行时错误产生,例如访问越界的数组。 + +Recover +>是一个内建的函数,可以让进入令人恐慌的流程中的`goroutine`恢复过来。`recover`仅在延迟函数中有效。在正常的执行过程中,调用`recover`会返回`nil`,并且没有其它任何效果。如果当前的`goroutine`陷入恐慌,调用`recover`可以捕获到`panic`的输入值,并且恢复正常的执行。 + +下面这个函数演示了如何在过程中使用`panic` + + var user = os.Getenv("USER") + + func init() { + if user == "" { + panic("no value for $USER") + } + } + +下面这个函数检查作为其参数的函数在执行时是否会产生`panic`: + + func throwsPanic(f func()) (b bool) { + defer func() { + if x := recover(); x != nil { + b = true + } + }() + f() //执行函数f,如果f中出现了panic,那么就可以恢复回来 + return + } + +### `main`函数和`init`函数 + +Go里面有两个保留的函数:`init`函数(能够应用于所有的`package`)和`main`函数(只能应用于`package main`)。这两个函数在定义时不能有任何的参数和返回值。虽然一个`package`里面可以写任意多个`init`函数,但这无论是对于可读性还是以后的可维护性来说,我们都强烈建议用户在一个`package`中只写一个`init`函数。 + +Go程序会自动调用`init()`和`main()`,所以你不需要在任何地方调用这两个函数。每个`package`中的`init`函数都是可选的,但`package main`就必须包含一个`main`函数。 + +程序的初始化和执行都起始于`main`包。如果`main`包还导入了其它的包,那么就会在编译时将它们依次导入。有时一个包会被多个包同时导入,那么它只会被导入一次(例如很多包可能都会用到`fmt`包,但它只会被导入一次,因为没有必要导入多次)。当一个包被导入时,如果该包还导入了其它的包,那么会先将其它包导入进来,然后再对这些包中的包级常量和变量进行初始化,接着执行`init`函数(如果有的话),依次类推。等所有被导入的包都加载完毕了,就会开始对`main`包中的包级常量和变量进行初始化,然后执行`main`包中的`init`函数(如果存在的话),最后执行`main`函数。下图详细地解释了整个执行过程: + +![](images/2.3.init.png?raw=true) + + +## links + * [目录]() + * 上一章: [Go基础](<2.2.md>) + * 下一节: [struct类型](<2.4.md>) diff --git a/2.4.md b/2.4.md index be985cb0dd80ad6a971780fca4034e527dba494c..dce9ab1b5c1cc117f5e68c8e1903f8287c9d93a8 100644 --- a/2.4.md +++ b/2.4.md @@ -1,210 +1,207 @@ -# 2.4 struct类型 -## struct -Go语言中,也和C或者其他语言一样,我们可以声明新的类型,作为其它类型的属性或字段的容器。例如,我们可以创建一个自定义类型`person`代表一个人的实体。这个实体拥有属性:姓名和年龄。这样的类型我们称之`struct`。如下代码所示: - - type person struct { - name string - age int - } -看到了吗?声明一个struct如此简单,上面的类型包含有两个字段 -- 一个string类型的字段name,用来保存用户名称这个属性 -- 一个int类型的字段age,用来保存用户年龄这个属性 - -如何使用struct呢?请看下面的代码 - - type person struct { - name string - age int - } - - var P person // P现在就是person类型的变量了 - - P.name = "Astaxie" // 赋值"Astaxie"给P的name属性. - P.age = 25 // 赋值"25"给变量P的age属性 - fmt.Printf("The person's name is %s", P.name) // 访问P的name属性. -除了上面这种P的声明使用之外,还有两种声明使用方式 - -- 1.按照顺序提供初始化值 - - P := person{"Tom", 25} - -- 2.通过`field:value`的方式初始化,这样可以任意顺序 - - P := person{age:24, name:"Tom"} - -下面我们看一个完整的使用struct的例子 - - package main - import "fmt" - - // 声明一个新的类型 - type person struct { - name string - age int - } - - // 比较两个人的年龄,返回年龄大的那个人,并且返回年龄差 - // struct也是传值的 - func Older(p1, p2 person) (person, int) { - if p1.age>p2.age { // 比较p1和p2这两个人的年龄 - return p1, p1.age-p2.age - } - return p2, p2.age-p1.age - } - - func main() { - var tom person - - // 赋值初始化 - tom.name, tom.age = "Tom", 18 - - // 两个字段都写清楚的初始化 - bob := person{age:25, name:"Bob"} - - // 按照struct定义顺序初始化值 - paul := person{"Paul", 43} - - tb_Older, tb_diff := Older(tom, bob) - tp_Older, tp_diff := Older(tom, paul) - bp_Older, bp_diff := Older(bob, paul) - - fmt.Printf("Of %s and %s, %s is older by %d years\n", - tom.name, bob.name, tb_Older.name, tb_diff) - - fmt.Printf("Of %s and %s, %s is older by %d years\n", - tom.name, paul.name, tp_Older.name, tp_diff) - - fmt.Printf("Of %s and %s, %s is older by %d years\n", - bob.name, paul.name, bp_Older.name, bp_diff) - } - -### struct的匿名字段 -我们上面介绍了如何定义一个struct,定义的时候是字段名与其类型一一对应,实际上Go支持只提供类型,而不写字段名的方式,也就是匿名字段,也称为嵌入字段。 - -当匿名字段是一个struct的时候,那么这个struct所拥有的全部字段都被隐式地引入了当前定义的这个struct。 - -让我们来看一个例子,让上面说的这些更具体化 - - package main - import "fmt" - - type Human struct { - name string - age int - weight int - } - - type Student struct { - Human // 匿名字段,那么默认Student就包含了Human的所有字段 - speciality string - } - - func main() { - // 我们初始化一个学生 - mark := Student{Human{"Mark", 25, 120}, "Computer Science"} - - // 我们访问相应的字段 - fmt.Println("His name is ", mark.name) - fmt.Println("His age is ", mark.age) - fmt.Println("His weight is ", mark.weight) - fmt.Println("His speciality is ", mark.speciality) - // 修改对应的备注信息 - mark.speciality = "AI" - fmt.Println("Mark changed his speciality") - fmt.Println("His speciality is ", mark.speciality) - // 修改他的年龄信息 - fmt.Println("Mark become old") - mark.age = 46 - fmt.Println("His age is", mark.age) - // 修改他的体重信息 - fmt.Println("Mark is not an athlet anymore") - mark.weight += 60 - fmt.Println("His weight is", mark.weight) - } - -图例如下: - -![](images/2.4.student_struct.png?raw=true) - -我们看到Student访问属性age和name的时候,就像访问自己所有用的字段一样,对,匿名字段就是这样,能够实现字段的继承。是不是很酷啊?还有比这个更酷的呢,那就是student还能访问Human这个字段作为字段名。请看下面的代码,是不是更酷了。 - - mark.Human = Human{"Marcus", 55, 220} - mark.Human.age -= 1 - -通过匿名访问和修改字段相当的有用,但是不仅仅是struct字段哦,所有的内置类型和自定义类型都是可以作为匿名字段的。请看下面的例子 - - package main - import "fmt" - - type Skills []string - - type Human struct { - name string - age int - weight int - } - - type Student struct { - Human // 匿名字段,struct - Skills // 匿名字段,自定义的类型string slice - int // 内置类型作为匿名字段 - speciality string - } - - func main() { - // 初始化学生Jane - jane := Student{Human:Human{"Jane", 35, 100}, speciality:"Biology"} - // 现在我们来访问相应的字段 - fmt.Println("Her name is ", jane.name) - fmt.Println("Her age is ", jane.age) - fmt.Println("Her weight is ", jane.weight) - fmt.Println("Her speciality is ", jane.speciality) - // 我们来修改他的skill技能字段 - jane.Skills = []string{"anatomy"} - fmt.Println("Her skills are ", jane.Skills) - fmt.Println("She acquired two new ones ") - jane.Skills = append(jane.Skills, "physics", "golang") - fmt.Println("Her skills now are ", jane.Skills) - // 修改匿名内置类型字段 - jane.int = 3 - fmt.Println("Her preferred number is", jane.int) - } - -从上面例子我们看出来struct不仅仅能够将struct作为匿名字段、自定义类型、内置类型都可以作为匿名字段,而且可以在相应的字段上面进行函数操作(如例子中的append)。 - -这里有一个问题:如果human里面有一个字段叫做phone,而student也有一个字段叫做phone,那么该怎么办呢? - -Go里面很简单的解决了这个问题,最外层的优先访问,也就是当你通过`student.phone`访问的时候,是访问student里面的字段,而不是human里面的字段。 - -这样就允许我们去重载通过匿名字段继承的一些字段,当然如果我们想访问重载后对应匿名类型里面的字段,可以通过匿名字段名来访问。请看下面的例子 - - package main - import "fmt" - - type Human struct { - name string - age int - phone string // Human类型拥有的字段 - } - - type Employee struct { - Human // 匿名字段Human - speciality string - phone string // 雇员的phone字段 - } - - func main() { - Bob := Employee{Human{"Bob", 34, "777-444-XXXX"}, "Designer", "333-222"} - fmt.Println("Bob's work phone is:", Bob.phone) - // 如果我们要访问Human的phone字段 - fmt.Println("Bob's personal phone is:", Bob.Human.phone) - } - - -## links - * [目录]() - * 上一章: [流程和函数](<2.3.md>) - * 下一节: [面向对象](<2.5.md>) - -## LastModified - * $Id$ +# 2.4 struct类型 +## struct +Go语言中,也和C或者其他语言一样,我们可以声明新的类型,作为其它类型的属性或字段的容器。例如,我们可以创建一个自定义类型`person`代表一个人的实体。这个实体拥有属性:姓名和年龄。这样的类型我们称之`struct`。如下代码所示: + + type person struct { + name string + age int + } +看到了吗?声明一个struct如此简单,上面的类型包含有两个字段 +- 一个string类型的字段name,用来保存用户名称这个属性 +- 一个int类型的字段age,用来保存用户年龄这个属性 + +如何使用struct呢?请看下面的代码 + + type person struct { + name string + age int + } + + var P person // P现在就是person类型的变量了 + + P.name = "Astaxie" // 赋值"Astaxie"给P的name属性. + P.age = 25 // 赋值"25"给变量P的age属性 + fmt.Printf("The person's name is %s", P.name) // 访问P的name属性. +除了上面这种P的声明使用之外,还有两种声明使用方式 + +- 1.按照顺序提供初始化值 + + P := person{"Tom", 25} + +- 2.通过`field:value`的方式初始化,这样可以任意顺序 + + P := person{age:24, name:"Tom"} + +下面我们看一个完整的使用struct的例子 + + package main + import "fmt" + + // 声明一个新的类型 + type person struct { + name string + age int + } + + // 比较两个人的年龄,返回年龄大的那个人,并且返回年龄差 + // struct也是传值的 + func Older(p1, p2 person) (person, int) { + if p1.age>p2.age { // 比较p1和p2这两个人的年龄 + return p1, p1.age-p2.age + } + return p2, p2.age-p1.age + } + + func main() { + var tom person + + // 赋值初始化 + tom.name, tom.age = "Tom", 18 + + // 两个字段都写清楚的初始化 + bob := person{age:25, name:"Bob"} + + // 按照struct定义顺序初始化值 + paul := person{"Paul", 43} + + tb_Older, tb_diff := Older(tom, bob) + tp_Older, tp_diff := Older(tom, paul) + bp_Older, bp_diff := Older(bob, paul) + + fmt.Printf("Of %s and %s, %s is older by %d years\n", + tom.name, bob.name, tb_Older.name, tb_diff) + + fmt.Printf("Of %s and %s, %s is older by %d years\n", + tom.name, paul.name, tp_Older.name, tp_diff) + + fmt.Printf("Of %s and %s, %s is older by %d years\n", + bob.name, paul.name, bp_Older.name, bp_diff) + } + +### struct的匿名字段 +我们上面介绍了如何定义一个struct,定义的时候是字段名与其类型一一对应,实际上Go支持只提供类型,而不写字段名的方式,也就是匿名字段,也称为嵌入字段。 + +当匿名字段是一个struct的时候,那么这个struct所拥有的全部字段都被隐式地引入了当前定义的这个struct。 + +让我们来看一个例子,让上面说的这些更具体化 + + package main + import "fmt" + + type Human struct { + name string + age int + weight int + } + + type Student struct { + Human // 匿名字段,那么默认Student就包含了Human的所有字段 + speciality string + } + + func main() { + // 我们初始化一个学生 + mark := Student{Human{"Mark", 25, 120}, "Computer Science"} + + // 我们访问相应的字段 + fmt.Println("His name is ", mark.name) + fmt.Println("His age is ", mark.age) + fmt.Println("His weight is ", mark.weight) + fmt.Println("His speciality is ", mark.speciality) + // 修改对应的备注信息 + mark.speciality = "AI" + fmt.Println("Mark changed his speciality") + fmt.Println("His speciality is ", mark.speciality) + // 修改他的年龄信息 + fmt.Println("Mark become old") + mark.age = 46 + fmt.Println("His age is", mark.age) + // 修改他的体重信息 + fmt.Println("Mark is not an athlet anymore") + mark.weight += 60 + fmt.Println("His weight is", mark.weight) + } + +图例如下: + +![](images/2.4.student_struct.png?raw=true) + +我们看到Student访问属性age和name的时候,就像访问自己所有用的字段一样,对,匿名字段就是这样,能够实现字段的继承。是不是很酷啊?还有比这个更酷的呢,那就是student还能访问Human这个字段作为字段名。请看下面的代码,是不是更酷了。 + + mark.Human = Human{"Marcus", 55, 220} + mark.Human.age -= 1 + +通过匿名访问和修改字段相当的有用,但是不仅仅是struct字段哦,所有的内置类型和自定义类型都是可以作为匿名字段的。请看下面的例子 + + package main + import "fmt" + + type Skills []string + + type Human struct { + name string + age int + weight int + } + + type Student struct { + Human // 匿名字段,struct + Skills // 匿名字段,自定义的类型string slice + int // 内置类型作为匿名字段 + speciality string + } + + func main() { + // 初始化学生Jane + jane := Student{Human:Human{"Jane", 35, 100}, speciality:"Biology"} + // 现在我们来访问相应的字段 + fmt.Println("Her name is ", jane.name) + fmt.Println("Her age is ", jane.age) + fmt.Println("Her weight is ", jane.weight) + fmt.Println("Her speciality is ", jane.speciality) + // 我们来修改他的skill技能字段 + jane.Skills = []string{"anatomy"} + fmt.Println("Her skills are ", jane.Skills) + fmt.Println("She acquired two new ones ") + jane.Skills = append(jane.Skills, "physics", "golang") + fmt.Println("Her skills now are ", jane.Skills) + // 修改匿名内置类型字段 + jane.int = 3 + fmt.Println("Her preferred number is", jane.int) + } + +从上面例子我们看出来struct不仅仅能够将struct作为匿名字段、自定义类型、内置类型都可以作为匿名字段,而且可以在相应的字段上面进行函数操作(如例子中的append)。 + +这里有一个问题:如果human里面有一个字段叫做phone,而student也有一个字段叫做phone,那么该怎么办呢? + +Go里面很简单的解决了这个问题,最外层的优先访问,也就是当你通过`student.phone`访问的时候,是访问student里面的字段,而不是human里面的字段。 + +这样就允许我们去重载通过匿名字段继承的一些字段,当然如果我们想访问重载后对应匿名类型里面的字段,可以通过匿名字段名来访问。请看下面的例子 + + package main + import "fmt" + + type Human struct { + name string + age int + phone string // Human类型拥有的字段 + } + + type Employee struct { + Human // 匿名字段Human + speciality string + phone string // 雇员的phone字段 + } + + func main() { + Bob := Employee{Human{"Bob", 34, "777-444-XXXX"}, "Designer", "333-222"} + fmt.Println("Bob's work phone is:", Bob.phone) + // 如果我们要访问Human的phone字段 + fmt.Println("Bob's personal phone is:", Bob.Human.phone) + } + + +## links + * [目录]() + * 上一章: [流程和函数](<2.3.md>) + * 下一节: [面向对象](<2.5.md>) diff --git a/2.5.md b/2.5.md index c81cc4e9000fd236b225093ce696cd6066077dd2..00b1eafca62c3c627bdc04eabc028f5b1d509d9e 100644 --- a/2.5.md +++ b/2.5.md @@ -34,7 +34,7 @@ 基于上面的原因所以就有了`method`的概念,`method`是附属在一个给定的类型上的,他的语法和函数的声明语法几乎一样,只是在`func`后面增加了一个receiver(也就是method所依从的主体)。 -用上面提到的形状的例子来说,method `area()` 是依赖于某个形状(比如说Rectangle)来发生作用的。Rectangle.area()的发出者是Rectangle, area()是属于Rectangle的方法,而非一个外围函数。 +用上面提到的形状的例子来说,method `area()` 是依赖于某个形状(比如说Rectangle)来发生作用的。Rectangle.area()的发出者是Rectangle, area()是属于Rectangle的方法,而非一个外围函数。 更具体地说,Rectangle存在字段length 和 width, 同时存在方法area(), 这些字段和方法都属于Rectangle。 @@ -311,7 +311,7 @@ method的语法如下: mark.SayHi() sam.SayHi() } - + 上面的代码设计的是如此的美妙,让人不自觉的为Go的设计惊叹! 通过这些内容,我们可以设计出基本的面向对象的程序了,但是Go里面的面向对象是如此的简单,没有任何的私有、共有关键字,通过大小写来实现(大写开头的为共有,小写开头的为私有),方法也同样适用这个原则。 @@ -319,6 +319,3 @@ method的语法如下: * [目录]() * 上一章: [struct类型](<2.4.md>) * 下一节: [interface](<2.6.md>) - -## LastModified - * $Id$ diff --git a/2.6.md b/2.6.md index 8c3c746569c532678c02c5da38488d74b77c1fe8..b284c0b67ce3383872248269a5681f95c5e9bd53 100644 --- a/2.6.md +++ b/2.6.md @@ -16,73 +16,73 @@ Go语言里面设计最精妙的应该算interface,它让面向对象,内容 interface类型定义了一组方法,如果某个对象实现了某个接口的所有方法,则此对象就实现了此接口。详细的语法参考下面这个例子 type Human struct { - name string - age int - phone string + name string + age int + phone string } - + type Student struct { - Human //匿名字段Human - school string - loan float32 + Human //匿名字段Human + school string + loan float32 } - + type Employee struct { - Human //匿名字段Human - company string - money float32 + Human //匿名字段Human + company string + money float32 } - + //Human对象实现Sayhi方法 func (h *Human) SayHi() { - fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone) + fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone) } - + // Human对象实现Sing方法 func (h *Human) Sing(lyrics string) { - fmt.Println("La la, la la la, la la la la la...", lyrics) + fmt.Println("La la, la la la, la la la la la...", lyrics) } - + //Human对象实现Guzzle方法 func (h *Human) Guzzle(beerStein string) { - fmt.Println("Guzzle Guzzle Guzzle...", beerStein) + fmt.Println("Guzzle Guzzle Guzzle...", beerStein) } - + // Employee重载Human的Sayhi方法 func (e *Employee) SayHi() { - fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name, - e.company, e.phone) //Yes you can split into 2 lines here. + fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name, + e.company, e.phone) //Yes you can split into 2 lines here. } - + //Student实现BorrowMoney方法 func (s *Student) BorrowMoney(amount float32) { - s.loan += amount // (again and again and...) + s.loan += amount // (again and again and...) } - + //Employee实现SpendSalary方法 func (e *Employee) SpendSalary(amount float32) { - e.money -= amount // More vodka please!!! Get me through the day! + e.money -= amount // More vodka please!!! Get me through the day! } - + // 定义interface type Men interface { - SayHi() - Sing(lyrics string) - Guzzle(beerStein string) + SayHi() + Sing(lyrics string) + Guzzle(beerStein string) } - + type YoungChap interface { - SayHi() - Sing(song string) - BorrowMoney(amount float32) + SayHi() + Sing(song string) + BorrowMoney(amount float32) } - + type ElderlyGent interface { - SayHi() - Sing(song string) - SpendSalary(amount float32) + SayHi() + Sing(song string) + SpendSalary(amount float32) } - + 通过上面的代码我们可以知道,interface可以被任意的对象实现。我们看到上面的Men interface被Human、Student和Employee实现。同理,一个对象可以实现任意多个interface,例如上面的Student实现了Men和YonggChap两个interface。 最后,任意的类型都实现了空interface(我们这样定义:interface{}),也就是包含0个method的interface。 @@ -96,78 +96,78 @@ interface类型定义了一组方法,如果某个对象实现了某个接口 package main import "fmt" - + type Human struct { - name string - age int - phone string + name string + age int + phone string } - + type Student struct { - Human //匿名字段 - school string - loan float32 + Human //匿名字段 + school string + loan float32 } - + type Employee struct { - Human //匿名字段 - company string - money float32 + Human //匿名字段 + company string + money float32 } - + //Human实现Sayhi方法 func (h Human) SayHi() { - fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone) + fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone) } - + //Human实现Sing方法 func (h Human) Sing(lyrics string) { - fmt.Println("La la la la...", lyrics) + fmt.Println("La la la la...", lyrics) } - + //Employee重载Human的SayHi方法 func (e Employee) SayHi() { - fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name, - e.company, e.phone) //Yes you can split into 2 lines here. + fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name, + e.company, e.phone) //Yes you can split into 2 lines here. } - + // Interface Men被Human,Student和Employee实现 // 因为这三个类型都实现了这两个方法 type Men interface { - SayHi() - Sing(lyrics string) + SayHi() + Sing(lyrics string) } - + func main() { - mike := Student{Human{"Mike", 25, "222-222-XXX"}, "MIT", 0.00} - paul := Student{Human{"Paul", 26, "111-222-XXX"}, "Harvard", 100} - sam := Employee{Human{"Sam", 36, "444-222-XXX"}, "Golang Inc.", 1000} - Tom := Employee{Human{"Sam", 36, "444-222-XXX"}, "Things Ltd.", 5000} - - //定义Men类型的变量i - var i Men - - //i能存储Student - i = mike - fmt.Println("This is Mike, a Student:") - i.SayHi() - i.Sing("November rain") - - //i也能存储Employee - i = Tom - fmt.Println("This is Tom, an Employee:") - i.SayHi() - i.Sing("Born to be wild") - - //定义了slice Men - fmt.Println("Let's use a slice of Men and see what happens") - x := make([]Men, 3) - //T这三个都是不同类型的元素,但是他们实现了interface同一个接口 - x[0], x[1], x[2] = paul, sam, mike - - for _, value := range x{ - value.SayHi() - } + mike := Student{Human{"Mike", 25, "222-222-XXX"}, "MIT", 0.00} + paul := Student{Human{"Paul", 26, "111-222-XXX"}, "Harvard", 100} + sam := Employee{Human{"Sam", 36, "444-222-XXX"}, "Golang Inc.", 1000} + Tom := Employee{Human{"Sam", 36, "444-222-XXX"}, "Things Ltd.", 5000} + + //定义Men类型的变量i + var i Men + + //i能存储Student + i = mike + fmt.Println("This is Mike, a Student:") + i.SayHi() + i.Sing("November rain") + + //i也能存储Employee + i = Tom + fmt.Println("This is Tom, an Employee:") + i.SayHi() + i.Sing("Born to be wild") + + //定义了slice Men + fmt.Println("Let's use a slice of Men and see what happens") + x := make([]Men, 3) + //T这三个都是不同类型的元素,但是他们实现了interface同一个接口 + x[0], x[1], x[2] = paul, sam, mike + + for _, value := range x{ + value.SayHi() + } } 通过上面的代码,你会发现interface就是一组抽象方法的集合,它必须由其他非interface类型实现,而不能自我实现, go 通过interface实现了duck-typing:即"当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子"。 @@ -182,41 +182,41 @@ interface类型定义了一组方法,如果某个对象实现了某个接口 // a可以存储任意类型的数值 a = i a = s - -一个函数把interface{}作为参数,那么他可以接受任意类型的值作为参数,如果一个函数返回interface{},那么也就可以返回任意类型的值。是不是很有用啊! + +一个函数把interface{}作为参数,那么他可以接受任意类型的值作为参数,如果一个函数返回interface{},那么也就可以返回任意类型的值。是不是很有用啊! ### interface函数参数 interface的变量可以持有任意实现该interface类型的对象,这给我们编写函数(包括method)提供了一些额外的思考,我们是不是可以通过定义interface参数,让函数接受各种类型的参数。 举个例子:我们已经知道fmt.Println是我们常用的一个函数,但是你是否注意到它可以接受任意类型的数据。打开fmt的源码文件,你会看到这样一个定义: type Stringer interface { - String() string + String() string } 任何实现了String方法的类型都能作为参数去调用fmt.Println,让我们来试一试 package main import ( - "fmt" - "strconv" + "fmt" + "strconv" ) - + type Human struct { name string - age int - phone string + age int + phone string } - + // 通过这个方法 Human 实现了 fmt.Stringer func (h Human) String() string { - return "❰"+h.name+" - "+strconv.Itoa(h.age)+" years - ✆ " +h.phone+"❱" + return "❰"+h.name+" - "+strconv.Itoa(h.age)+" years - ✆ " +h.phone+"❱" } - + func main() { - Bob := Human{"Bob", 39, "000-7777-XXX"} - fmt.Println("This Human is : ", Bob) + Bob := Human{"Bob", 39, "000-7777-XXX"} + fmt.Println("This Human is : ", Bob) } 现在我们再回顾一下前面的Box示例,你会发现Color结构也定义了一个method:String。其实这也是实现了fmt.Stringer这个interface,即如果需要某个类型能被fmt包以特殊的格式输出,你就必须实现Stringer这个接口。如果没有实现这个接口,fmt将以默认的方式输出。 - + //实现同样的功能 fmt.Println("The biggest one is", boxes.BiggestsColor().String()) fmt.Println("The biggest one is", boxes.BiggestsColor()) @@ -229,98 +229,98 @@ interface的变量可以持有任意实现该interface类型的对象,这给 - Comma-ok断言 Go语言里面有一个语法,可以直接判断是否是该类型的变量: value, ok = element.(T),这里value就是变量的值,ok是一个bool类型,element是interface变量,T是断言的类型。 - + 如果element里面确实存储了T类型的数值,那么ok返回true,否则返回false。 - + 让我们通过一个例子来更加深入的理解。 - - package main - - import ( - "fmt" - "strconv" - ) - - type Element interface{} - type List [] Element - - type Person struct { - name string - age int - } - //定义了String方法,实现了fmt.Stringer - func (p Person) String() string { - return "(name: " + p.name + " - age: "+strconv.Itoa(p.age)+ " years)" - } + package main - func main() { - list := make(List, 3) - list[0] = 1 // an int - list[1] = "Hello" // a string - list[2] = Person{"Dennis", 70} - - for index, element := range list { - if value, ok := element.(int); ok { - fmt.Printf("list[%d] is an int and its value is %d\n", index, value) - } else if value, ok := element.(string); ok { - fmt.Printf("list[%d] is a string and its value is %s\n", index, value) - } else if value, ok := element.(Person); ok { - fmt.Printf("list[%d] is a Person and its value is %s\n", index, value) - } else { - fmt.Println("list[%d] is of a different type", index) - } - } + import ( + "fmt" + "strconv" + ) + + type Element interface{} + type List [] Element + + type Person struct { + name string + age int + } + + //定义了String方法,实现了fmt.Stringer + func (p Person) String() string { + return "(name: " + p.name + " - age: "+strconv.Itoa(p.age)+ " years)" + } + + func main() { + list := make(List, 3) + list[0] = 1 // an int + list[1] = "Hello" // a string + list[2] = Person{"Dennis", 70} + + for index, element := range list { + if value, ok := element.(int); ok { + fmt.Printf("list[%d] is an int and its value is %d\n", index, value) + } else if value, ok := element.(string); ok { + fmt.Printf("list[%d] is a string and its value is %s\n", index, value) + } else if value, ok := element.(Person); ok { + fmt.Printf("list[%d] is a Person and its value is %s\n", index, value) + } else { + fmt.Println("list[%d] is of a different type", index) + } } - - 是不是很简单啊,同时你是否注意到了多个ifs里面,还记得我前面介绍流程里面讲过,if里面允许初始化变量。 - - 也许你注意到了,我们断言的类型越多,那么ifelse也就越多,所以才引出了下面要介绍的switch。 + } + +是不是很简单啊,同时你是否注意到了多个ifs里面,还记得我前面介绍流程里面讲过,if里面允许初始化变量。 + +也许你注意到了,我们断言的类型越多,那么ifelse也就越多,所以才引出了下面要介绍的switch。 - switch测试 - 最好的讲解就是代码例子,现在让我们重写上面的这个实现 - - package main - - import ( - "fmt" - "strconv" - ) - - type Element interface{} - type List [] Element - - type Person struct { - name string - age int - } +最好的讲解就是代码例子,现在让我们重写上面的这个实现 - //打印 - func (p Person) String() string { - return "(name: " + p.name + " - age: "+strconv.Itoa(p.age)+ " years)" - } + package main + + import ( + "fmt" + "strconv" + ) - func main() { - list := make(List, 3) - list[0] = 1 //an int - list[1] = "Hello" //a string - list[2] = Person{"Dennis", 70} - - for index, element := range list{ - switch value := element.(type) { - case int: - fmt.Printf("list[%d] is an int and its value is %d\n", index, value) - case string: - fmt.Printf("list[%d] is a string and its value is %s\n", index, value) - case Person: - fmt.Printf("list[%d] is a Person and its value is %s\n", index, value) - default: - fmt.Println("list[%d] is of a different type", index) - } - } + type Element interface{} + type List [] Element + + type Person struct { + name string + age int + } + + //打印 + func (p Person) String() string { + return "(name: " + p.name + " - age: "+strconv.Itoa(p.age)+ " years)" + } + + func main() { + list := make(List, 3) + list[0] = 1 //an int + list[1] = "Hello" //a string + list[2] = Person{"Dennis", 70} + + for index, element := range list{ + switch value := element.(type) { + case int: + fmt.Printf("list[%d] is an int and its value is %d\n", index, value) + case string: + fmt.Printf("list[%d] is a string and its value is %s\n", index, value) + case Person: + fmt.Printf("list[%d] is a Person and its value is %s\n", index, value) + default: + fmt.Println("list[%d] is of a different type", index) + } } - - 这里有一点需要强调的是:`element.(type)`语法不能在switch外的任何逻辑里面使用,如果你要在switch外面判断一个类型就使用`comma-ok`。 + } + + 这里有一点需要强调的是:`element.(type)`语法不能在switch外的任何逻辑里面使用,如果你要在switch外面判断一个类型就使用`comma-ok`。 ### 嵌入interface Go里面真正吸引人的是他内置的逻辑语法,就像我们在学习Struct时学习的匿名字段,多么的优雅啊,那么相同的逻辑引入到interface里面,那不是更加完美了。如果一个interface1作为interface2的一个嵌入字段,那么interface2隐式的包含了interface1里面的method。 @@ -328,21 +328,21 @@ Go里面真正吸引人的是他内置的逻辑语法,就像我们在学习Str 我们可以看到源码包container/heap里面有这样的一个定义 type Interface interface { - sort.Interface //嵌入字段sort.Interface - Push(x interface{}) //a Push method to push elements into the heap - Pop() interface{} //a Pop elements that pops elements from the heap + sort.Interface //嵌入字段sort.Interface + Push(x interface{}) //a Push method to push elements into the heap + Pop() interface{} //a Pop elements that pops elements from the heap } 我们看到sort.Interface其实就是嵌入字段,把sort.Interface的所有method给隐式的包含进来了。也就是下面三个方法 type Interface interface { - // Len is the number of elements in the collection. - Len() int - // Less returns whether the element with index i should sort - // before the element with index j. - Less(i, j int) bool - // Swap swaps the elements with indexes i and j. - Swap(i, j int) + // Len is the number of elements in the collection. + Len() int + // Less returns whether the element with index i should sort + // before the element with index j. + Less(i, j int) bool + // Swap swaps the elements with indexes i and j. + Swap(i, j int) } 另一个例子就是io包下面的 io.ReadWriter ,他包含了io包下面的Reader和Writer两个interface。 @@ -350,9 +350,9 @@ Go里面真正吸引人的是他内置的逻辑语法,就像我们在学习Str // io.ReadWriter type ReadWriter interface { Reader - Writer - } - + Writer + } + ### 反射 Go语言实现了反射,所谓反射就是动态运行时的状态。我们一般用到的包是reflect包。如何运用reflect包,官方的这篇文章详细的讲解了reflect包的实现原理,[laws of reflection](http://golang.org/doc/articles/laws_of_reflection.html) @@ -365,34 +365,31 @@ Go语言实现了反射,所谓反射就是动态运行时的状态。我们一 tag := t.Elem().Field(0).Tag //获取定义在strcut里面的标签 name := v.Elem().Field(0).String() //获取存储在第一个字段里面的值 - + 获取反射值能返回相应的类型和数值 - + var x float64 = 3.4 - v := reflect.ValueOf(x) - fmt.Println("type:", v.Type()) - fmt.Println("kind is float64:", v.Kind() == reflect.Float64) - fmt.Println("value:", v.Float()) - + v := reflect.ValueOf(x) + fmt.Println("type:", v.Type()) + fmt.Println("kind is float64:", v.Kind() == reflect.Float64) + fmt.Println("value:", v.Float()) + 最后,反射的话,那么反射的字段必须是可修改的,我们前面学习过传值和传引用,这个里面也是一样的道理,反射的字段必须是可读写的意思是,如果下面这样写,那么会发生错误 var x float64 = 3.4 - v := reflect.ValueOf(x) - v.SetFloat(7.1) - + v := reflect.ValueOf(x) + v.SetFloat(7.1) + 如果要修改相应的值,必须这样写 var x float64 = 3.4 - p := reflect.ValueOf(&x) - v := p.Elem() - v.SetFloat(7.1) - + p := reflect.ValueOf(&x) + v := p.Elem() + v.SetFloat(7.1) + 使用反射需要自己在编程中不断的深入去了解,我这边只能大概的介绍一些。 ## links * [目录]() * 上一章: [面向对象](<2.5.md>) * 下一节: [并发](<2.7.md>) - -## LastModified - * $Id$ diff --git a/2.7.md b/2.7.md index 088edbdfa372930c1f5869ad6890776f23262274..ea296df1ea3079f483da6c9dcdc7b770a4f3e8f5 100644 --- a/2.7.md +++ b/2.7.md @@ -115,19 +115,19 @@ channel通过操作符`<-`来接收和发送数据 ) func fibonacci(n int, c chan int) { - x, y := 1, 1 - for i := 0; i < n; i++ { - c <- x - x, y = y, x + y - } - close(c) + x, y := 1, 1 + for i := 0; i < n; i++ { + c <- x + x, y = y, x + y + } + close(c) } func main() { c := make(chan int, 10) go fibonacci(cap(c), c) for i := range c { - fmt.Println(i) + fmt.Println(i) } } @@ -149,13 +149,13 @@ channel通过操作符`<-`来接收和发送数据 func fibonacci(c, quit chan int) { x, y := 1, 1 for { - select { - case c <- x: - x, y = y, x + y - case <-quit: + select { + case c <- x: + x, y = y, x + y + case <-quit: fmt.Println("quit") - return - } + return + } } } @@ -187,6 +187,3 @@ channel通过操作符`<-`来接收和发送数据 * [目录]() * 上一章: [interface](<2.6.md>) * 下一节: [总结](<2.8.md>) - -## LastModified - * $Id$ diff --git a/2.8.md b/2.8.md index 695689f9ea047803a0bd33c000af015d1de4e628..770b8b8658d16d9477a7b9cd0cda129fa9450c4f 100644 --- a/2.8.md +++ b/2.8.md @@ -1,34 +1,31 @@ -# 2.8总结 - -这一章我们主要介绍了Go语言的一些语法,通过语法我们可以发现Go是多么的简单,只有二十五个关键字。让我们再来回顾一下这些关键字都是用来干什么的。 - - break default func interface select - case defer go map struct - chan else goto package switch - const fallthrough if range type - continue for import return var - -- var和const参考2.2Go语言基础里面的变量和常量申明 -- package和import已经有过短暂的接触 -- func 用于定义函数和方法 -- return 用于从函数返回 -- defer 用于类似析构函数 -- go 用于并行 -- select 用于选择不同类型的通讯 -- interface 用于定义接口,参考2.6小节 -- struct 用于定义抽象数据类型,参考2.5小节 -- break、case、continue、for、fallthrough、else、if、switch、goto、default这些参考2.3流程介绍里面 -- chan用于channel通讯 -- type用于声明自定义类型 -- map用于声明map类型数据 -- range用于读取slice、map、channel数据 - -上面这二十五个关键字记住了,那么Go你也已经差不多学会了。 - -## links - * [目录]() - * 上一节: [并发](<2.7.md>) - * 下一章: [Web基础](<3.md>) - -## LastModified - * $Id$ +# 2.8总结 + +这一章我们主要介绍了Go语言的一些语法,通过语法我们可以发现Go是多么的简单,只有二十五个关键字。让我们再来回顾一下这些关键字都是用来干什么的。 + + break default func interface select + case defer go map struct + chan else goto package switch + const fallthrough if range type + continue for import return var + +- var和const参考2.2Go语言基础里面的变量和常量申明 +- package和import已经有过短暂的接触 +- func 用于定义函数和方法 +- return 用于从函数返回 +- defer 用于类似析构函数 +- go 用于并行 +- select 用于选择不同类型的通讯 +- interface 用于定义接口,参考2.6小节 +- struct 用于定义抽象数据类型,参考2.5小节 +- break、case、continue、for、fallthrough、else、if、switch、goto、default这些参考2.3流程介绍里面 +- chan用于channel通讯 +- type用于声明自定义类型 +- map用于声明map类型数据 +- range用于读取slice、map、channel数据 + +上面这二十五个关键字记住了,那么Go你也已经差不多学会了。 + +## links + * [目录]() + * 上一节: [并发](<2.7.md>) + * 下一章: [Web基础](<3.md>) diff --git a/2.md b/2.md index c6e28b1eb40657594bee12c91947b3248b07ac62..8ba57fd87684713849ae08fe5bfc11ae1be5be0e 100644 --- a/2.md +++ b/2.md @@ -1,30 +1,27 @@ -# 2 Go语言基础 - -## 目录 - * 1. [你好,Go](2.1.md) - * 2. [Go基础](2.2.md) - * 3. [流程和函数](2.3.md) - * 4. [struct类型](2.4.md) - * 5. [面向对象](2.5.md) - * 6. [interface](2.6.md) - * 7. [并发](2.7.md) - * 8. [小结](2.8.md) - -Go是一门类似C的编译型语言,但是它的编译速度非常快。这门语言的关键字总共也就二十五个,比英文字母还少一个,这对于我们的学习来说就简单了很多。先让我们看一眼这些关键字都长什么样: - - break default func interface select - case defer go map struct - chan else goto package switch - const fallthrough if range type - continue for import return var - -在接下来的这一章中,我将带领你去学习这门语言的基础。通过每一小节的介绍,你将发现,Go的世界是那么地简洁,设计是如此地美妙,编写Go将会是一件愉快的事情。等回过头来,你就会发现这二十五个关键字是多么地亲切。 - - -## links - * [目录]() - * 上一章: [第一章总结](<1.5.md>) - * 下一节: [你好,Go](<2.1.md>) - -## LastModified - * $Id$ +# 2 Go语言基础 + +## 目录 + * 1. [你好,Go](2.1.md) + * 2. [Go基础](2.2.md) + * 3. [流程和函数](2.3.md) + * 4. [struct类型](2.4.md) + * 5. [面向对象](2.5.md) + * 6. [interface](2.6.md) + * 7. [并发](2.7.md) + * 8. [小结](2.8.md) + +Go是一门类似C的编译型语言,但是它的编译速度非常快。这门语言的关键字总共也就二十五个,比英文字母还少一个,这对于我们的学习来说就简单了很多。先让我们看一眼这些关键字都长什么样: + + break default func interface select + case defer go map struct + chan else goto package switch + const fallthrough if range type + continue for import return var + +在接下来的这一章中,我将带领你去学习这门语言的基础。通过每一小节的介绍,你将发现,Go的世界是那么地简洁,设计是如此地美妙,编写Go将会是一件愉快的事情。等回过头来,你就会发现这二十五个关键字是多么地亲切。 + + +## links + * [目录]() + * 上一章: [第一章总结](<1.5.md>) + * 下一节: [你好,Go](<2.1.md>) diff --git a/3.1.md b/3.1.md index 8998288bbfd925d6081c99905476a3069ff827ab..767504c97a1fbbf0dc2f3f92536e752df1100abf 100644 --- a/3.1.md +++ b/3.1.md @@ -1,149 +1,146 @@ -# 3.1 Web工作方式 - -我们平时浏览网页的时候,会打开浏览器,输入网址后按下回车键,然后就会显示出你想要浏览的内容。在这个看似简单的用户行为背后,到底隐藏了些什么呢? - -对于普通的上网过程,系统其实是这样做的:浏览器本身是一个客户端,当你输入URL的时候,首先浏览器会去请求DNS服务器,通过DNS获取相应的域名对应的IP,然后通过IP地址找到IP对应的服务器后,要求建立TCP连接,等浏览器发送完HTTP Request(请求)包后,服务器接收到请求包之后才开始处理请求包,服务器调用自身服务,返回HTTP Response(响应)包;客户端收到来自服务器的响应后开始渲染这个Response包里的主体(body),等收到全部的内容随后断开与该服务器之间的TCP连接。 - -![](images/3.1.web2.png?raw=true) - - 一个Web服务器也被称为HTTP服务器,它通过HTTP协议与客户端通信。这个客户端通常指的是Web浏览器(其实手机端客户端内部也是浏览器实现的)。 - -Web服务器的工作原理可以简单地归纳为: - -- 客户机通过TCP/IP协议建立到服务器的TCP连接 -- 客户端向服务器发送HTTP协议请求包,请求服务器里的资源文档 -- 服务器向客户机发送HTTP协议应答包,如果请求的资源包含有动态语言的内容,那么服务器会调用动态语言的解释引擎负责处理“动态内容”,并将处理得到的数据返回给客户端 -- 客户机与服务器断开。由客户端解释HTML文档,在客户端屏幕上渲染图形结果 - -一个简单的HTTP事务就是这样实现的,看起来很复杂,原理其实是挺简单的。需要注意的是客户机与服务器之间的通信是非持久连接的,也就是当服务器发送了应答后就与客户机断开连接,等待下一次请求。 - -## URL和DNS解析 -我们浏览网页都是通过URL访问的,那么URL到底是怎么样的呢? - -URL(Uniform Resource Locator)是“统一资源定位符”的英文缩写,用于描述一个网络上的资源, 基本格式如下 - - schema://host[:port#]/path/.../[?query-string][#anchor] - scheme 指定低层使用的协议(例如:http, https, ftp) - host HTTP服务器的IP地址或者域名 - port# HTTP服务器的默认端口是80,这种情况下端口号可以省略。如果使用了别的端口,必须指明,例如 http://www.cnblogs.com:8080/ - path 访问资源的路径 - query-string 发送给http服务器的数据 - anchor 锚 - - DNS(Domain Name System)是“域名系统”的英文缩写,是一种组织成域层次结构的计算机和网络服务命名系统,它用于TCP/IP网络,它从事将主机名或域名转换为实际IP地址的工作。DNS就是这样的一位“翻译官”,它的基本工作原理可用下图来表示。 - -![](images/3.1.dns_hierachy.png?raw=true) - -更详细的DNS解析的过程如下,这个过程有助于我们理解DNS的工作模式 - -1、在浏览器中输入www.qq.com域名,操作系统会先检查自己本地的hosts文件是否有这个网址映射关系,如果有,就先调用这个IP地址映射,完成域名解析。 - -2、如果hosts里没有这个域名的映射,则查找本地DNS解析器缓存,是否有这个网址映射关系,如果有,直接返回,完成域名解析。 - -3、如果hosts与本地DNS解析器缓存都没有相应的网址映射关系,首先会找TCP/IP参数中设置的首选DNS服务器,在此我们叫它本地DNS服务器,此服务器收到查询时,如果要查询的域名,包含在本地配置区域资源中,则返回解析结果给客户机,完成域名解析,此解析具有权威性。 - -4、如果要查询的域名,不由本地DNS服务器区域解析,但该服务器已缓存了此网址映射关系,则调用这个IP地址映射,完成域名解析,此解析不具有权威性。 - -5、如果本地DNS服务器本地区域文件与缓存解析都失效,则根据本地DNS服务器的设置(是否设置转发器)进行查询,如果未用转发模式,本地DNS就把请求发至 “根DNS服务器”,“根DNS服务器”收到请求后会判断这个域名(.com)是谁来授权管理,并会返回一个负责该顶级域名服务器的一个IP。本地DNS服务器收到IP信息后,将会联系负责.com域的这台服务器。这台负责.com域的服务器收到请求后,如果自己无法解析,它就会找一个管理.com域的下一级DNS服务器地址(qq.com)给本地DNS服务器。当本地DNS服务器收到这个地址后,就会找qq.com域服务器,重复上面的动作,进行查询,直至找到www.qq.com主机。 - -6、如果用的是转发模式,此DNS服务器就会把请求转发至上一级DNS服务器,由上一级服务器进行解析,上一级服务器如果不能解析,或找根DNS或把转请求转至上上级,以此循环。不管是本地DNS服务器用是是转发,还是根提示,最后都是把结果返回给本地DNS服务器,由此DNS服务器再返回给客户机。 - -![](images/3.1.dns_inquery.png?raw=true) - -> 所谓 `递归查询过程` 就是 “查询的递交者” 更替, 而 `迭代查询过程` 则是 “查询的递交者”不变。 -> -> 举个例子来说,你想知道某个一起上法律课的女孩的电话,并且你偷偷拍了她的照片,回到寝室告诉一个很仗义的哥们儿,这个哥们儿二话没说,拍着胸脯告诉你,甭急,我替你查(此处完成了一次递归查询,即,问询者的角色更替)。然后他拿着照片问了学院大四学长,学长告诉他,这姑娘是xx系的;然后这哥们儿马不停蹄又问了xx系的办公室主任助理同学,助理同学说是xx系yy班的,然后很仗义的哥们儿去xx系yy班的班长那里取到了该女孩儿电话。(此处完成若干次迭代查询,即,问询者角色不变,但反复更替问询对象)最后,他把号码交到了你手里。完成整个查询过程。 - -通过上面的步骤,我们最后获取的是IP地址,也就是浏览器最后发起请求的时候是基于IP来和服务器做信息交互的。 - -## HTTP协议详解 - -HTTP协议是Web工作的核心,所以要了解清楚Web的工作方式就需要详细的了解清楚HTTP是怎么样工作的。 - -HTTP是一种让Web服务器与浏览器(客户端)通过Internet发送与接收数据的协议,它建立在TCP协议之上,一般采用TCP的80端口。它是一个请求、响应协议--客户端发出一个请求,服务器响应这个请求。在HTTP中,客户端总是通过建立一个连接与发送一个HTTP请求来发起一个事务。服务器不能主动去与客户端联系,也不能给客户端发出一个回调连接。客户端与服务器端都可以提前中断一个连接。例如,当浏览器下载一个文件时,你可以通过点击“停止”键来中断文件的下载,关闭与服务器的HTTP连接。 - -HTTP协议是无状态的,同一个客户端的这次请求和上次请求是没有对应关系,对HTTP服务器来说,它并不知道这两个请求是否来自同一个客户端。为了解决这个问题, Web程序引入了Cookie机制来维护连接的可持续状态。 - ->HTTP协议是建立在TCP协议之上的,因此TCP攻击一样会影响HTTP的通讯,例如比较常见的一些攻击:SYN Flood是当前最流行的DoS(拒绝服务攻击)与DdoS(分布式拒绝服务攻击)的方式之一,这是一种利用TCP协议缺陷,发送大量伪造的TCP连接请求,从而使得被攻击方资源耗尽(CPU满负荷或内存不足)的攻击方式。 - -### HTTP请求包(浏览器信息) - -我们先来看看Request包的结构, Request包分为3部分,第一部分叫Request line(请求行), 第二部分叫Request header(请求头),第三部分是body(主体)。header和body之间有个空行,请求包的例子所示: - - GET /domains/example/ HTTP/1.1 //请求行: 请求方法 请求URI HTTP协议/协议版本 - Host:www.iana.org //服务端的主机名 - User-Agent:Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.4 (KHTML, like Gecko) Chrome/22.0.1229.94 Safari/537.4 //浏览器信息 - Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 //客户端能接收的mine - Accept-Encoding:gzip,deflate,sdch //是否支持流压缩 - Accept-Charset:UTF-8,*;q=0.5 //客户端字符编码集 - //空行,用于分割请求头和消息体 - //消息体,请求资源参数,例如POST传递的参数 - -我们通过fiddler抓包可以看到如下请求信息 - -![](images/3.1.http.png?raw=true) - -![](images/3.1.httpPOST.png?raw=true) - -**我们可以看到GET请求消息体为空,POST请求带有消息体**。 - -HTTP协议定义了很多与服务器交互的请求方法,最基本的有4种,分别是GET,POST,PUT,DELETE. 一个URL地址用于描述一个网络上的资源,而HTTP中的GET, POST, PUT, DELETE就对应着对这个资源的查,改,增,删4个操作。 我们最常见的就是GET和POST了。GET一般用于获取/查询资源信息,而POST一般用于更新资源信息. -我们看看GET和POST的区别 -1. GET提交的数据会放在URL之后,以?分割URL和传输数据,参数之间以&相连,如EditPosts.aspx?name=test1&id=123456. POST方法是把提交的数据放在HTTP包的Body中. -2. GET提交的数据大小有限制(因为浏览器对URL的长度有限制),而POST方法提交的数据没有限制. -3. GET方式提交数据,会带来安全问题,比如一个登录页面,通过GET方式提交数据时,用户名和密码将出现在URL上,如果页面可以被缓存或者其他人可以访问这台机器,就可以从历史记录获得该用户的账号和密码。 - -### HTTP响应包(服务器信息) -我们再来看看HTTP的response包,他的结构如下: - - HTTP/1.1 200 OK //状态行 - Server: nginx/1.0.8 //服务器使用的WEB软件名及版本 - Date:Date: Tue, 30 Oct 2012 04:14:25 GMT //发送时间 - Content-Type: text/html //服务器发送信息的类型 - Transfer-Encoding: chunked //表示发送HTTP包是分段发的 - Connection: keep-alive //保持连接状态 - Content-Length: 90 //主体内容长度 - //空行 用来分割消息头和主体 - 网页优化方面有一项措施是减少HTTP请求次数,就是把尽量多的css和js资源合并在一起,目的是尽量减少网页请求静态资源的次数,提高网页加载速度,同时减缓服务器的压力。 - -## links - * [目录]() - * 上一节: [Web基础](<3.md>) - * 下一节: [GO搭建一个web服务器](<3.2.md>) - -## LastModified - * $Id$ +# 3.1 Web工作方式 + +我们平时浏览网页的时候,会打开浏览器,输入网址后按下回车键,然后就会显示出你想要浏览的内容。在这个看似简单的用户行为背后,到底隐藏了些什么呢? + +对于普通的上网过程,系统其实是这样做的:浏览器本身是一个客户端,当你输入URL的时候,首先浏览器会去请求DNS服务器,通过DNS获取相应的域名对应的IP,然后通过IP地址找到IP对应的服务器后,要求建立TCP连接,等浏览器发送完HTTP Request(请求)包后,服务器接收到请求包之后才开始处理请求包,服务器调用自身服务,返回HTTP Response(响应)包;客户端收到来自服务器的响应后开始渲染这个Response包里的主体(body),等收到全部的内容随后断开与该服务器之间的TCP连接。 + +![](images/3.1.web2.png?raw=true) + + 一个Web服务器也被称为HTTP服务器,它通过HTTP协议与客户端通信。这个客户端通常指的是Web浏览器(其实手机端客户端内部也是浏览器实现的)。 + +Web服务器的工作原理可以简单地归纳为: + +- 客户机通过TCP/IP协议建立到服务器的TCP连接 +- 客户端向服务器发送HTTP协议请求包,请求服务器里的资源文档 +- 服务器向客户机发送HTTP协议应答包,如果请求的资源包含有动态语言的内容,那么服务器会调用动态语言的解释引擎负责处理“动态内容”,并将处理得到的数据返回给客户端 +- 客户机与服务器断开。由客户端解释HTML文档,在客户端屏幕上渲染图形结果 + +一个简单的HTTP事务就是这样实现的,看起来很复杂,原理其实是挺简单的。需要注意的是客户机与服务器之间的通信是非持久连接的,也就是当服务器发送了应答后就与客户机断开连接,等待下一次请求。 + +## URL和DNS解析 +我们浏览网页都是通过URL访问的,那么URL到底是怎么样的呢? + +URL(Uniform Resource Locator)是“统一资源定位符”的英文缩写,用于描述一个网络上的资源, 基本格式如下 + + schema://host[:port#]/path/.../[?query-string][#anchor] + scheme 指定低层使用的协议(例如:http, https, ftp) + host HTTP服务器的IP地址或者域名 + port# HTTP服务器的默认端口是80,这种情况下端口号可以省略。如果使用了别的端口,必须指明,例如 http://www.cnblogs.com:8080/ + path 访问资源的路径 + query-string 发送给http服务器的数据 + anchor 锚 + + DNS(Domain Name System)是“域名系统”的英文缩写,是一种组织成域层次结构的计算机和网络服务命名系统,它用于TCP/IP网络,它从事将主机名或域名转换为实际IP地址的工作。DNS就是这样的一位“翻译官”,它的基本工作原理可用下图来表示。 + +![](images/3.1.dns_hierachy.png?raw=true) + +更详细的DNS解析的过程如下,这个过程有助于我们理解DNS的工作模式 + +1、在浏览器中输入www.qq.com域名,操作系统会先检查自己本地的hosts文件是否有这个网址映射关系,如果有,就先调用这个IP地址映射,完成域名解析。 + +2、如果hosts里没有这个域名的映射,则查找本地DNS解析器缓存,是否有这个网址映射关系,如果有,直接返回,完成域名解析。 + +3、如果hosts与本地DNS解析器缓存都没有相应的网址映射关系,首先会找TCP/IP参数中设置的首选DNS服务器,在此我们叫它本地DNS服务器,此服务器收到查询时,如果要查询的域名,包含在本地配置区域资源中,则返回解析结果给客户机,完成域名解析,此解析具有权威性。 + +4、如果要查询的域名,不由本地DNS服务器区域解析,但该服务器已缓存了此网址映射关系,则调用这个IP地址映射,完成域名解析,此解析不具有权威性。 + +5、如果本地DNS服务器本地区域文件与缓存解析都失效,则根据本地DNS服务器的设置(是否设置转发器)进行查询,如果未用转发模式,本地DNS就把请求发至 “根DNS服务器”,“根DNS服务器”收到请求后会判断这个域名(.com)是谁来授权管理,并会返回一个负责该顶级域名服务器的一个IP。本地DNS服务器收到IP信息后,将会联系负责.com域的这台服务器。这台负责.com域的服务器收到请求后,如果自己无法解析,它就会找一个管理.com域的下一级DNS服务器地址(qq.com)给本地DNS服务器。当本地DNS服务器收到这个地址后,就会找qq.com域服务器,重复上面的动作,进行查询,直至找到www.qq.com主机。 + +6、如果用的是转发模式,此DNS服务器就会把请求转发至上一级DNS服务器,由上一级服务器进行解析,上一级服务器如果不能解析,或找根DNS或把转请求转至上上级,以此循环。不管是本地DNS服务器用是是转发,还是根提示,最后都是把结果返回给本地DNS服务器,由此DNS服务器再返回给客户机。 + +![](images/3.1.dns_inquery.png?raw=true) + +> 所谓 `递归查询过程` 就是 “查询的递交者” 更替, 而 `迭代查询过程` 则是 “查询的递交者”不变。 +> +> 举个例子来说,你想知道某个一起上法律课的女孩的电话,并且你偷偷拍了她的照片,回到寝室告诉一个很仗义的哥们儿,这个哥们儿二话没说,拍着胸脯告诉你,甭急,我替你查(此处完成了一次递归查询,即,问询者的角色更替)。然后他拿着照片问了学院大四学长,学长告诉他,这姑娘是xx系的;然后这哥们儿马不停蹄又问了xx系的办公室主任助理同学,助理同学说是xx系yy班的,然后很仗义的哥们儿去xx系yy班的班长那里取到了该女孩儿电话。(此处完成若干次迭代查询,即,问询者角色不变,但反复更替问询对象)最后,他把号码交到了你手里。完成整个查询过程。 + +通过上面的步骤,我们最后获取的是IP地址,也就是浏览器最后发起请求的时候是基于IP来和服务器做信息交互的。 + +## HTTP协议详解 + +HTTP协议是Web工作的核心,所以要了解清楚Web的工作方式就需要详细的了解清楚HTTP是怎么样工作的。 + +HTTP是一种让Web服务器与浏览器(客户端)通过Internet发送与接收数据的协议,它建立在TCP协议之上,一般采用TCP的80端口。它是一个请求、响应协议--客户端发出一个请求,服务器响应这个请求。在HTTP中,客户端总是通过建立一个连接与发送一个HTTP请求来发起一个事务。服务器不能主动去与客户端联系,也不能给客户端发出一个回调连接。客户端与服务器端都可以提前中断一个连接。例如,当浏览器下载一个文件时,你可以通过点击“停止”键来中断文件的下载,关闭与服务器的HTTP连接。 + +HTTP协议是无状态的,同一个客户端的这次请求和上次请求是没有对应关系,对HTTP服务器来说,它并不知道这两个请求是否来自同一个客户端。为了解决这个问题, Web程序引入了Cookie机制来维护连接的可持续状态。 + +>HTTP协议是建立在TCP协议之上的,因此TCP攻击一样会影响HTTP的通讯,例如比较常见的一些攻击:SYN Flood是当前最流行的DoS(拒绝服务攻击)与DdoS(分布式拒绝服务攻击)的方式之一,这是一种利用TCP协议缺陷,发送大量伪造的TCP连接请求,从而使得被攻击方资源耗尽(CPU满负荷或内存不足)的攻击方式。 + +### HTTP请求包(浏览器信息) + +我们先来看看Request包的结构, Request包分为3部分,第一部分叫Request line(请求行), 第二部分叫Request header(请求头),第三部分是body(主体)。header和body之间有个空行,请求包的例子所示: + + GET /domains/example/ HTTP/1.1 //请求行: 请求方法 请求URI HTTP协议/协议版本 + Host:www.iana.org //服务端的主机名 + User-Agent:Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.4 (KHTML, like Gecko) Chrome/22.0.1229.94 Safari/537.4 //浏览器信息 + Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 //客户端能接收的mine + Accept-Encoding:gzip,deflate,sdch //是否支持流压缩 + Accept-Charset:UTF-8,*;q=0.5 //客户端字符编码集 + //空行,用于分割请求头和消息体 + //消息体,请求资源参数,例如POST传递的参数 + +我们通过fiddler抓包可以看到如下请求信息 + +![](images/3.1.http.png?raw=true) + +![](images/3.1.httpPOST.png?raw=true) + +**我们可以看到GET请求消息体为空,POST请求带有消息体**。 + +HTTP协议定义了很多与服务器交互的请求方法,最基本的有4种,分别是GET,POST,PUT,DELETE. 一个URL地址用于描述一个网络上的资源,而HTTP中的GET, POST, PUT, DELETE就对应着对这个资源的查,改,增,删4个操作。 我们最常见的就是GET和POST了。GET一般用于获取/查询资源信息,而POST一般用于更新资源信息. +我们看看GET和POST的区别 +1. GET提交的数据会放在URL之后,以?分割URL和传输数据,参数之间以&相连,如EditPosts.aspx?name=test1&id=123456. POST方法是把提交的数据放在HTTP包的Body中. +2. GET提交的数据大小有限制(因为浏览器对URL的长度有限制),而POST方法提交的数据没有限制. +3. GET方式提交数据,会带来安全问题,比如一个登录页面,通过GET方式提交数据时,用户名和密码将出现在URL上,如果页面可以被缓存或者其他人可以访问这台机器,就可以从历史记录获得该用户的账号和密码。 + +### HTTP响应包(服务器信息) +我们再来看看HTTP的response包,他的结构如下: + + HTTP/1.1 200 OK //状态行 + Server: nginx/1.0.8 //服务器使用的WEB软件名及版本 + Date:Date: Tue, 30 Oct 2012 04:14:25 GMT //发送时间 + Content-Type: text/html //服务器发送信息的类型 + Transfer-Encoding: chunked //表示发送HTTP包是分段发的 + Connection: keep-alive //保持连接状态 + Content-Length: 90 //主体内容长度 + //空行 用来分割消息头和主体 + 网页优化方面有一项措施是减少HTTP请求次数,就是把尽量多的css和js资源合并在一起,目的是尽量减少网页请求静态资源的次数,提高网页加载速度,同时减缓服务器的压力。 + +## links + * [目录]() + * 上一节: [Web基础](<3.md>) + * 下一节: [GO搭建一个web服务器](<3.2.md>) diff --git a/3.2.md b/3.2.md index 9f0f6ea329ea98e8ca3502c627498b7ff17fc6d0..dba698642f92080aabaf11db452b6336ba6e7f61 100644 --- a/3.2.md +++ b/3.2.md @@ -62,6 +62,3 @@ * [目录]() * 上一节: [Web工作方式](<3.1.md>) * 下一节: [Go如何使得web工作](<3.3.md>) - -## LastModified - * $Id$ diff --git a/3.3.md b/3.3.md index 1cb06a15ec38407a23eea456f60a2a2f820a3e28..4e78daf0f20f77ebd528bc150138f8d8d58be0a5 100644 --- a/3.3.md +++ b/3.3.md @@ -48,6 +48,3 @@ Handler:处理请求和生成返回信息的处理逻辑 * [目录]() * 上一节: [GO搭建一个简单的web服务](<3.2.md>) * 下一节: [Go的http包详解](<3.4.md>) - -## LastModified - * $Id$ diff --git a/3.4.md b/3.4.md index 24fa7da88d577953ca7b89b46b213cc997d8718e..6c11e96262545d3ede64e453cada57fa964ee607 100644 --- a/3.4.md +++ b/3.4.md @@ -1,160 +1,157 @@ -# 3.4 Go的http包详解 -前面小节介绍了Go怎么样实现了Web工作模式的一个流程,这一小节,我们将来详细的解剖一下http包,看它到底怎么样实现整个的过程的。 - -Go的http有两个核心功能:Conn、ServeMux - -## Conn的goroutine -与我们一般编写的http服务器不同, Go为了实现高并发和高性能, 使用了goroutines来处理Conn的读写事件, 这样每个请求都能保持独立,相互不会阻塞,可以高效的响应网络事件。这是Go高效的保证。 - -Go在等待客户端请求里面是这样写的: - - c, err := srv.newConn(rw) - if err != nil { - continue - } - go c.serve() - -这里我们可以看到客户端的每次请求都会创建一个Conn,这个Conn里面保存了该次请求的信息,然后再传递到handler的时候可以读取到相应的header信息,这样保证了每个请求的独立性。 - -## ServeMux的自定义 -我们前面小节讲述conn.server的时候,其实内部是调用了http包默认的路由器,通过路由器把本次请求的信息传递到了后端的处理函数。那么这个路由器是怎么实现的呢? - -它的结构如下: - - type ServeMux struct { - mu sync.RWMutex //锁,由于请求设计到并发处理,因此这里需要一个锁机制 - m map[string]muxEntry // 路由规则,一个string对应一个mux实体,这里的string就是注册的路由表达式 - } - -下面看一下muxEntry - - type muxEntry struct { - explicit bool // 是否精确匹配 - h Handler // 这个路由表达式对应哪个handler - } - -下面看一下handler的定义 - - type Handler interface { - ServeHTTP(ResponseWriter, *Request) // 路由实现器 - } - -handler是一个接口,但是前一小节中的`sayhelloName`函数并没有实现ServeHTTP这个接口,为什么能添加呢?原来在http包里面还定义了一个类型`HandlerFunc`,我们定义的函数`sayhelloName`就是这个HandlerFunc调用之后的结果,这个类型默认就实现了ServeHTTP这个接口,即我们调用了HandlerFunc(f),类似强制类型转换f成为handlerFunc类型,这样f就拥有了ServHTTP方法。 - - type HandlerFunc func(ResponseWriter, *Request) - - // ServeHTTP calls f(w, r). - func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { - f(w, r) - } - -路由器里面存储好了相应的路由规则之后,那么具体的请求又是怎么分发的呢? - -路由器接收到请求之后调用`mux.handler(r).ServeHTTP(w, r)` - -也就是调用对应路由的handler的ServerHTTP接口,那么mux.handler(r)怎么处理的呢? - - func (mux *ServeMux) handler(r *Request) Handler { - mux.mu.RLock() - defer mux.mu.RUnlock() - - // Host-specific pattern takes precedence over generic ones - h := mux.match(r.Host + r.URL.Path) - if h == nil { - h = mux.match(r.URL.Path) - } - if h == nil { - h = NotFoundHandler() - } - return h - } - -原来他是根据用户请求的URL和路由器里面存储的map去匹配的,当匹配到之后返回存储的handler,调用这个handler的ServHTTP接口就可以执行到相应的函数了。 - -通过上面这个介绍,我们了解了整个路由过程,Go其实支持外部实现的路由器 `ListenAndServe`的第二个参数就是用以配置外部路由器的,它是一个Handler接口,即外部路由器只要实现了Handler接口就可以,我们可以在自己实现的路由器的ServHTTP里面实现自定义路由功能。 - -如下代码所示,我们自己实现了一个简易的路由器 - - package main - - import ( - "fmt" - "net/http" - ) - - type MyMux struct { - } - - func (p *MyMux) ServeHTTP(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/" { - sayhelloName(w, r) - return - } - http.NotFound(w, r) - return - } - - func sayhelloName(w http.ResponseWriter, r *http.Request) { - fmt.Fprintf(w, "Hello myroute!") - } - - func main() { - mux := &MyMux{} - http.ListenAndServe(":9090", mux) - } - -## Go代码的执行流程 - -通过对http包的分析之后,现在让我们来梳理一下整个的代码执行过程。 - -- 首先调用Http.HandleFunc - - 按顺序做了几件事: - - 1 调用了DefaultServerMux的HandleFunc - - 2 调用了DefaultServerMux的Handle - - 3 往DefaultServeMux的map[string]muxEntry中增加对应的handler和路由规则 - -- 其次调用http.ListenAndServe(":9090", nil) - - 按顺序做了几件事情: - - 1 实例化Server - - 2 调用Server的ListenAndServe() - - 3 调用net.Listen("tcp", addr)监听端口 - - 4 启动一个for循环,在循环体中Accept请求 - - 5 对每个请求实例化一个Conn,并且开启一个goroutine为这个请求进行服务go c.serve() - - 6 读取每个请求的内容w, err := c.readRequest() - - 7 判断handler是否为空,如果没有设置handler(这个例子就没有设置handler),handler就设置为DefaultServeMux - - 8 调用handler的ServeHttp - - 9 在这个例子中,下面就进入到DefaultServerMux.ServeHttp - - 10 根据request选择handler,并且进入到这个handler的ServeHTTP - - mux.handler(r).ServeHTTP(w, r) - - 11 选择handler: - - A 判断是否有路由能满足这个request(循环遍历ServerMux的muxEntry) - - B 如果有路由满足,调用这个路由handler的ServeHttp - - C 如果没有路由满足,调用NotFoundHandler的ServeHttp - -## links - * [目录]() - * 上一节: [Go如何使得web工作](<3.3.md>) - * 下一节: [小结](<3.5.md>) - -## LastModified - * $Id$ +# 3.4 Go的http包详解 +前面小节介绍了Go怎么样实现了Web工作模式的一个流程,这一小节,我们将来详细的解剖一下http包,看它到底怎么样实现整个的过程的。 + +Go的http有两个核心功能:Conn、ServeMux + +## Conn的goroutine +与我们一般编写的http服务器不同, Go为了实现高并发和高性能, 使用了goroutines来处理Conn的读写事件, 这样每个请求都能保持独立,相互不会阻塞,可以高效的响应网络事件。这是Go高效的保证。 + +Go在等待客户端请求里面是这样写的: + + c, err := srv.newConn(rw) + if err != nil { + continue + } + go c.serve() + +这里我们可以看到客户端的每次请求都会创建一个Conn,这个Conn里面保存了该次请求的信息,然后再传递到handler的时候可以读取到相应的header信息,这样保证了每个请求的独立性。 + +## ServeMux的自定义 +我们前面小节讲述conn.server的时候,其实内部是调用了http包默认的路由器,通过路由器把本次请求的信息传递到了后端的处理函数。那么这个路由器是怎么实现的呢? + +它的结构如下: + + type ServeMux struct { + mu sync.RWMutex //锁,由于请求设计到并发处理,因此这里需要一个锁机制 + m map[string]muxEntry // 路由规则,一个string对应一个mux实体,这里的string就是注册的路由表达式 + } + +下面看一下muxEntry + + type muxEntry struct { + explicit bool // 是否精确匹配 + h Handler // 这个路由表达式对应哪个handler + } + +下面看一下handler的定义 + + type Handler interface { + ServeHTTP(ResponseWriter, *Request) // 路由实现器 + } + +handler是一个接口,但是前一小节中的`sayhelloName`函数并没有实现ServeHTTP这个接口,为什么能添加呢?原来在http包里面还定义了一个类型`HandlerFunc`,我们定义的函数`sayhelloName`就是这个HandlerFunc调用之后的结果,这个类型默认就实现了ServeHTTP这个接口,即我们调用了HandlerFunc(f),类似强制类型转换f成为handlerFunc类型,这样f就拥有了ServHTTP方法。 + + type HandlerFunc func(ResponseWriter, *Request) + + // ServeHTTP calls f(w, r). + func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { + f(w, r) + } + +路由器里面存储好了相应的路由规则之后,那么具体的请求又是怎么分发的呢? + +路由器接收到请求之后调用`mux.handler(r).ServeHTTP(w, r)` + +也就是调用对应路由的handler的ServerHTTP接口,那么mux.handler(r)怎么处理的呢? + + func (mux *ServeMux) handler(r *Request) Handler { + mux.mu.RLock() + defer mux.mu.RUnlock() + + // Host-specific pattern takes precedence over generic ones + h := mux.match(r.Host + r.URL.Path) + if h == nil { + h = mux.match(r.URL.Path) + } + if h == nil { + h = NotFoundHandler() + } + return h + } + +原来他是根据用户请求的URL和路由器里面存储的map去匹配的,当匹配到之后返回存储的handler,调用这个handler的ServHTTP接口就可以执行到相应的函数了。 + +通过上面这个介绍,我们了解了整个路由过程,Go其实支持外部实现的路由器 `ListenAndServe`的第二个参数就是用以配置外部路由器的,它是一个Handler接口,即外部路由器只要实现了Handler接口就可以,我们可以在自己实现的路由器的ServHTTP里面实现自定义路由功能。 + +如下代码所示,我们自己实现了一个简易的路由器 + + package main + + import ( + "fmt" + "net/http" + ) + + type MyMux struct { + } + + func (p *MyMux) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/" { + sayhelloName(w, r) + return + } + http.NotFound(w, r) + return + } + + func sayhelloName(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "Hello myroute!") + } + + func main() { + mux := &MyMux{} + http.ListenAndServe(":9090", mux) + } + +## Go代码的执行流程 + +通过对http包的分析之后,现在让我们来梳理一下整个的代码执行过程。 + +- 首先调用Http.HandleFunc + + 按顺序做了几件事: + + 1. 调用了DefaultServerMux的HandleFunc + + 2. 调用了DefaultServerMux的Handle + + 3. 往DefaultServeMux的map[string]muxEntry中增加对应的handler和路由规则 + +- 其次调用http.ListenAndServe(":9090", nil) + + 按顺序做了几件事情: + + 1. 实例化Server + + 2. 调用Server的ListenAndServe() + + 3. 调用net.Listen("tcp", addr)监听端口 + + 4. 启动一个for循环,在循环体中Accept请求 + + 5. 对每个请求实例化一个Conn,并且开启一个goroutine为这个请求进行服务go c.serve() + + 6. 读取每个请求的内容w, err := c.readRequest() + + 7. 判断handler是否为空,如果没有设置handler(这个例子就没有设置handler),handler就设置为DefaultServeMux + + 8. 调用handler的ServeHttp + + 9. 在这个例子中,下面就进入到DefaultServerMux.ServeHttp + + 10. 根据request选择handler,并且进入到这个handler的ServeHTTP + + mux.handler(r).ServeHTTP(w, r) + + 11 选择handler: + + A. 判断是否有路由能满足这个request(循环遍历ServerMux的muxEntry) + + B. 如果有路由满足,调用这个路由handler的ServeHttp + + C. 如果没有路由满足,调用NotFoundHandler的ServeHttp + +## links + * [目录]() + * 上一节: [Go如何使得web工作](<3.3.md>) + * 下一节: [小结](<3.5.md>) diff --git a/3.5.md b/3.5.md index ab0e5360210c9401b50f032059b5bef429727838..b4488e3d51b82f127e4c654c5c7c76fc2d1d624e 100644 --- a/3.5.md +++ b/3.5.md @@ -1,12 +1,9 @@ -# 3.5小结 -这一章我们介绍了HTTP协议, DNS解析的过程, 如何用go实现一个简陋的web server。并深入到net/http包的源码中为大家揭开如何实现此server的秘密。 - -我希望通过这一章的学习,你能够对Go开发Web有了初步的了解,我们也看到相应的代码了,Go开发Web应用是很方便的,同时又是相当的灵活。 - -## links - * [目录]() - * 上一节: [Go的http包详解](<3.4.md>) - * 下一章: [表单](<4.md>) - -## LastModified - * $Id$ \ No newline at end of file +# 3.5小结 +这一章我们介绍了HTTP协议, DNS解析的过程, 如何用go实现一个简陋的web server。并深入到net/http包的源码中为大家揭开如何实现此server的秘密。 + +我希望通过这一章的学习,你能够对Go开发Web有了初步的了解,我们也看到相应的代码了,Go开发Web应用是很方便的,同时又是相当的灵活。 + +## links + * [目录]() + * 上一节: [Go的http包详解](<3.4.md>) + * 下一章: [表单](<4.md>) diff --git a/3.md b/3.md index e519512de7fc6509a63e8c3606d3f71579cf8cd3..573e3c46375bb894873c6fc2a53c63ab16681179 100644 --- a/3.md +++ b/3.md @@ -1,18 +1,15 @@ -# 3 Web基础 - -## 目录 - * 1. [web工作方式](3.1.md) - * 2. [GO搭建一个简单的web服务](3.2.md) - * 3. [Go如何使得web工作](3.3.md) - * 4. [Go的http包详解](3.4.md) - * 5. [小结](3.5.md) - -学习基于Web的编程可能正是你读本书的原因。事实上,如何通过Go来编写Web应用也是我编写这本书的初衷。前面已经介绍过,Go目前已经拥有了成熟的Http处理包,这使得编写能做任何事情的动态Web程序易如反掌。在接下来的各章中将要介绍的内容,都是属于Web编程的范畴。本章则集中讨论一些与Web相关的概念和Go如何运行Web程序的话题。 - -## links - * [目录]() - * 上一章: [第二章总结](<2.8.md>) - * 下一节: [web工作方式](<3.1.md>) - -## LastModified - * $Id$ \ No newline at end of file +# 3 Web基础 + +## 目录 + * 1. [web工作方式](3.1.md) + * 2. [GO搭建一个简单的web服务](3.2.md) + * 3. [Go如何使得web工作](3.3.md) + * 4. [Go的http包详解](3.4.md) + * 5. [小结](3.5.md) + +学习基于Web的编程可能正是你读本书的原因。事实上,如何通过Go来编写Web应用也是我编写这本书的初衷。前面已经介绍过,Go目前已经拥有了成熟的Http处理包,这使得编写能做任何事情的动态Web程序易如反掌。在接下来的各章中将要介绍的内容,都是属于Web编程的范畴。本章则集中讨论一些与Web相关的概念和Go如何运行Web程序的话题。 + +## links + * [目录]() + * 上一章: [第二章总结](<2.8.md>) + * 下一节: [web工作方式](<3.1.md>) diff --git a/4.1.md b/4.1.md index 07216d097a519a678de29f841d30e123aa8a0afd..ebabce88a631a924aed5acec619062fd69e1c486 100644 --- a/4.1.md +++ b/4.1.md @@ -8,9 +8,9 @@
- 用户名: - 密码: - + 用户名: + 密码: +
@@ -64,7 +64,7 @@ http包里面有一个很简单的方式就可以获取,我们在前面web的 } } - + 通过上面的代码我们可以看出获取请求方法是通过`r.Method`来完成的,这是个字符串类型的变量,返回GET, POST, PUT等method信息。 login函数中我们根据`r.Method`来判断是显示登录界面还是处理登录逻辑。当GET方式请求时显示登录界面,其他方式请求时则处理登录逻辑,如查询数据库、验证登录信息等。 @@ -75,7 +75,7 @@ login函数中我们根据`r.Method`来判断是显示登录界面还是处理 我们输入用户名和密码之后发现在服务器端是不会打印出来任何东西的,为什么呢?默认情况下,Handler里面是不会自动解析form的,必须显式的调用`r.ParseForm()`后,你才能对这个参数进行操作。我们修改一下代码,在`fmt.Println("username:", r.Form["username"])`之前加一行`r.ParseForm()`,重新编译,再次测试输入递交,现在是不是在服务器端有输出你的输入的用户名和密码了。 -`r.Form`里面包含了所有请求的URL中query-string、POST的数据、PUT的数据,所有当你在URL的query-string字段和POST冲突时,会保存成一个slice,里面存储了多个值,Go官方文档中说在接下来的版本里面将会把POST、GET这些数据分离开来。 +`r.Form`里面包含了所有请求的URL中query-string、POST的数据、PUT的数据,所有当你在URL的query-string字段和POST冲突时,会保存成一个slice,里面存储了多个值,Go官方文档中说在接下来的版本里面将会把POST、GET这些数据分离开来。 现在我们修改一下login.gtpl里面form的action值`http://127.0.0.1:9090/login`修改为`http://127.0.0.1:9090/login?username=astaxie`,再次测试,服务器的输出username是不是一个slice。服务器端的输出如下: @@ -99,6 +99,3 @@ Tips: Request请求也提供了FormValue()函数来获取用户提交的参数 * [目录]() * 上一节: [表单](<4.md>) * 下一节: [验证表单的输入](<4.2.md>) - -## LastModified - * $Id$ diff --git a/4.2.md b/4.2.md index 4abe75416dff92a498057924333912a668a22932..47e5583cddfcc391dcc609bc453914a963e61657 100644 --- a/4.2.md +++ b/4.2.md @@ -29,7 +29,7 @@ } 还有一种方式就是正则匹配的方式 - + if m, _ := regexp.MatchString("^[0-9]+$", r.Form.Get("age")); !m { return false } @@ -76,13 +76,13 @@ 如果我们想要判断表单里面` - + 那么我们可以这样来验证 slice:=[]string{"apple","pear","banane"} @@ -93,7 +93,7 @@ } } return false - + 上面这个函数包含在我开源的一个库里面(操作slice和map的库),[https://github.com/astaxie/beeku](https://github.com/astaxie/beeku) ## 单选按钮 @@ -129,7 +129,7 @@ } return false - + ## 日期和时间 你想确定用户填写的日期或时间是否有效。例如 @@ -155,12 +155,9 @@ Go里面提供了一个time的处理包,我们可以把用户的输入年月 return false } -上面列出了我们一些常用的服务器端的表单元素验证,希望通过这个引导入门,能够让你对Go的数据验证有所了解,特别是Go里面的正则处理。 +上面列出了我们一些常用的服务器端的表单元素验证,希望通过这个引导入门,能够让你对Go的数据验证有所了解,特别是Go里面的正则处理。 ## links * [目录]() * 上一节: [处理表单的输入](<4.1.md>) * 下一节: [预防跨站脚本](<4.3.md>) - -## LastModified - * $Id$ \ No newline at end of file diff --git a/4.3.md b/4.3.md index fcd9379932d56ff950036b55b955fc546c2720cd..c6680497db1f57beb06ff03cc236d0310d3a00d3 100644 --- a/4.3.md +++ b/4.3.md @@ -1,69 +1,66 @@ -# 4.3 预防跨站脚本 - -现在的网站包含大量的动态内容以提高用户体验,比过去要复杂得多。所谓动态内容,就是根据用户环境和需要,Web应用程序能够输出相应的内容。动态站点会受到一种名为“跨站脚本攻击”(Cross Site Scripting, 安全专家们通常将其缩写成 XSS)的威胁,而静态站点则完全不受其影响。 - -攻击者通常会在有漏洞的程序中插入JavaScript、VBScript、 ActiveX或Flash以欺骗用户。一旦得手,他们可以盗取用户帐户,修改用户设置,盗取/污染cookie,做虚假广告等。 - -对XSS最佳的防护应该结合以下两种方法:验证所有输入数据,有效检测攻击(这个我们前面小节已经有过介绍);对所有输出数据进行适当的编码,以防止任何已成功注入的脚本在浏览器端运行。 - -那么Go里面是怎么做这个有效防护的呢?Go的html/template里面带有下面几个函数可以帮你转义 - -- func HTMLEscape(w io.Writer, b []byte) //把b进行转义之后写到w -- func HTMLEscapeString(s string) string //转义s之后返回结果字符串 -- func HTMLEscaper(args ...interface{}) string //支持多个参数一起转义,返回结果字符串 - - -我们看4.1小节的例子 - - fmt.Println("username:", template.HTMLEscapeString(r.Form.Get("username"))) //输出到服务器端 - fmt.Println("password:", template.HTMLEscapeString(r.Form.Get("password"))) - template.HTMLEscape(w, []byte(r.Form.Get("username"))) //输出到客户端 - -如果我们输入的username是``,那么我们可以在浏览器上面看到输出如下所示: - -![](images/4.3.escape.png?raw=true) - -那么我们在输出我们的模板的时候怎么处理的呢?Go的html/template包默认帮你过滤了html元素,但是有时候你又想输出这样的信息,请使用text/template。请看下面的例子所示 - - import "text/template" - ... - t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`) - err = t.ExecuteTemplate(out, "T", "") - -输出 - - Hello, ! - -或者使用template.HTML类型 - - import "html/template" - ... - t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`) - err = t.ExecuteTemplate(out, "T", template.HTML("")) - -输出 - - Hello, ! - -转换成template.HTML后,变量的内容也不会被转义 - -转义的例子: - - import "html/template" - ... - t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`) - err = t.ExecuteTemplate(out, "T", "") - -转义之后的输出: - - Hello, <script>alert('you have been pwned')</script>! - - - -## links - * [目录]() - * 上一节: [验证的输入](<4.2.md>) - * 下一节: [防止多次递交表单](<4.4.md>) - -## LastModified - * $Id$ +# 4.3 预防跨站脚本 + +现在的网站包含大量的动态内容以提高用户体验,比过去要复杂得多。所谓动态内容,就是根据用户环境和需要,Web应用程序能够输出相应的内容。动态站点会受到一种名为“跨站脚本攻击”(Cross Site Scripting, 安全专家们通常将其缩写成 XSS)的威胁,而静态站点则完全不受其影响。 + +攻击者通常会在有漏洞的程序中插入JavaScript、VBScript、 ActiveX或Flash以欺骗用户。一旦得手,他们可以盗取用户帐户,修改用户设置,盗取/污染cookie,做虚假广告等。 + +对XSS最佳的防护应该结合以下两种方法:验证所有输入数据,有效检测攻击(这个我们前面小节已经有过介绍);对所有输出数据进行适当的编码,以防止任何已成功注入的脚本在浏览器端运行。 + +那么Go里面是怎么做这个有效防护的呢?Go的html/template里面带有下面几个函数可以帮你转义 + +- func HTMLEscape(w io.Writer, b []byte) //把b进行转义之后写到w +- func HTMLEscapeString(s string) string //转义s之后返回结果字符串 +- func HTMLEscaper(args ...interface{}) string //支持多个参数一起转义,返回结果字符串 + + +我们看4.1小节的例子 + + fmt.Println("username:", template.HTMLEscapeString(r.Form.Get("username"))) //输出到服务器端 + fmt.Println("password:", template.HTMLEscapeString(r.Form.Get("password"))) + template.HTMLEscape(w, []byte(r.Form.Get("username"))) //输出到客户端 + +如果我们输入的username是``,那么我们可以在浏览器上面看到输出如下所示: + +![](images/4.3.escape.png?raw=true) + +那么我们在输出我们的模板的时候怎么处理的呢?Go的html/template包默认帮你过滤了html元素,但是有时候你又想输出这样的信息,请使用text/template。请看下面的例子所示 + + import "text/template" + ... + t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`) + err = t.ExecuteTemplate(out, "T", "") + +输出 + + Hello, ! + +或者使用template.HTML类型 + + import "html/template" + ... + t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`) + err = t.ExecuteTemplate(out, "T", template.HTML("")) + +输出 + + Hello, ! + +转换成template.HTML后,变量的内容也不会被转义 + +转义的例子: + + import "html/template" + ... + t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`) + err = t.ExecuteTemplate(out, "T", "") + +转义之后的输出: + + Hello, <script>alert('you have been pwned')</script>! + + + +## links + * [目录]() + * 上一节: [验证的输入](<4.2.md>) + * 下一节: [防止多次递交表单](<4.4.md>) diff --git a/4.4.md b/4.4.md index aeaf4e91fed78eb9dd48b9c9df8016f9361b0da3..87033c86b2038a50fa0a56313608b28892bb846f 100644 --- a/4.4.md +++ b/4.4.md @@ -1,56 +1,53 @@ -# 4.4防止多次递交表单 - -不知道你是否曾经看到过一个论坛或者博客,在一个帖子或者文章后面出现多条重复的记录,这些大多数是因为用户重复递交了留言的表单引起的。由于种种原因,用户经常会重复递交表单。通常这只是鼠标的误操作,如双击了递交按钮,也可能是为了编辑或者再次核对填写过的信息,点击了浏览器的后退按钮,然后又再次点击了递交按钮而不是浏览器的前进按钮。当然,也可能是故意的——比如,在某项在线调查或者博彩活动中重复投票。那我们如何有效的防止用户多次递交相同的表单呢? - -解决方案是在表单中添加一个带有唯一值的隐藏字段。在验证表单时,先检查带有该惟一值的表单是否已经递交过了。如果是,拒绝再次递交;如果不是,则处理表单进行逻辑处理。另外,如果是采用了Ajax模式递交表单的话,当表单递交后,通过javascript来禁用表单的递交按钮。 - -我继续拿4.2小节的例子优化: - - 用户名: - 密码: - - - -我们在模版里面增加了一个隐藏字段`token`,这个值我们通过MD5(时间戳)来获取惟一值,然后我们把这个值存储到服务器端(session来控制,我们将在第六章讲解如何保存),以方便表单提交时比对判定。 - - func login(w http.ResponseWriter, r *http.Request) { - fmt.Println("method:", r.Method) //获取请求的方法 - if r.Method == "GET" { - crutime := time.Now().Unix() - h := md5.New() - io.WriteString(h, strconv.FormatInt(crutime, 10)) - token := fmt.Sprintf("%x", h.Sum(nil)) - - t, _ := template.ParseFiles("login.gtpl") - t.Execute(w, token) - } else { - //请求的是登陆数据,那么执行登陆的逻辑判断 - r.ParseForm() - token := r.Form.Get("token") - if token != "" { - //验证token的合法性 - } else { - //不存在token报错 - } - fmt.Println("username length:", len(r.Form["username"][0])) - fmt.Println("username:", template.HTMLEscapeString(r.Form.Get("username"))) //输出到服务器端 - fmt.Println("password:", template.HTMLEscapeString(r.Form.Get("password"))) - template.HTMLEscape(w, []byte(r.Form.Get("username"))) //输出到客户端 - } - } - -上面的代码输出到页面的源码如下: - -![](images/4.4.token.png?raw=true) - -我们看到token已经有输出值,你可以不断的刷新,可以看到这个值在不断的变化。这样就保证了每次显示form表单的时候都是唯一的,用户递交的表单保持了唯一性。 - -我们的解决方案可以防止非恶意的攻击,并能使恶意用户暂时不知所措,然后,它却不能排除所有的欺骗性的动机,对此类情况还需要更复杂的工作。 - -## links - * [目录]() - * 上一节: [预防跨站脚本](<4.3.md>) - * 下一节: [处理文件上传](<4.5.md>) - -## LastModified - * $Id$ \ No newline at end of file +# 4.4防止多次递交表单 + +不知道你是否曾经看到过一个论坛或者博客,在一个帖子或者文章后面出现多条重复的记录,这些大多数是因为用户重复递交了留言的表单引起的。由于种种原因,用户经常会重复递交表单。通常这只是鼠标的误操作,如双击了递交按钮,也可能是为了编辑或者再次核对填写过的信息,点击了浏览器的后退按钮,然后又再次点击了递交按钮而不是浏览器的前进按钮。当然,也可能是故意的——比如,在某项在线调查或者博彩活动中重复投票。那我们如何有效的防止用户多次递交相同的表单呢? + +解决方案是在表单中添加一个带有唯一值的隐藏字段。在验证表单时,先检查带有该惟一值的表单是否已经递交过了。如果是,拒绝再次递交;如果不是,则处理表单进行逻辑处理。另外,如果是采用了Ajax模式递交表单的话,当表单递交后,通过javascript来禁用表单的递交按钮。 + +我继续拿4.2小节的例子优化: + + 用户名: + 密码: + + + +我们在模版里面增加了一个隐藏字段`token`,这个值我们通过MD5(时间戳)来获取惟一值,然后我们把这个值存储到服务器端(session来控制,我们将在第六章讲解如何保存),以方便表单提交时比对判定。 + + func login(w http.ResponseWriter, r *http.Request) { + fmt.Println("method:", r.Method) //获取请求的方法 + if r.Method == "GET" { + crutime := time.Now().Unix() + h := md5.New() + io.WriteString(h, strconv.FormatInt(crutime, 10)) + token := fmt.Sprintf("%x", h.Sum(nil)) + + t, _ := template.ParseFiles("login.gtpl") + t.Execute(w, token) + } else { + //请求的是登陆数据,那么执行登陆的逻辑判断 + r.ParseForm() + token := r.Form.Get("token") + if token != "" { + //验证token的合法性 + } else { + //不存在token报错 + } + fmt.Println("username length:", len(r.Form["username"][0])) + fmt.Println("username:", template.HTMLEscapeString(r.Form.Get("username"))) //输出到服务器端 + fmt.Println("password:", template.HTMLEscapeString(r.Form.Get("password"))) + template.HTMLEscape(w, []byte(r.Form.Get("username"))) //输出到客户端 + } + } + +上面的代码输出到页面的源码如下: + +![](images/4.4.token.png?raw=true) + +我们看到token已经有输出值,你可以不断的刷新,可以看到这个值在不断的变化。这样就保证了每次显示form表单的时候都是唯一的,用户递交的表单保持了唯一性。 + +我们的解决方案可以防止非恶意的攻击,并能使恶意用户暂时不知所措,然后,它却不能排除所有的欺骗性的动机,对此类情况还需要更复杂的工作。 + +## links + * [目录]() + * 上一节: [预防跨站脚本](<4.3.md>) + * 下一节: [处理文件上传](<4.5.md>) diff --git a/4.5.md b/4.5.md index 3872d22887bce2ae00c900da6a6517fe789eb99a..c54279401f6a10ed286716cba4c667895cbd5d0c 100644 --- a/4.5.md +++ b/4.5.md @@ -1,157 +1,154 @@ -# 4.5处理文件上传 -你想处理一个由用户上传的文件。比如你正在建设一个类似Instagram的网站,你需要存储用户拍摄的照片。这种需求该如何实现呢? - -文件要能够上传,首先第一步就是要修改form的`enctype`属性,`enctype`属性有如下三种情况: - - application/x-www-form-urlencoded 表示在发送前编码所有字符(默认) - multipart/form-data 不对字符编码。在使用包含文件上传控件的表单时,必须使用该值。 - text/plain 空格转换为 "+" 加号,但不对特殊字符编码。 - -如果要支持文件上传,我们的html应该类似于: - - - - 上传文件 - - -
- - - -
- - - -在服务器端,我们增加一个handlerFunc: - - http.HandleFunc("/upload", upload) - - // upload - func upload(w http.ResponseWriter, r *http.Request) { - fmt.Println("method:", r.Method) //获取请求的方法 - if r.Method == "GET" { - crutime := time.Now().Unix() - h := md5.New() - io.WriteString(h, strconv.FormatInt(crutime, 10)) - token := fmt.Sprintf("%x", h.Sum(nil)) - - t, _ := template.ParseFiles("upload.gtpl") - t.Execute(w, token) - } else { - r.ParseMultipartForm(32 << 20) - file, handler, err := r.FormFile("file") - if err != nil { - fmt.Println(err) - return - } - defer file.Close() - fmt.Fprintf(w, "%v", handler.Header) - f, err := os.OpenFile("./test/"+handler.Filename, os.O_WRONLY|os.O_CREATE, 0666) - if err != nil { - fmt.Println(err) - return - } - defer f.Close() - io.Copy(f, file) - } - } - -通过上面的代码可以看到,处理文件上传我们需要调用`r.ParseMultipartForm`,里面的参数表示`maxMemory`,调用`ParseMultipartForm`之后,上传的文件存储在`maxMemory`大小的内存里面,如果文件大小超过了`maxMemory`,那么剩下的部分将存储在系统的临时文件中。我们可以通过`r.FormFile`获取上面的文件句柄,然后实例中使用了`io.Copy`来存储文件。 - ->获取其他非文件字段信息的时候就不需要调用`r.ParseForm`,因为在需要的时候Go自动会去调用。而且`ParseMultipartForm`调用一次之后,后面再次调用不会再有效果。 - -通过上面的实例我们可以看到我们上传文件主要三步处理: - -- 1、表单中增加enctype="multipart/form-data" -- 2、服务端调用`r.ParseMultipartForm`,把上传的文件存储在内存和临时文件中 -- 3、使用`r.FormFile`获取文件句柄,然后对文件进行存储等处理。 - -文件handler是multipart.FileHeader,里面存储了如下结构信息 - - type FileHeader struct { - Filename string - Header textproto.MIMEHeader - // contains filtered or unexported fields - } - -我们通过上面的实例代码打印出来上传文件的信息如下 - -![](images/4.5.upload2.png?raw=true) - - -## 客户端上传文件 - -我们上面的例子演示了如何通过表单上传文件,然后在服务器端处理文件,其实Go支持模拟客户端表单功能支持文件上传,详细用法请看如下示例: - - package main - - import ( - "bytes" - "fmt" - "io" - "io/ioutil" - "mime/multipart" - "net/http" - "os" - ) - - func postFile(filename string, targetUrl string) error { - bodyBuf := &bytes.Buffer{} - bodyWriter := multipart.NewWriter(bodyBuf) - - //关键的一步操作 - fileWriter, err := bodyWriter.CreateFormFile("file", filename) - if err != nil { - fmt.Println("error writing to buffer") - return err - } - - //打开文件句柄操作 - fh, err := os.Open(filename) - if err != nil { - fmt.Println("error opening file") - return err - } - - //iocopy - _, err = io.Copy(fileWriter, fh) - if err != nil { - return err - } - - contentType := bodyWriter.FormDataContentType() - bodyWriter.Close() - - resp, err := http.Post(targetUrl, contentType, bodyBuf) - if err != nil { - return err - } - defer resp.Body.Close() - resp_body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return err - } - fmt.Println(resp.Status) - fmt.Println(string(resp_body)) - return nil - } - - // sample usage - func main() { - target_url := "http://localhost:9090/upload" - filename := "./astaxie.pdf" - postFile(filename, target_url) - } - - -上面的例子详细展示了如何上传一个文件,客户端上传文件通过multipart的Write把文件信息写入缓存,然后调用http的post方法上传文件。 - ->如果你还有其他普通字段例如username之类的需要同时写入,那么可以调用multipart的WriteField方法写很多其他类似的字段。 - -## links - * [目录]() - * 上一节: [防止多次递交表单](<4.4.md>) - * 下一节: [小结](<4.6.md>) - -## LastModified - * $Id$ +# 4.5处理文件上传 +你想处理一个由用户上传的文件。比如你正在建设一个类似Instagram的网站,你需要存储用户拍摄的照片。这种需求该如何实现呢? + +文件要能够上传,首先第一步就是要修改form的`enctype`属性,`enctype`属性有如下三种情况: + + application/x-www-form-urlencoded 表示在发送前编码所有字符(默认) + multipart/form-data 不对字符编码。在使用包含文件上传控件的表单时,必须使用该值。 + text/plain 空格转换为 "+" 加号,但不对特殊字符编码。 + +如果要支持文件上传,我们的html应该类似于: + + + + 上传文件 + + +
+ + + +
+ + + +在服务器端,我们增加一个handlerFunc: + + http.HandleFunc("/upload", upload) + + // upload + func upload(w http.ResponseWriter, r *http.Request) { + fmt.Println("method:", r.Method) //获取请求的方法 + if r.Method == "GET" { + crutime := time.Now().Unix() + h := md5.New() + io.WriteString(h, strconv.FormatInt(crutime, 10)) + token := fmt.Sprintf("%x", h.Sum(nil)) + + t, _ := template.ParseFiles("upload.gtpl") + t.Execute(w, token) + } else { + r.ParseMultipartForm(32 << 20) + file, handler, err := r.FormFile("file") + if err != nil { + fmt.Println(err) + return + } + defer file.Close() + fmt.Fprintf(w, "%v", handler.Header) + f, err := os.OpenFile("./test/"+handler.Filename, os.O_WRONLY|os.O_CREATE, 0666) + if err != nil { + fmt.Println(err) + return + } + defer f.Close() + io.Copy(f, file) + } + } + +通过上面的代码可以看到,处理文件上传我们需要调用`r.ParseMultipartForm`,里面的参数表示`maxMemory`,调用`ParseMultipartForm`之后,上传的文件存储在`maxMemory`大小的内存里面,如果文件大小超过了`maxMemory`,那么剩下的部分将存储在系统的临时文件中。我们可以通过`r.FormFile`获取上面的文件句柄,然后实例中使用了`io.Copy`来存储文件。 + +>获取其他非文件字段信息的时候就不需要调用`r.ParseForm`,因为在需要的时候Go自动会去调用。而且`ParseMultipartForm`调用一次之后,后面再次调用不会再有效果。 + +通过上面的实例我们可以看到我们上传文件主要三步处理: + +- 1、表单中增加enctype="multipart/form-data" +- 2、服务端调用`r.ParseMultipartForm`,把上传的文件存储在内存和临时文件中 +- 3、使用`r.FormFile`获取文件句柄,然后对文件进行存储等处理。 + +文件handler是multipart.FileHeader,里面存储了如下结构信息 + + type FileHeader struct { + Filename string + Header textproto.MIMEHeader + // contains filtered or unexported fields + } + +我们通过上面的实例代码打印出来上传文件的信息如下 + +![](images/4.5.upload2.png?raw=true) + + +## 客户端上传文件 + +我们上面的例子演示了如何通过表单上传文件,然后在服务器端处理文件,其实Go支持模拟客户端表单功能支持文件上传,详细用法请看如下示例: + + package main + + import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "mime/multipart" + "net/http" + "os" + ) + + func postFile(filename string, targetUrl string) error { + bodyBuf := &bytes.Buffer{} + bodyWriter := multipart.NewWriter(bodyBuf) + + //关键的一步操作 + fileWriter, err := bodyWriter.CreateFormFile("file", filename) + if err != nil { + fmt.Println("error writing to buffer") + return err + } + + //打开文件句柄操作 + fh, err := os.Open(filename) + if err != nil { + fmt.Println("error opening file") + return err + } + + //iocopy + _, err = io.Copy(fileWriter, fh) + if err != nil { + return err + } + + contentType := bodyWriter.FormDataContentType() + bodyWriter.Close() + + resp, err := http.Post(targetUrl, contentType, bodyBuf) + if err != nil { + return err + } + defer resp.Body.Close() + resp_body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + fmt.Println(resp.Status) + fmt.Println(string(resp_body)) + return nil + } + + // sample usage + func main() { + target_url := "http://localhost:9090/upload" + filename := "./astaxie.pdf" + postFile(filename, target_url) + } + + +上面的例子详细展示了如何上传一个文件,客户端上传文件通过multipart的Write把文件信息写入缓存,然后调用http的post方法上传文件。 + +>如果你还有其他普通字段例如username之类的需要同时写入,那么可以调用multipart的WriteField方法写很多其他类似的字段。 + +## links + * [目录]() + * 上一节: [防止多次递交表单](<4.4.md>) + * 下一节: [小结](<4.6.md>) diff --git a/4.6.md b/4.6.md index a70cafa60a12704d79c74963e9bd1495ea407e75..408b26abe4372edeb8fcd678fa2f21cbfb58d178 100644 --- a/4.6.md +++ b/4.6.md @@ -1,12 +1,9 @@ -# 4.6 小结 -这一章里面我们学习了Go里面如何处理表单信息,我们通过一个登陆、一个上传例子展示了Go处理form表单信息,处理文件上传的能力。但是在处理表单过程中我们需要验证用户输入的信息,考虑到网站安全、数据过滤就变得相当重要了,因此专门一个小节讲解了各方面的数据过滤,顺带讲了一下Go里面对正则的处理。 - -通过这一章能够让你了解客户端和服务器端如何进行数据的交互,让客户端的数据进入我们的系服务器统,让我们系统处理之后的数据展现给客户端。 - -## links - * [目录]() - * 上一节: [处理文件上传](<4.5.md>) - * 下一章: [访问数据库](<5.md>) - -## LastModified - * $Id$ \ No newline at end of file +# 4.6 小结 +这一章里面我们学习了Go里面如何处理表单信息,我们通过一个登陆、一个上传例子展示了Go处理form表单信息,处理文件上传的能力。但是在处理表单过程中我们需要验证用户输入的信息,考虑到网站安全、数据过滤就变得相当重要了,因此专门一个小节讲解了各方面的数据过滤,顺带讲了一下Go里面对正则的处理。 + +通过这一章能够让你了解客户端和服务器端如何进行数据的交互,让客户端的数据进入我们的系服务器统,让我们系统处理之后的数据展现给客户端。 + +## links + * [目录]() + * 上一节: [处理文件上传](<4.5.md>) + * 下一章: [访问数据库](<5.md>) diff --git a/4.md b/4.md index d4a52cf8c6fc52b8ef12ac85faea7fce2769aded..0a20efb070d245810ab32482dfbedefd5731c3f8 100644 --- a/4.md +++ b/4.md @@ -1,34 +1,31 @@ -# 4 表单 - -## 目录 - * 1. [处理表单的输入](4.1.md) - * 2. [验证表单的输入](4.2.md) - * 3. [预防跨站脚本](4.3.md) - * 4. [防止多次递交表单](4.4.md) - * 5. [处理文件上传](4.5.md) - * 6. [小结](4.6.md) - -表单是我们平常编写Web应用常用的工具,通过表单我们可以方便的让客户端和服务器进行数据的交互。对于以前开发过Web的用户来说表单都非常熟悉,但是对于C/C++程序员来说,这可能是一个有些陌生的东西,那么什么是表单呢? - -表单是一个包含表单元素的区域。表单元素是允许用户在表单中(比如:文本域、下拉列表、单选框、复选框等等)输入信息的元素。表单使用表单标签(\)定义。 - -
- ... - input 元素 - ... -
- -Go里面对于form处理已经有很方便的方法了,在Request里面的有专门的form处理,可以很方便的整合到Web开发里面来,4.1小节里面将讲解Go如何处理表单的输入。由于不能信任任何用户的输入,所以我们需要对这些输入进行有效性验证,4.2小节将就如何进行一些普通的验证进行详细的演示。 - -HTTP协议是一种无状态的协议,那么如何才能辨别是否是同一个用户呢?同时又如何保证一个表单不出现多次递交的情况呢?4.3和4.4小节里面将对cookie(cookie是存储在客户端的信息,能够每次通过header和服务器进行交互的数据)等进行详细讲解。 - -表单还有一个很大的功能就是能够上传文件,那么Go是如何处理文件上传的呢?针对大文件上传我们如何有效的处理呢?4.5小节我们将一起学习Go处理文件上传的知识。 - - -## links - * [目录]() - * 上一章: [第三章总结](<3.5.md>) - * 下一节: [处理表单的输入](<4.1.md>) - -## LastModified - * $Id$ \ No newline at end of file +# 4 表单 + +## 目录 + * 1. [处理表单的输入](4.1.md) + * 2. [验证表单的输入](4.2.md) + * 3. [预防跨站脚本](4.3.md) + * 4. [防止多次递交表单](4.4.md) + * 5. [处理文件上传](4.5.md) + * 6. [小结](4.6.md) + +表单是我们平常编写Web应用常用的工具,通过表单我们可以方便的让客户端和服务器进行数据的交互。对于以前开发过Web的用户来说表单都非常熟悉,但是对于C/C++程序员来说,这可能是一个有些陌生的东西,那么什么是表单呢? + +表单是一个包含表单元素的区域。表单元素是允许用户在表单中(比如:文本域、下拉列表、单选框、复选框等等)输入信息的元素。表单使用表单标签(\)定义。 + +
+ ... + input 元素 + ... +
+ +Go里面对于form处理已经有很方便的方法了,在Request里面的有专门的form处理,可以很方便的整合到Web开发里面来,4.1小节里面将讲解Go如何处理表单的输入。由于不能信任任何用户的输入,所以我们需要对这些输入进行有效性验证,4.2小节将就如何进行一些普通的验证进行详细的演示。 + +HTTP协议是一种无状态的协议,那么如何才能辨别是否是同一个用户呢?同时又如何保证一个表单不出现多次递交的情况呢?4.3和4.4小节里面将对cookie(cookie是存储在客户端的信息,能够每次通过header和服务器进行交互的数据)等进行详细讲解。 + +表单还有一个很大的功能就是能够上传文件,那么Go是如何处理文件上传的呢?针对大文件上传我们如何有效的处理呢?4.5小节我们将一起学习Go处理文件上传的知识。 + + +## links + * [目录]() + * 上一章: [第三章总结](<3.5.md>) + * 下一节: [处理表单的输入](<4.1.md>) diff --git a/5.1.md b/5.1.md index 557dccda8d29a8a2ff28641b8d4bf49bec139237..f0d1859e7b69cc37cbbd346245c603a90c1502d8 100644 --- a/5.1.md +++ b/5.1.md @@ -1,82 +1,82 @@ -# 5.1 database/sql接口 -Go和PHP不同的地方是,他没有官方提供数据库驱动,而是为开发数据库驱动定义了一些标准接口,第三方用户可以根据定义的接口来开发相应的数据库驱动,这样做有一个好处,我们按照标准接口开发的代码, 在需要迁移数据库时,不需要任何修改。那么Go都定义了那些标准接口呢?让我们来详细的分析一下 - -## sql.Register -这个存在于database/sql的函数是用来注册数据库驱动的,当第三方开发者开发数据库驱动时,都会实现init函数,在init里面会调用这个`Register(name string, driver driver.Driver)`完成本驱动的注册。 - -我们来看一下mymysql、sqlite3的驱动里面都是怎么调用的: - - //https://github.com/mattn/go-sqlite3驱动 - func init() { - sql.Register("sqlite3", &SQLiteDriver{}) - } - - //https://github.com/mikespook/mymysql驱动 - // Driver automatically registered in database/sql - var d = Driver{proto: "tcp", raddr: "127.0.0.1:3306"} - func init() { - Register("SET NAMES utf8") - sql.Register("mymysql", &d) - } - -我们看到第三方驱动都是通过调用这个函数来注册自己的驱动名称以及相应的driver。在database/sql内部通过一个map来存储相应的驱动。 - - var drivers = make(map[string]driver.Driver) - - drivers[name] = driver - -因此通过database/sql的注册函数可以同时注册多个数据库驱动,只要不重复。 - ->在我们使用database/sql接口和第三方库的时候经常看到如下的import -> -> "database/sql" -> _ "github.com/mattn/go-sqlite3" -> ->新手都会被这个`_`所迷惑,其实这个就是Go设计的巧妙之处,我们在变量赋值的时候经常看到这个,它是用来忽略变量的占位符,那么这个包引入也是,这儿的意思是引入此包而不直接使用这个包中定义的函数,变量等资源,我们在2.3流程和函数里面介绍过init函数的初始化过程,包在引入的时候会去调用包的init函数以完成对包的初始化,因此我们引入上面的数据库驱动包之后会去调用init函数,然后在init函数里面注册了这个数据库驱动,这样我们就可以在接下来的代码中直接使用这个数据库驱动了。 - -## driver.Driver -Driver是一个数据库驱动的接口,他定义了一个method: Open(name string),这个方法返回一个数据库的Conn接口。 - - type Driver interface { - Open(name string) (Conn, error) - } - -返回的Conn只能用来进行一次goroutine的操作,也就是说不能把这个Conn应用于Go的多个goroutine里面。如下代码会出现错误 - - ... - go goroutineA (Conn) //执行查询操作 - go goroutineB (Conn) //执行插入操作 - ... - -上面这样的代码可能会导致不知道某个操作究竟是由哪个goroutine发起的,从而数据混乱,比如可能会把goroutine A里面执行的查询操作的结果返回给goroutine B从而使B错误的把此结果当成自己执行的插入结果。 - -第三方驱动都会定义这个函数,它会解析name参数来获取相关数据库的连接信息,解析完成后,它将使用此信息来初始化一个Conn并返回它。 - -## driver.Conn -Conn是一个数据库连接的接口定义,他定义了一系列方法,这个Conn只能应用在一个goroutine里面,不能使用在多个goroutine里面,详情请参考上面的说明。 - - type Conn interface { - Prepare(query string) (Stmt, error) - Close() error - Begin() (Tx, error) - } - -Prepare函数返回与当前连接相关的准备好Sql语句的状态,可以进行查询、删除等操作。 - -Close函数关闭当前的连接,以及执行释放连接拥有的资源等清理工作。因为database/sql里面实现了建议的conn pool,所以你不要再自己去实现缓存conn之类的,这样容易引起问题。 - -Begin函数返回一个代表事务处理的Tx,通过它你可以进行查询,更新等操作,或者对事务进行回滚、递交。 - -## driver.Stmt -Stmt是一种准备好的状态,和Conn相关联,而且是只能应用于一个goroutine中,不能应用在多个goroutine中。 - - type Stmt interface { - Close() error - NumInput() int - Exec(args []Value) (Result, error) - Query(args []Value) (Rows, error) - } - +h# 5.1 database/sql接口 +Go和PHP不同的地方是,他没有官方提供数据库驱动,而是为开发数据库驱动定义了一些标准接口,第三方用户可以根据定义的接口来开发相应的数据库驱动,这样做有一个好处,我们按照标准接口开发的代码, 在需要迁移数据库时,不需要任何修改。那么Go都定义了那些标准接口呢?让我们来详细的分析一下 + +## sql.Register +这个存在于database/sql的函数是用来注册数据库驱动的,当第三方开发者开发数据库驱动时,都会实现init函数,在init里面会调用这个`Register(name string, driver driver.Driver)`完成本驱动的注册。 + +我们来看一下mymysql、sqlite3的驱动里面都是怎么调用的: + + //https://github.com/mattn/go-sqlite3驱动 + func init() { + sql.Register("sqlite3", &SQLiteDriver{}) + } + + //https://github.com/mikespook/mymysql驱动 + // Driver automatically registered in database/sql + var d = Driver{proto: "tcp", raddr: "127.0.0.1:3306"} + func init() { + Register("SET NAMES utf8") + sql.Register("mymysql", &d) + } + +我们看到第三方驱动都是通过调用这个函数来注册自己的驱动名称以及相应的driver。在database/sql内部通过一个map来存储相应的驱动。 + + var drivers = make(map[string]driver.Driver) + + drivers[name] = driver + +因此通过database/sql的注册函数可以同时注册多个数据库驱动,只要不重复。 + +>在我们使用database/sql接口和第三方库的时候经常看到如下的import + + "database/sql" + _ "github.com/mattn/go-sqlite3" + +>新手都会被这个`_`所迷惑,其实这个就是Go设计的巧妙之处,我们在变量赋值的时候经常看到这个,它是用来忽略变量的占位符,那么这个包引入也是,这儿的意思是引入此包而不直接使用这个包中定义的函数,变量等资源,我们在2.3流程和函数里面介绍过init函数的初始化过程,包在引入的时候会去调用包的init函数以完成对包的初始化,因此我们引入上面的数据库驱动包之后会去调用init函数,然后在init函数里面注册了这个数据库驱动,这样我们就可以在接下来的代码中直接使用这个数据库驱动了。 + +## driver.Driver +Driver是一个数据库驱动的接口,他定义了一个method: Open(name string),这个方法返回一个数据库的Conn接口。 + + type Driver interface { + Open(name string) (Conn, error) + } + +返回的Conn只能用来进行一次goroutine的操作,也就是说不能把这个Conn应用于Go的多个goroutine里面。如下代码会出现错误 + + ... + go goroutineA (Conn) //执行查询操作 + go goroutineB (Conn) //执行插入操作 + ... + +上面这样的代码可能会导致不知道某个操作究竟是由哪个goroutine发起的,从而数据混乱,比如可能会把goroutine A里面执行的查询操作的结果返回给goroutine B从而使B错误的把此结果当成自己执行的插入结果。 + +第三方驱动都会定义这个函数,它会解析name参数来获取相关数据库的连接信息,解析完成后,它将使用此信息来初始化一个Conn并返回它。 + +## driver.Conn +Conn是一个数据库连接的接口定义,他定义了一系列方法,这个Conn只能应用在一个goroutine里面,不能使用在多个goroutine里面,详情请参考上面的说明。 + + type Conn interface { + Prepare(query string) (Stmt, error) + Close() error + Begin() (Tx, error) + } + +Prepare函数返回与当前连接相关的准备好Sql语句的状态,可以进行查询、删除等操作。 + +Close函数关闭当前的连接,以及执行释放连接拥有的资源等清理工作。因为database/sql里面实现了建议的conn pool,所以你不要再自己去实现缓存conn之类的,这样容易引起问题。 + +Begin函数返回一个代表事务处理的Tx,通过它你可以进行查询,更新等操作,或者对事务进行回滚、递交。 + +## driver.Stmt +Stmt是一种准备好的状态,和Conn相关联,而且是只能应用于一个goroutine中,不能应用在多个goroutine中。 + + type Stmt interface { + Close() error + NumInput() int + Exec(args []Value) (Result, error) + Query(args []Value) (Rows, error) + } + Close函数关闭当前的链接状态,但是如果当前正在执行query,query还是有效返回rows数据。 NumInput函数返回当前预留参数的个数,当返回>=0时数据库驱动就会智能检查调用者的参数。当数据库驱动包不知道预留参数的时候,返回-1。 @@ -84,67 +84,64 @@ NumInput函数返回当前预留参数的个数,当返回>=0时数据库驱动 Exec函数执行Prepare准备好的sql,传入参数执行update/insert等操作,返回Result数据 Query函数执行Prepare准备好的sql,传入需要的参数执行select操作,返回Rows结果集 - - -## driver.Tx -事务处理一般就两个过程,递交或者回滚。数据库驱动里面也只需要实现这两个函数就可以 - - type Tx interface { - Commit() error - Rollback() error - } + + +## driver.Tx +事务处理一般就两个过程,递交或者回滚。数据库驱动里面也只需要实现这两个函数就可以 + + type Tx interface { + Commit() error + Rollback() error + } 这两个函数一个用来递交一个事务,一个用来回滚事务。 - + ## driver.Execer 这是一个Conn可选择实现的接口 type Execer interface { - Exec(query string, args []Value) (Result, error) + Exec(query string, args []Value) (Result, error) } - -如果这个接口没有定义,那么在调用DB.Exec,就会首先调用Prepare返回Stmt,然后执行Stmt的Exec,然后关闭Stmt。 - + +如果这个接口没有定义,那么在调用DB.Exec,就会首先调用Prepare返回Stmt,然后执行Stmt的Exec,然后关闭Stmt。 + ## driver.Result 这个是执行Update/Insert等操作返回的结果接口定义 type Result interface { - LastInsertId() (int64, error) - RowsAffected() (int64, error) + LastInsertId() (int64, error) + RowsAffected() (int64, error) } - -LastInsertId函数返回由数据库执行插入操作得到的自动增长ID号。 - -RowsAffected函数返回query操作影响的数据条目数。 - + +LastInsertId函数返回由数据库执行插入操作得到的自动增长ID号。 + +RowsAffected函数返回query操作影响的数据条目数。 + ## driver.Rows Rows是执行查询返回的结果集接口定义 type Rows interface { - - Columns() []string - - Close() error - - Next(dest []Value) error + Columns() []string + Close() error + Next(dest []Value) error } - + Columns函数返回查询数据库表的字段信息,这个返回的slice和你sql查询的字段一一对应,而不是返回整个表的所有字段。 Close函数用来关闭Rows迭代器。 Next函数用来返回下一条数据,把数据赋值给dest。dest里面的元素必须是driver.Value的值除了string,返回的数据里面所有的string都必须要转换成[]byte。如果最后没数据了,Next函数最后返回io.EOF。 - - + + ## driver.RowsAffected RowsAffested其实就是一个int64的别名,但是他实现了Result接口,用来底层实现Result的表示方式 type RowsAffected int64 - + func (RowsAffected) LastInsertId() (int64, error) - - func (v RowsAffected) RowsAffected() (int64, error) - + + func (v RowsAffected) RowsAffected() (int64, error) + ## driver.Value Value其实就是一个空接口,他可以容纳任何的数据 @@ -157,13 +154,13 @@ Value的值必须所有的驱动里面控制的,Value要么是nil,要么是 bool []byte string [*]除了Rows.Next返回的不能是string. - time.Time + time.Time ## driver.ValueConverter ValueConverter接口定义了如何把一个普通的值转化成driver.Value的接口 type ValueConverter interface { - ConvertValue(v interface{}) (Value, error) + ConvertValue(v interface{}) (Value, error) } 在开发的数据库驱动包里面实现这个接口的函数在很多地方会使用到,这个ValueConverter有很多好处: @@ -176,7 +173,7 @@ ValueConverter接口定义了如何把一个普通的值转化成driver.Value的 Valuer接口定义了返回一个driver.Value的方式 type Valuer interface { - Value() (Value, error) + Value() (Value, error) } 很多类型都实现了这个Value方法,用来自身与driver.Value的转化。 @@ -192,14 +189,11 @@ database/sql在database/sql/driver提供的接口基础上定义了一些更高 freeConn []driver.Conn closed bool } - + 我们可以看到Open函数返回的是DB对象,里面有一个freeConn,,它就是那个简易的连接池。它的实现相当简单或者说简陋,就是当执行Db.prepare的时候会defer:db.putConn(ci, err),也就是放入连接池,每次调用conn的时候会先判断freeConn的长度是否大于0,大于0说明有可以复用的conn,直接拿出来用就是了,如果不大于0,则创建一个conn,然后再返回之。 - -## links - * [目录]() - * 上一节: [访问数据库](<5.md>) - * 下一节: [使用MySQL数据库](<5.2.md>) - -## LastModified - * $Id$ \ No newline at end of file + +## links + * [目录]() + * 上一节: [访问数据库](<5.md>) + * 下一节: [使用MySQL数据库](<5.2.md>) diff --git a/5.2.md b/5.2.md index e6f93c86357bdf89c7574c775578013cc283bf1b..cfc2fbe26276687a5345f23ca250a3ae6b103f6e 100644 --- a/5.2.md +++ b/5.2.md @@ -126,13 +126,10 @@ db.Query()函数用来直接执行Sql返回Rows结果。 stmt.Exec()函数用来执行stmt准备好的SQL语句 我们可以看到我们传入的参数都是=?对应的数据,这样做的方式可以一定程度上防止SQL注入。 - -## links - * [目录]() - * 上一节: [database/sql接口](<5.1.md>) - * 下一节: [使用SQLite数据库](<5.3.md>) - -## LastModified - * $Id$ \ No newline at end of file + +## links + * [目录]() + * 上一节: [database/sql接口](<5.1.md>) + * 下一节: [使用SQLite数据库](<5.3.md>) diff --git a/5.3.md b/5.3.md index be095fe927f3c0ec19ec22c7e22733503aa03c80..5029aec78a8bac9eed80f934e5467076d12ccf6d 100644 --- a/5.3.md +++ b/5.3.md @@ -13,7 +13,7 @@ Go支持sqlite的驱动也比较多,但是好多都是不支持database/sql接 ## 实例代码 示例的数据库表结构如下所示,相应的建表SQL: - + CREATE TABLE `userinfo` ( `uid` INTEGER PRIMARY KEY AUTOINCREMENT, `username` VARCHAR(64) NULL, @@ -110,10 +110,7 @@ Go支持sqlite的驱动也比较多,但是好多都是不支持database/sql接 >可以方便的新建数据库管理。 -## links - * [目录]() - * 上一节: [使用MySQL数据库](<5.2.md>) - * 下一节: [使用PostgreSQL数据库](<5.4.md>) - -## LastModified - * $Id$ \ No newline at end of file +## links + * [目录]() + * 上一节: [使用MySQL数据库](<5.2.md>) + * 下一节: [使用PostgreSQL数据库](<5.4.md>) diff --git a/5.4.md b/5.4.md index cd189c4437681e7b4b7154931bce1216b9407601..bf0980bb05ebdf5cebf7724daa07f795010bd98f 100644 --- a/5.4.md +++ b/5.4.md @@ -20,19 +20,19 @@ Go实现的支持PostgreSQL的驱动也很多,因为国外很多人在开发 CREATE TABLE userinfo ( - uid serial NOT NULL, - username character varying(100) NOT NULL, - departname character varying(500) NOT NULL, - Created date, - CONSTRAINT userinfo_pkey PRIMARY KEY (uid) + uid serial NOT NULL, + username character varying(100) NOT NULL, + departname character varying(500) NOT NULL, + Created date, + CONSTRAINT userinfo_pkey PRIMARY KEY (uid) ) WITH (OIDS=FALSE); - + CREATE TABLE userdeatail ( - uid integer, - intro character varying(100), - profile character varying(100) + uid integer, + intro character varying(100), + profile character varying(100) ) WITH(OIDS=FALSE); @@ -116,10 +116,7 @@ package main 还有pg不支持LastInsertId函数,因为PostgreSQL内部没有实现类似MySQL的自增ID返回,其他的代码几乎是一模一样。 -## links - * [目录]() - * 上一节: [使用SQLite数据库](<5.3.md>) - * 下一节: [使用beedb库进行ORM开发](<5.5.md>) - -## LastModified - * $Id$ \ No newline at end of file +## links + * [目录]() + * 上一节: [使用SQLite数据库](<5.3.md>) + * 下一节: [使用beedb库进行ORM开发](<5.5.md>) diff --git a/5.5.md b/5.5.md index 7e159b295b1ecb0923cfe142ee33eb7175ab9a06..fff0cdc24cfafd1d59124d5932649c2d2a6c2afd 100644 --- a/5.5.md +++ b/5.5.md @@ -25,41 +25,40 @@ beedb支持go get方式安装,是完全按照Go Style的方式来实现的。 首先你需要import相应的数据库驱动包、database/sql标准接口包以及beedb包,如下所示: import ( - "database/sql" + "database/sql" "github.com/astaxie/beedb" _ "github.com/ziutek/mymysql/godrv" - ) 导入必须的package之后,我们需要打开到数据库的链接,然后创建一个beedb对象(以MySQL为例),如下所示 db, err := sql.Open("mymysql", "test/xiemengjun/123456") if err != nil { - panic(err) + panic(err) } orm := beedb.New(db) - + beedb的New函数实际上应该有两个参数,第一个参数标准接口的db,第二个参数是使用的数据库引擎,如果你使用的数据库引擎是MySQL/Sqlite,那么第二个参数都可以省略。 如果你使用的数据库是SQLServer,那么初始化需要: orm = beedb.New(db, "mssql") - -如果你使用了PostgreSQL,那么初始化需要: + +如果你使用了PostgreSQL,那么初始化需要: orm = beedb.New(db, "pg") - + 目前beedb支持打印调试,你可以通过如下的代码实现调试 - + beedb.OnDebug=true 接下来我们的例子采用前面的数据库表Userinfo,现在我们建立相应的struct type Userinfo struct { - Uid int `PK` //如果表的主键不是id,那么需要加上pk注释,显式的说这个字段是主键 - Username string - Departname string - Created time.Time + Uid int `PK` //如果表的主键不是id,那么需要加上pk注释,显式的说这个字段是主键 + Username string + Departname string + Created time.Time } >注意一点,beedb针对驼峰命名会自动帮你转化成下划线字段,例如你定义了Struct名字为`UserInfo`,那么转化成底层实现的时候是`user_info`,字段命名也遵循该规则。 @@ -76,13 +75,13 @@ beedb的New函数实际上应该有两个参数,第一个参数标准接口的 我们看到插入之后`saveone.Uid`就是插入成功之后的自增ID。Save接口会自动帮你存进去。 beedb接口提供了另外一种插入的方式,map数据插入。 - + add := make(map[string]interface{}) add["username"] = "astaxie" add["departname"] = "cloud develop" add["created"] = "2012-12-02" orm.SetTable("userinfo").Insert(add) - + 插入多条数据 addslice := make([]map[string]interface{}) @@ -92,19 +91,19 @@ beedb接口提供了另外一种插入的方式,map数据插入。 add["departname"] = "cloud develop" add["created"] = "2012-12-02" add2["username"] = "astaxie2" - add2["departname"] = "cloud develop2" + add2["departname"] = "cloud develop2" add2["created"] = "2012-12-02" addslice =append(addslice, add, add2) - orm.SetTable("userinfo").Insert(addslice) - + orm.SetTable("userinfo").Insert(addslice) + 上面的操作方式有点类似链式查询,熟悉jquery的同学应该会觉得很亲切,每次调用的method都会返回原orm对象,以便可以继续调用该对象上的其他method。 -上面我们调用的SetTable函数是显式的告诉ORM,我要执行的这个map对应的数据库表是`userinfo`。 +上面我们调用的SetTable函数是显式的告诉ORM,我要执行的这个map对应的数据库表是`userinfo`。 ## 更新数据 继续上面的例子来演示更新操作,现在saveone的主键已经有值了,此时调用save接口,beedb内部会自动调用update以进行数据的更新而非插入操作。 - saveone.Username = "Update Username" + saveone.Username = "Update Username" saveone.Departname = "Update Departname" saveone.Created = time.Now() orm.Save(&saveone) //现在saveone有了主键值,就执行更新操作 @@ -130,24 +129,24 @@ beedb的查询接口比较灵活,具体使用请看下面的例子 var user Userinfo //Where接受两个参数,支持整形参数 orm.Where("uid=?", 27).Find(&user) - - + + 例子2: var user2 Userinfo orm.Where(3).Find(&user2) // 这是上面版本的缩写版,可以省略主键 例子3,不是主键类型的的条件: - + var user3 Userinfo //Where接受两个参数,支持字符型的参数 - orm.Where("name = ?", "john").Find(&user3) + orm.Where("name = ?", "john").Find(&user3) 例子4,更加复杂的条件: - + var user4 Userinfo //Where支持三个参数 - orm.Where("name = ? and age < ?", "john", 88).Find(&user4) - + orm.Where("name = ? and age < ?", "john", 88).Find(&user4) + 可以通过如下接口获取多条数据,请看示例 @@ -155,17 +154,17 @@ beedb的查询接口比较灵活,具体使用请看下面的例子 var allusers []Userinfo err := orm.Where("id > ?", "3").Limit(10,20).FindAll(&allusers) - + 例子2,省略limit第二个参数,默认从0开始,获取10条数据 - + var tenusers []Userinfo err := orm.Where("id > ?", "3").Limit(10).FindAll(&tenusers) 例子3,获取全部数据 var everyone []Userinfo - err := orm.OrderBy("uid desc,username asc").FindAll(&everyone) - + err := orm.OrderBy("uid desc,username asc").FindAll(&everyone) + 上面这些里面里面我们看到一个函数Limit,他是用来控制查询结构条数的。 Limit:支持两个参数,第一个参数表示查询的条数,第二个参数表示读取数据的起始位置,默认为0。 @@ -175,9 +174,9 @@ OrderBy:这个函数用来进行查询排序,参数是需要排序的条件。 上面这些例子都是将获取的的数据直接映射成struct对象,如果我们只是想获取一些数据到map,以下方式可以实现: a, _ := orm.SetTable("userinfo").SetPK("uid").Where(2).Select("uid,username").FindMap() - + 上面和这个例子里面又出现了一个新的接口函数Select,这个函数用来指定需要查询多少个字段。默认为全部字段`*`。 - + FindMap()函数返回的是`[]map[string][]byte`类型,所以你需要自己作类型转换。 ## 删除数据 @@ -192,24 +191,24 @@ beedb提供了丰富的删除数据接口,请看下面的例子 //alluser就是上面定义的获取多条数据的slice orm.DeleteAll(&alluser) - + 例子3,根据sql删除数据 orm.SetTable("userinfo").Where("uid>?", 3).DeleteRow() - + ## 关联查询 目前beedb还不支持struct的关联关系,但是有些应用却需要用到连接查询,所以现在beedb提供了一个简陋的实现方案: a, _ := orm.SetTable("userinfo").Join("LEFT", "userdeatail", "userinfo.uid=userdeatail.uid").Where("userinfo.uid=?", 1).Select("userinfo.uid,userinfo.username,userdeatail.profile").FindMap() - + 上面代码中我们看到了一个新的接口Join函数,这个函数带有三个参数 - 第一个参数可以是:INNER, LEFT, OUTER, CROSS等 - 第二个参数表示连接的表 - 第三个参数表示连接的条件 - + ## Group By和Having 针对有些应用需要用到group by和having的功能,beedb也提供了一个简陋的实现 @@ -227,31 +226,24 @@ Having:用来指定having执行的时候的条件 - 实现interface设计,类似databse/sql/driver的设计,设计beedb的接口,然后去实现相应数据库的CRUD操作 - 实现关联数据库设计,支持一对一,一对多,多对多的实现,示例代码如下: - - type Profile struct{ - Nickname string - Mobile string - } - - type Userinfo struct { - Uid int `PK` - Username string - Departname string - Created time.Time - Profile `HasOne` - } - - + + type Profile struct{ + Nickname string + Mobile string + } + + type Userinfo struct { + Uid int `PK` + Username string + Departname string + Created time.Time + Profile `HasOne` + } + - 自动建库建表建索引 - 实现连接池的实现,采用goroutine - - - -## links - * [目录]() - * 上一节: [使用PostgreSQL数据库](<5.4.md>) - * 下一节: [NOSQL数据库操作](<5.6.md>) - -## LastModified - * $Id$ \ No newline at end of file +## links + * [目录]() + * 上一节: [使用PostgreSQL数据库](<5.4.md>) + * 下一节: [NOSQL数据库操作](<5.6.md>) diff --git a/5.6.md b/5.6.md index 625b31303fd7553fd0963d5da12e6a04e9a5401a..fcd1d4b937d1c3e917c02b8222029fd2c0cf3690 100644 --- a/5.6.md +++ b/5.6.md @@ -28,7 +28,7 @@ https://github.com/astaxie/goredis ) func main() { - var client goredis.Client + var client goredis.Client //字符串操作 var client goredis.Client client.Set("a", []byte("hello")) @@ -41,15 +41,15 @@ https://github.com/astaxie/goredis var client goredis.Client vals := []string{"a", "b", "c", "d", "e"} for _, v := range vals { - client.Rpush("l", []byte(v)) + client.Rpush("l", []byte(v)) } dbvals,_ := client.Lrange("l", 0, 4) for i, v := range dbvals { - println(i,":",string(v)) + println(i,":",string(v)) } client.Del("l") } - + 我们可以看到操作redis非常的方便,而且我实际项目中应用下来性能也很高。client的命令和redis的命令基本保持一致。所以和原生态操作redis非常类似。 ## mongoDB @@ -64,53 +64,49 @@ MongoDB是一个高性能,开源,无模式的文档型数据库,是一个 下面我将演示如果通过Go来操作mongoDB: - package main - import ( - "fmt" - "labix.org/v2/mgo" - "labix.org/v2/mgo/bson" - ) - - type Person struct { - Name string - Phone string - } - - func main() { - session, err := mgo.Dial("server1.example.com,server2.example.com") - if err != nil { - panic(err) - } - defer session.Close() - - session.SetMode(mgo.Monotonic, true) - - c := session.DB("test").C("people") - err = c.Insert(&Person{"Ale", "+55 53 8116 9639"}, - &Person{"Cla", "+55 53 8402 8510"}) - if err != nil { - panic(err) - } - - result := Person{} - err = c.Find(bson.M{"name": "Ale"}).One(&result) - if err != nil { - panic(err) - } - - fmt.Println("Phone:", result.Phone) - } + import ( + "fmt" + "labix.org/v2/mgo" + "labix.org/v2/mgo/bson" + ) + + type Person struct { + Name string + Phone string + } + + func main() { + session, err := mgo.Dial("server1.example.com,server2.example.com") + if err != nil { + panic(err) + } + defer session.Close() + + session.SetMode(mgo.Monotonic, true) + + c := session.DB("test").C("people") + err = c.Insert(&Person{"Ale", "+55 53 8116 9639"}, + &Person{"Cla", "+55 53 8402 8510"}) + if err != nil { + panic(err) + } + + result := Person{} + err = c.Find(bson.M{"name": "Ale"}).One(&result) + if err != nil { + panic(err) + } + + fmt.Println("Phone:", result.Phone) + } 我们可以看出来mgo的操作方式和beedb的操作方式几乎类似,都是基于strcut的操作方式,这个就是Go Style。 -## links - * [目录]() - * 上一节: [使用beedb库进行ORM开发](<5.5.md>) - * 下一节: [小结](<5.7.md>) - -## LastModified - * $Id$ \ No newline at end of file +## links + * [目录]() + * 上一节: [使用beedb库进行ORM开发](<5.5.md>) + * 下一节: [小结](<5.7.md>) diff --git a/5.7.md b/5.7.md index fbcce609ac9e7a4000d8a026c1f0b1be1600089d..478ede9059f7657907dc8c3244cd1d15cfb32d03 100644 --- a/5.7.md +++ b/5.7.md @@ -3,10 +3,7 @@ 通过这一章的学习,我们学会了如何操作各种数据库,那么就解决了我们数据存储的问题,这是Web里面最重要的一部分,所以希望大家能够深入的去了解database/sql的设计思想。 -## links - * [目录]() - * 上一节: [NOSQL数据库操作](<5.6.md>) - * 下一章: [session和数据存储](<6.md>) - -## LastModified - * $Id$ \ No newline at end of file +## links + * [目录]() + * 上一节: [NOSQL数据库操作](<5.6.md>) + * 下一章: [session和数据存储](<6.md>) diff --git a/5.md b/5.md index ed2ea4aa95f6c560250531aee4e9cb305b743f89..d3f6ec85a8b54e242accb901c2d5bfeea70aa923 100644 --- a/5.md +++ b/5.md @@ -5,19 +5,16 @@ Go没有内置的驱动支持任何的数据库,但是Go定义了database/sql 目前NOSQL已经成为Web开发的一个潮流,很多应用采用了NOSQL作为数据库,而不是以前的缓存,5.6小节将介绍MongoDB和Redis两种NOSQL数据库。 -## 目录 - * 1. [database/sql接口](5.1.md) - * 2. [使用MySQL数据库](5.2.md) +## 目录 + * 1. [database/sql接口](5.1.md) + * 2. [使用MySQL数据库](5.2.md) * 3. [使用SQLite数据库](5.3.md) - * 4. [使用PostgreSQL数据库](5.4.md) - * 5. [使用beedb库进行ORM开发](5.5.md) - * 6. [NOSQL数据库操作](5.6.md) + * 4. [使用PostgreSQL数据库](5.4.md) + * 5. [使用beedb库进行ORM开发](5.5.md) + * 6. [NOSQL数据库操作](5.6.md) * 7. [小结](5.7.md) -## links - * [目录]() - * 上一章: [第四章总结](<4.6.md>) - * 下一节: [database/sql接口](<5.1.md>) - -## LastModified - * $Id$ \ No newline at end of file +## links + * [目录]() + * 上一章: [第四章总结](<4.6.md>) + * 下一节: [database/sql接口](<5.1.md>) diff --git a/6.1.md b/6.1.md index bd43b025b9c9d84c43c0d025d8bfe0d7de92dd0c..439f55e5c935c77f97781b184f9978dea06a9f38 100644 --- a/6.1.md +++ b/6.1.md @@ -29,27 +29,27 @@ cookie是有时间限制的,根据生命期不同分成两种:会话cookie ### Go设置cookie Go语言中通过net/http包中的SetCookie来设置: - + http.SetCookie(w ResponseWriter, cookie *Cookie) - + w表示需要写入的response,cookie是一个struct,让我们来看一下cookie对象是怎么样的 type Cookie struct { - Name string - Value string - Path string - Domain string - Expires time.Time - RawExpires string - - // MaxAge=0 means no 'Max-Age' attribute specified. - // MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0' - // MaxAge>0 means Max-Age attribute present and given in seconds - MaxAge int - Secure bool - HttpOnly bool - Raw string - Unparsed []string // Raw text of unparsed attribute-value pairs + Name string + Value string + Path string + Domain string + Expires time.Time + RawExpires string + + // MaxAge=0 means no 'Max-Age' attribute specified. + // MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0' + // MaxAge>0 means Max-Age attribute present and given in seconds + MaxAge int + Secure bool + HttpOnly bool + Raw string + Unparsed []string // Raw text of unparsed attribute-value pairs } 我们来看一个例子,如何设置cookie @@ -57,21 +57,21 @@ w表示需要写入的response,cookie是一个struct,让我们来看一下co expiration := *time.LocalTime() expiration.Year += 1 cookie := http.Cookie{Name: "username", Value: "astaxie", Expires: expiration} - http.SetCookie(w, &cookie) + http.SetCookie(w, &cookie)    ### Go读取cookie 上面的例子演示了如何设置cookie数据,我们这里来演示一下如何读取cookie cookie, _ := r.Cookie("username") - fmt.Fprint(w, cookie) - + fmt.Fprint(w, cookie) + 还有另外一种读取方式 for _, cookie := range r.Cookies() { fmt.Fprint(w, cookie.Name) } - + 可以看到通过request获取cookie非常方便。 ## session @@ -93,10 +93,7 @@ session机制本身并不复杂,然而其实现和配置上的灵活性却使 通过上面的一些简单介绍我们了解了cookie和session的一些基础知识,知道他们之间的联系和区别,做web开发之前,有必要将一些必要知识了解清楚,才不会在用到时捉襟见肘,或是在调bug时候如无头苍蝇乱转。接下来的几小节我们将详细介绍session相关的知识。 -## links - * [目录]() - * 上一节: [session和数据存储](<6.md>) - * 下一节: [Go如何使用session](<6.2.md>) - -## LastModified - * $Id$ \ No newline at end of file +## links + * [目录]() + * 上一节: [session和数据存储](<6.md>) + * 下一节: [Go如何使用session](<6.2.md>) diff --git a/6.2.md b/6.2.md index 36f5d5d97d0f516832f283dae01c4e09815e6a37..43a74a2724533a414e445293384917cbecba8b1b 100644 --- a/6.2.md +++ b/6.2.md @@ -39,7 +39,7 @@ session的基本原理是由服务器为每个会话维护一份信息数据, provider Provider maxlifetime int64 } - + func NewManager(provideName, cookieName string, maxlifetime int64) (*Manager, error) { provider, ok := provides[provideName] if !ok { @@ -55,7 +55,7 @@ Go实现整个的流程应该也是这样的,在main包中创建一个全局 func init() { globalSessions = NewManager("memory","gosessionid",3600) } - + 我们知道session是保存在服务器端的数据,它可以以任何的方式存储,比如存储在内存、数据库或者文件中。因此我们抽象出一个Provider接口,用以表征session管理器底层存储结构。 type Provider interface { @@ -63,12 +63,12 @@ Go实现整个的流程应该也是这样的,在main包中创建一个全局 SessionRead(sid string) (Session, error) SessionDestroy(sid string) error SessionGC(maxLifeTime int64) - } - + } + - SessionInit函数实现Session的初始化,操作成功则返回此新的Session变量 - SSessionRead函数返回sid所代表的Session变量,如果不存在,那么将以sid为参数调用SessionInit函数创建并返回一个新的Session变量 - SessionDestroy函数用来销毁sid对应的Session变量 -- SessionGC根据maxLifeTime来删除过期的数据 +- SessionGC根据maxLifeTime来删除过期的数据 那么Session接口需要实现什么样的功能呢?有过Web开发经验的读者知道,对Session的处理基本就 设置值、读取值、删除值以及获取当前sessionID这四个操作,所以我们的Session接口也就实现这四个操作。 @@ -78,7 +78,7 @@ Go实现整个的流程应该也是这样的,在main包中创建一个全局 Delete(key interface{}) error //delete session value SessionID() string //back current sessionID } - + >以上设计思路来源于database/sql/driver,先定义好接口,然后具体的存储session的结构实现相应的接口并注册后,相应功能这样就可以使用了,以下是用来随需注册存储session的结构的Register函数的实现。 var provides = make(map[string]Provide) @@ -94,7 +94,7 @@ Go实现整个的流程应该也是这样的,在main包中创建一个全局 panic("session: Register called twice for provide " + name) } provides[name] = provide - } + } ### 全局唯一的Session ID @@ -126,7 +126,7 @@ Session ID是用来识别访问Web应用的每一个用户,因此必须保证 } return } - + 我们用前面login操作来演示session的运用: func login(w http.ResponseWriter, r *http.Request) { @@ -140,8 +140,8 @@ Session ID是用来识别访问Web应用的每一个用户,因此必须保证 sess.Set("username", r.Form["username"]) http.Redirect(w, r, "/", 302) } - } - + } + ### 操作值:设置、读取和删除 SessionStart函数返回的是一个满足Session接口的变量,那么我们该如何用他来对session数据进行操作呢? @@ -168,7 +168,7 @@ SessionStart函数返回的是一个满足Session接口的变量,那么我们 } 通过上面的例子可以看到,Session的操作和操作key/value数据库类似:Set、Get、Delete等操作 - + 因为Session有过期的概念,所以我们定义了GC操作,当访问过期时间满足GC的触发条件后将会引起GC,但是当我们进行了任意一个session操作,都会对Session实体进行更新,都会触发对最后访问时间的修改,这样当GC的时候就不会误删除还在使用的Session实体。 ### session重置 @@ -188,13 +188,13 @@ SessionStart函数返回的是一个满足Session接口的变量,那么我们 http.SetCookie(w, &cookie) } } - + ### session销毁 我们来看一下Session管理器如何来管理销毁,只要我们在Main启动的时候启动: - func init() { - go globalSessions.GC() + func init() { + go globalSessions.GC() } func (manager *Manager) GC() { @@ -203,16 +203,13 @@ SessionStart函数返回的是一个满足Session接口的变量,那么我们 manager.provider.SessionGC(manager.maxlifetime) time.AfterFunc(time.Duration(manager.maxlifetime), func() { manager.GC() }) } - + 我们可以看到GC充分利用了time包中的定时器功能,当超时`maxLifeTime`之后调用GC函数,这样就可以保证`maxLifeTime`时间内的session都是可用的,类似的方案也可以用于统计在线用户数之类的。 ## 总结 至此 我们实现了一个用来在Web应用中全局管理Session的SessionManager,定义了用来提供Session存储实现Provider的接口,下一小节,我们将会通过接口定义来实现一些Provider,供大家参考学习。 -## links - * [目录]() - * 上一节: [session和cookie](<6.1.md>) - * 下一节: [session存储](<6.3.md>) - -## LastModified - * $Id$ \ No newline at end of file +## links + * [目录]() + * 上一节: [session和cookie](<6.1.md>) + * 下一节: [session存储](<6.3.md>) diff --git a/6.3.md b/6.3.md index d181c87ff922e0948a22ffeb0bfea9c78d8b8499..210b86aad4c0ff4316a582d24431942b4583ea04 100644 --- a/6.3.md +++ b/6.3.md @@ -2,28 +2,28 @@ 上一节我们介绍了Session管理器的实现原理,定义了存储session的接口,这小节我们将示例一个基于内存的session存储接口的实现,其他的存储方式,读者可以自行参考示例来实现,内存的实现请看下面的例子代码 package memory - + import ( "container/list" "github.com/astaxie/session" "sync" "time" ) - + var pder = &Provider{list: list.New()} - + type SessionStore struct { - sid string //session id唯一标示 - timeAccessed time.Time //最后访问时间 + sid string //session id唯一标示 + timeAccessed time.Time //最后访问时间 value map[interface{}]interface{} //session里面存储的值 } - + func (st *SessionStore) Set(key, value interface{}) error { st.value[key] = value pder.SessionUpdate(st.sid) return nil } - + func (st *SessionStore) Get(key interface{}) interface{} { pder.SessionUpdate(st.sid) if v, ok := st.value[key]; ok { @@ -33,23 +33,23 @@ } return nil } - + func (st *SessionStore) Delete(key interface{}) error { delete(st.value, key) pder.SessionUpdate(st.sid) return nil } - + func (st *SessionStore) SessionID() string { return st.sid } - + type Provider struct { lock sync.Mutex //用来锁 sessions map[string]*list.Element //用来存储在内存 list *list.List //用来做gc } - + func (pder *Provider) SessionInit(sid string) (session.Session, error) { pder.lock.Lock() defer pder.lock.Unlock() @@ -59,7 +59,7 @@ pder.sessions[sid] = element return newsess, nil } - + func (pder *Provider) SessionRead(sid string) (session.Session, error) { if element, ok := pder.sessions[sid]; ok { return element.Value.(*SessionStore), nil @@ -69,7 +69,7 @@ } return nil, nil } - + func (pder *Provider) SessionDestroy(sid string) error { if element, ok := pder.sessions[sid]; ok { delete(pder.sessions, sid) @@ -78,11 +78,11 @@ } return nil } - + func (pder *Provider) SessionGC(maxlifetime int64) { pder.lock.Lock() defer pder.lock.Unlock() - + for { element := pder.list.Back() if element == nil { @@ -96,7 +96,7 @@ } } } - + func (pder *Provider) SessionUpdate(sid string) error { pder.lock.Lock() defer pder.lock.Unlock() @@ -107,7 +107,7 @@ } return nil } - + func init() { pder.sessions = make(map[string]*list.Element, 0) session.Register("memory", pder) @@ -123,7 +123,7 @@ 当import的时候已经执行了memory函数里面的init函数,这样就已经注册到session管理器中,我们就可以使用了,通过如下方式就可以初始化一个session管理器: var globalSessions *session.Manager - + //然后在init函数中初始化 func init() { globalSessions, _ = session.NewManager("memory", "gosessionid", 3600) @@ -131,10 +131,7 @@ } -## links - * [目录]() - * 上一节: [Go如何使用session](<6.2.md>) - * 下一节: [预防session劫持](<6.4.md>) - -## LastModified - * $Id$ \ No newline at end of file +## links + * [目录]() + * 上一节: [Go如何使用session](<6.2.md>) + * 下一节: [预防session劫持](<6.4.md>) diff --git a/6.4.md b/6.4.md index be2b4c54cca1cdfeb6d7f9a21430ee6a9135495f..db19658b8431946a6ecedf8ebe6062cdab14216c 100644 --- a/6.4.md +++ b/6.4.md @@ -17,20 +17,20 @@ session劫持是一种广泛存在的比较严重的安全威胁,在session技 w.Header().Set("Content-Type", "text/html") t.Execute(w, sess.Get("countnum")) } - + count.gtpl的代码如下所示: Hi. Now count:{{.}} - + 然后我们在浏览器里面刷新可以看到如下内容: -![](images/6.4.hijack.png?raw=true) +![](images/6.4.hijack.png?raw=true) 随着刷新,数字将不断增长,当数字显示为6的时候,打开浏览器(以chrome为例)的cookie管理器,可以看到类似如下的信息: -![](images/6.4.cookie.png?raw=true) +![](images/6.4.cookie.png?raw=true) 下面这个步骤最为关键: 打开另一个浏览器(这里我打开了firefox浏览器),复制chrome地址栏里的地址到新打开的浏览器的地址栏中。然后打开firefox的cookie模拟插件,新建一个cookie,把按上图中cookie内容原样在firefox中重建一份: @@ -57,7 +57,7 @@ count.gtpl的代码如下所示: //提示登录 } sess.Set("token",token) - + ### 间隔生成新的SID 还有一个解决方案就是,我们给session额外设置一个创建时间的值,一旦过了一定的时间,我们销毁这个sessionID,重新生成新的session,这样可以一定程度上防止session劫持的问题。 @@ -75,10 +75,7 @@ session启动后,我们设置了一个值,用于记录生成sessionID的时 上面两个手段的组合可以在实践中消除session劫持的风险,一方面, 由于sessionID频繁改变,使攻击者难有机会获取有效的sessionID;另一方面,因为sessionID只能在cookie中传递,然后设置了httponly,所以基于URL攻击的可能性为零,同时被XSS获取sessionID也不可能。最后,由于我们还设置了MaxAge=0,这样就相当于session cookie不会留在浏览器的历史记录里面。 -## links - * [目录]() - * 上一节: [session存储](<6.3.md>) - * 下一节: [小结](<6.5.md>) - -## LastModified - * $Id$ \ No newline at end of file +## links + * [目录]() + * 上一节: [session存储](<6.3.md>) + * 下一节: [小结](<6.5.md>) diff --git a/6.5.md b/6.5.md index ac01f48c5bb06b2d1aef39e61fb2123859aaddba..eb0ad2fdc49b894c9661ca0cad7fb895d9d309e1 100644 --- a/6.5.md +++ b/6.5.md @@ -1,9 +1,6 @@ # 6.5 小结 这章我们学习了什么是session,什么是cookie,以及他们两者之间的关系。但是目前Go官方标准包里面不支持session,所以我们设计了一个session管理器,实现了session从创建到销毁的整个过程。然后定义了Provider的接口,使得可以支持各种后端的session存储,然后我们在第三小节里面介绍了如何使用内存存储来实现session的管理。第四小节我们讲解了session劫持的过程,以及我们如何有效的来防止session劫持。通过这一章的讲解,希望能够让读者了解整个sesison的执行原理以及如何实现,而且是如何更加安全的使用session。 -## links - * [目录]() - * 上一节: [session存储](<6.4.md>) - * 下一章: [文本处理](<7.md>) - -## LastModified - * $Id$ \ No newline at end of file +## links + * [目录]() + * 上一节: [session存储](<6.4.md>) + * 下一章: [文本处理](<7.md>) diff --git a/6.md b/6.md index fab479cbe8404bb079b156aba803e524665696d0..b5dc262e62a6cc232669feb6d83461b50c5cda71 100644 --- a/6.md +++ b/6.md @@ -3,17 +3,14 @@ Web开发中一个很重要的议题就是如何做好用户的整个浏览过 6.1小节里面讲介绍session机制和cookie机制的关系和区别,6.2讲解Go语言如何来实现session,里面讲实现一个简易的session管理器,6.3小节讲解如何防止session被劫持的情况,如何有效的保护session。我们知道session其实可以存储在任何地方,6.3小节里面实现的session是存储在内存中的,但是如果我们的应用进一步扩展了,要实现应用的session共享,那么我们可以把session存储在数据库中(memcache或者redis),6.4小节将详细的讲解如何实现这些功能。 -## 目录 - * 1. [session和cookie](6.1.md) - * 2. [Go如何使用session](6.2.md) +## 目录 + * 1. [session和cookie](6.1.md) + * 2. [Go如何使用session](6.2.md) * 3. [session存储](6.3.md) - * 4. [预防session劫持](6.4.md) + * 4. [预防session劫持](6.4.md) * 5. [小结](6.5.md) -## links - * [目录]() - * 上一章: [第五章总结](<5.7.md>) - * 下一节: [session和cookie](<6.1.md>) - -## LastModified - * $Id$ \ No newline at end of file +## links + * [目录]() + * 上一章: [第五章总结](<5.7.md>) + * 下一节: [session和cookie](<6.1.md>) diff --git a/7.1.md b/7.1.md index 541670da8fc643be9fac8922a141b638d5a598cd..63e66cbf3493a10a58369ce3ec63c2172d12b9dd 100644 --- a/7.1.md +++ b/7.1.md @@ -1,4 +1,4 @@ -# 7.1 XML处理 +# 7.1 XML处理 XML作为一种数据交换和信息传递的格式已经十分普及。而随着Web服务日益广泛的应用,现在XML在日常的开发工作中也扮演了愈发重要的角色。这一小节, 我们将就Go语言标准包中的XML相关处理的包进行介绍。 这个小节不会涉及XML规范相关的内容(如需了解相关知识请参考其他文献),而是介绍如何用Go语言来编解码XML文件相关的知识。 @@ -16,10 +16,10 @@ XML作为一种数据交换和信息传递的格式已经十分普及。而随 127.0.0.2 - -上面的XML文档描述了两个服务器的信息,包含了服务器名和服务器的IP信息,接下来的Go例子以此XML描述的信息进行操作。 - -## 解析XML + +上面的XML文档描述了两个服务器的信息,包含了服务器名和服务器的IP信息,接下来的Go例子以此XML描述的信息进行操作。 + +## 解析XML 如何解析如上这个XML文件喃呢? 我们可以通过xml包的`Unmarshal`函数来达到我们的目的 func Unmarshal(data []byte, v interface{}) error @@ -29,27 +29,27 @@ data接收的是XML数据流,v是需要输出的结构,定义为interface, 示例代码如下: package main - + import ( "encoding/xml" "fmt" "io/ioutil" "os" ) - + type Recurlyservers struct { XMLName xml.Name `xml:"servers"` Version string `xml:"version,attr"` Svs []server `xml:"server"` Description string `xml:",innerxml"` } - + type server struct { XMLName xml.Name `xml:"server"` ServerName string `xml:"serverName"` ServerIP string `xml:"serverIP"` } - + func main() { file, err := os.Open("servers.xml") // For read access. defer file.Close() @@ -68,14 +68,14 @@ data接收的是XML数据流,v是需要输出的结构,定义为interface, fmt.Printf("error: %v", err) return } - + fmt.Println(v) } XML本质上是一种树形的数据格式,而我们可以定义与之匹配的go 语言的 struct类型,然后通过xml.Unmarshal来将xml中的数据解析成对应的struct对象。如上例子输出如下数据 - {{ servers} 1 [{{ server} Shanghai_VPN 127.0.0.1} {{ server} Beijing_VPN 127.0.0.2}] + {{ servers} 1 [{{ server} Shanghai_VPN 127.0.0.1} {{ server} Beijing_VPN 127.0.0.2}] Shanghai_VPN 127.0.0.1 @@ -99,14 +99,14 @@ Go语言的反射机制,可以利用这些tag信息来将来自XML文件中的 - 如果struct的一个字段是string或者[]byte类型且它的tag含有`",innerxml"`,Unmarshal将会将此字段所对应的元素内所有内嵌的原始xml累加到此字段上,如上面例子Description定义。最后的输出是 - - Shanghai_VPN - 127.0.0.1 - - - Beijing_VPN - 127.0.0.2 - + + Shanghai_VPN + 127.0.0.1 + + + Beijing_VPN + 127.0.0.2 + - 如果struct中有一个叫做XMLName,且类型为xml.Name字段,那么在解析的时候就会保存这个element的名字到该字段,如上面例子中的servers。 - 如果某个struct字段的tag定义中含有XML结构中element的名称,那么解析的时候就会把相应的element值赋值给该字段,如上servername和serverip定义。 @@ -118,8 +118,8 @@ Go语言的反射机制,可以利用这些tag信息来将来自XML文件中的 上面详细讲述了如何定义struct的tag。 只要设置对了tag,那么XML解析就如上面示例般简单,tag和XML的element是一一对应的关系,如上所示,我们还可以通过slice来表示多个同级元素。 ->注意: 为了正确解析,go语言的xml包要求struct定义中的所有字段必须是可导出的(即首字母大写) - +>注意: 为了正确解析,go语言的xml包要求struct定义中的所有字段必须是可导出的(即首字母大写) + ## 输出XML 假若我们不是要解析如上所示的XML文件,而是生成它,那么在go语言中又该如何实现呢? xml包中提供了`Marshal`和`MarshalIndent`两个函数,来满足我们的需求。这两个函数主要的区别是第二个函数会增加前缀和缩进,函数的定义如下所示: @@ -131,24 +131,24 @@ Go语言的反射机制,可以利用这些tag信息来将来自XML文件中的 下面我们来看一下如何输出如上的XML: package main - + import ( "encoding/xml" "fmt" "os" ) - + type Servers struct { XMLName xml.Name `xml:"servers"` Version string `xml:"version,attr"` Svs []server `xml:"server"` } - + type server struct { ServerName string `xml:"serverName"` ServerIP string `xml:"serverIP"` } - + func main() { v := &Servers{Version: "1"} v.Svs = append(v.Svs, server{"Shanghai_VPN", "127.0.0.1"}) @@ -158,22 +158,22 @@ Go语言的反射机制,可以利用这些tag信息来将来自XML文件中的 fmt.Printf("error: %v\n", err) } os.Stdout.Write([]byte(xml.Header)) - + os.Stdout.Write(output) } 上面的代码输出如下信息: - - Shanghai_VPN - 127.0.0.1 - - - Beijing_VPN - 127.0.0.2 - - + + Shanghai_VPN + 127.0.0.1 + + + Beijing_VPN + 127.0.0.2 + + 和我们之前定义的文件的格式一模一样,之所以会有`os.Stdout.Write([]byte(xml.Header))` 这句代码的出现,是因为`xml.MarshalIndent`或者`xml.Marshal`输出的信息都是不带XML头的,为了生成正确的xml文件,我们使用了xml包预定义的Header变量。 @@ -203,22 +203,19 @@ Go语言的反射机制,可以利用这些tag信息来将来自XML文件中的 - tag中含有`",comment"`,将被当作xml注释来输出,而不会进行常规的编码过程,字段值中不能含有"--"字符串 - tag中含有`"omitempty"`,如果该字段的值为空值那么该字段就不会被输出到XML,空值包括:false、0、nil指针或nil接口,任何长度为0的array, slice, map或者string - tag中含有`"a>b>c"`,那么就会循环输出三个元素a包含b,b包含c,例如如下代码就会输出 - - FirstName string `xml:"name>first"` - LastName string `xml:"name>last"` - - - Asta - Xie - - - -上面我们介绍了如何使用Go语言的xml包来编/解码XML文件,重要的一点是对XML的所有操作都是通过struct tag来实现的,所以学会对struct tag的运用变得非常重要,在文章中我们简要的列举了如何定义tag。更多内容或tag定义请参看相应的官方资料。 - -## links - * [目录]() - * 上一节: [文本处理](<7.md>) - * 下一节: [Json处理](<7.2.md>) - -## LastModified - * $Id$ \ No newline at end of file + + FirstName string `xml:"name>first"` + LastName string `xml:"name>last"` + + + Asta + Xie + + + +上面我们介绍了如何使用Go语言的xml包来编/解码XML文件,重要的一点是对XML的所有操作都是通过struct tag来实现的,所以学会对struct tag的运用变得非常重要,在文章中我们简要的列举了如何定义tag。更多内容或tag定义请参看相应的官方资料。 + +## links + * [目录]() + * 上一节: [文本处理](<7.md>) + * 下一节: [Json处理](<7.2.md>) diff --git a/7.2.md b/7.2.md index 278ec255d1cc75878dbff27e1b8adfa342b938a7..7233041b67f209ceb4cd8f753ab16c80be1faf36 100644 --- a/7.2.md +++ b/7.2.md @@ -1,225 +1,222 @@ -# 7.2 JSON处理 -JSON(Javascript Object Notation)是一种轻量级的数据交换语言,以文字为基础,具有自我描述性且易于让人阅读。尽管JSON是Javascript的一个子集,但JSON是独立于语言的文本格式,并且采用了类似于C语言家族的一些习惯。JSON与XML最大的不同在于XML是一个完整的标记语言,而JSON不是。JSON由于比XML更小、更快,更易解析,以及浏览器的内建快速解析支持,使得其更适用于网络数据传输领域。目前我们看到很多的开放平台,基本上都是采用了JSON作为他们的数据交互的接口。既然JSON在Web开发中如此重要,那么Go语言对JSON支持的怎么样呢?Go语言的标准库已经非常好的支持了JSON,可以很容易的对JSON数据进行编、解码的工作。 - -前一小节的运维的例子用json来表示,结果描述如下: - - {"servers":[{"serverName":"Shanghai_VPN","serverIP":"127.0.0.1"},{"serverName":"Beijing_VPN","serverIP":"127.0.0.2"}]} - -本小节余下的内容将以此JSON数据为基础,来介绍go语言的json包对JSON数据的编、解码。 -## 解析JSON - -### 解析到结构体 -假如有了上面的JSON串,那么我们如何来解析这个JSON串呢?Go的JSON包中有如下函数 - - func Unmarshal(data []byte, v interface{}) error - -通过这个函数我们就可以实现解析的目的,详细的解析例子请看如下代码: - - package main - - import ( - "encoding/json" - "fmt" - ) - - type Server struct { - ServerName string - ServerIP string - } - - type Serverslice struct { - Servers []Server - } - - func main() { - var s Serverslice - str := `{"servers":[{"serverName":"Shanghai_VPN","serverIP":"127.0.0.1"},{"serverName":"Beijing_VPN","serverIP":"127.0.0.2"}]}` - json.Unmarshal([]byte(str), &s) - fmt.Println(s) - } - -通在上面的示例代码中,我们首先定义了与json数据对应的结构体,数组对应slice,字段名对应JSON里面的KEY,在解析的时候,如何将json数据与struct字段相匹配呢?例如JSON的key是`Foo`,那么怎么找对应的字段呢? - -- 首先查找tag含有`Foo`的可导出的struct字段(首字母大写) -- 其次查找字段名是`Foo`的导出字段 -- 最后查找类似`FOO`或者`FoO`这样的除了首字母之外其他大小写不敏感的导出字段 - -聪明的你一定注意到了这一点:能够被赋值的字段必须是可导出字段(即首字母大写)。同时JSON解析的时候只会解析能找得到的字段,如果找不到的字段会被忽略,这样的一个好处是:当你接收到一个很大的JSON数据结构而你却只想获取其中的部分数据的时候,你只需将你想要的数据对应的字段名大写,即可轻松解决这个问题。 - -### 解析到interface -上面那种解析方式是在我们知晓被解析的JSON数据的结构的前提下采取的方案,如果我们不知道被解析的数据的格式,又应该如何来解析呢? - -我们知道interface{}可以用来存储任意数据类型的对象,这种数据结构正好用于存储解析的未知结构的json数据的结果。JSON包中采用map[string]interface{}和[]interface{}结构来存储任意的JSON对象和数组。Go类型和JSON类型的对应关系如下: - -- bool 代表 JSON booleans, -- float64 代表 JSON numbers, -- string 代表 JSON strings, -- nil 代表 JSON null. - -现在我们假设有如下的JSON数据 - - b := []byte(`{"Name":"Wednesday","Age":6,"Parents":["Gomez","Morticia"]}`) - -如果在我们不知道他的结构的情况下,我们把他解析到interface{}里面 - - var f interface{} - err := json.Unmarshal(b, &f) - -这个时候f里面存储了一个map类似,他们的key是string,值存储在空的interface{}里 - - f = map[string]interface{}{ - "Name": "Wednesday", - "Age": 6, - "Parents": []interface{}{ - "Gomez", - "Morticia", - }, - } - -那么如何来访问这些数据呢?通过断言的方式: - - m := f.(map[string]interface{}) - -通过断言之后,你就可以通过如下方式来访问里面的数据了 - - for k, v := range m { - switch vv := v.(type) { - case string: - fmt.Println(k, "is string", vv) - case int: - fmt.Println(k, "is int", vv) - case []interface{}: - fmt.Println(k, "is an array:") - for i, u := range vv { - fmt.Println(i, u) - } - default: - fmt.Println(k, "is of a type I don't know how to handle") - } - } -通过上面的示例可以看到,通过interface{}与type assert的配合,我们就可以解析未知结构的JSON数了。 - -上面这个是官方提供的解决方案,其实很多时候我们通过类型断言,操作起来不是很方便,目前bitly公司开源了一个叫做`simplejson`的包,在处理未知结构体的JSON时相当方便,详细例子如下所示: - - js, err := NewJson([]byte(`{ - "test": { - "array": [1, "2", 3], - "int": 10, - "float": 5.150, - "bignum": 9223372036854775807, - "string": "simplejson", - "bool": true - } - }`)) - - arr, _ := js.Get("test").Get("array").Array() - i, _ := js.Get("test").Get("int").Int() - ms := js.Get("test").Get("string").MustString() - -可以看到,使用这个库操作JSON比起官方包来说,简单的多,详细的请参考如下地址:https://github.com/bitly/go-simplejson - -## 生成JSON -我们开发很多应用的时候,最后都是要输出JSON数据串,那么如何来处理呢?JSON包里面通过`Marshal`函数来处理,函数定义如下: - - func Marshal(v interface{}) ([]byte, error) - -假设我们还是需要生成上面的服务器列表信息,那么如何来处理呢?请看下面的例子: - - package main - - import ( - "encoding/json" - "fmt" - ) - - type Server struct { - ServerName string - ServerIP string - } - - type Serverslice struct { - Servers []Server - } - - func main() { - var s Serverslice - s.Servers = append(s.Servers, Server{ServerName: "Shanghai_VPN", ServerIP: "127.0.0.1"}) - s.Servers = append(s.Servers, Server{ServerName: "Beijing_VPN", ServerIP: "127.0.0.2"}) - b, err := json.Marshal(s) - if err != nil { - fmt.Println("json err:", err) - } - fmt.Println(string(b)) - } - -输出如下内容: - - {"Servers":[{"ServerName":"Shanghai_VPN","ServerIP":"127.0.0.1"},{"ServerName":"Beijing_VPN","ServerIP":"127.0.0.2"}]} - -我们看到上面的输出字段名都是大写的,如果你想用小写的怎么办呢?把结构体的字段名改成小写的?JSON输出的时候必须注意,只有导出的字段才会被输出,如果修改字段名,那么就会发现什么都不会输出,所以必须通过struct tag定义来实现: - - type Server struct { - ServerName string `json:"serverName"` - ServerIP string `json:"serverIP"` - } - - type Serverslice struct { - Servers []Server `json:"servers"` - } - -通过修改上面的结构体定义,输出的JSON串就和我们最开始定义的JSON串保持一致了。 - -针对JSON的输出,我们在定义struct tag的时候需要注意的几点是: - -- 字段的tag是`"-"`,那么这个字段不会输出到JSON -- tag中带有自定义名称,那么这个自定义名称会出现在JSON的字段名中,例如上面例子中serverName -- tag中如果带有`"omitempty"`选项,那么如果该字段值为空,就不会输出到JSON串中 -- 如果字段类型是bool, string, int, int64等,而tag中带有`",string"`选项,那么这个字段在输出到JSON的时候会把该字段对应的值转换成JSON字符串 - - -举例来说: - - type Server struct { - // ID 不会导出到JSON中 - ID int `json:"-"` - - // ServerName 的值会进行二次JSON编码 - ServerName string `json:"serverName"` - ServerName2 string `json:"serverName2,string"` - - // 如果 ServerIP 为空,则不输出到JSON串中 - ServerIP string `json:"serverIP,omitempty"` - } - - s := Server { - ID: 3, - ServerName: `Go "1.0" `, - ServerName2: `Go "1.0" `, - ServerIP: ``, - } - b, _ := json.Marshal(s) - os.Stdout.Write(b) - -会输出以下内容: - - {"serverName":"Go \"1.0\" ","serverName2":"\"Go \\\"1.0\\\" \""} - - -Marshal函数只有在转换成功的时候才会返回数据,在转换的过程中我们需要注意几点: - - -- JSON对象只支持string作为key,所以要编码一个map,那么必须是map[string]T这种类型(T是Go语言中任意的类型) -- Channel, complex和function是不能被编码成JSON的 -- 嵌套的数据是不能编码的,不然会让JSON编码进入死循环 -- 指针在编码的时候会输出指针指向的内容,而空指针会输出null - - -本小节,我们介绍了如何使用Go语言的json标准包来编解码JSON数据,同时也简要介绍了如何使用第三方包`go-simplejson`来在一些情况下简化操作,学会并熟练运用它们将对我们接下来的Web开发相当重要。 - -## links - * [目录]() - * 上一节: [XML处理](<7.1.md>) - * 下一节: [正则处理](<7.3.md>) - -## LastModified - * $Id$ \ No newline at end of file +# 7.2 JSON处理 +JSON(Javascript Object Notation)是一种轻量级的数据交换语言,以文字为基础,具有自我描述性且易于让人阅读。尽管JSON是Javascript的一个子集,但JSON是独立于语言的文本格式,并且采用了类似于C语言家族的一些习惯。JSON与XML最大的不同在于XML是一个完整的标记语言,而JSON不是。JSON由于比XML更小、更快,更易解析,以及浏览器的内建快速解析支持,使得其更适用于网络数据传输领域。目前我们看到很多的开放平台,基本上都是采用了JSON作为他们的数据交互的接口。既然JSON在Web开发中如此重要,那么Go语言对JSON支持的怎么样呢?Go语言的标准库已经非常好的支持了JSON,可以很容易的对JSON数据进行编、解码的工作。 + +前一小节的运维的例子用json来表示,结果描述如下: + + {"servers":[{"serverName":"Shanghai_VPN","serverIP":"127.0.0.1"},{"serverName":"Beijing_VPN","serverIP":"127.0.0.2"}]} + +本小节余下的内容将以此JSON数据为基础,来介绍go语言的json包对JSON数据的编、解码。 +## 解析JSON + +### 解析到结构体 +假如有了上面的JSON串,那么我们如何来解析这个JSON串呢?Go的JSON包中有如下函数 + + func Unmarshal(data []byte, v interface{}) error + +通过这个函数我们就可以实现解析的目的,详细的解析例子请看如下代码: + + package main + + import ( + "encoding/json" + "fmt" + ) + + type Server struct { + ServerName string + ServerIP string + } + + type Serverslice struct { + Servers []Server + } + + func main() { + var s Serverslice + str := `{"servers":[{"serverName":"Shanghai_VPN","serverIP":"127.0.0.1"},{"serverName":"Beijing_VPN","serverIP":"127.0.0.2"}]}` + json.Unmarshal([]byte(str), &s) + fmt.Println(s) + } + +通在上面的示例代码中,我们首先定义了与json数据对应的结构体,数组对应slice,字段名对应JSON里面的KEY,在解析的时候,如何将json数据与struct字段相匹配呢?例如JSON的key是`Foo`,那么怎么找对应的字段呢? + +- 首先查找tag含有`Foo`的可导出的struct字段(首字母大写) +- 其次查找字段名是`Foo`的导出字段 +- 最后查找类似`FOO`或者`FoO`这样的除了首字母之外其他大小写不敏感的导出字段 + +聪明的你一定注意到了这一点:能够被赋值的字段必须是可导出字段(即首字母大写)。同时JSON解析的时候只会解析能找得到的字段,如果找不到的字段会被忽略,这样的一个好处是:当你接收到一个很大的JSON数据结构而你却只想获取其中的部分数据的时候,你只需将你想要的数据对应的字段名大写,即可轻松解决这个问题。 + +### 解析到interface +上面那种解析方式是在我们知晓被解析的JSON数据的结构的前提下采取的方案,如果我们不知道被解析的数据的格式,又应该如何来解析呢? + +我们知道interface{}可以用来存储任意数据类型的对象,这种数据结构正好用于存储解析的未知结构的json数据的结果。JSON包中采用map[string]interface{}和[]interface{}结构来存储任意的JSON对象和数组。Go类型和JSON类型的对应关系如下: + +- bool 代表 JSON booleans, +- float64 代表 JSON numbers, +- string 代表 JSON strings, +- nil 代表 JSON null. + +现在我们假设有如下的JSON数据 + + b := []byte(`{"Name":"Wednesday","Age":6,"Parents":["Gomez","Morticia"]}`) + +如果在我们不知道他的结构的情况下,我们把他解析到interface{}里面 + + var f interface{} + err := json.Unmarshal(b, &f) + +这个时候f里面存储了一个map类似,他们的key是string,值存储在空的interface{}里 + + f = map[string]interface{}{ + "Name": "Wednesday", + "Age": 6, + "Parents": []interface{}{ + "Gomez", + "Morticia", + }, + } + +那么如何来访问这些数据呢?通过断言的方式: + + m := f.(map[string]interface{}) + +通过断言之后,你就可以通过如下方式来访问里面的数据了 + + for k, v := range m { + switch vv := v.(type) { + case string: + fmt.Println(k, "is string", vv) + case int: + fmt.Println(k, "is int", vv) + case []interface{}: + fmt.Println(k, "is an array:") + for i, u := range vv { + fmt.Println(i, u) + } + default: + fmt.Println(k, "is of a type I don't know how to handle") + } + } +通过上面的示例可以看到,通过interface{}与type assert的配合,我们就可以解析未知结构的JSON数了。 + +上面这个是官方提供的解决方案,其实很多时候我们通过类型断言,操作起来不是很方便,目前bitly公司开源了一个叫做`simplejson`的包,在处理未知结构体的JSON时相当方便,详细例子如下所示: + + js, err := NewJson([]byte(`{ + "test": { + "array": [1, "2", 3], + "int": 10, + "float": 5.150, + "bignum": 9223372036854775807, + "string": "simplejson", + "bool": true + } + }`)) + + arr, _ := js.Get("test").Get("array").Array() + i, _ := js.Get("test").Get("int").Int() + ms := js.Get("test").Get("string").MustString() + +可以看到,使用这个库操作JSON比起官方包来说,简单的多,详细的请参考如下地址:https://github.com/bitly/go-simplejson + +## 生成JSON +我们开发很多应用的时候,最后都是要输出JSON数据串,那么如何来处理呢?JSON包里面通过`Marshal`函数来处理,函数定义如下: + + func Marshal(v interface{}) ([]byte, error) + +假设我们还是需要生成上面的服务器列表信息,那么如何来处理呢?请看下面的例子: + + package main + + import ( + "encoding/json" + "fmt" + ) + + type Server struct { + ServerName string + ServerIP string + } + + type Serverslice struct { + Servers []Server + } + + func main() { + var s Serverslice + s.Servers = append(s.Servers, Server{ServerName: "Shanghai_VPN", ServerIP: "127.0.0.1"}) + s.Servers = append(s.Servers, Server{ServerName: "Beijing_VPN", ServerIP: "127.0.0.2"}) + b, err := json.Marshal(s) + if err != nil { + fmt.Println("json err:", err) + } + fmt.Println(string(b)) + } + +输出如下内容: + + {"Servers":[{"ServerName":"Shanghai_VPN","ServerIP":"127.0.0.1"},{"ServerName":"Beijing_VPN","ServerIP":"127.0.0.2"}]} + +我们看到上面的输出字段名都是大写的,如果你想用小写的怎么办呢?把结构体的字段名改成小写的?JSON输出的时候必须注意,只有导出的字段才会被输出,如果修改字段名,那么就会发现什么都不会输出,所以必须通过struct tag定义来实现: + + type Server struct { + ServerName string `json:"serverName"` + ServerIP string `json:"serverIP"` + } + + type Serverslice struct { + Servers []Server `json:"servers"` + } + +通过修改上面的结构体定义,输出的JSON串就和我们最开始定义的JSON串保持一致了。 + +针对JSON的输出,我们在定义struct tag的时候需要注意的几点是: + +- 字段的tag是`"-"`,那么这个字段不会输出到JSON +- tag中带有自定义名称,那么这个自定义名称会出现在JSON的字段名中,例如上面例子中serverName +- tag中如果带有`"omitempty"`选项,那么如果该字段值为空,就不会输出到JSON串中 +- 如果字段类型是bool, string, int, int64等,而tag中带有`",string"`选项,那么这个字段在输出到JSON的时候会把该字段对应的值转换成JSON字符串 + + +举例来说: + + type Server struct { + // ID 不会导出到JSON中 + ID int `json:"-"` + + // ServerName 的值会进行二次JSON编码 + ServerName string `json:"serverName"` + ServerName2 string `json:"serverName2,string"` + + // 如果 ServerIP 为空,则不输出到JSON串中 + ServerIP string `json:"serverIP,omitempty"` + } + + s := Server { + ID: 3, + ServerName: `Go "1.0" `, + ServerName2: `Go "1.0" `, + ServerIP: ``, + } + b, _ := json.Marshal(s) + os.Stdout.Write(b) + +会输出以下内容: + + {"serverName":"Go \"1.0\" ","serverName2":"\"Go \\\"1.0\\\" \""} + + +Marshal函数只有在转换成功的时候才会返回数据,在转换的过程中我们需要注意几点: + + +- JSON对象只支持string作为key,所以要编码一个map,那么必须是map[string]T这种类型(T是Go语言中任意的类型) +- Channel, complex和function是不能被编码成JSON的 +- 嵌套的数据是不能编码的,不然会让JSON编码进入死循环 +- 指针在编码的时候会输出指针指向的内容,而空指针会输出null + + +本小节,我们介绍了如何使用Go语言的json标准包来编解码JSON数据,同时也简要介绍了如何使用第三方包`go-simplejson`来在一些情况下简化操作,学会并熟练运用它们将对我们接下来的Web开发相当重要。 + +## links + * [目录]() + * 上一节: [XML处理](<7.1.md>) + * 下一节: [正则处理](<7.3.md>) diff --git a/7.3.md b/7.3.md index c735e7d3aed8680d236cc238549c9c1505921031..92c5326226db74e4064edb0e2320769cf6725c79 100644 --- a/7.3.md +++ b/7.3.md @@ -1,218 +1,218 @@ -# 7.3 正则处理 -正则表达式是一种进行模式匹配和文本操纵的复杂而又强大的工具。虽然正则表达式比纯粹的文本匹配效率低,但是它却更灵活。按照它的语法规则,随需构造出的匹配模式就能够从原始文本中筛选出几乎任何想你要得到的字符组合。如果你在Web开发中需要从一些文本数据源中获取数据,那么你只需要按照它的语法规则,随需构造出正确的模式字符串就能够从原数据源提取出有意义的文本信息。 - -Go语言通过`regexp`标准包为正则表达式提供了官方支持,如果你已经使用过其他编程语言提供的正则相关功能,那么你应该对Go语言版本的不会太陌生,但是它们之间也有一些小的差异,因为Go实现的是RE2标准,除了\C,详细的语法描述参考:http://code.google.com/p/re2/wiki/Syntax。 - -其实字符串处理我们可以使用`strings`包来进行搜索(Contains、Index)、替换(Replace)和解析(Split、Join)等操作,但是这些都是简单的字符串操作,他们的搜索都是大小写敏感,而且固定的字符串,如果我们需要匹配可变的那种就没办法实现了,当然如果`strings`包能解决你的问题,那么就尽量使用它来解决。因为他们足够简单、而且性能和可读性都会比正则好。 - -如果你还记得,在前面表单验证的小节里,我们已经接触过正则处理,在那里我们利用了它来验证输入的信息是否满足某些预设的条件。在使用中需要注意的一点就是:所有的字符都是UTF-8编码的。接下来让我们更加深入的来学习Go语言的`regexp`包相关知识吧。 - -## 通过正则判断是否匹配 -`regexp`包中含有三个函数用来判断是否匹配,如果匹配返回true,否则返回false - - func Match(pattern string, b []byte) (matched bool, error error) - func MatchReader(pattern string, r io.RuneReader) (matched bool, error error) - func MatchString(pattern string, s string) (matched bool, error error) - -上面的三个函数实现了同一个功能,就是判断`pattern`是否和输入源匹配,匹配的话就返回true,如果解析正则出错则返回error。三个函数的输入源分别是byte slice、RuneReader和string。 - -如果要验证一个输入是不是IP地址,那么如何来判断呢?请看如下实现 - - func IsIP(ip string) (b bool) { - if m, _ := regexp.MatchString("^[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}$", ip); !m { - return false - } - return true - } - -可以看到,`regexp`的pattern和我们平常使用的正则一模一样。再来看一个例子:当用户输入一个字符串,我们想知道是不是一次合法的输入: - - func main() { - if len(os.Args) == 1 { - fmt.Println("Usage: regexp [string]") - os.Exit(1) - } else if m, _ := regexp.MatchString("^[0-9]+$", os.Args[1]); m { - fmt.Println("数字") - } else { - fmt.Println("不是数字") - } - } - -在上面的两个小例子中,我们采用了Match(Reader|String)来判断一些字符串是否符合我们的描述需求,它们使用起来非常方便。 - -## 通过正则获取内容 -Match模式只能用来对字符串的判断,而无法截取字符串的一部分、过滤字符串、或者提取出符合条件的一批字符串。如果想要满足这些需求,那就需要使用正则表达式的复杂模式。 - -我们经常需要一些爬虫程序,下面就以爬虫为例来说明如何使用正则来过滤或截取抓取到的数据: - - package main - - import ( - "fmt" - "io/ioutil" - "net/http" - "regexp" - "strings" - ) - - func main() { - resp, err := http.Get("http://www.baidu.com") - if err != nil { - fmt.Println("http get error.") - } - defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - fmt.Println("http read error") - return - } - - src := string(body) - - //将HTML标签全转换成小写 - re, _ := regexp.Compile("\\<[\\S\\s]+?\\>") - src = re.ReplaceAllStringFunc(src, strings.ToLower) - - //去除STYLE - re, _ = regexp.Compile("\\") - src = re.ReplaceAllString(src, "") - - //去除SCRIPT - re, _ = regexp.Compile("\\") - src = re.ReplaceAllString(src, "") - - //去除所有尖括号内的HTML代码,并换成换行符 - re, _ = regexp.Compile("\\<[\\S\\s]+?\\>") - src = re.ReplaceAllString(src, "\n") - - //去除连续的换行符 - re, _ = regexp.Compile("\\s{2,}") - src = re.ReplaceAllString(src, "\n") - - fmt.Println(strings.TrimSpace(src)) - } - -从这个示例可以看出,使用复杂的正则首先是Compile,它会解析正则表达式是否合法,如果正确,那么就会返回一个Regexp,然后就可以利用返回的Regexp在任意的字符串上面执行需要的操作。 - -解析正则表达式的有如下几个方法: - - func Compile(expr string) (*Regexp, error) - func CompilePOSIX(expr string) (*Regexp, error) - func MustCompile(str string) *Regexp - func MustCompilePOSIX(str string) *Regexp - -CompilePOSIX和Compile的不同点在于POSIX必须使用POSIX语法,它使用最左最长方式搜索,而Compile是采用的则只采用最左方式搜索(例如[a-z]{2,4}这样一个正则表达式,应用于"aa09aaa88aaaa"这个文本串时,CompilePOSIX返回了aaaa,而Compile的返回的是aa)。前缀有Must的函数表示,在解析正则语法的时候,如果匹配模式串不满足正确的语法则直接panic,而不加Must的则只是返回错误。 - -在了解了如何新建一个Regexp之后,我们再来看一下这个struct提供了哪些方法来辅助我们操作字符串,首先我们来看下面这写用来搜索的函数: - - func (re *Regexp) Find(b []byte) []byte - func (re *Regexp) FindAll(b []byte, n int) [][]byte - func (re *Regexp) FindAllIndex(b []byte, n int) [][]int - func (re *Regexp) FindAllString(s string, n int) []string - func (re *Regexp) FindAllStringIndex(s string, n int) [][]int - func (re *Regexp) FindAllStringSubmatch(s string, n int) [][]string - func (re *Regexp) FindAllStringSubmatchIndex(s string, n int) [][]int - func (re *Regexp) FindAllSubmatch(b []byte, n int) [][][]byte - func (re *Regexp) FindAllSubmatchIndex(b []byte, n int) [][]int - func (re *Regexp) FindIndex(b []byte) (loc []int) - func (re *Regexp) FindReaderIndex(r io.RuneReader) (loc []int) - func (re *Regexp) FindReaderSubmatchIndex(r io.RuneReader) []int - func (re *Regexp) FindString(s string) string - func (re *Regexp) FindStringIndex(s string) (loc []int) - func (re *Regexp) FindStringSubmatch(s string) []string - func (re *Regexp) FindStringSubmatchIndex(s string) []int - func (re *Regexp) FindSubmatch(b []byte) [][]byte - func (re *Regexp) FindSubmatchIndex(b []byte) []int - -上面这18个函数我们根据输入源(byte slice、string和io.RuneReader)不同还可以继续简化成如下几个,其他的只是输入源不一样,其他功能基本是一样的: - - func (re *Regexp) Find(b []byte) []byte - func (re *Regexp) FindAll(b []byte, n int) [][]byte - func (re *Regexp) FindAllIndex(b []byte, n int) [][]int - func (re *Regexp) FindAllSubmatch(b []byte, n int) [][][]byte - func (re *Regexp) FindAllSubmatchIndex(b []byte, n int) [][]int - func (re *Regexp) FindIndex(b []byte) (loc []int) - func (re *Regexp) FindSubmatch(b []byte) [][]byte - func (re *Regexp) FindSubmatchIndex(b []byte) []int - -对于这些函数的使用我们来看下面这个例子 - - package main - - import ( - "fmt" - "regexp" - ) - - func main() { - a := "I am learning Go language" - - re, _ := regexp.Compile("[a-z]{2,4}") - - //查找符合正则的第一个 - one := re.Find([]byte(a)) - fmt.Println("Find:", string(one)) - - //查找符合正则的所有slice,n小于0表示返回全部符合的字符串,不然就是返回指定的长度 - all := re.FindAll([]byte(a), -1) - fmt.Println("FindAll", all) - - //查找符合条件的index位置,开始位置和结束位置 - index := re.FindIndex([]byte(a)) - fmt.Println("FindIndex", index) - - //查找符合条件的所有的index位置,n同上 - allindex := re.FindAllIndex([]byte(a), -1) - fmt.Println("FindAllIndex", allindex) - - re2, _ := regexp.Compile("am(.*)lang(.*)") - - //查找Submatch,返回数组,第一个元素是匹配的全部元素,第二个元素是第一个()里面的,第三个是第二个()里面的 - //下面的输出第一个元素是"am learning Go language" - //第二个元素是" learning Go ",注意包含空格的输出 - //第三个元素是"uage" - submatch := re2.FindSubmatch([]byte(a)) - fmt.Println("FindSubmatch", submatch) - for _, v := range submatch { - fmt.Println(string(v)) - } - - //定义和上面的FindIndex一样 - submatchindex := re2.FindSubmatchIndex([]byte(a)) - fmt.Println(submatchindex) - - //FindAllSubmatch,查找所有符合条件的子匹配 - submatchall := re2.FindAllSubmatch([]byte(a), -1) - fmt.Println(submatchall) - - //FindAllSubmatchIndex,查找所有字匹配的index - submatchallindex := re2.FindAllSubmatchIndex([]byte(a), -1) - fmt.Println(submatchallindex) - } - +# 7.3 正则处理 +正则表达式是一种进行模式匹配和文本操纵的复杂而又强大的工具。虽然正则表达式比纯粹的文本匹配效率低,但是它却更灵活。按照它的语法规则,随需构造出的匹配模式就能够从原始文本中筛选出几乎任何想你要得到的字符组合。如果你在Web开发中需要从一些文本数据源中获取数据,那么你只需要按照它的语法规则,随需构造出正确的模式字符串就能够从原数据源提取出有意义的文本信息。 + +Go语言通过`regexp`标准包为正则表达式提供了官方支持,如果你已经使用过其他编程语言提供的正则相关功能,那么你应该对Go语言版本的不会太陌生,但是它们之间也有一些小的差异,因为Go实现的是RE2标准,除了\C,详细的语法描述参考:http://code.google.com/p/re2/wiki/Syntax。 + +其实字符串处理我们可以使用`strings`包来进行搜索(Contains、Index)、替换(Replace)和解析(Split、Join)等操作,但是这些都是简单的字符串操作,他们的搜索都是大小写敏感,而且固定的字符串,如果我们需要匹配可变的那种就没办法实现了,当然如果`strings`包能解决你的问题,那么就尽量使用它来解决。因为他们足够简单、而且性能和可读性都会比正则好。 + +如果你还记得,在前面表单验证的小节里,我们已经接触过正则处理,在那里我们利用了它来验证输入的信息是否满足某些预设的条件。在使用中需要注意的一点就是:所有的字符都是UTF-8编码的。接下来让我们更加深入的来学习Go语言的`regexp`包相关知识吧。 + +## 通过正则判断是否匹配 +`regexp`包中含有三个函数用来判断是否匹配,如果匹配返回true,否则返回false + + func Match(pattern string, b []byte) (matched bool, error error) + func MatchReader(pattern string, r io.RuneReader) (matched bool, error error) + func MatchString(pattern string, s string) (matched bool, error error) + +上面的三个函数实现了同一个功能,就是判断`pattern`是否和输入源匹配,匹配的话就返回true,如果解析正则出错则返回error。三个函数的输入源分别是byte slice、RuneReader和string。 + +如果要验证一个输入是不是IP地址,那么如何来判断呢?请看如下实现 + + func IsIP(ip string) (b bool) { + if m, _ := regexp.MatchString("^[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}$", ip); !m { + return false + } + return true + } + +可以看到,`regexp`的pattern和我们平常使用的正则一模一样。再来看一个例子:当用户输入一个字符串,我们想知道是不是一次合法的输入: + + func main() { + if len(os.Args) == 1 { + fmt.Println("Usage: regexp [string]") + os.Exit(1) + } else if m, _ := regexp.MatchString("^[0-9]+$", os.Args[1]); m { + fmt.Println("数字") + } else { + fmt.Println("不是数字") + } + } + +在上面的两个小例子中,我们采用了Match(Reader|String)来判断一些字符串是否符合我们的描述需求,它们使用起来非常方便。 + +## 通过正则获取内容 +Match模式只能用来对字符串的判断,而无法截取字符串的一部分、过滤字符串、或者提取出符合条件的一批字符串。如果想要满足这些需求,那就需要使用正则表达式的复杂模式。 + +我们经常需要一些爬虫程序,下面就以爬虫为例来说明如何使用正则来过滤或截取抓取到的数据: + + package main + + import ( + "fmt" + "io/ioutil" + "net/http" + "regexp" + "strings" + ) + + func main() { + resp, err := http.Get("http://www.baidu.com") + if err != nil { + fmt.Println("http get error.") + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + fmt.Println("http read error") + return + } + + src := string(body) + + //将HTML标签全转换成小写 + re, _ := regexp.Compile("\\<[\\S\\s]+?\\>") + src = re.ReplaceAllStringFunc(src, strings.ToLower) + + //去除STYLE + re, _ = regexp.Compile("\\") + src = re.ReplaceAllString(src, "") + + //去除SCRIPT + re, _ = regexp.Compile("\\") + src = re.ReplaceAllString(src, "") + + //去除所有尖括号内的HTML代码,并换成换行符 + re, _ = regexp.Compile("\\<[\\S\\s]+?\\>") + src = re.ReplaceAllString(src, "\n") + + //去除连续的换行符 + re, _ = regexp.Compile("\\s{2,}") + src = re.ReplaceAllString(src, "\n") + + fmt.Println(strings.TrimSpace(src)) + } + +从这个示例可以看出,使用复杂的正则首先是Compile,它会解析正则表达式是否合法,如果正确,那么就会返回一个Regexp,然后就可以利用返回的Regexp在任意的字符串上面执行需要的操作。 + +解析正则表达式的有如下几个方法: + + func Compile(expr string) (*Regexp, error) + func CompilePOSIX(expr string) (*Regexp, error) + func MustCompile(str string) *Regexp + func MustCompilePOSIX(str string) *Regexp + +CompilePOSIX和Compile的不同点在于POSIX必须使用POSIX语法,它使用最左最长方式搜索,而Compile是采用的则只采用最左方式搜索(例如[a-z]{2,4}这样一个正则表达式,应用于"aa09aaa88aaaa"这个文本串时,CompilePOSIX返回了aaaa,而Compile的返回的是aa)。前缀有Must的函数表示,在解析正则语法的时候,如果匹配模式串不满足正确的语法则直接panic,而不加Must的则只是返回错误。 + +在了解了如何新建一个Regexp之后,我们再来看一下这个struct提供了哪些方法来辅助我们操作字符串,首先我们来看下面这写用来搜索的函数: + + func (re *Regexp) Find(b []byte) []byte + func (re *Regexp) FindAll(b []byte, n int) [][]byte + func (re *Regexp) FindAllIndex(b []byte, n int) [][]int + func (re *Regexp) FindAllString(s string, n int) []string + func (re *Regexp) FindAllStringIndex(s string, n int) [][]int + func (re *Regexp) FindAllStringSubmatch(s string, n int) [][]string + func (re *Regexp) FindAllStringSubmatchIndex(s string, n int) [][]int + func (re *Regexp) FindAllSubmatch(b []byte, n int) [][][]byte + func (re *Regexp) FindAllSubmatchIndex(b []byte, n int) [][]int + func (re *Regexp) FindIndex(b []byte) (loc []int) + func (re *Regexp) FindReaderIndex(r io.RuneReader) (loc []int) + func (re *Regexp) FindReaderSubmatchIndex(r io.RuneReader) []int + func (re *Regexp) FindString(s string) string + func (re *Regexp) FindStringIndex(s string) (loc []int) + func (re *Regexp) FindStringSubmatch(s string) []string + func (re *Regexp) FindStringSubmatchIndex(s string) []int + func (re *Regexp) FindSubmatch(b []byte) [][]byte + func (re *Regexp) FindSubmatchIndex(b []byte) []int + +上面这18个函数我们根据输入源(byte slice、string和io.RuneReader)不同还可以继续简化成如下几个,其他的只是输入源不一样,其他功能基本是一样的: + + func (re *Regexp) Find(b []byte) []byte + func (re *Regexp) FindAll(b []byte, n int) [][]byte + func (re *Regexp) FindAllIndex(b []byte, n int) [][]int + func (re *Regexp) FindAllSubmatch(b []byte, n int) [][][]byte + func (re *Regexp) FindAllSubmatchIndex(b []byte, n int) [][]int + func (re *Regexp) FindIndex(b []byte) (loc []int) + func (re *Regexp) FindSubmatch(b []byte) [][]byte + func (re *Regexp) FindSubmatchIndex(b []byte) []int + +对于这些函数的使用我们来看下面这个例子 + + package main + + import ( + "fmt" + "regexp" + ) + + func main() { + a := "I am learning Go language" + + re, _ := regexp.Compile("[a-z]{2,4}") + + //查找符合正则的第一个 + one := re.Find([]byte(a)) + fmt.Println("Find:", string(one)) + + //查找符合正则的所有slice,n小于0表示返回全部符合的字符串,不然就是返回指定的长度 + all := re.FindAll([]byte(a), -1) + fmt.Println("FindAll", all) + + //查找符合条件的index位置,开始位置和结束位置 + index := re.FindIndex([]byte(a)) + fmt.Println("FindIndex", index) + + //查找符合条件的所有的index位置,n同上 + allindex := re.FindAllIndex([]byte(a), -1) + fmt.Println("FindAllIndex", allindex) + + re2, _ := regexp.Compile("am(.*)lang(.*)") + + //查找Submatch,返回数组,第一个元素是匹配的全部元素,第二个元素是第一个()里面的,第三个是第二个()里面的 + //下面的输出第一个元素是"am learning Go language" + //第二个元素是" learning Go ",注意包含空格的输出 + //第三个元素是"uage" + submatch := re2.FindSubmatch([]byte(a)) + fmt.Println("FindSubmatch", submatch) + for _, v := range submatch { + fmt.Println(string(v)) + } + + //定义和上面的FindIndex一样 + submatchindex := re2.FindSubmatchIndex([]byte(a)) + fmt.Println(submatchindex) + + //FindAllSubmatch,查找所有符合条件的子匹配 + submatchall := re2.FindAllSubmatch([]byte(a), -1) + fmt.Println(submatchall) + + //FindAllSubmatchIndex,查找所有字匹配的index + submatchallindex := re2.FindAllSubmatchIndex([]byte(a), -1) + fmt.Println(submatchallindex) + } + 前面介绍过匹配函数,Regexp也定义了三个函数,它们和同名的外部函数功能一模一样,其实外部函数就是调用了这Regexp的三个函数来实现的: func (re *Regexp) Match(b []byte) bool - func (re *Regexp) MatchReader(r io.RuneReader) bool - func (re *Regexp) MatchString(s string) bool - + func (re *Regexp) MatchReader(r io.RuneReader) bool + func (re *Regexp) MatchString(s string) bool + 接下里让我们来了解替换函数是怎么操作的? func (re *Regexp) ReplaceAll(src, repl []byte) []byte - func (re *Regexp) ReplaceAllFunc(src []byte, repl func([]byte) []byte) []byte - func (re *Regexp) ReplaceAllLiteral(src, repl []byte) []byte - func (re *Regexp) ReplaceAllLiteralString(src, repl string) string - func (re *Regexp) ReplaceAllString(src, repl string) string - func (re *Regexp) ReplaceAllStringFunc(src string, repl func(string) string) string - + func (re *Regexp) ReplaceAllFunc(src []byte, repl func([]byte) []byte) []byte + func (re *Regexp) ReplaceAllLiteral(src, repl []byte) []byte + func (re *Regexp) ReplaceAllLiteralString(src, repl string) string + func (re *Regexp) ReplaceAllString(src, repl string) string + func (re *Regexp) ReplaceAllStringFunc(src string, repl func(string) string) string + 这些替换函数我们在上面的抓网页的例子有详细应用示例, 接下来我们看一下Expand的解释: func (re *Regexp) Expand(dst []byte, template []byte, src []byte, match []int) []byte - func (re *Regexp) ExpandString(dst []byte, template string, src string, match []int) []byte - -那么这个Expand到底用来干嘛的呢?请看下面的例子: + func (re *Regexp) ExpandString(dst []byte, template string, src string, match []int) []byte + +那么这个Expand到底用来干嘛的呢?请看下面的例子: func main() { src := []byte(` @@ -226,15 +226,12 @@ CompilePOSIX和Compile的不同点在于POSIX必须使用POSIX语法,它使用 res = pat.Expand(res, []byte("$cmd('$arg')\n"), src, s) } fmt.Println(string(res)) - } - -至此我们已经全部介绍完Go语言的`regexp`包,通过对它的主要函数介绍及演示,相信大家应该能够通过Go语言的正则包进行一些基本的正则的操作了。 - - -## links - * [目录]() - * 上一节: [Json处理](<7.2.md>) - * 下一节: [模板处理](<7.4.md>) - -## LastModified - * $Id$ \ No newline at end of file + } + +至此我们已经全部介绍完Go语言的`regexp`包,通过对它的主要函数介绍及演示,相信大家应该能够通过Go语言的正则包进行一些基本的正则的操作了。 + + +## links + * [目录]() + * 上一节: [Json处理](<7.2.md>) + * 下一节: [模板处理](<7.4.md>) diff --git a/7.4.md b/7.4.md index 9d88eb3cfbdd56e40b2b9a8ead840b57685226d9..fba8e8ccd760eff88b0eb0777c9f1cf48697db26 100644 --- a/7.4.md +++ b/7.4.md @@ -1,344 +1,341 @@ -# 7.4 模板处理 -## 什么是模板 -你一定听说过一种叫做MVC的设计模式,Model处理数据,View展现结果,Controller控制用户的请求,至于View层的处理,在很多动态语言里面都是通过在静态HTML中插入动态语言生成的数据,例如JSP中通过插入`<%=....=%>`,PHP中通过插入``来实现的。 - -通过下面这个图可以说明模板的机制 - -![](images/7.4.template.png?raw=true) - -Web应用反馈给客户端的信息中的大部分内容是静态的,不变的,而另外少部分是根据用户的请求来动态生成的,例如要显示用户的访问记录列表。用户之间只有记录数据是不同的,而列表的样式则是固定的,此时采用模板可以复用很多静态代码。 - -## Go模板使用 -在Go语言中,我们使用`template`包来进行模板处理,使用类似`Parse`、`ParseFile`、`Execute`等方法从文件或者字符串加载模板,然后执行类似上面图片展示的模板的merge操作。请看下面的例子: - - func handler(w http.ResponseWriter, r *http.Request) { - t := template.New("some template") //创建一个模板 - t, _ = t.ParseFiles("tmpl/welcome.html", nil) //解析模板文件 - user := GetUser() //获取当前用户信息 - t.Execute(w, user) //执行模板的merger操作 - } - -通过上面的例子我们可以看到Go语言的模板操作非常的简单方便,和其他语言的模板处理类似,都是先获取数据,然后渲染数据。 - -为了演示和测试代码的方便,我们在接下来的例子中采用如下格式的代码 - -- 使用Parse代替ParseFiles,因为Parse可以直接测试一个字符串,而不需要额外的文件 -- 不使用handler来写演示代码,而是每个测试一个main,方便测试 -- 使用`os.Stdout`代替`http.ResponseWriter`,因为`os.Stdout`实现了`io.Writer`接口 - -## 模板中如何插入数据? -上面我们演示了如何解析并渲染模板,接下来让我们来更加详细的了解如何把数据渲染出来。一个模板都是应用在一个Go的对象之上,Go对象的字段如何插入到模板中呢? - -### 字段操作 -Go语言的模板通过`{{}}`来包含需要在渲染时被替换的字段,`{{.}}`表示当前的对象,这和Java或者C++中的this类似,如果要访问当前对象的字段通过`{{.FieldName}}`,但是需要注意一点:这个字段必须是导出的(字段首字母必须是大写的),否则在渲染的时候就会报错,请看下面的这个例子: - - package main - - import ( - "html/template" - "os" - ) - - type Person struct { - UserName string - } - - func main() { - t := template.New("fieldname example") - t, _ = t.Parse("hello {{.UserName}}!") - p := Person{UserName: "Astaxie"} - t.Execute(os.Stdout, p) - } - -上面的代码我们可以正确的输出`hello Astaxie`,但是如果我们稍微修改一下代码,在模板中含有了未导出的字段,那么就会报错 - - type Person struct { - UserName string - email string //未导出的字段,首字母是小写的 - } - - t, _ = t.Parse("hello {{.UserName}}! {{.email}}") - -上面的代码就会报错,因为我们调用了一个未导出的字段,但是如果我们调用了一个不存在的字段是不会报错的,而是输出为空。 - -如果模板中输出`{{.}}`,这个一般应用与字符串对象,默认会调用fmt包输出字符串的内容。 - -### 输出嵌套字段内容 -上面我们例子展示了如何针对一个对象的字段输出,那么如果字段里面还有对象,如何来循环的输出这些内容呢?我们可以使用`{{with …}}…{{end}}`和`{{range …}}{{end}}`来进行数据的输出。详细的使用请看下面的例子: - - package main - - import ( - "html/template" - "os" - ) - - type Friend struct { - Fname string - } - - type Person struct { - UserName string - Emails []string - Friends []*Friend - } - - func main() { - f1 := Friend{Fname: "minux.ma"} - f2 := Friend{Fname: "xushiwei"} - t := template.New("fieldname example") - t, _ = t.Parse(`hello {{.UserName}}! - {{range .Emails}} - an email {{.}} - {{end}} - {{with .Friends}} - {{range .}} - my friend name is {{.Fname}} - {{end}} - {{end}} - `) - p := Person{UserName: "Astaxie", - Emails: []string{"astaxie@beego.me", "astaxie@gmail.com"}, - Friends: []*Friend{&f1, &f2}} - t.Execute(os.Stdout, p) - } - -### pipelines -Unix用户已经很熟悉什么是`pipe`了,`ls | grep "name"`类似这样的语法你是不是经常使用,过滤当前目录下面的文件,显示含有"name"的数据,他表达的意思就是前面的输出可以当做后面的输入,最后显示我们想要的数据,而Go语言模板最强大的一点就是支持pipe数据,在Go语言里面任何`{{}}`里面的都是pipelines数据,例如我们上面输出的email里面如果还有一些可能引起XSS注入的,那么我们如何来进行转化呢? - - {{. | html}} - -在email输出的地方我们可以采用如上方式可以把输出全部转化html的实体,上面的这种方式和我们平常写Unix的方式是不是一模一样,操作起来相当的简便,调用其他的函数也是类似的方式。 - -### 条件处理 -在Go模板里面如果需要进行条件判断,那么我们可以使用和Go语言的`if-else`语法类似的方式来咱先,如果pipeline为空,那么if就认为是false,下面的例子展示了如何使用`if-else`语法: - - package main - - import ( - "os" - "text/template" - ) - - func main() { - tEmpty := template.New("template test") - tEmpty = template.Must(tEmpty.Parse("空 pipeline if demo: {{if ``}} 不会输出. {{end}}\n")) - tEmpty.Execute(os.Stdout, nil) - - tWithValue := template.New("template test") - tWithValue = template.Must(tWithValue.Parse("不为空的 pipeline if demo: {{if `anything`}} 我有内容,我会输出. {{end}}\n")) - tWithValue.Execute(os.Stdout, nil) - - tIfElse := template.New("template test") - tIfElse = template.Must(tIfElse.Parse("if-else demo: {{if `anything`}} if部分 {{else}} else部分.{{end}}\n")) - tIfElse.Execute(os.Stdout, nil) - } - -通过上面的演示代码我们知道`if-else`语法相当的简单,在使用过程中很容易集成到我们的模板代码中。 - -### 模板变量 -有时候,我们在模板使用过程中需要定义一些局部变量,我们可以在一些操作中申明局部变量,例如`with``range``if`过程中申明局部变量,这个变量的作用域是`{{end}}`之前,Go语言通过申明的局部变量格式如下所示: - - $variable := pipeline - -详细的例子看下面的: - - {{with $x := "output" | printf "%q"}}{{$x}}{{end}} - {{with $x := "output"}}{{printf "%q" $x}}{{end}} - {{with $x := "output"}}{{$x | printf "%q"}}{{end}} -### 模板函数 -模板在输出对象的字段值时,采用了`fmt`包把对象转化成了字符串。但是有时候我们的需求可能不是这样的,例如有时候我们为了防止垃圾邮件发送者通过采集网页的方式来发送给我们的邮箱信息,我们希望把`@`替换成`at`例如:`astaxie at beego.me`,如果要实现这样的功能,我们就需要自定义函数来做这个功能。 - -每一个模板函数都有一个唯一值的名字,然后与一个Go函数关联,通过如下的方式来关联 - - type FuncMap map[string]interface{} - -例如,如果我们想要的email函数的模板函数名是`emailDeal`,它关联的Go函数名称是`EmailDealWith`,n那么我们可以通过下面的方式来注册这个函数 - - t = t.Funcs(template.FuncMap{"emailDeal": EmailDealWith}) - -`EmailDealWith`这个函数的参数和返回值定义如下: - - func EmailDealWith(args …interface{}) string - -我们来看下面的实现例子: - - package main - - import ( - "fmt" - "html/template" - "os" - "strings" - ) - - type Friend struct { - Fname string - } - - type Person struct { - UserName string - Emails []string - Friends []*Friend - } - - func EmailDealWith(args ...interface{}) string { - ok := false - var s string - if len(args) == 1 { - s, ok = args[0].(string) - } - if !ok { - s = fmt.Sprint(args...) - } - // find the @ symbol - substrs := strings.Split(s, "@") - if len(substrs) != 2 { - return s - } - // replace the @ by " at " - return (substrs[0] + " at " + substrs[1]) - } - - func main() { - f1 := Friend{Fname: "minux.ma"} - f2 := Friend{Fname: "xushiwei"} - t := template.New("fieldname example") - t = t.Funcs(template.FuncMap{"emailDeal": EmailDealWith}) - t, _ = t.Parse(`hello {{.UserName}}! - {{range .Emails}} - an emails {{.|emailDeal}} - {{end}} - {{with .Friends}} - {{range .}} - my friend name is {{.Fname}} - {{end}} - {{end}} - `) - p := Person{UserName: "Astaxie", - Emails: []string{"astaxie@beego.me", "astaxie@gmail.com"}, - Friends: []*Friend{&f1, &f2}} - t.Execute(os.Stdout, p) - } - - -上面演示了如何自定义函数,其实,在模板包内部已经有内置的实现函数,下面代码截取自模板包里面 - - var builtins = FuncMap{ - "and": and, - "call": call, - "html": HTMLEscaper, - "index": index, - "js": JSEscaper, - "len": length, - "not": not, - "or": or, - "print": fmt.Sprint, - "printf": fmt.Sprintf, - "println": fmt.Sprintln, - "urlquery": URLQueryEscaper, - } - - -## Must操作 -模板包里面有一个函数`Must`,它的作用是检测模板是否正确,例如大括号是否匹配,注释是否正确的关闭,变量是否正确的书写。接下来我们演示一个例子,用Must来判断模板是否正确: - - package main - - import ( - "fmt" - "text/template" - ) - - func main() { - tOk := template.New("first") - template.Must(tOk.Parse(" some static text /* and a comment */")) - fmt.Println("The first one parsed OK.") - - template.Must(template.New("second").Parse("some static text {{ .Name }}")) - fmt.Println("The second one parsed OK.") - - fmt.Println("The next one ought to fail.") - tErr := template.New("check parse error with Must") - template.Must(tErr.Parse(" some static text {{ .Name }")) - } - -讲输出如下内容 - - The first one parsed OK. - The second one parsed OK. - The next one ought to fail. - panic: template: check parse error with Must:1: unexpected "}" in command - -## 嵌套模板 -我们平常开发Web应用的时候,经常会遇到一些模板有些部分是固定不变的,然后可以抽取出来作为一个独立的部分,例如一个博客的头部和尾部是不变的,而唯一改变的是中间的内容部分。所以我们可以定义成`header`、`content`、`footer`三个部分。Go语言中通过如下的语法来申明 - - {{define "子模板名称"}}内容{{end}} - -通过如下方式来调用: - - {{template "子模板名称"}} - -接下来我们演示如何使用嵌套模板,我们定义三个文件,`header.tmpl`、`content.tmpl`、`footer.tmpl`文件,里面的内容如下 - - //header.tmpl - {{define "header"}} - - - 演示信息 - - - {{end}} - - //content.tmpl - {{define "content"}} - {{template "header"}} -

演示嵌套

-
    -
  • 嵌套使用define定义子模板
  • -
  • 调用使用template
  • -
- {{template "footer"}} - {{end}} - - //footer.tmpl - {{define "footer"}} - - - {{end}} - -演示代码如下: - - package main - - import ( - "fmt" - "os" - "text/template" - ) - - func main() { - s1, _ := template.ParseFiles("header.tmpl", "content.tmpl", "footer.tmpl") - s1.ExecuteTemplate(os.Stdout, "header", nil) - fmt.Println() - s1.ExecuteTemplate(os.Stdout, "content", nil) - fmt.Println() - s1.ExecuteTemplate(os.Stdout, "footer", nil) - fmt.Println() - s1.Execute(os.Stdout, nil) - } - -通过上面的例子我们可以看到通过`template.ParseFiles`把所有的嵌套模板全部解析到模板里面,其实每一个定义的{{define}}都是一个独立的模板,他们相互独立,是并行存在的关系,内部其实存储的是类似map的一种关系(key是模板的名称,value是模板的内容),然后我们通过`ExecuteTemplate`来执行相应的子模板内容,我们可以看到header、footer都是相对独立的,都能输出内容,contenrt中因为嵌套了header和footer的内容,就会同时输出三个的内容。但是当我们执行`s1.Execute`,没有任何的输出,因为在默认的情况下没有默认的子模板,所以不会输出任何的东西。 - ->同一个集合类的模板是互相知晓的,如果同一模板被多个集合使用,则它需要在多个集合中分别解析 - -## 总结 -通过上面对模板的详细介绍,我们了解了如何把动态数据与模板融合:如何输出循环数据、如何自定义函数、如何嵌套模板等等。通过模板技术的应用,我们可以完成MVC模式中V的处理,接下来的章节我们将介绍如何来处理M和C。 - -## links - * [目录]() - * 上一节: [正则处理](<7.3.md>) - * 下一节: [小结](<7.5.md>) - -## LastModified - * $Id$ \ No newline at end of file +# 7.4 模板处理 +## 什么是模板 +你一定听说过一种叫做MVC的设计模式,Model处理数据,View展现结果,Controller控制用户的请求,至于View层的处理,在很多动态语言里面都是通过在静态HTML中插入动态语言生成的数据,例如JSP中通过插入`<%=....=%>`,PHP中通过插入``来实现的。 + +通过下面这个图可以说明模板的机制 + +![](images/7.4.template.png?raw=true) + +Web应用反馈给客户端的信息中的大部分内容是静态的,不变的,而另外少部分是根据用户的请求来动态生成的,例如要显示用户的访问记录列表。用户之间只有记录数据是不同的,而列表的样式则是固定的,此时采用模板可以复用很多静态代码。 + +## Go模板使用 +在Go语言中,我们使用`template`包来进行模板处理,使用类似`Parse`、`ParseFile`、`Execute`等方法从文件或者字符串加载模板,然后执行类似上面图片展示的模板的merge操作。请看下面的例子: + + func handler(w http.ResponseWriter, r *http.Request) { + t := template.New("some template") //创建一个模板 + t, _ = t.ParseFiles("tmpl/welcome.html", nil) //解析模板文件 + user := GetUser() //获取当前用户信息 + t.Execute(w, user) //执行模板的merger操作 + } + +通过上面的例子我们可以看到Go语言的模板操作非常的简单方便,和其他语言的模板处理类似,都是先获取数据,然后渲染数据。 + +为了演示和测试代码的方便,我们在接下来的例子中采用如下格式的代码 + +- 使用Parse代替ParseFiles,因为Parse可以直接测试一个字符串,而不需要额外的文件 +- 不使用handler来写演示代码,而是每个测试一个main,方便测试 +- 使用`os.Stdout`代替`http.ResponseWriter`,因为`os.Stdout`实现了`io.Writer`接口 + +## 模板中如何插入数据? +上面我们演示了如何解析并渲染模板,接下来让我们来更加详细的了解如何把数据渲染出来。一个模板都是应用在一个Go的对象之上,Go对象的字段如何插入到模板中呢? + +### 字段操作 +Go语言的模板通过`{{}}`来包含需要在渲染时被替换的字段,`{{.}}`表示当前的对象,这和Java或者C++中的this类似,如果要访问当前对象的字段通过`{{.FieldName}}`,但是需要注意一点:这个字段必须是导出的(字段首字母必须是大写的),否则在渲染的时候就会报错,请看下面的这个例子: + + package main + + import ( + "html/template" + "os" + ) + + type Person struct { + UserName string + } + + func main() { + t := template.New("fieldname example") + t, _ = t.Parse("hello {{.UserName}}!") + p := Person{UserName: "Astaxie"} + t.Execute(os.Stdout, p) + } + +上面的代码我们可以正确的输出`hello Astaxie`,但是如果我们稍微修改一下代码,在模板中含有了未导出的字段,那么就会报错 + + type Person struct { + UserName string + email string //未导出的字段,首字母是小写的 + } + + t, _ = t.Parse("hello {{.UserName}}! {{.email}}") + +上面的代码就会报错,因为我们调用了一个未导出的字段,但是如果我们调用了一个不存在的字段是不会报错的,而是输出为空。 + +如果模板中输出`{{.}}`,这个一般应用与字符串对象,默认会调用fmt包输出字符串的内容。 + +### 输出嵌套字段内容 +上面我们例子展示了如何针对一个对象的字段输出,那么如果字段里面还有对象,如何来循环的输出这些内容呢?我们可以使用`{{with …}}…{{end}}`和`{{range …}}{{end}}`来进行数据的输出。详细的使用请看下面的例子: + + package main + + import ( + "html/template" + "os" + ) + + type Friend struct { + Fname string + } + + type Person struct { + UserName string + Emails []string + Friends []*Friend + } + + func main() { + f1 := Friend{Fname: "minux.ma"} + f2 := Friend{Fname: "xushiwei"} + t := template.New("fieldname example") + t, _ = t.Parse(`hello {{.UserName}}! + {{range .Emails}} + an email {{.}} + {{end}} + {{with .Friends}} + {{range .}} + my friend name is {{.Fname}} + {{end}} + {{end}} + `) + p := Person{UserName: "Astaxie", + Emails: []string{"astaxie@beego.me", "astaxie@gmail.com"}, + Friends: []*Friend{&f1, &f2}} + t.Execute(os.Stdout, p) + } + +### pipelines +Unix用户已经很熟悉什么是`pipe`了,`ls | grep "name"`类似这样的语法你是不是经常使用,过滤当前目录下面的文件,显示含有"name"的数据,他表达的意思就是前面的输出可以当做后面的输入,最后显示我们想要的数据,而Go语言模板最强大的一点就是支持pipe数据,在Go语言里面任何`{{}}`里面的都是pipelines数据,例如我们上面输出的email里面如果还有一些可能引起XSS注入的,那么我们如何来进行转化呢? + + {{. | html}} + +在email输出的地方我们可以采用如上方式可以把输出全部转化html的实体,上面的这种方式和我们平常写Unix的方式是不是一模一样,操作起来相当的简便,调用其他的函数也是类似的方式。 + +### 条件处理 +在Go模板里面如果需要进行条件判断,那么我们可以使用和Go语言的`if-else`语法类似的方式来咱先,如果pipeline为空,那么if就认为是false,下面的例子展示了如何使用`if-else`语法: + + package main + + import ( + "os" + "text/template" + ) + + func main() { + tEmpty := template.New("template test") + tEmpty = template.Must(tEmpty.Parse("空 pipeline if demo: {{if ``}} 不会输出. {{end}}\n")) + tEmpty.Execute(os.Stdout, nil) + + tWithValue := template.New("template test") + tWithValue = template.Must(tWithValue.Parse("不为空的 pipeline if demo: {{if `anything`}} 我有内容,我会输出. {{end}}\n")) + tWithValue.Execute(os.Stdout, nil) + + tIfElse := template.New("template test") + tIfElse = template.Must(tIfElse.Parse("if-else demo: {{if `anything`}} if部分 {{else}} else部分.{{end}}\n")) + tIfElse.Execute(os.Stdout, nil) + } + +通过上面的演示代码我们知道`if-else`语法相当的简单,在使用过程中很容易集成到我们的模板代码中。 + +### 模板变量 +有时候,我们在模板使用过程中需要定义一些局部变量,我们可以在一些操作中申明局部变量,例如`with``range``if`过程中申明局部变量,这个变量的作用域是`{{end}}`之前,Go语言通过申明的局部变量格式如下所示: + + $variable := pipeline + +详细的例子看下面的: + + {{with $x := "output" | printf "%q"}}{{$x}}{{end}} + {{with $x := "output"}}{{printf "%q" $x}}{{end}} + {{with $x := "output"}}{{$x | printf "%q"}}{{end}} +### 模板函数 +模板在输出对象的字段值时,采用了`fmt`包把对象转化成了字符串。但是有时候我们的需求可能不是这样的,例如有时候我们为了防止垃圾邮件发送者通过采集网页的方式来发送给我们的邮箱信息,我们希望把`@`替换成`at`例如:`astaxie at beego.me`,如果要实现这样的功能,我们就需要自定义函数来做这个功能。 + +每一个模板函数都有一个唯一值的名字,然后与一个Go函数关联,通过如下的方式来关联 + + type FuncMap map[string]interface{} + +例如,如果我们想要的email函数的模板函数名是`emailDeal`,它关联的Go函数名称是`EmailDealWith`,n那么我们可以通过下面的方式来注册这个函数 + + t = t.Funcs(template.FuncMap{"emailDeal": EmailDealWith}) + +`EmailDealWith`这个函数的参数和返回值定义如下: + + func EmailDealWith(args …interface{}) string + +我们来看下面的实现例子: + + package main + + import ( + "fmt" + "html/template" + "os" + "strings" + ) + + type Friend struct { + Fname string + } + + type Person struct { + UserName string + Emails []string + Friends []*Friend + } + + func EmailDealWith(args ...interface{}) string { + ok := false + var s string + if len(args) == 1 { + s, ok = args[0].(string) + } + if !ok { + s = fmt.Sprint(args...) + } + // find the @ symbol + substrs := strings.Split(s, "@") + if len(substrs) != 2 { + return s + } + // replace the @ by " at " + return (substrs[0] + " at " + substrs[1]) + } + + func main() { + f1 := Friend{Fname: "minux.ma"} + f2 := Friend{Fname: "xushiwei"} + t := template.New("fieldname example") + t = t.Funcs(template.FuncMap{"emailDeal": EmailDealWith}) + t, _ = t.Parse(`hello {{.UserName}}! + {{range .Emails}} + an emails {{.|emailDeal}} + {{end}} + {{with .Friends}} + {{range .}} + my friend name is {{.Fname}} + {{end}} + {{end}} + `) + p := Person{UserName: "Astaxie", + Emails: []string{"astaxie@beego.me", "astaxie@gmail.com"}, + Friends: []*Friend{&f1, &f2}} + t.Execute(os.Stdout, p) + } + + +上面演示了如何自定义函数,其实,在模板包内部已经有内置的实现函数,下面代码截取自模板包里面 + + var builtins = FuncMap{ + "and": and, + "call": call, + "html": HTMLEscaper, + "index": index, + "js": JSEscaper, + "len": length, + "not": not, + "or": or, + "print": fmt.Sprint, + "printf": fmt.Sprintf, + "println": fmt.Sprintln, + "urlquery": URLQueryEscaper, + } + + +## Must操作 +模板包里面有一个函数`Must`,它的作用是检测模板是否正确,例如大括号是否匹配,注释是否正确的关闭,变量是否正确的书写。接下来我们演示一个例子,用Must来判断模板是否正确: + + package main + + import ( + "fmt" + "text/template" + ) + + func main() { + tOk := template.New("first") + template.Must(tOk.Parse(" some static text /* and a comment */")) + fmt.Println("The first one parsed OK.") + + template.Must(template.New("second").Parse("some static text {{ .Name }}")) + fmt.Println("The second one parsed OK.") + + fmt.Println("The next one ought to fail.") + tErr := template.New("check parse error with Must") + template.Must(tErr.Parse(" some static text {{ .Name }")) + } + +讲输出如下内容 + + The first one parsed OK. + The second one parsed OK. + The next one ought to fail. + panic: template: check parse error with Must:1: unexpected "}" in command + +## 嵌套模板 +我们平常开发Web应用的时候,经常会遇到一些模板有些部分是固定不变的,然后可以抽取出来作为一个独立的部分,例如一个博客的头部和尾部是不变的,而唯一改变的是中间的内容部分。所以我们可以定义成`header`、`content`、`footer`三个部分。Go语言中通过如下的语法来申明 + + {{define "子模板名称"}}内容{{end}} + +通过如下方式来调用: + + {{template "子模板名称"}} + +接下来我们演示如何使用嵌套模板,我们定义三个文件,`header.tmpl`、`content.tmpl`、`footer.tmpl`文件,里面的内容如下 + + //header.tmpl + {{define "header"}} + + + 演示信息 + + + {{end}} + + //content.tmpl + {{define "content"}} + {{template "header"}} +

演示嵌套

+
    +
  • 嵌套使用define定义子模板
  • +
  • 调用使用template
  • +
+ {{template "footer"}} + {{end}} + + //footer.tmpl + {{define "footer"}} + + + {{end}} + +演示代码如下: + + package main + + import ( + "fmt" + "os" + "text/template" + ) + + func main() { + s1, _ := template.ParseFiles("header.tmpl", "content.tmpl", "footer.tmpl") + s1.ExecuteTemplate(os.Stdout, "header", nil) + fmt.Println() + s1.ExecuteTemplate(os.Stdout, "content", nil) + fmt.Println() + s1.ExecuteTemplate(os.Stdout, "footer", nil) + fmt.Println() + s1.Execute(os.Stdout, nil) + } + +通过上面的例子我们可以看到通过`template.ParseFiles`把所有的嵌套模板全部解析到模板里面,其实每一个定义的{{define}}都是一个独立的模板,他们相互独立,是并行存在的关系,内部其实存储的是类似map的一种关系(key是模板的名称,value是模板的内容),然后我们通过`ExecuteTemplate`来执行相应的子模板内容,我们可以看到header、footer都是相对独立的,都能输出内容,contenrt中因为嵌套了header和footer的内容,就会同时输出三个的内容。但是当我们执行`s1.Execute`,没有任何的输出,因为在默认的情况下没有默认的子模板,所以不会输出任何的东西。 + +>同一个集合类的模板是互相知晓的,如果同一模板被多个集合使用,则它需要在多个集合中分别解析 + +## 总结 +通过上面对模板的详细介绍,我们了解了如何把动态数据与模板融合:如何输出循环数据、如何自定义函数、如何嵌套模板等等。通过模板技术的应用,我们可以完成MVC模式中V的处理,接下来的章节我们将介绍如何来处理M和C。 + +## links + * [目录]() + * 上一节: [正则处理](<7.3.md>) + * 下一节: [小结](<7.5.md>) diff --git a/7.5.md b/7.5.md index 15910aae85f45dea353c007d8e156d940fddd4b6..f6818565a9d7191a6cb5d56824d87f5c5c36652a 100644 --- a/7.5.md +++ b/7.5.md @@ -5,6 +5,3 @@ * [目录]() * 上一节: [模板处理](<7.4.md>) * 下一节: [Web服务](<8.md>) - -## LastModified - * $Id$ \ No newline at end of file diff --git a/7.md b/7.md index f82fbf382619df8d4081b7e4650a5cd11c84b94e..dc77f24f8c7775487e43bcbc2f229f49cf6909c4 100644 --- a/7.md +++ b/7.md @@ -3,17 +3,14 @@ Web开发中对于文本处理是非常重要的一部分,我们往往需要 XML是目前很多标准接口的交互语言,很多时候和一些Java编写的webserver进行交互都是基于XML标准进行交互,7.1小节将介绍如何处理XML文本,我们使用XML之后发现它太复杂了,现在很多互联网企业对外的API大多数采用了JSON格式,这种格式描述简单,但是又能很好的表达意思,7.2小节我们将讲述如何来处理这样的JSON格式数据。正则是一个让人又爱又恨的工具,它处理文本的能力非常强大,我们在前面表单验证里面已经有所领略它的强大,7.3小节将详细的更深入的讲解如何利用好Go的正则。Web开发中一个很重要的部分就是MVC分离,在Go语言的Web开发中V有一个专门的包来支持`template`,7.4小节将详细的讲解如何使用模版来进行输出内容。 -## 目录 - * 1. [XML处理](7.1.md) - * 2. [JSON处理](7.2.md) +## 目录 + * 1. [XML处理](7.1.md) + * 2. [JSON处理](7.2.md) * 3. [正则处理](7.3.md) - * 4. [模板处理](7.4.md) + * 4. [模板处理](7.4.md) * 5. [小结](7.5.md) -## links - * [目录]() - * 上一章: [第六章总结](<6.5.md>) - * 下一节: [XML处理](<7.1.md>) - -## LastModified - * $Id$ \ No newline at end of file +## links + * [目录]() + * 上一章: [第六章总结](<6.5.md>) + * 下一节: [XML处理](<7.1.md>) diff --git a/8.1.md b/8.1.md index b821fc3bf28a3a0a8f47518d8aa691e99f176dcb..f323aaa9a79058eea095557818186c76e6b54a82 100644 --- a/8.1.md +++ b/8.1.md @@ -31,7 +31,7 @@ IPv6是下一版本的互联网协议,也可以说是下一代互联网的协 在Go的`net`包中定义了很多类型、函数和方法用来网络编程,其中IP的定义如下: type IP []byte - + 在`net`包中有很多函数来操作IP,但是其中比较有用的也就几个,其中`ParseIP(s string) IP`函数会把一个IPv4或者IPv6的地址转化成IP类型,请看下面的例子: package main @@ -55,7 +55,7 @@ IPv6是下一版本的互联网协议,也可以说是下一代互联网的协 os.Exit(0) } -执行之后你就会发现只要你输入一个IP地址就会给出相应的IP格式 +执行之后你就会发现只要你输入一个IP地址就会给出相应的IP格式 ## TCP Socket 当我们知道如何通过网络端口访问一个服务时,那么我们能够做什么呢?作为客户端来说,我们可以通过向远端某台机器的的某个网络端口发送一个请求,然后得到在机器的此端口上监听的服务反馈的信息。作为服务端,我们需要把服务绑定到某个指定端口,并且在此端口上监听,当有客户端来访问时能够读取信息并且写入反馈信息。 @@ -78,14 +78,14 @@ IPv6是下一版本的互联网协议,也可以说是下一代互联网的协 func ResolveTCPAddr(net, addr string) (*TCPAddr, os.Error) - net参数是"tcp4"、"tcp6"、"tcp"中的任意一个,分别表示TCP(IPv4-only),TCP(IPv6-only)或者TCP(IPv4,IPv6的任意一个). -- addr表示域名或者IP地址,例如"www.google.com:80" 或者"127.0.0.1:22". - +- addr表示域名或者IP地址,例如"www.google.com:80" 或者"127.0.0.1:22". + ### TCP client Go语言中通过net包中的`DialTCP`函数来建立一个TCP连接,并返回一个`TCPConn`类型的对象,当连接建立时服务器端也创建一个同类型的对象,此时客户端和服务器段通过各自拥有的`TCPConn`对象来进行数据交换。一般而言,客户端通过`TCPConn`对象将请求信息发送到服务器端,读取服务器端响应的信息。服务器端读取并解析来自客户端的请求,并返回应答信息,这个连接只有当任一端关闭了连接之后才失效,不然这连接可以一直在使用。建立连接的函数定义如下: func DialTCP(net string, laddr, raddr *TCPAddr) (c *TCPConn, err os.Error) - + - net参数是"tcp4"、"tcp6"、"tcp"中的任意一个,分别表示TCP(IPv4-only)、TCP(IPv6-only)或者TCP(IPv4,IPv6的任意一个) - laddr表示本机地址,一般设置为nil - raddr表示远程的服务地址 @@ -93,9 +93,9 @@ Go语言中通过net包中的`DialTCP`函数来建立一个TCP连接,并返回 接下来我们写一个简单的例子,模拟一个基于HTTP协议的客户端请求去连接一个Web服务端。我们要写一个简单的http请求头,格式类似如下: "HEAD / HTTP/1.0\r\n\r\n" - + 从服务端接收到的响应信息格式可能如下: - + HTTP/1.0 200 OK ETag: "-9985996" Last-Modified: Thu, 25 Mar 2010 17:51:10 GMT @@ -138,7 +138,7 @@ Go语言中通过net包中的`DialTCP`函数来建立一个TCP连接,并返回 os.Exit(1) } } - + 通过上面的代码我们可以看出:首先程序将用户的输入作为参数`service`传入`net.ResolveTCPAddr`获取一个tcpAddr,然后把tcpAddr传入DialTCP后创建了一个TCP连接`conn`,通过`conn`来发送请求信息,最后通过`ioutil.ReadAll`从`conn`中读取全部的文本,也就是服务端响应反馈的信息。 ### TCP server @@ -147,7 +147,7 @@ Go语言中通过net包中的`DialTCP`函数来建立一个TCP连接,并返回 func ListenTCP(net string, laddr *TCPAddr) (l *TCPListener, err os.Error) func (l *TCPListener) Accept() (c Conn, err os.Error) -参数说明同DialTCP的参数一样。下面我们实现一个简单的时间同步服务,监听7777端口 +参数说明同DialTCP的参数一样。下面我们实现一个简单的时间同步服务,监听7777端口 package main @@ -226,15 +226,15 @@ Go语言中通过net包中的`DialTCP`函数来建立一个TCP连接,并返回 ### 控制TCP连接 TCP有很多连接控制函数,我们平常用到比较多的有如下几个函数: - + func (c *TCPConn) SetTimeout(nsec int64) os.Error func (c *TCPConn) SetKeepAlive(keepalive bool) os.Error - + 第一个函数用来设置连接的超时时间,客户端和服务器端都适用,当超过设置的时间时该连接就会失效。 第二个函数用来设置客户端是否和服务器端一直保持着连接,即使没有任何的数据发送。 -更多的内容请查看`net`包的文档。 +更多的内容请查看`net`包的文档。 ## UDP Socket Go语言包中处理UDP Socket和TCP Socket不同的地方就是在服务器端处理多个客户端请求数据包的方式不同,UDP缺少了对客户端连接请求的Accept函数。其他基本几乎一模一样,只有TCP换成了UDP而已。UDP的几个主要函数如下所示: @@ -243,7 +243,7 @@ Go语言包中处理UDP Socket和TCP Socket不同的地方就是在服务器端 func ListenUDP(net string, laddr *UDPAddr) (c *UDPConn, err os.Error) func (c *UDPConn) ReadFromUDP(b []byte) (n int, addr *UDPAddr, err os.Error func (c *UDPConn) WriteToUDP(b []byte, addr *UDPAddr) (n int, err os.Error) - + 一个UDP的客户端代码如下所示,我们可以看到不同的就是TCP换成了UDP而已: package main @@ -318,12 +318,9 @@ Go语言包中处理UDP Socket和TCP Socket不同的地方就是在服务器端 ## 总结 通过对TCP和UDP Socket编程的描述和实现,可见Go已经完备地支持了Socket编程,而且使用起来相当的方便,Go提供了很多函数,通过这些函数可以很容易就编写出高性能的Socket应用。 - + ## links * [目录]() * 上一节: [Web服务](<8.md>) * 下一节: [WebSocket](<8.2.md>) - -## LastModified - * $Id$ \ No newline at end of file diff --git a/8.2.md b/8.2.md index a901d733d10387ce98d7b9da19bca97499371a4c..23e705f9ec74f4553cc93ef13b1da9056b6c5b06 100644 --- a/8.2.md +++ b/8.2.md @@ -21,7 +21,7 @@ WebSocket的协议颇为简单,在第一次handshake通过以后,连接便 ![](images/8.2.websocket2.png?raw=true) 在请求中的"Sec-WebSocket-Key"是随机的,对于整天跟编码打交到的程序员,一眼就可以看出来:这个是一个经过base64编码后的数据。服务器端接收到这个请求之后需要把这个字符串连接上一个固定的字符串: - + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 即:`f7cb4ezEAl6C3wRaU6JORA==`连接上那一串固定字符串,生成一个这样的字符串: @@ -44,41 +44,41 @@ WebSocket分为客户端和服务端,接下来我们将实现一个简单的 - -

WebSocket Echo Test

-
-

- Message: -

-
- + +

WebSocket Echo Test

+
+

+ Message: +

+
+ @@ -139,10 +139,7 @@ WebSocket分为客户端和服务端,接下来我们将实现一个简单的 通过上面的例子我们看到客户端和服务器端实现WebSocket非常的方便,Go的源码net分支中已经实现了这个的协议,我们可以直接拿来用,目前随着HTML5的发展,我想未来WebSocket会是Web开发的一个重点,我们需要储备这方面的知识。 -## links - * [目录]() - * 上一节: [Socket编程](<8.1.md>) - * 下一节: [REST](<8.3.md>) - -## LastModified - * $Id$ \ No newline at end of file +## links + * [目录]() + * 上一节: [Socket编程](<8.1.md>) + * 下一节: [REST](<8.3.md>) diff --git a/8.3.md b/8.3.md index 17dfce9c3d731654a25321326e306ae22dd544c6..64630f6f33dee2e02e7dabb2f9e729fc236d5341 100644 --- a/8.3.md +++ b/8.3.md @@ -1,112 +1,109 @@ -# 8.3 REST -RESTful,是目前最为流行的一种互联网软件架构。因为它结构清晰、符合标准、易于理解、扩展方便,所以正得到越来越多网站的采用。本小节我们将来学习它到底是一种什么样的架构?以及在Go里面如何来实现它。 -## 什么是REST -REST(REpresentational State Transfer)这个概念,首次出现是在 2000年Roy Thomas Fielding(他是HTTP规范的主要编写者之一)的博士论文中,它指的是一组架构约束条件和原则。满足这些约束条件和原则的应用程序或设计就是RESTful的。 - -要理解什么是REST,我们需要理解下面几个概念: - -- 资源(Resources) - - REST是"表现层状态转化",其实它省略了主语。"表现层"其实指的是"资源"的"表现层"。 - - 那么什么是资源呢?就是我们平常上网访问的一张图片、一个文档、一个视频等。这些资源我们通过URI来定位,也就是一个URI表示一个资源。 -- 表现层(Representation) - - 资源是做一个具体的实体信息,他可以有多种的展现方式。而把实体展现出来就是表现层,例如一个txt文本信息,他可以输出成html、json、xml等格式,一个图片他可以jpg、png等方式展现,这个就是表现层的意思。 - - URI确定一个资源,但是如何确定它的具体表现形式呢?应该在HTTP请求的头信息中用Accept和Content-Type字段指定,这两个字段才是对"表现层"的描述。 - -- 状态转化(State Transfer) - - 访问一个网站,就代表了客户端和服务器的一个互动过程。在这个过程中,肯定涉及到数据和状态的变化。而HTTP协议是无状态的,那么这些状态肯定保存在服务器端,所以如果客户端想要通知服务器端改变数据和状态的变化,肯定要通过某种方式来通知它。 - - 客户端能通知服务器端的手段,只能是HTTP协议。具体来说,就是HTTP协议里面,四个表示操作方式的动词:GET、POST、PUT、DELETE。它们分别对应四种基本操作:GET用来获取资源,POST用来新建资源(也可以用于更新资源),PUT用来更新资源,DELETE用来删除资源。 - -综合上面的解释,我们总结一下什么是RESTful架构: - -- (1)每一个URI代表一种资源; -- (2)客户端和服务器之间,传递这种资源的某种表现层; -- (3)客户端通过四个HTTP动词,对服务器端资源进行操作,实现"表现层状态转化"。 - - -Web应用要满足REST最重要的原则是:客户端和服务器之间的交互在请求之间是无状态的,即从客户端到服务器的每个请求都必须包含理解请求所必需的信息。如果服务器在请求之间的任何时间点重启,客户端不会得到通知。此外此请求可以由任何可用服务器回答,这十分适合云计算之类的环境。因为是无状态的,所以客户端可以缓存数据以改进性能。 - -另一个重要的REST原则是系统分层,这表示组件无法了解除了与它直接交互的层次以外的组件。通过将系统知识限制在单个层,可以限制整个系统的复杂性,从而促进了底层的独立性。 - -下图即是REST的架构图: - -![](images/8.3.rest2.png?raw=true) - -当REST架构的约束条件作为一个整体应用时,将生成一个可以扩展到大量客户端的应用程序。它还降低了客户端和服务器之间的交互延迟。统一界面简化了整个系统架构,改进了子系统之间交互的可见性。REST简化了客户端和服务器的实现,而且对于使用REST开发的应用程序更加容易扩展。 - -下图展示了REST的扩展性: - -![](images/8.3.rest.png?raw=true) - -## RESTful的实现 -Go没有为REST提供直接支持,但是因为RESTful是基于HTTP协议实现的,所以我们可以利用`net/http`包来自己实现,当然需要针对REST做一些改造,REST是根据不同的method来处理相应的资源,目前已经存在的很多自称是REST的应用,其实并没有真正的实现REST,我暂且把这些应用根据实现的method分成几个级别,请看下图: - -![](images/8.3.rest3.png?raw=true) - -上图展示了我们目前实现REST的三个level,我们在应用开发的时候也不一定全部按照RESTful的规则全部实现他的方式,因为有些时候完全按照RESTful的方式未必是可行的,RESTful服务充分利用每一个HTTP方法,包括`DELETE`和`PUT`。可有时,HTTP客户端只能发出`GET`和`POST`请求: - -- HTML标准只能通过链接和表单支持`GET`和`POST`。在没有Ajax支持的网页浏览器中不能发出`PUT`或`DELETE`命令 - -- 有些防火墙会挡住HTTP `PUT`和`DELETE`请求要绕过这个限制,客户端需要把实际的`PUT`和`DELETE`请求通过 POST 请求穿透过来。RESTful 服务则要负责在收到的 POST 请求中找到原始的 HTTP 方法并还原。 - -我们现在可以通过`POST`里面增加隐藏字段`_method`这种方式可以来模拟`PUT`、`DELETE`等方式,但是服务器端需要做转换。我现在的项目里面就按照这种方式来做的REST接口。当然Go语言里面完全按照RSETful来实现是很容易的,我们通过下面的例子来说明如何实现RESTful的应用设计。 - - package main - - import ( - "fmt" - "github.com/drone/routes" - "net/http" - ) - - func getuser(w http.ResponseWriter, r *http.Request) { - params := r.URL.Query() - uid := params.Get(":uid") - fmt.Fprintf(w, "you are get user %s", uid) - } - - func modifyuser(w http.ResponseWriter, r *http.Request) { - params := r.URL.Query() - uid := params.Get(":uid") - fmt.Fprintf(w, "you are modify user %s", uid) - } - - func deleteuser(w http.ResponseWriter, r *http.Request) { - params := r.URL.Query() - uid := params.Get(":uid") - fmt.Fprintf(w, "you are delete user %s", uid) - } - - func adduser(w http.ResponseWriter, r *http.Request) { - params := r.URL.Query() - uid := params.Get(":uid") - fmt.Fprint(w, "you are add user %s", uid) - } - - func main() { - mux := routes.New() - mux.Get("/user/:uid", getuser) - mux.Post("/user/:uid", modifyuser) - mux.Del("/user/:uid", deleteuser) - mux.Put("/user/", adduser) - http.Handle("/", mux) - http.ListenAndServe(":8088", nil) - } - -上面的代码演示了如何编写一个REST的应用,我们访问的资源是用户,我们通过不同的method来访问不同的函数,这里使用了第三方库`github.com/drone/routes`,在前面章节我们介绍过如何实现自定义的路由器,这个库实现了自定义路由和方便的路由规则映射,通过它,我们可以很方便的实现REST的架构。通过上面的代码可知,REST就是根据不同的method访问同一个资源的时候实现不同的逻辑处理。 - -## 总结 -REST是一种架构风格,汲取了WWW的成功经验:无状态,以资源为中心,充分利用HTTP协议和URI协议,提供统一的接口定义,使得它作为一种设计Web服务的方法而变得流行。在某种意义上,通过强调URI和HTTP等早期Internet标准,REST是对大型应用程序服务器时代之前的Web方式的回归。目前Go对于REST的支持还是很简单的,通过实现自定义的路由规则,我们就可以为不同的method实现不同的handle,这样就实现了REST的架构。 - -## links - * [目录]() - * 上一节: [WebSocket](<8.2.md>) - * 下一节: [RPC](<8.4.md>) - -## LastModified - * $Id$ \ No newline at end of file +# 8.3 REST +RESTful,是目前最为流行的一种互联网软件架构。因为它结构清晰、符合标准、易于理解、扩展方便,所以正得到越来越多网站的采用。本小节我们将来学习它到底是一种什么样的架构?以及在Go里面如何来实现它。 +## 什么是REST +REST(REpresentational State Transfer)这个概念,首次出现是在 2000年Roy Thomas Fielding(他是HTTP规范的主要编写者之一)的博士论文中,它指的是一组架构约束条件和原则。满足这些约束条件和原则的应用程序或设计就是RESTful的。 + +要理解什么是REST,我们需要理解下面几个概念: + +- 资源(Resources) + REST是"表现层状态转化",其实它省略了主语。"表现层"其实指的是"资源"的"表现层"。 + + 那么什么是资源呢?就是我们平常上网访问的一张图片、一个文档、一个视频等。这些资源我们通过URI来定位,也就是一个URI表示一个资源。 + +- 表现层(Representation) + + 资源是做一个具体的实体信息,他可以有多种的展现方式。而把实体展现出来就是表现层,例如一个txt文本信息,他可以输出成html、json、xml等格式,一个图片他可以jpg、png等方式展现,这个就是表现层的意思。 + + URI确定一个资源,但是如何确定它的具体表现形式呢?应该在HTTP请求的头信息中用Accept和Content-Type字段指定,这两个字段才是对"表现层"的描述。 + +- 状态转化(State Transfer) + + 访问一个网站,就代表了客户端和服务器的一个互动过程。在这个过程中,肯定涉及到数据和状态的变化。而HTTP协议是无状态的,那么这些状态肯定保存在服务器端,所以如果客户端想要通知服务器端改变数据和状态的变化,肯定要通过某种方式来通知它。 + + 客户端能通知服务器端的手段,只能是HTTP协议。具体来说,就是HTTP协议里面,四个表示操作方式的动词:GET、POST、PUT、DELETE。它们分别对应四种基本操作:GET用来获取资源,POST用来新建资源(也可以用于更新资源),PUT用来更新资源,DELETE用来删除资源。 + +综合上面的解释,我们总结一下什么是RESTful架构: + +- (1)每一个URI代表一种资源; +- (2)客户端和服务器之间,传递这种资源的某种表现层; +- (3)客户端通过四个HTTP动词,对服务器端资源进行操作,实现"表现层状态转化"。 + + +Web应用要满足REST最重要的原则是:客户端和服务器之间的交互在请求之间是无状态的,即从客户端到服务器的每个请求都必须包含理解请求所必需的信息。如果服务器在请求之间的任何时间点重启,客户端不会得到通知。此外此请求可以由任何可用服务器回答,这十分适合云计算之类的环境。因为是无状态的,所以客户端可以缓存数据以改进性能。 + +另一个重要的REST原则是系统分层,这表示组件无法了解除了与它直接交互的层次以外的组件。通过将系统知识限制在单个层,可以限制整个系统的复杂性,从而促进了底层的独立性。 + +下图即是REST的架构图: + +![](images/8.3.rest2.png?raw=true) + +当REST架构的约束条件作为一个整体应用时,将生成一个可以扩展到大量客户端的应用程序。它还降低了客户端和服务器之间的交互延迟。统一界面简化了整个系统架构,改进了子系统之间交互的可见性。REST简化了客户端和服务器的实现,而且对于使用REST开发的应用程序更加容易扩展。 + +下图展示了REST的扩展性: + +![](images/8.3.rest.png?raw=true) + +## RESTful的实现 +Go没有为REST提供直接支持,但是因为RESTful是基于HTTP协议实现的,所以我们可以利用`net/http`包来自己实现,当然需要针对REST做一些改造,REST是根据不同的method来处理相应的资源,目前已经存在的很多自称是REST的应用,其实并没有真正的实现REST,我暂且把这些应用根据实现的method分成几个级别,请看下图: + +![](images/8.3.rest3.png?raw=true) + +上图展示了我们目前实现REST的三个level,我们在应用开发的时候也不一定全部按照RESTful的规则全部实现他的方式,因为有些时候完全按照RESTful的方式未必是可行的,RESTful服务充分利用每一个HTTP方法,包括`DELETE`和`PUT`。可有时,HTTP客户端只能发出`GET`和`POST`请求: + +- HTML标准只能通过链接和表单支持`GET`和`POST`。在没有Ajax支持的网页浏览器中不能发出`PUT`或`DELETE`命令 + +- 有些防火墙会挡住HTTP `PUT`和`DELETE`请求要绕过这个限制,客户端需要把实际的`PUT`和`DELETE`请求通过 POST 请求穿透过来。RESTful 服务则要负责在收到的 POST 请求中找到原始的 HTTP 方法并还原。 + +我们现在可以通过`POST`里面增加隐藏字段`_method`这种方式可以来模拟`PUT`、`DELETE`等方式,但是服务器端需要做转换。我现在的项目里面就按照这种方式来做的REST接口。当然Go语言里面完全按照RSETful来实现是很容易的,我们通过下面的例子来说明如何实现RESTful的应用设计。 + + package main + + import ( + "fmt" + "github.com/drone/routes" + "net/http" + ) + + func getuser(w http.ResponseWriter, r *http.Request) { + params := r.URL.Query() + uid := params.Get(":uid") + fmt.Fprintf(w, "you are get user %s", uid) + } + + func modifyuser(w http.ResponseWriter, r *http.Request) { + params := r.URL.Query() + uid := params.Get(":uid") + fmt.Fprintf(w, "you are modify user %s", uid) + } + + func deleteuser(w http.ResponseWriter, r *http.Request) { + params := r.URL.Query() + uid := params.Get(":uid") + fmt.Fprintf(w, "you are delete user %s", uid) + } + + func adduser(w http.ResponseWriter, r *http.Request) { + params := r.URL.Query() + uid := params.Get(":uid") + fmt.Fprint(w, "you are add user %s", uid) + } + + func main() { + mux := routes.New() + mux.Get("/user/:uid", getuser) + mux.Post("/user/:uid", modifyuser) + mux.Del("/user/:uid", deleteuser) + mux.Put("/user/", adduser) + http.Handle("/", mux) + http.ListenAndServe(":8088", nil) + } + +上面的代码演示了如何编写一个REST的应用,我们访问的资源是用户,我们通过不同的method来访问不同的函数,这里使用了第三方库`github.com/drone/routes`,在前面章节我们介绍过如何实现自定义的路由器,这个库实现了自定义路由和方便的路由规则映射,通过它,我们可以很方便的实现REST的架构。通过上面的代码可知,REST就是根据不同的method访问同一个资源的时候实现不同的逻辑处理。 + +## 总结 +REST是一种架构风格,汲取了WWW的成功经验:无状态,以资源为中心,充分利用HTTP协议和URI协议,提供统一的接口定义,使得它作为一种设计Web服务的方法而变得流行。在某种意义上,通过强调URI和HTTP等早期Internet标准,REST是对大型应用程序服务器时代之前的Web方式的回归。目前Go对于REST的支持还是很简单的,通过实现自定义的路由规则,我们就可以为不同的method实现不同的handle,这样就实现了REST的架构。 + +## links + * [目录]() + * 上一节: [WebSocket](<8.2.md>) + * 下一节: [RPC](<8.4.md>) diff --git a/8.4.md b/8.4.md index ca9637f04e0879094d8d250d63604a26c880c1b3..a7d044cdd27320bb65fb655d009018f4fb7ba681 100644 --- a/8.4.md +++ b/8.4.md @@ -35,12 +35,12 @@ Go RPC的函数只有符合下面的条件才能被远程访问,不然会被 举个例子,正确的RPC函数格式如下: func (t *T) MethodName(argType T1, replyType *T2) error - -T、T1和T2类型必须能被`encoding/gob`包编解码。 + +T、T1和T2类型必须能被`encoding/gob`包编解码。 任何的RPC都需要通过网络来传递数据,Go RPC可以利用HTTP和TCP来传递数据,利用HTTP的好处是可以直接复用`net/http`里面的一些函数。详细的例子请看下面的实现 -### HTTP RPC +### HTTP RPC http的服务端代码实现如下: package main @@ -212,7 +212,7 @@ http的服务端代码实现如下: 上面这个代码和http的服务器相比,不同在于:在此处我们采用了TCP协议,然后需要自己控制连接,当有客户端连接上来后,我们需要把这个连接交给rpc来处理。 -如果你留心了,你会发现这它是一个阻塞型的单用户的程序,如果想要实现多并发,那么可以使用goroutine来实现,我们前面在socket小节的时候已经介绍过如何处理goroutine。 +如果你留心了,你会发现这它是一个阻塞型的单用户的程序,如果想要实现多并发,那么可以使用goroutine来实现,我们前面在socket小节的时候已经介绍过如何处理goroutine。 下面展现了TCP实现的RPC客户端: package main @@ -378,16 +378,13 @@ JSON RPC是数据编码采用了JSON,而不是gob编码,其他和上面介 fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem) } - + ## 总结 -Go已经提供了对RPC的良好支持,通过上面HTTP、TCP、JSON RPC的实现,我们就可以很方便的开发很多分布式的Web应用,我想作为读者的你已经领会到这一点。但遗憾的是目前Go尚未提供对SOAP RPC的支持,欣慰的是现在已经有第三方的开源实现了。 +Go已经提供了对RPC的良好支持,通过上面HTTP、TCP、JSON RPC的实现,我们就可以很方便的开发很多分布式的Web应用,我想作为读者的你已经领会到这一点。但遗憾的是目前Go尚未提供对SOAP RPC的支持,欣慰的是现在已经有第三方的开源实现了。 -## links - * [目录]() - * 上一节: [REST](<8.3.md>) - * 下一节: [小结](<8.5.md>) - -## LastModified - * $Id$ \ No newline at end of file +## links + * [目录]() + * 上一节: [REST](<8.3.md>) + * 下一节: [小结](<8.5.md>) diff --git a/8.5.md b/8.5.md index c1a7b38b53cbeb6e3d4a3573011eb87c189558d8..60712d6615c14ed82dc0349ed8c89aedfdc2948c 100644 --- a/8.5.md +++ b/8.5.md @@ -1,9 +1,6 @@ # 8.5 小结 这一章我们介绍了目前流行的几种主要的网络应用开发方式,第一小节介绍了网络编程中的基础:Socket编程,因为现在网络正在朝云的方向快速进化,作为这一技术演进的基石的的socket知识,作为开发者的你,是必须要掌握的。第二小节介绍了正愈发流行的HTML5中一个重要的特性WebSocket,通过它,服务器可以实现主动的push消息,以简化以前ajax轮询的模式。第三小节介绍了REST编写模式,这种模式特别适合来开发网络应用API,目前移动应用的快速发展,我觉得将来会是一个潮流。第四小节介绍了Go实现的RPC相关知识,对于上面四种开发方式,Go都已经提供了良好的支持,net包及其子包,是所有涉及到网络编程的工具的所在地。如果你想更加深入的了解相关实现细节,可以尝试阅读这个包下面的源码。 -## links - * [目录]() - * 上一节: [RPC](<8.4.md>) - * 下一章: [安全与加密](<9.md>) - -## LastModified - * $Id$ \ No newline at end of file +## links + * [目录]() + * 上一节: [RPC](<8.4.md>) + * 下一章: [安全与加密](<9.md>) diff --git a/8.md b/8.md index 707cf3faa5b44ad343e37af84fdc987623b77f55..265152bd1f7b9355ff737c6a506ede6cc8967c72 100644 --- a/8.md +++ b/8.md @@ -11,17 +11,14 @@ SOAP是W3C在跨网络信息传递和远程计算机函数调用方面的一个 Go语言是21世纪的C语言,我们追求的是性能、简单,所以我们在8.1小节里面介绍如何使用Socket编程,很多游戏服务都是采用Socket来编写服务段,因为HTTP协议相对而言比较耗费性能,让我们看看Go语言如何来Socket编程。目前随着HTML5的发展,webSockets也逐渐的成为很多页游公司接下来开发的一些手段,我们将在8.2小节里面讲解Go语言如何编写webSockets的代码。 -## 目录 - * 1. [Socket编程](8.1.md) - * 2. [WebSocket](8.2.md) +## 目录 + * 1. [Socket编程](8.1.md) + * 2. [WebSocket](8.2.md) * 3. [REST](8.3.md) - * 4. [RPC](8.4.md) + * 4. [RPC](8.4.md) * 5. [小结](8.5.md) -## links - * [目录]() - * 上一章: [第七章总结](<7.5.md>) - * 下一节: [Socket编程](<8.1.md>) - -## LastModified - * $Id$ \ No newline at end of file +## links + * [目录]() + * 上一章: [第七章总结](<7.5.md>) + * 下一节: [Socket编程](<8.1.md>) diff --git a/9.1.md b/9.1.md index b9e9beb519143da3637c91d8986c6c6a40bf96b4..d74c537cd29caa544a135a107eadda324f798702 100644 --- a/9.1.md +++ b/9.1.md @@ -47,7 +47,7 @@ CSRF的防御可以从服务端和客户端两方面着手,防御效果是从 mux.Get("/user/:uid", getuser) mux.Post("/user/:uid", modifyuser) - + 这样处理后,因为我们限定了修改只能使用POST,当GET方式请求时就拒绝响应,所以上面图示中GET方式的CSRF攻击就可以防止了,但这样就能全部解决问题了吗?当然不是,因为POST也是可以模拟的。 因此我们需要实施第二步,在非GET方式的请求中增加随机数,这个大概有三种方式来进行: @@ -56,39 +56,36 @@ CSRF的防御可以从服务端和客户端两方面着手,防御效果是从 - 每个请求使用验证码,这个方案是完美的,因为要多次输入验证码,所以用户友好性很差,所以不适合实际运用。 - 不同的表单包含一个不同的伪随机值,我们在4.4小节介绍“如何防止表单多次递交”时介绍过此方案,复用相关代码,实现如下: - 生成随机数token - - h := md5.New() - io.WriteString(h, strconv.FormatInt(crutime, 10)) - io.WriteString(h, "ganraomaxxxxxxxxx") - token := fmt.Sprintf("%x", h.Sum(nil)) - - t, _ := template.ParseFiles("login.gtpl") - t.Execute(w, token) - - 输出token - - - - 验证token - - r.ParseForm() - token := r.Form.Get("token") - if token != "" { - //验证token的合法性 - } else { - //不存在token报错 - } +生成随机数token + + h := md5.New() + io.WriteString(h, strconv.FormatInt(crutime, 10)) + io.WriteString(h, "ganraomaxxxxxxxxx") + token := fmt.Sprintf("%x", h.Sum(nil)) + + t, _ := template.ParseFiles("login.gtpl") + t.Execute(w, token) + +输出token + + + +验证token + + r.ParseForm() + token := r.Form.Get("token") + if token != "" { + //验证token的合法性 + } else { + //不存在token报错 + } 这样基本就实现了安全的POST,但是也许你会说如果破解了token的算法呢,按照理论上是,但是实际上破解是基本不可能的,因为有人曾计算过,暴力破解该串大概需要2的11次方时间。 ## 总结 跨站请求伪造,即CSRF,是一种非常危险的Web安全威胁,它被Web安全界称为“沉睡的巨人”,其威胁程度由此“美誉”便可见一斑。本小节不仅对跨站请求伪造本身进行了简单介绍,还详细说明造成这种漏洞的原因所在,然后以此提了一些防范该攻击的建议,希望对读者编写安全的Web应用能够有所启发。 - -## links - * [目录]() - * 上一节: [安全与加密](<9.md>) - * 下一节: [确保输入过滤](<9.2.md>) - -## LastModified - * $Id$ + +## links + * [目录]() + * 上一节: [安全与加密](<9.md>) + * 下一节: [确保输入过滤](<9.2.md>) diff --git a/9.2.md b/9.2.md index ed53b5082e24b7dd10a055b3c617770ce46180cd..5a4b0ea09d50cca14dbbc2ee3c97586107920bbb 100644 --- a/9.2.md +++ b/9.2.md @@ -1,4 +1,4 @@ -# 9.2 确保输入过滤 +# 9.2 确保输入过滤 过滤用户数据是Web应用安全的基础。它是验证数据合法性的过程。通过对所有的输入数据进行过滤,可以避免恶意数据在程序中被误信或误用。大多数Web应用的漏洞都是因为没有对用户输入的数据进行恰当过滤所引起的。 我们介绍的过滤数据分成三个步骤: @@ -33,13 +33,13 @@ 接下来,让我们通过一个例子来巩固这些概念,请看下面这个表单
- 我是谁: - - + 我是谁: + +
在处理这个表单的编程逻辑中,非常容易犯的错误是认为只能提交三个选择中的一个。其实攻击者可以模拟POST操作,递交`name=attack`这样的数据,所以在此时我们需要做类似白名单的处理 @@ -64,12 +64,9 @@ } ## 总结 -数据过滤在Web安全中起到一个基石的作用,大多数的安全问题都是由于没有过滤数据和验证数据引起的,例如前面小节的CSRF攻击,以及接下来将要介绍的XSS攻击、SQL注入等都是没有认真地过滤数据引起的,因此我们需要特别重视这部分的内容。 - -## links - * [目录]() - * 上一节: [预防CSRF攻击](<9.1.md>) - * 下一节: [避免XSS攻击](<9.3.md>) - -## LastModified - * $Id$ +数据过滤在Web安全中起到一个基石的作用,大多数的安全问题都是由于没有过滤数据和验证数据引起的,例如前面小节的CSRF攻击,以及接下来将要介绍的XSS攻击、SQL注入等都是没有认真地过滤数据引起的,因此我们需要特别重视这部分的内容。 + +## links + * [目录]() + * 上一节: [预防CSRF攻击](<9.1.md>) + * 下一节: [避免XSS攻击](<9.3.md>) diff --git a/9.3.md b/9.3.md index 769e12055cf38726785270c1700b373a42dc41e8..7f6569cb1f0717dd1c20cd3eac919a89c3679850 100644 --- a/9.3.md +++ b/9.3.md @@ -1,4 +1,4 @@ -# 9.3 避免XSS攻击 +# 9.3 避免XSS攻击 随着互联网技术的发展,现在的Web应用都含有大量的动态内容以提高用户体验。所谓动态内容,就是应用程序能够根据用户环境和用户请求,输出相应的内容。动态站点会受到一种名为“跨站脚本攻击”(Cross Site Scripting, 安全专家们通常将其缩写成 XSS)的威胁,而静态站点则完全不受其影响。 ## 什么是XSS @@ -31,13 +31,13 @@ Web应用未对用户提交请求的数据做充分的检查过滤,允许用 目前防御XSS主要有如下几种方式: - 过滤特殊字符 - + 避免XSS的方法之一主要是将用户所提供的内容进行过滤,Go语言提供了HTML的过滤函数: text/template包下面的HTMLEscapeString、JSEscapeString等函数 - 使用HTTP头指定类型 - + `w.Header().Set("Content-Type","text/javascript")` 这样就可以让浏览器解析javascript代码,而不会是html输出。 @@ -46,10 +46,7 @@ Web应用未对用户提交请求的数据做充分的检查过滤,允许用 ## 总结 XSS漏洞是相当有危害的,在开发Web应用的时候,一定要记住过滤数据,特别是在输出到客户端之前,这是现在行之有效的防止XSS的手段。 -## links - * [目录]() - * 上一节: [确保输入过滤](<9.2.md>) - * 下一节: [避免SQL注入](<9.4.md>) - -## LastModified - * $Id$ +## links + * [目录]() + * 上一节: [确保输入过滤](<9.2.md>) + * 下一节: [避免SQL注入](<9.4.md>) diff --git a/9.4.md b/9.4.md index 96021e9ebf0a31c244c5bea72e06fe54cc586786..983212ac1723e5714e35192015fc4532217c4324 100644 --- a/9.4.md +++ b/9.4.md @@ -1,4 +1,4 @@ -# 9.4 避免SQL注入 +# 9.4 避免SQL注入 ## 什么是SQL注入 SQL注入攻击(SQL Injection),简称注入攻击,是Web开发中最常见的一种安全漏洞。可以用它来从数据库获取敏感信息,或者利用数据库的特性执行添加用户,导出文件等一系列恶意操作,甚至有可能获取数据库乃至系统用户最高权限。 @@ -20,7 +20,7 @@ SQL注入攻击(SQL Injection),简称注入攻击,是Web开发中最常 username:=r.Form.Get("username") password:=r.Form.Get("password") - sql:="SELECT * FROM user WHERE username='"+username+"' AND password='"+password+"'" + sql:="SELECT * FROM user WHERE username='"+username+"' AND password='"+password+"'" 如果用户的输入的用户名如下,密码任意 @@ -29,7 +29,7 @@ SQL注入攻击(SQL Injection),简称注入攻击,是Web开发中最常 那么我们的SQL变成了如下所示: SELECT * FROM user WHERE username='myuser' or 'foo'=='foo' --'' AND password='xxx' - + 在SQL里面`--`是注释标记,所以查询语句会在此中断。这就让攻击者在不知道任何合法用户名和密码的情况下成功登录了。 对于MSSQL还有更加危险的一种SQL注入,就是控制系统,下面这个可怕的例子将演示如何在某些版本的MSSQL数据库上执行系统命令。 @@ -37,15 +37,15 @@ SQL注入攻击(SQL Injection),简称注入攻击,是Web开发中最常 sql:="SELECT * FROM products WHERE name LIKE '%"+prod+"%'" Db.Exec(sql) -如果攻击提交`a%' exec master..xp_cmdshell 'net user test testpass /ADD' --`作为变量 prod的值,那么sql将会变成 +如果攻击提交`a%' exec master..xp_cmdshell 'net user test testpass /ADD' --`作为变量 prod的值,那么sql将会变成 sql:="SELECT * FROM products WHERE name LIKE '%a%' exec master..xp_cmdshell 'net user test testpass /ADD'--%'" - + MSSQL服务器会执行这条SQL语句,包括它后面那个用于向系统添加新用户的命令。如果这个程序是以sa运行而 MSSQLSERVER服务又有足够的权限的话,攻击者就可以获得一个系统帐号来访问主机了。 - + >虽然以上的例子是针对某一特定的数据库系统的,但是这并不代表不能对其它数据库系统实施类似的攻击。针对这种安全漏洞,只要使用不同方法,各种数据库都有可能遭殃。 - + ## 如何预防SQL注入 也许你会说攻击者要知道数据库结构的信息才能实施SQL注入攻击。确实如此,但没人能保证攻击者一定拿不到这些信息,一旦他们拿到了,数据库就存在泄露的危险。如果你在用开放源代码的软件包来访问数据库,比如论坛程序,攻击者就很容易得到相关的代码。如果这些代码设计不良的话,风险就更大了。目前Discuz、phpwind、phpcms等这些流行的开源程序都有被SQL注入攻击的先例。 @@ -63,10 +63,7 @@ SQL注入攻击的危害这么大,那么该如何来防治呢?下面这些建 ## 总结 通过上面的示例我们可以知道,SQL注入是危害相当大的安全漏洞。所以对于我们平常编写的Web应用,应该对于每一个小细节都要非常重视,细节决定命运,生活如此,编写Web应用也是这样。 -## links - * [目录]() - * 上一节: [避免XSS攻击](<9.3.md>) - * 下一节: [存储密码](<9.5.md>) - -## LastModified - * $Id$ +## links + * [目录]() + * 上一节: [避免XSS攻击](<9.3.md>) + * 下一节: [存储密码](<9.5.md>) diff --git a/9.5.md b/9.5.md index ee6908c15c133fbac74cfffff1bd2f85fed77980..fd5d60213286755d79d2753bb6d137d64da23d8c 100644 --- a/9.5.md +++ b/9.5.md @@ -1,4 +1,4 @@ -# 9.5 存储密码 +# 9.5 存储密码 过去一段时间以来, 许多的网站遭遇用户密码数据泄露事件, 这其中包括顶级的互联网企业–Linkedin, 国内诸如CSDN,该事件横扫整个国内互联网,随后又爆出多玩游戏800万用户资料被泄露,另有传言人人网、开心网、天涯社区、世纪佳缘、百合网等社区都有可能成为黑客下一个目标。层出不穷的类似事件给用户的网上生活造成巨大的影响,人人自危,因为人们往往习惯在不同网站使用相同的密码,所以一家“暴库”,全部遭殃。 那么我们作为一个Web应用开发者,在选择密码存储方案时, 容易掉入哪些陷阱, 以及如何避免这些陷阱? @@ -39,25 +39,25 @@ Go语言对这三种加密算法的实现如下所示: 但是单纯的多次哈希,依然阻挡不住黑客。两次 MD5、三次 MD5之类的方法,我们能想到,黑客自然也能想到。特别是对于一些开源代码,这样哈希更是相当于直接把算法告诉了黑客。 没有攻不破的盾,但也没有折不断的矛。现在安全性比较好的网站,都会用一种叫做“加盐”的方式来存储密码,也就是常说的 “salt”。他们通常的做法是,先将用户输入的密码进行一次MD5(或其它哈希算法)加密;将得到的 MD5 值前后加上一些只有管理员自己知道的随机串,再进行一次MD5加密。这个随机串中可以包括某些固定的串,也可以包括用户名(用来保证每个用户加密使用的密钥都不一样)。 - + //import "crypto/md5" //假设用户名abc,密码123456 h := md5.New() io.WriteString(h, "需要加密的密码") - + //pwmd5等于e10adc3949ba59abbe56e057f20f883e pwmd5 :=fmt.Sprintf("%x", h.Sum(nil)) - + //指定两个 salt: salt1 = @#$% salt2 = ^&*() - salt1 := "@#$%" + salt1 := "@#$%" salt2 := "^&*()" - + //salt1+用户名+salt2+MD5拼接 io.WriteString(h, salt1) io.WriteString(h, "abc") io.WriteString(h, salt2) io.WriteString(h, pwmd5) - + last :=fmt.Sprintf("%x", h.Sum(nil)) 在两个salt没有泄露的情况下,黑客如果拿到的是最后这个加密串,就几乎不可能推算出原始的密码是什么了。 @@ -74,7 +74,7 @@ Go语言对这三种加密算法的实现如下所示: 目前Go语言里面支持的库http://code.google.com/p/go/source/browse?repo=crypto#hg%2Fscrypt dk := scrypt.Key([]byte("some password"), []byte(salt), 16384, 8, 1, 32) - + 通过上面的的方法可以获取唯一的相应的密码值,这是目前为止最难破解的。 ## 总结 @@ -83,10 +83,7 @@ Go语言对这三种加密算法的实现如下所示: - 1)如果你是普通用户,那么我们建议使用LastPass进行密码存储和生成,对不同的网站使用不同的密码; - 2)如果你是开发人员, 那么我们强烈建议你采用专家方案进行密码存储。 -## links - * [目录]() - * 上一节: [确保输入过滤](<9.4.md>) - * 下一节: [加密和解密数据](<9.6.md>) - -## LastModified - * $Id$ +## links + * [目录]() + * 上一节: [确保输入过滤](<9.4.md>) + * 下一节: [加密和解密数据](<9.6.md>) diff --git a/9.6.md b/9.6.md index c506d8d14db4c83fb4fad7a7b61a8f9f3fef6329..982896ed4dd8eb5d6f120fdaf779386122269073 100644 --- a/9.6.md +++ b/9.6.md @@ -1,4 +1,4 @@ -# 9.6 加密和解密数据 +# 9.6 加密和解密数据 前面小节介绍了如何存储密码,但是有的时候,我们想把一些敏感数据加密后存储起来,在将来的某个时候,随需将它们解密出来,此时我们应该在选用对称加密算法来满足我们的需求。 ## base64加解密 @@ -20,11 +20,11 @@ } func main() { - // encode + // encode hello := "你好,世界! hello world" debyte := base64Encode([]byte(hello)) fmt.Println(debyte) - // decode + // decode enbyte, err := base64Decode(debyte) if err != nil { fmt.Println(err.Error()) @@ -98,16 +98,16 @@ Go语言的`crypto`里面支持对称加密的高级加解密包有: 上面通过调用函数`aes.NewCipher`(参数key必须是16、24或者32位的[]byte,分别对应AES-128, AES-192或AES-256算法),返回了一个`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) + // 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) } 这三个函数实现了加解密操作,详细的操作请看上面的例子。 @@ -116,10 +116,7 @@ Go语言的`crypto`里面支持对称加密的高级加解密包有: 这小节介绍了几种加解密的算法,在开发Web应用的时候可以根据需求采用不同的方式进行加解密,一般的应用可以采用base64算法,更加高级的话可以采用aes或者des算法。 -## links - * [目录]() - * 上一节: [存储密码](<9.5.md>) - * 下一节: [小结](<9.7.md>) - -## LastModified - * $Id$ +## links + * [目录]() + * 上一节: [存储密码](<9.5.md>) + * 下一节: [小结](<9.7.md>) diff --git a/9.7.md b/9.7.md index add6005fd7f8a3a247b92ba9eb1a033e3fd521e3..eafec4c99b2da7171b056e863cafb4dfb3301c02 100644 --- a/9.7.md +++ b/9.7.md @@ -1,10 +1,9 @@ # 9.7 小结 -这一章主要介绍了如:CSRF攻击、XSS攻击、SQL注入攻击等一些Web应用中典型的攻击手法,它们都是由于应用对用户的输入没有很好的过滤引起的,所以除了介绍攻击的方法外,我们也介绍了了如何有效的进行数据过滤,以防止这些攻击的发生的方法。然后针对日异严重的密码泄漏事件,介绍了在设计Web应用中可采用的从基本到专家的加密方案。最后针对敏感数据的加解密简要介绍了,Go语言提供三种对称加密算法:base64、aes和des的实现。 编写这一章的目的是希望读者能够在意识里面加强安全概念,在编写Web应用的时候多留心一点,以使我们编写的Web应用能远离黑客们的攻击。Go语言在支持防攻击方面已经提供大量的工具包,我们可以充分的利用这些包来做出一个安全的Web应用。 +这一章主要介绍了如:CSRF攻击、XSS攻击、SQL注入攻击等一些Web应用中典型的攻击手法,它们都是由于应用对用户的输入没有很好的过滤引起的,所以除了介绍攻击的方法外,我们也介绍了了如何有效的进行数据过滤,以防止这些攻击的发生的方法。然后针对日异严重的密码泄漏事件,介绍了在设计Web应用中可采用的从基本到专家的加密方案。最后针对敏感数据的加解密简要介绍了,Go语言提供三种对称加密算法:base64、aes和des的实现。 -## links - * [目录]() - * 上一节: [加密和解密数据](<9.6.md>) - * 下一节: [国际化和本地化](<10.md>) - -## LastModified - * $Id$ +编写这一章的目的是希望读者能够在意识里面加强安全概念,在编写Web应用的时候多留心一点,以使我们编写的Web应用能远离黑客们的攻击。Go语言在支持防攻击方面已经提供大量的工具包,我们可以充分的利用这些包来做出一个安全的Web应用。 + +## links + * [目录]() + * 上一节: [加密和解密数据](<9.6.md>) + * 下一节: [国际化和本地化](<10.md>) diff --git a/9.md b/9.md index 067017d39cb55261d0b5142da57859f0b57cbcac..6e8164b9ece6f45a17d7f63d9eba041fbbe04354 100644 --- a/9.md +++ b/9.md @@ -11,19 +11,16 @@ 加密的本质就是扰乱数据,某些不可恢复的数据扰乱我们称为单项加密或者散列算法。另外还有一种双向加密方式,也就是可以对加密后的数据进行解密。我们将会在9.6小节介绍如何实现这种双向加密方式。 -## 目录 - * 1 [预防CSRF攻击](9.1.md) - * 2 [确保输入过滤](9.2.md) - * 3 [避免XSS攻击](9.3.md) - * 4 [避免SQL注入](9.4.md) - * 5 [存储密码](9.5.md) +## 目录 + * 1 [预防CSRF攻击](9.1.md) + * 2 [确保输入过滤](9.2.md) + * 3 [避免XSS攻击](9.3.md) + * 4 [避免SQL注入](9.4.md) + * 5 [存储密码](9.5.md) * 6 [加密和解密数据](9.6.md) * 7 [小结](9.7.md) -## links - * [目录]() - * 上一章: [第八章总结](<8.5.md>) - * 下一节: [预防CSRF攻击](<9.1.md>) - -## LastModified - * $Id$ +## links + * [目录]() + * 上一章: [第八章总结](<8.5.md>) + * 下一节: [预防CSRF攻击](<9.1.md>) diff --git a/README.md b/README.md index dd96d7e7691945dce5329177e5cd6799d2c87fa5..d96f80197d2189a9b2cac5e59710d27d2e48e847 100644 --- a/README.md +++ b/README.md @@ -1,57 +1,57 @@ -# 《Go Web 编程》 -因为自己对Web开发比较感兴趣,所以最近抽空在写一本开源的书籍《Go Web编程》《Build Web Application with Golang》。写这本书不表示我能力很强,而是我愿意分享,和大家一起分享Go写Web应用的一些东西。 - -- 对于从PHP/Python/Ruby转过来的同学了解Go怎么写Web应用开发的 - -- 对于从C/C++转过来的同学了解Web到底是怎么运行起来的 - -我一直认为知识是用来分享的,让更多的人分享自己拥有的一切知识这个才是人生最大的快乐。 - -这本书目前我放在Github上,我现在基本每天晚上抽空会写一些,时间有限、能力有限,所以希望更多的朋友参与到这个开源项目中来。 - -**参加了51CTO博客大赛,希望你能够投出宝贵的一票:[http://blog.51cto.com/contest2012/6177767](http://blog.51cto.com/contest2012/6177767)** - -## 撰写方法 -### 文件命名 -每个章节建立一个md文件,如第11章的第3节,则建立**11.3.md**。 -### 代码文件 -代码文件置于src目录之下。每小节代码按目录存放。如第11章的第3节的代码保存于**src/11.3/**目录下。在正文中按需要添加代码。 - -## 格式规范 -### 正文 -请参看已有章节的规范,要注意的是,每个章节在底部都需要有一个links节,包含“目录”,“上一节”和“下一节”的链接。 -### 代码 -代码要**`go fmt`**后提交。注释文件注明其所属章节。 - -## 如何编译 -`build.go`依赖markdown的一个解析包,所以第一步先 - - go get github.com/russross/blackfriday - -这样读者就可以把相应的Markdown文件编译成html文件,执行`go build build.go`,执行生成的文件,就会在底目录下生成相应的html文件 - -## 如何编译 +# 《Go Web 编程》 +因为自己对Web开发比较感兴趣,所以最近抽空在写一本开源的书籍《Go Web编程》《Build Web Application with Golang》。写这本书不表示我能力很强,而是我愿意分享,和大家一起分享Go写Web应用的一些东西。 + +- 对于从PHP/Python/Ruby转过来的同学了解Go怎么写Web应用开发的 + +- 对于从C/C++转过来的同学了解Web到底是怎么运行起来的 + +我一直认为知识是用来分享的,让更多的人分享自己拥有的一切知识这个才是人生最大的快乐。 + +这本书目前我放在Github上,我现在基本每天晚上抽空会写一些,时间有限、能力有限,所以希望更多的朋友参与到这个开源项目中来。 + +**参加了51CTO博客大赛,希望你能够投出宝贵的一票:[http://blog.51cto.com/contest2012/6177767](http://blog.51cto.com/contest2012/6177767)** + +## 撰写方法 +### 文件命名 +每个章节建立一个md文件,如第11章的第3节,则建立**11.3.md**。 +### 代码文件 +代码文件置于src目录之下。每小节代码按目录存放。如第11章的第3节的代码保存于**src/11.3/**目录下。在正文中按需要添加代码。 + +## 格式规范 +### 正文 +请参看已有章节的规范,要注意的是,每个章节在底部都需要有一个links节,包含“目录”,“上一节”和“下一节”的链接。 +### 代码 +代码要**`go fmt`**后提交。注释文件注明其所属章节。 + +## 如何编译 +`build.go`依赖markdown的一个解析包,所以第一步先 + + go get github.com/russross/blackfriday + +这样读者就可以把相应的Markdown文件编译成html文件,执行`go build build.go`,执行生成的文件,就会在底目录下生成相应的html文件 + +## 如何编译 目前可以把相应的Markdown编译成html文件,执行`go build build.go`,执行生成的文件,就会在底目录下生成相应的html文件。 ## 交流 -欢迎大家加入QQ群:259316004 《Go Web编程》专用交流群 - -大家有问题还可以上德问上一起交流学习:http://www.dewen.org/topic/165 - -## 致谢 -首先要感谢Golang-China的QQ群102319854,里面的每一个人都很热心,同时要特别感谢几个人 - - - [四月份平民](https://plus.google.com/110445767383269817959) (review代码) - - [Hong Ruiqi](https://github.com/hongruiqi) (review代码) - - [BianJiang](https://github.com/border) (编写go开发工具Vim和Emacs的设置) - - [Oling Cat](https://github.com/OlingCat)(review代码) - - [Wenlei Wu](mailto:spadesacn@gmail.com)(提供一些图片展示) - -## 授权许可 -除特别声明外,本书中的内容使用[CC BY-SA 3.0 License](http://creativecommons.org/licenses/by-sa/3.0/)(创作共用 署名-相同方式共享3.0许可协议)授权,代码遵循[BSD 3-Clause License]()(3项条款的BSD许可协议)。 - -## 开始阅读 -[开始阅读]() - - -[![githalytics.com alpha](https://cruel-carlota.pagodabox.com/44c98c9d398b8319b6e87edcd3e34144 "githalytics.com")](http://githalytics.com/astaxie/build-web-application-with-golang) +欢迎大家加入QQ群:259316004 《Go Web编程》专用交流群 + +大家有问题还可以上德问上一起交流学习:http://www.dewen.org/topic/165 + +## 致谢 +首先要感谢Golang-China的QQ群102319854,里面的每一个人都很热心,同时要特别感谢几个人 + + - [四月份平民](https://plus.google.com/110445767383269817959) (review代码) + - [Hong Ruiqi](https://github.com/hongruiqi) (review代码) + - [BianJiang](https://github.com/border) (编写go开发工具Vim和Emacs的设置) + - [Oling Cat](https://github.com/OlingCat)(review代码) + - [Wenlei Wu](mailto:spadesacn@gmail.com)(提供一些图片展示) + +## 授权许可 +除特别声明外,本书中的内容使用[CC BY-SA 3.0 License](http://creativecommons.org/licenses/by-sa/3.0/)(创作共用 署名-相同方式共享3.0许可协议)授权,代码遵循[BSD 3-Clause License]()(3项条款的BSD许可协议)。 + +## 开始阅读 +[开始阅读]() + + +[![githalytics.com alpha](https://cruel-carlota.pagodabox.com/44c98c9d398b8319b6e87edcd3e34144 "githalytics.com")](http://githalytics.com/astaxie/build-web-application-with-golang) diff --git a/build.go b/build.go index 12eb1f300d19c7e04ca4bda351163323feb0f4a5..f82340bf7b3790321f01d2b9cc436f9b8257aba6 100644 --- a/build.go +++ b/build.go @@ -1,58 +1,58 @@ -package main - -import ( - "fmt" - "github.com/russross/blackfriday" - "io/ioutil" - "os" - "path/filepath" - "strings" - "regexp" -) - -// 定义一个访问者结构体 -type Visitor struct{} - -func (self *Visitor) visit(path string, f os.FileInfo, err error) error { - if f == nil { - return err - } - if f.IsDir() { - return nil - } else if (f.Mode() & os.ModeSymlink) > 0 { - return nil - } else { - if strings.HasSuffix(f.Name(), ".md") { - fmt.Println(f) - file, err := os.Open(f.Name()) - if err != nil { - return err - } - input, _ := ioutil.ReadAll(file) - input = regexp.MustCompile("\\[(.*?)\\]\\(?\\)").ReplaceAll(input, []byte("[$1](<$2.html>)")) - output := blackfriday.MarkdownCommon(input) - var out *os.File - if out, err = os.Create(strings.Replace(f.Name(), ".md", ".html", -1)); err != nil { - fmt.Fprintf(os.Stderr, "Error creating %s: %v", f.Name(), err) - os.Exit(-1) - } - defer out.Close() - if _, err = out.Write(output); err != nil { - fmt.Fprintln(os.Stderr, "Error writing output:", err) - os.Exit(-1) - } - } - } - return nil -} - -func main() { - v := &Visitor{} - err := filepath.Walk("./", func(path string, f os.FileInfo, err error) error { - return v.visit(path, f, err) - }) - - if err != nil { - fmt.Printf("filepath.Walk() returned %v\n", err) - } -} +package main + +import ( + "fmt" + "github.com/russross/blackfriday" + "io/ioutil" + "os" + "path/filepath" + "strings" + "regexp" +) + +// 定义一个访问者结构体 +type Visitor struct{} + +func (self *Visitor) visit(path string, f os.FileInfo, err error) error { + if f == nil { + return err + } + if f.IsDir() { + return nil + } else if (f.Mode() & os.ModeSymlink) > 0 { + return nil + } else { + if strings.HasSuffix(f.Name(), ".md") { + fmt.Println(f) + file, err := os.Open(f.Name()) + if err != nil { + return err + } + input, _ := ioutil.ReadAll(file) + input = regexp.MustCompile("\\[(.*?)\\]\\(?\\)").ReplaceAll(input, []byte("[$1](<$2.html>)")) + output := blackfriday.MarkdownCommon(input) + var out *os.File + if out, err = os.Create(strings.Replace(f.Name(), ".md", ".html", -1)); err != nil { + fmt.Fprintf(os.Stderr, "Error creating %s: %v", f.Name(), err) + os.Exit(-1) + } + defer out.Close() + if _, err = out.Write(output); err != nil { + fmt.Fprintln(os.Stderr, "Error writing output:", err) + os.Exit(-1) + } + } + } + return nil +} + +func main() { + v := &Visitor{} + err := filepath.Walk("./", func(path string, f os.FileInfo, err error) error { + return v.visit(path, f, err) + }) + + if err != nil { + fmt.Printf("filepath.Walk() returned %v\n", err) + } +} diff --git a/images/.DS_Store b/images/.DS_Store deleted file mode 100644 index 5008ddfcf53c02e82d7eee2e57c38e5672ef89f6..0000000000000000000000000000000000000000 Binary files a/images/.DS_Store and /dev/null differ diff --git a/preface.md b/preface.md index cb49f263790b1fbd858d915cc5dede5bb814fade..33d52ee9bd64ec4021582e9c2668f7f1f62710e7 100644 --- a/preface.md +++ b/preface.md @@ -1,91 +1,91 @@ -* 1.[Go环境配置](1.md) - - 1.1. [Go安装](1.1.md) - - 1.2. [GOPATH 与工作空间](1.2.md) - - 1.3. [Go 命令](1.3.md) - - 1.4. [Go开发工具](1.4.md) - - 1.5. [小结](1.5.md) -* 2.[Go语言基础](2.md) - - 2.1. [你好,Go](2.1.md) - - 2.2. [Go基础](2.2.md) - - 2.3. [流程和函数](2.3.md) - - 2.4. [struct](2.4.md) - - 2.5. [面向对象](2.5.md) - - 2.6. [interface](2.6.md) - - 2.7. [并发](2.7.md) - - 2.8. [小结](2.8.md) -* 3.[Web基础](3.md) - - 3.1 [web工作方式](3.1.md) - - 3.2 [Go搭建一个简单的web服务](3.2.md) - - 3.3 [Go如何使得web工作](3.3.md) - - 3.4 [Go的http包详解](3.4.md) - - 3.5 [小结](3.5.md) -* 4.[表单](4.md) - - 4.1 [处理表单的输入](4.1.md) - - 4.2 [验证表单的输入](4.2.md) - - 4.3 [预防跨站脚本](4.3.md) - - 4.4 [防止多次递交表单](4.4.md) - - 4.5 [处理文件上传](4.5.md) - - 4.6 [小结](4.6.md) -* 5.[访问数据库](5.md) - - 5.1 [database/sql接口](5.1.md) - - 5.2 [使用MySQL数据库](5.2.md) - - 5.3 [使用SQLite数据库](5.3.md) - - 5.4 [使用PostgreSQL数据库](5.4.md) - - 5.5 [使用beedb库进行ORM开发](5.5.md) - - 5.6 [NOSQL数据库操作](5.6.md) - - 5.7 [小结](5.7.md) -* 6.[session和数据存储](6.md) - - 6.1 [session和cookie](6.1.md) - - 6.2 [Go如何使用session](6.2.md) - - 6.3 [session存储](6.3.md) - - 6.4 [预防session劫持](6.4.md) - - 6.5 [小结](6.5.md) -* 7.[文本处理](7.md) - - 7.1 [XML处理](7.1.md) - - 7.2 [JSON处理](7.2.md) - - 7.3 [正则处理](7.3.md) - - 7.4 [模板处理](7.4.md) - - 7.5 [小结](7.5.md) -* 8.[Web服务](8.md) - - 8.1 [Socket编程](8.1.md) - - 8.2 [WebSocket](8.2.md) - - 8.3 [REST](8.3.md) - - 8.4 [RPC](8.4.md) - - 8.5 [小结](8.5.md) -* 9.[安全与加密](9.md) - - 9.1 [预防CSRF攻击](9.1.md) - - 9.2 [确保输入过滤](9.2.md) - - 9.3 [避免XSS攻击](9.3.md) - - 9.4 [避免SQL注入](9.4.md) - - 9.5 [存储密码](9.5.md) - - 9.6 [加密和解密数据](9.6.md) - - 9.7 [小结](9.7.md) -* 10.[国际化和本地化](10.md) - - 10.1 [设置默认地区](10.1.md) - - 10.2 [本地化资源](10.2.md) - - 10.3 [国际化站点](10.3.md) - - 10.4 [小结](10.4.md) -* 11.[错误处理,调试和测试](11.md) - - 11.1 [错误处理](11.1.md) - - 11.2 [使用GDB调试](11.2.md) - - 11.3 [Go怎么写测试用例](11.3.md) - - 11.4 [小结](11.4.md) -* 12.部署与维护 - - 12.1 应用日志 - - 12.2 网站错误处理 - - 12.3 应用部署 - - 12.4 备份和恢复 - - 12.5 小结 -* 13.构建博客系统  - - 13.1 创建数据库表  - - 13.2 建立对象类 - - 13.3 创建控制器管理博客帖子 - - 13.4 创建和编辑博客帖子  - - 13.5 预览博客帖子 - - 13.6 更新博客帖子的状态 - - 13.7 小结  -* 14.扩展博客管理系统  - - 14.1 在博客管理系统索引页面上列出帖子 - - 14.2 为博客各月概要增加Ajax功能 - - 14.3 集成WYSIWYG编辑器 +* 1.[Go环境配置](1.md) + - 1.1. [Go安装](1.1.md) + - 1.2. [GOPATH 与工作空间](1.2.md) + - 1.3. [Go 命令](1.3.md) + - 1.4. [Go开发工具](1.4.md) + - 1.5. [小结](1.5.md) +* 2.[Go语言基础](2.md) + - 2.1. [你好,Go](2.1.md) + - 2.2. [Go基础](2.2.md) + - 2.3. [流程和函数](2.3.md) + - 2.4. [struct](2.4.md) + - 2.5. [面向对象](2.5.md) + - 2.6. [interface](2.6.md) + - 2.7. [并发](2.7.md) + - 2.8. [小结](2.8.md) +* 3.[Web基础](3.md) + - 3.1 [web工作方式](3.1.md) + - 3.2 [Go搭建一个简单的web服务](3.2.md) + - 3.3 [Go如何使得web工作](3.3.md) + - 3.4 [Go的http包详解](3.4.md) + - 3.5 [小结](3.5.md) +* 4.[表单](4.md) + - 4.1 [处理表单的输入](4.1.md) + - 4.2 [验证表单的输入](4.2.md) + - 4.3 [预防跨站脚本](4.3.md) + - 4.4 [防止多次递交表单](4.4.md) + - 4.5 [处理文件上传](4.5.md) + - 4.6 [小结](4.6.md) +* 5.[访问数据库](5.md) + - 5.1 [database/sql接口](5.1.md) + - 5.2 [使用MySQL数据库](5.2.md) + - 5.3 [使用SQLite数据库](5.3.md) + - 5.4 [使用PostgreSQL数据库](5.4.md) + - 5.5 [使用beedb库进行ORM开发](5.5.md) + - 5.6 [NOSQL数据库操作](5.6.md) + - 5.7 [小结](5.7.md) +* 6.[session和数据存储](6.md) + - 6.1 [session和cookie](6.1.md) + - 6.2 [Go如何使用session](6.2.md) + - 6.3 [session存储](6.3.md) + - 6.4 [预防session劫持](6.4.md) + - 6.5 [小结](6.5.md) +* 7.[文本处理](7.md) + - 7.1 [XML处理](7.1.md) + - 7.2 [JSON处理](7.2.md) + - 7.3 [正则处理](7.3.md) + - 7.4 [模板处理](7.4.md) + - 7.5 [小结](7.5.md) +* 8.[Web服务](8.md) + - 8.1 [Socket编程](8.1.md) + - 8.2 [WebSocket](8.2.md) + - 8.3 [REST](8.3.md) + - 8.4 [RPC](8.4.md) + - 8.5 [小结](8.5.md) +* 9.[安全与加密](9.md) + - 9.1 [预防CSRF攻击](9.1.md) + - 9.2 [确保输入过滤](9.2.md) + - 9.3 [避免XSS攻击](9.3.md) + - 9.4 [避免SQL注入](9.4.md) + - 9.5 [存储密码](9.5.md) + - 9.6 [加密和解密数据](9.6.md) + - 9.7 [小结](9.7.md) +* 10.[国际化和本地化](10.md) + - 10.1 [设置默认地区](10.1.md) + - 10.2 [本地化资源](10.2.md) + - 10.3 [国际化站点](10.3.md) + - 10.4 [小结](10.4.md) +* 11.[错误处理,调试和测试](11.md) + - 11.1 [错误处理](11.1.md) + - 11.2 [使用GDB调试](11.2.md) + - 11.3 [Go怎么写测试用例](11.3.md) + - 11.4 [小结](11.4.md) +* 12.部署与维护 + - 12.1 应用日志 + - 12.2 网站错误处理 + - 12.3 应用部署 + - 12.4 备份和恢复 + - 12.5 小结 +* 13.构建博客系统  + - 13.1 创建数据库表  + - 13.2 建立对象类 + - 13.3 创建控制器管理博客帖子 + - 13.4 创建和编辑博客帖子  + - 13.5 预览博客帖子 + - 13.6 更新博客帖子的状态 + - 13.7 小结  +* 14.扩展博客管理系统  + - 14.1 在博客管理系统索引页面上列出帖子 + - 14.2 为博客各月概要增加Ajax功能 + - 14.3 集成WYSIWYG编辑器 - 14.4 小结  \ No newline at end of file